carlos / platform Public

edge: the home map orders unstamped entries by (incarnation, serving_seq), pair-less entries counting as (0, 0)

edgesHostView and homeEntry gain incarnation, serving_seq and target_host;
foldHomes copies all three with the dial winner. dialBeats keeps its order
of checks and adds the pair only between two unstamped entries: two real
routes always, two aliases only for one non-empty target. Equal pairs fall
to the existing rule. FoldedHome is the fold for witnesses outside the
package.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev 3571b2648770c2304d5fff0dcc4ec30b005974f0 parent 026b14f
2 files changed, +276 −7
  • internal/edge/homemap.go +69 −7
  • internal/edge/homemap_test.go +207 −0
diff --git a/internal/edge/homemap.go b/internal/edge/homemap.go
index f91901da4..fac1840e8 100644
--- a/internal/edge/homemap.go
+++ b/internal/edge/homemap.go
@@ -195,6 +195,17 @@ type edgesHostView struct {
App string `json:"app,omitempty"`
// Stamp orders two boxes' claims on one host: see foldHomes.
Stamp int64 `json:"stamp,omitempty"`
+ // Incarnation and ServingSeq are the publishing row's lifetime pair
+ // (self-serve moves plan 1, design §6.4): a claims-off box publishes
+ // them, a claims-on box and every older writer do not, and an entry
+ // without them is (0, 0). They order two unstamped entries in
+ // dialBeats.
+ Incarnation uint64 `json:"incarnation,omitempty"`
+ ServingSeq uint64 `json:"serving_seq,omitempty"`
+ // TargetHost is the host an alias or a served catch-all is served by
+ // (its primary). Two aliases compare pairs only when they name the
+ // same target: a pair is a fact about one host's lifetime.
+ TargetHost string `json:"target_host,omitempty"`
}
// homeEntry is one folded answer: where to dial, and which box that is.
@@ -217,6 +228,10 @@ type homeEntry struct {
account, app string
// stamp is the dial winner's installation stamp, 0 when unstamped.
stamp int64
+ // incarnation, servingSeq and targetHost are the dial winner's pair
+ // and target (edgesHostView), (0, 0) and "" when it published none.
+ incarnation, servingSeq uint64
+ targetHost string
}
// homeSnapshot is one poll's result, swapped in whole and immutable once
@@ -659,7 +674,7 @@ func foldHomes(files map[string]edgesFileView, self string, privateDial bool) (m
continue
}
in := homeEntry{eip: addr, box: f.Box, platform: h.Platform && !h.Alias && !h.Wildcard, alias: h.Alias, wildcard: h.Wildcard, dial: h.Role == edgeRoleHome,
- account: h.Account, app: h.App, stamp: h.Stamp}
+ account: h.Account, app: h.App, stamp: h.Stamp, incarnation: h.Incarnation, servingSeq: h.ServingSeq, targetHost: h.TargetHost}
prev, dup := hosts[host]
if !dup {
hosts[host] = in
@@ -670,16 +685,19 @@ func foldHomes(files map[string]edgesFileView, self string, privateDial bool) (m
// must load everywhere even if some other box also routes the
// key), the dial target is a choice. First box in sorted order
// wins the dial — except that a ROUTE beats an ALIAS whatever
- // the order (#287), and any dial target beats none. The owner
- // is part of the dial choice, not a class: it comes whole from
- // the winner, so a route that replaces an alias never inherits
- // the alias's owner, even when it publishes none of its own.
+ // the order (#287), any dial target beats none, and stamps and
+ // then lifetime pairs order the rest (dialBeats). The owner, the
+ // stamp, the pair and the target are part of the dial choice,
+ // not classes: they come whole from the winner, so a route that
+ // replaces an alias never inherits the alias's owner or pair,
+ // even when it publishes none of its own.
merged := prev
merged.platform = prev.platform || in.platform
merged.wildcard = prev.wildcard || in.wildcard
if dialBeats(in, prev) {
merged.eip, merged.box, merged.alias, merged.dial = in.eip, in.box, in.alias, true
merged.account, merged.app, merged.stamp = in.account, in.app, in.stamp
+ merged.incarnation, merged.servingSeq, merged.targetHost = in.incarnation, in.servingSeq, in.targetHost
}
hosts[host] = merged
}
@@ -687,6 +705,30 @@ func foldHomes(files map[string]edgesFileView, self string, privateDial bool) (m
return hosts, tiedStamps(files)
}
+// FoldedHome folds raw edges-file bodies, keyed by bucket key, as
+// refreshHosts folds one poll (parseEdgesFile, then foldHomes with the
+// private dial off) and answers the box whose entry won host's dial, as an
+// edge labelled self sees it. It is the real fold for witnesses outside
+// this package: internal/host's publication tests drive its real writer
+// into it. A body that does not parse is an error; ok is false when host
+// has no dial target.
+func FoldedHome(bodies map[string][]byte, self, host string) (box string, ok bool, err error) {
+ files := make(map[string]edgesFileView, len(bodies))
+ for key, body := range bodies {
+ f, err := parseEdgesFile(key, body)
+ if err != nil {
+ return "", false, err
+ }
+ files[key] = f
+ }
+ hosts, _ := foldHomes(files, self, false)
+ h, found := hosts[host]
+ if !found || !h.dial {
+ return "", false, nil
+ }
+ return h.box, true, nil
+}
+
// tiedStamps lists, sorted, the hosts that two boxes publish as home at
// the same positive stamp. It looks at every file, this box's own
// included, and at every pair, not only at whichever entry won the dial:
@@ -739,8 +781,17 @@ func tiedStamps(files map[string]edgesFileView) []string {
// (region HA spec 1 HA4). A dead box's file lingers; its successor's
// stamp is always higher, so the stale one loses without any liveness
// rule;
-// - otherwise the earlier file keeps it, which is the first box in
-// sorted order, as before.
+// - between two unstamped entries, the higher lifetime pair
+// (incarnation, serving_seq) wins (self-serve moves plan 1, design
+// §6.4), an entry without the pair counting as (0, 0): two real routes
+// always compare, two aliases only when they name the same non-empty
+// target, since a pair is a fact about one host's lifetime. A stamp
+// still outranks any pair, so a claims-on publication stamped by its
+// claim beats every unstamped one; an unstamped claims-on publication
+// (0, 0) loses to a retained claims-off file carrying a pair, until
+// that file refreshes;
+// - otherwise, equal pairs included, the earlier file keeps it, which is
+// the first box in sorted order, as before.
func dialBeats(in, prev homeEntry) bool {
if !in.dial {
return false
@@ -751,6 +802,17 @@ func dialBeats(in, prev homeEntry) bool {
if prev.alias != in.alias {
return prev.alias && !in.alias
}
+ if in.stamp != 0 || prev.stamp != 0 {
+ return in.stamp > prev.stamp
+ }
+ if !in.alias || (prev.targetHost != "" && prev.targetHost == in.targetHost) {
+ if in.incarnation != prev.incarnation {
+ return in.incarnation > prev.incarnation
+ }
+ if in.servingSeq != prev.servingSeq {
+ return in.servingSeq > prev.servingSeq
+ }
+ }
return in.stamp > prev.stamp
}
diff --git a/internal/edge/homemap_test.go b/internal/edge/homemap_test.go
index cdf518660..5fd4e03f8 100644
--- a/internal/edge/homemap_test.go
+++ b/internal/edge/homemap_test.go
@@ -1784,3 +1784,210 @@ func TestLocalWildcardYieldsToARemoteExactHost(t *testing.T) {
t.Fatalf("home edge saw %d request(s), want still 1", n)
}
}
+
+// ---- the lifetime pair (self-serve moves plan 1, Task 8) -------------------
+
+// pairHost is the one name the pair-fold tests fold.
+const pairHost = "shop.example.com"
+
+// foldTwo folds a as annie's entry for pairHost and b as betty's, the
+// order the sorted file keys give, and returns the folded entry.
+func foldTwo(t *testing.T, a, b edgesHostView) homeEntry {
+ t.Helper()
+ files := map[string]edgesFileView{
+ "control/edges/annie.json": file("annie", "1.1.1.1", map[string]edgesHostView{pairHost: a}),
+ "control/edges/betty.json": file("betty", "2.2.2.2", map[string]edgesHostView{pairHost: b}),
+ }
+ hosts, _ := foldHomes(files, "carol", false)
+ return hosts[pairHost]
+}
+
+// winsBothOrders asserts that x takes the dial from y whichever file
+// sorts first.
+func winsBothOrders(t *testing.T, why string, x, y edgesHostView) {
+ t.Helper()
+ if got := foldTwo(t, x, y); !got.dial || got.box != "annie" {
+ t.Errorf("%s: x first: dial %v box %q, want x (annie)", why, got.dial, got.box)
+ }
+ if got := foldTwo(t, y, x); !got.dial || got.box != "betty" {
+ t.Errorf("%s: x second: dial %v box %q, want x (betty)", why, got.dial, got.box)
+ }
+}
+
+// firstFileWins asserts today's rule: the earlier file keeps the dial,
+// whichever of x and y it holds.
+func firstFileWins(t *testing.T, why string, x, y edgesHostView) {
+ t.Helper()
+ if got := foldTwo(t, x, y); got.box != "annie" {
+ t.Errorf("%s: x first: box %q, want the earlier file (annie)", why, got.box)
+ }
+ if got := foldTwo(t, y, x); got.box != "annie" {
+ t.Errorf("%s: y first: box %q, want the earlier file (annie)", why, got.box)
+ }
+}
+
+func TestFoldPrefersTheHigherPairOfOneLifetime(t *testing.T) {
+ route := func(inc, seq uint64) edgesHostView {
+ return edgesHostView{Role: "home", Incarnation: inc, ServingSeq: seq}
+ }
+ winsBothOrders(t, "open (1, 2) over the hold (1, 1)", route(1, 2), route(1, 1))
+ winsBothOrders(t, "a recreated host (2, 0) over lifetime 1", route(2, 0), route(1, 9))
+ winsBothOrders(t, "the first hold (1, 1) over a pair-less (0, 0)", route(1, 1), edgesHostView{Role: "home"})
+ firstFileWins(t, "equal pairs fall to the existing rule", route(1, 1), route(1, 1))
+ firstFileWins(t, "two pair-less entries fall to the existing rule", edgesHostView{Role: "home"}, edgesHostView{Role: "home"})
+
+ // The same rule read off the wire through a real Refresh.
+ s := meshTestStore(t)
+ putJSON(t, s, "control/edges/annie.json", `{"v":1,"box":"annie","eip":"1.1.1.1","hosts":{"shop.example.com":{"role":"home","incarnation":1,"serving_seq":1}}}`)
+ putJSON(t, s, "control/edges/betty.json", `{"v":1,"box":"betty","eip":"2.2.2.2","hosts":{"shop.example.com":{"role":"home","incarnation":1,"serving_seq":2}}}`)
+ m := newHomeMap(s, "carol")
+ refreshHomeMap(t, m)
+ if _, box, ok := m.Home(pairHost); !ok || box != "betty" {
+ t.Fatalf("Home = %q %v, want betty: (1, 2) over (1, 1) off the wire", box, ok)
+ }
+}
+
+func TestFoldStampBeatsPair(t *testing.T) {
+ retained := edgesHostView{Role: "home", Incarnation: 1, ServingSeq: 3}
+ stamped := edgesHostView{Role: "home", Stamp: 5}
+ winsBothOrders(t, "a stamped (0, 0) over an unstamped (1, 3)", stamped, retained)
+ winsBothOrders(t, "the higher stamp whatever the pairs",
+ edgesHostView{Role: "home", Stamp: 9}, edgesHostView{Role: "home", Stamp: 5, Incarnation: 3, ServingSeq: 3})
+ firstFileWins(t, "equal stamps keep the earlier file whatever the pairs",
+ edgesHostView{Role: "home", Stamp: 5}, edgesHostView{Role: "home", Stamp: 5, Incarnation: 3, ServingSeq: 3})
+}
+
+// The mixed-history statement's one case where the pair outranks an
+// unstamped claims-on publication: a retained claims-off file (its box's
+// file could not be refreshed after the deployment turned claims on) wins
+// until it refreshes.
+func TestFoldUnstampedClaimsOnAgainstRetainedPair(t *testing.T) {
+ winsBothOrders(t, "a retained (1, 3) over an unstamped claims-on (0, 0)",
+ edgesHostView{Role: "home", Incarnation: 1, ServingSeq: 3}, edgesHostView{Role: "home"})
+}
+
+func TestFoldKeepsRealRouteOverAHigherPairAlias(t *testing.T) {
+ winsBothOrders(t, "a pair-less route over an alias at (5, 5)",
+ edgesHostView{Role: "home"}, edgesHostView{Role: "home", Alias: true, Incarnation: 5, ServingSeq: 5, TargetHost: "bab.example.com"})
+ winsBothOrders(t, "a route at (1, 1) over an alias at (5, 5)",
+ edgesHostView{Role: "home", Incarnation: 1, ServingSeq: 1}, edgesHostView{Role: "home", Alias: true, Incarnation: 5, ServingSeq: 5, TargetHost: "bab.example.com"})
+}
+
+func TestFoldComparesAliasesOnlyForOneTarget(t *testing.T) {
+ alias := func(target string, inc, seq uint64) edgesHostView {
+ return edgesHostView{Role: "home", Alias: true, TargetHost: target, Incarnation: inc, ServingSeq: seq}
+ }
+ winsBothOrders(t, "one target: the higher pair", alias("bab.example.com", 1, 2), alias("bab.example.com", 1, 1))
+ firstFileWins(t, "one target, equal pairs: the existing rule", alias("bab.example.com", 1, 1), alias("bab.example.com", 1, 1))
+ firstFileWins(t, "two targets: the existing rule whatever the pairs", alias("bab.example.com", 2, 0), alias("shop.example.net", 1, 1))
+ firstFileWins(t, "no target on either: the existing rule", alias("", 2, 0), alias("", 1, 1))
+ firstFileWins(t, "a target on one only: the existing rule", alias("bab.example.com", 2, 0), alias("", 1, 1))
+}
+
+func TestFoldNeverDialsACertOnlyEntryForItsPair(t *testing.T) {
+ const key = "*.shop.example.com"
+ certOnly := edgesHostView{Role: "wildcard", Wildcard: true, Incarnation: 9, ServingSeq: 9}
+ dial := edgesHostView{Role: "home", Wildcard: true, Alias: true, TargetHost: "bab.example.com", Incarnation: 1, ServingSeq: 1}
+ for _, order := range [][2]edgesHostView{{certOnly, dial}, {dial, certOnly}} {
+ files := map[string]edgesFileView{
+ "control/edges/annie.json": file("annie", "1.1.1.1", map[string]edgesHostView{key: order[0]}),
+ "control/edges/betty.json": file("betty", "2.2.2.2", map[string]edgesHostView{key: order[1]}),
+ }
+ hosts, _ := foldHomes(files, "carol", false)
+ want := "annie"
+ if order[0].Role == "wildcard" {
+ want = "betty"
+ }
+ if got := hosts[key]; !got.dial || got.box != want || !got.wildcard {
+ t.Errorf("cert-only (9, 9) against a dial (1, 1): %+v, want the dial on %s", got, want)
+ }
+ }
+ // A home entry with no dial address is folded cert-only: its pair
+ // cannot make it a dial target either.
+ files := map[string]edgesFileView{
+ "control/edges/annie.json": file("annie", "", map[string]edgesHostView{key: {Role: "home", Wildcard: true, Alias: true, TargetHost: "bab.example.com", Incarnation: 9, ServingSeq: 9}}),
+ "control/edges/betty.json": file("betty", "2.2.2.2", map[string]edgesHostView{key: dial}),
+ }
+ hosts, _ := foldHomes(files, "carol", false)
+ if got := hosts[key]; !got.dial || got.box != "betty" {
+ t.Errorf("an addressless (9, 9) against a dial (1, 1): %+v, want betty", got)
+ }
+ // A cert-only entry alone dials nothing, whatever its pair.
+ files = map[string]edgesFileView{"control/edges/annie.json": file("annie", "1.1.1.1", map[string]edgesHostView{key: certOnly})}
+ hosts, _ = foldHomes(files, "carol", false)
+ if got := hosts[key]; got.dial {
+ t.Errorf("a cert-only entry alone = %+v, want no dial", got)
+ }
+}
+
+func TestFoldCopiesTheWinnersFields(t *testing.T) {
+ low := edgesHostView{Role: "home", Alias: true, TargetHost: "bab.example.com", Incarnation: 1, ServingSeq: 1}
+ high := edgesHostView{Role: "home", Alias: true, TargetHost: "bab.example.com", Incarnation: 1, ServingSeq: 2}
+ for _, got := range []homeEntry{foldTwo(t, low, high), foldTwo(t, high, low)} {
+ if got.incarnation != 1 || got.servingSeq != 2 || got.targetHost != "bab.example.com" {
+ t.Errorf("winner (1, 2): folded (%d, %d) %q", got.incarnation, got.servingSeq, got.targetHost)
+ }
+ }
+ // The earlier file keeps the dial on two targets: its fields stay, the
+ // loser's do not leak in.
+ other := edgesHostView{Role: "home", Alias: true, TargetHost: "shop.example.net", Incarnation: 7, ServingSeq: 7}
+ if got := foldTwo(t, low, other); got.box != "annie" || got.incarnation != 1 || got.servingSeq != 1 || got.targetHost != "bab.example.com" {
+ t.Errorf("kept dial: %+v, want annie's (1, 1) bab.example.com", got)
+ }
+ // A route that takes the dial from an alias brings its own (here
+ // empty) target and pair.
+ route := edgesHostView{Role: "home", Incarnation: 0, ServingSeq: 0}
+ if got := foldTwo(t, other, route); got.box != "betty" || got.alias || got.incarnation != 0 || got.servingSeq != 0 || got.targetHost != "" {
+ t.Errorf("route over alias: %+v, want betty's pair-less route", got)
+ }
+ // One entry alone is copied whole.
+ files := map[string]edgesFileView{"control/edges/annie.json": file("annie", "1.1.1.1", map[string]edgesHostView{pairHost: high})}
+ hosts, _ := foldHomes(files, "carol", false)
+ if got := hosts[pairHost]; got.incarnation != 1 || got.servingSeq != 2 || got.targetHost != "bab.example.com" {
+ t.Errorf("a lone entry folded (%d, %d) %q", got.incarnation, got.servingSeq, got.targetHost)
+ }
+}
+
+// Every pair-less publication folds as it did before the pair existed: the
+// rules below are the ones dialBeats documented before Task 8, case by case.
+func TestFoldLegacyPublicationsUnchanged(t *testing.T) {
+ route := edgesHostView{Role: "home"}
+ alias := edgesHostView{Role: "home", Alias: true}
+ cases := []struct {
+ name string
+ annie edgesHostView
+ betty edgesHostView
+ wantBox string
+ wantDialed bool
+ }{
+ {"two routes: the earlier file", route, route, "annie", true},
+ {"two aliases: the earlier file", alias, alias, "annie", true},
+ {"route over alias", alias, route, "betty", true},
+ {"route over alias, route first", route, alias, "annie", true},
+ {"stamped over unstamped", route, edgesHostView{Role: "home", Stamp: 3}, "betty", true},
+ {"higher stamp", edgesHostView{Role: "home", Stamp: 9}, edgesHostView{Role: "home", Stamp: 3}, "annie", true},
+ {"higher stamp second", edgesHostView{Role: "home", Stamp: 3}, edgesHostView{Role: "home", Stamp: 9}, "betty", true},
+ {"a stamped alias does not beat a route", route, edgesHostView{Role: "home", Alias: true, Stamp: 9}, "annie", true},
+ {"stamped alias over unstamped alias", alias, edgesHostView{Role: "home", Alias: true, Stamp: 2}, "betty", true},
+ {"a dial over a cert-only entry", edgesHostView{Role: "wildcard", Wildcard: true}, edgesHostView{Role: "home", Wildcard: true, Alias: true}, "betty", true},
+ {"cert-only twice dials nothing", edgesHostView{Role: "wildcard", Wildcard: true}, edgesHostView{Role: "wildcard", Wildcard: true}, "annie", false},
+ }
+ for _, c := range cases {
+ key := pairHost
+ if c.annie.Wildcard || c.betty.Wildcard {
+ key = "*." + pairHost
+ }
+ files := map[string]edgesFileView{
+ "control/edges/annie.json": file("annie", "1.1.1.1", map[string]edgesHostView{key: c.annie}),
+ "control/edges/betty.json": file("betty", "2.2.2.2", map[string]edgesHostView{key: c.betty}),
+ }
+ hosts, _ := foldHomes(files, "carol", false)
+ got := hosts[key]
+ if got.box != c.wantBox || got.dial != c.wantDialed {
+ t.Errorf("%s: box %q dial %v, want %q %v", c.name, got.box, got.dial, c.wantBox, c.wantDialed)
+ }
+ if got.incarnation != 0 || got.servingSeq != 0 || got.targetHost != "" {
+ t.Errorf("%s: a pair-less fold carries (%d, %d) %q", c.name, got.incarnation, got.servingSeq, got.targetHost)
+ }
+ }
+}