carlos / platform Public

edge: one file's holds of different targets for one alias are ranked by target, so the page never depends on collection order

Invariant (n) of plan 3: the holding winner is independent of input order
except through order. Within one order, two same-kind holds of different
targets were left in collection order by the stable sort; the sort now
breaks that tie by target. TestTwoHoldsForOneAlias pins it in every input
order, with the lower order, local first and the same-target pair rule.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev e239b8a3bd231fa1e46c15c6f1bb0bdef8444fef parent 8b1437b
2 files changed, +74 −3
  • internal/edge/alias_test.go +70 −0
  • internal/edge/rank.go +4 −3
diff --git a/internal/edge/alias_test.go b/internal/edge/alias_test.go
index 2627c867b..6d450f2b2 100644
--- a/internal/edge/alias_test.go
+++ b/internal/edge/alias_test.go
@@ -590,3 +590,73 @@ func TestHTTPFallbackRedirectsTunnelHost(t *testing.T) {
t.Fatalf("tunnel.test: Location = %q, want https://tunnel.test/register", loc)
}
}
+
+// The ranking's alias witnesses (self-serve moves plan 3 Task 3). An alias
+// candidate is a lifetime of its target (its primary), so a hold of the
+// moving instance reaches the alias's stale ready publications of that
+// target and nothing else, and two candidates of different targets never
+// compare pairs. rankAlias is the alias name every witness below asks
+// about; rankHost (rank_test.go) is the moving instance it points at.
+const (
+ rankAlias = "www.shop.example"
+ // otherHost is a second instance the alias can point at instead.
+ otherHost = "cart.other.example"
+)
+
+// lhold is this box's own holding candidate for target: the real one
+// unless alias, named by its move.
+func lhold(target string, alias bool, inc, seq uint64, move string) candidate {
+ return candidate{kind: candHolding, local: true, order: -1, alias: alias, account: "bab", app: "shop",
+ targetHost: target, incarnation: inc, servingSeq: seq,
+ hold: &holdingView{Host: target, Account: "bab", App: "shop", MoveID: move, Incarnation: inc, ServingSeq: seq}}
+}
+
+// permutations is every order of cs (cs is small).
+func permutations(cs []candidate) [][]candidate {
+ if len(cs) <= 1 {
+ return [][]candidate{slices.Clone(cs)}
+ }
+ var out [][]candidate
+ for i := range cs {
+ rest := append(slices.Clone(cs[:i]), cs[i+1:]...)
+ for _, p := range permutations(rest) {
+ out = append(out, append([]candidate{cs[i]}, p...))
+ }
+ }
+ return out
+}
+
+// TestTwoHoldsForOneAlias: when only holds answer an alias, the holding
+// fold decides. Between holds of different targets no pair is compared:
+// the lower order (the earlier file, this box's own first) keeps it, and
+// within one file the lower target name does, so the page shown never
+// depends on the order the candidates were collected in. Between two holds
+// of one target, the higher pair is the newer lifetime and wins wherever it
+// was published.
+func TestTwoHoldsForOneAlias(t *testing.T) {
+ cases := []struct {
+ name string
+ cands []candidate
+ move string
+ }{
+ {"different targets: the lower order, whatever the pairs",
+ []candidate{rhold(1, "betty", rankHost, true, 5, 5), rhold(0, "annie", otherHost, true, 1, 0)}, "mv_annie"},
+ {"one target: the higher pair, whatever the order",
+ []candidate{rhold(0, "annie", rankHost, true, 1, 1), rhold(1, "betty", rankHost, true, 1, 2)}, "mv_betty"},
+ {"local first against another target",
+ []candidate{rhold(0, "annie", otherHost, true, 1, 0), lhold(rankHost, true, 0, 0, "mv_local")}, "mv_local"},
+ {"a remote newer lifetime of the local hold's own target replaces it",
+ []candidate{rhold(0, "annie", rankHost, true, 1, 2), lhold(rankHost, true, 1, 1, "mv_local")}, "mv_annie"},
+ {"one file, two targets: the lower target name",
+ []candidate{rhold(0, "annie-shop", rankHost, true, 1, 1), rhold(0, "annie-cart", otherHost, true, 1, 1)}, "mv_annie-cart"},
+ {"one file, two targets and a third file: still the lower target name of the first file",
+ []candidate{rhold(1, "betty", rankHost, true, 9, 9), rhold(0, "annie-shop", rankHost, true, 1, 1), rhold(0, "annie-cart", otherHost, true, 1, 1)}, "mv_annie-cart"},
+ }
+ for _, tc := range cases {
+ t.Run(tc.name, func(t *testing.T) {
+ for _, p := range permutations(tc.cands) {
+ wantHolding(t, rank(rankAlias, p, rankNow), tc.move, tc.name)
+ }
+ })
+ }
+}
diff --git a/internal/edge/rank.go b/internal/edge/rank.go
index a6cd354ea..1fba14b3a 100644
--- a/internal/edge/rank.go
+++ b/internal/edge/rank.go
@@ -155,8 +155,9 @@ func rank(name string, cands []candidate, now time.Time) decision {
live = append(live, c)
}
// Sorted by order, local (-1) first; within one order (one file), a real
- // candidate before an alias, so the answer never depends on the order
- // the candidates were collected in.
+ // candidate before an alias, then by target, so the answer never
+ // depends on the order the candidates were collected in (one file's two
+ // holds of different targets for one alias included).
slices.SortStableFunc(live, func(a, b candidate) int {
if a.order != b.order {
return a.order - b.order
@@ -167,7 +168,7 @@ func rank(name string, cands []candidate, now time.Time) decision {
}
return -1
}
- return 0
+ return strings.Compare(a.targetHost, b.targetHost)
})
var d decision
decides := false