| new file mode 100644 |
| index 000000000..07401bb3d |
| --- /dev/null |
| +++ b/internal/edge/rank_property_test.go |
| @@ -0,0 +1,902 @@ |
| +package edge |
| + |
| +// The ranking over its small model (self-serve moves plan 3 Task 3, Review |
| +// Focus 2): every candidate set of up to four candidates, two targets, |
| +// pairs up to 2, stamps 0 or 1, fresh or expired holds, local or remote. |
| +// |
| +// The model, and what it leaves out on purpose: |
| +// |
| +// - The name is modelName. A real candidate (a route, a held row) is a |
| +// lifetime of the name itself, so its target is always modelName; an |
| +// alias candidate's target is its primary, modelPrimary, or "" for a |
| +// legacy ready alias publication that names none. Those are the two |
| +// targets. (A real candidate of another target cannot exist: a file |
| +// publishes a name once, and a held row's entry is keyed by its host.) |
| +// - Pairs are modelPairs, ascending: (1,0) < (1,1) < (2,0). The spec |
| +// below compares a pair's index, never the fields rank compares, so the |
| +// lexicographic order is pinned too ((1,1) is below (2,0) although its |
| +// serving_seq is higher). |
| +// - Remote candidates sit in one of two files (order 0 or 1); a file |
| +// publishes a name once, so it carries at most one ready candidate, and |
| +// the box has at most one table entry and one hold for the name. |
| +// - Only a remote ready candidate is ever stamped. Local ready candidates |
| +// always carry stamp 0 (localReady), so a hold CAN suppress this box's |
| +// own ready candidate, and invariant (b) never protects a claims-on |
| +// box's own stamped route locally. That is intended: local first makes |
| +// stamps moot on the box itself, and a claims-on box publishes no holds. |
| +// TestLocalReadyIsUnstamped pins it. |
| +// - Expiry is rank's alone here: a remote hold fetched 4 m 59 s ago is |
| +// fresh and one fetched 5 m ago is expired (the clock that sets |
| +// fetchedAt is Task 5's). A ready candidate is believed whatever its |
| +// file's age. |
| +// - The classes are fixed marks (platform on a real candidate in the |
| +// first file or local, wildcard on a ready candidate in the second), so |
| +// varying a pair never changes them. |
| + |
| +import ( |
| + "fmt" |
| + "reflect" |
| + "slices" |
| + "strings" |
| + "testing" |
| + "time" |
| + |
| + "github.com/carlosframework/platform/internal/registry" |
| +) |
| + |
| +var modelPairs = [][2]uint64{{1, 0}, {1, 1}, {2, 0}} |
| + |
| +const ( |
| + modelName = "x.example.com" |
| + modelPrimary = "app.example.net" |
| +) |
| + |
| +// mc is one candidate of the small model. |
| +type mc struct { |
| + kind candKind |
| + local bool |
| + order int |
| + alias bool |
| + target string |
| + pair int |
| + stamp int64 |
| + expired bool |
| +} |
| + |
| +func (m mc) String() string { |
| + var b strings.Builder |
| + if m.local { |
| + b.WriteString("local ") |
| + } else { |
| + fmt.Fprintf(&b, "file%d ", m.order) |
| + } |
| + if m.kind == candHolding { |
| + b.WriteString("hold ") |
| + } else { |
| + b.WriteString("ready ") |
| + } |
| + if m.alias { |
| + fmt.Fprintf(&b, "alias->%q ", m.target) |
| + } else { |
| + b.WriteString("real ") |
| + } |
| + fmt.Fprintf(&b, "%v", modelPairs[m.pair]) |
| + if m.stamp != 0 { |
| + fmt.Fprintf(&b, " stamp %d", m.stamp) |
| + } |
| + if m.expired { |
| + b.WriteString(" expired") |
| + } |
| + return b.String() |
| +} |
| + |
| +// build is the candidate m stands for, the i-th of its set: its app names |
| +// it, so the winner of any decision is identifiable. |
| +func (m mc) build(i int) candidate { |
| + c := candidate{kind: m.kind, local: m.local, order: m.order, alias: m.alias, targetHost: m.target, |
| + incarnation: modelPairs[m.pair][0], servingSeq: modelPairs[m.pair][1], stamp: m.stamp, |
| + account: "acct", app: fmt.Sprintf("c%d", i), fetchedAt: rankNow.Add(-holdingValidity + time.Second), |
| + platform: !m.alias && m.order <= 0, wildcard: m.kind == candReady && m.order == 1} |
| + if !m.local { |
| + c.box = fmt.Sprintf("box%d", m.order) |
| + c.eip = c.box + "-ip" |
| + } |
| + if m.expired { |
| + c.fetchedAt = rankNow.Add(-holdingValidity) |
| + } |
| + switch { |
| + case m.kind == candHolding: |
| + c.hold = &holdingView{Host: m.target, Account: c.account, App: c.app, MoveID: "mv_" + c.app, |
| + Incarnation: c.incarnation, ServingSeq: c.servingSeq} |
| + case m.local: |
| + up := upstream{alias: m.alias, route: registry.Route{Host: m.target, App: c.app}} |
| + c.up = &up |
| + c.table = map[string]upstream{modelName: up} |
| + } |
| + return c |
| +} |
| + |
| +func (m mc) live() bool { return !(m.kind == candHolding && !m.local && m.expired) } |
| + |
| +// modelAlphabet is every candidate of the model: 72 of them. |
| +func modelAlphabet() []mc { |
| + var out []mc |
| + for p := range modelPairs { |
| + out = append(out, |
| + mc{kind: candReady, local: true, order: -1, target: modelName, pair: p}, |
| + mc{kind: candReady, local: true, order: -1, alias: true, target: modelPrimary, pair: p}, |
| + mc{kind: candHolding, local: true, order: -1, target: modelName, pair: p}, |
| + mc{kind: candHolding, local: true, order: -1, alias: true, target: modelPrimary, pair: p}) |
| + for order := 0; order < 2; order++ { |
| + for stamp := int64(0); stamp < 2; stamp++ { |
| + out = append(out, |
| + mc{kind: candReady, order: order, target: modelName, pair: p, stamp: stamp}, |
| + mc{kind: candReady, order: order, alias: true, target: modelPrimary, pair: p, stamp: stamp}, |
| + mc{kind: candReady, order: order, alias: true, target: "", pair: p, stamp: stamp}) |
| + } |
| + for _, expired := range []bool{false, true} { |
| + out = append(out, |
| + mc{kind: candHolding, order: order, target: modelName, pair: p, expired: expired}, |
| + mc{kind: candHolding, order: order, alias: true, target: modelPrimary, pair: p, expired: expired}) |
| + } |
| + } |
| + } |
| + return out |
| +} |
| + |
| +// admissible is the model's shape rule: at most one table entry and one |
| +// hold of this box's own, and at most one ready candidate per file. |
| +func admissible(set []mc) bool { |
| + localReady, localHold := 0, 0 |
| + readyIn := map[int]int{} |
| + for _, m := range set { |
| + switch { |
| + case m.local && m.kind == candReady: |
| + localReady++ |
| + case m.local: |
| + localHold++ |
| + case m.kind == candReady: |
| + readyIn[m.order]++ |
| + } |
| + } |
| + if localReady > 1 || localHold > 1 { |
| + return false |
| + } |
| + for _, n := range readyIn { |
| + if n > 1 { |
| + return false |
| + } |
| + } |
| + return true |
| +} |
| + |
| +// eachSubset calls fn with every subset of alphabet of 1 to max members, |
| +// in alphabet order. |
| +func eachSubset(alphabet []mc, max int, fn func([]mc)) { |
| + var set []mc |
| + var rec func(start int) |
| + rec = func(start int) { |
| + if len(set) > 0 { |
| + fn(set) |
| + } |
| + if len(set) == max { |
| + return |
| + } |
| + for i := start; i < len(alphabet); i++ { |
| + set = append(set, alphabet[i]) |
| + rec(i + 1) |
| + set = set[:len(set)-1] |
| + } |
| + } |
| + rec(0) |
| +} |
| + |
| +func buildSet(set []mc) []candidate { |
| + cs := make([]candidate, len(set)) |
| + for i, m := range set { |
| + cs[i] = m.build(i) |
| + } |
| + return cs |
| +} |
| + |
| +// tablePtr is a table's identity. |
| +func tablePtr(m map[string]upstream) uintptr { return reflect.ValueOf(m).Pointer() } |
| + |
| +// sameDecision compares two decisions whole: the winner by identity (the |
| +// upstream, the table, the hold) and every field the request path reads. |
| +func sameDecision(a, b decision) bool { |
| + return a.kind == b.kind && a.up == b.up && tablePtr(a.table) == tablePtr(b.table) && a.hold == b.hold && |
| + a.eip == b.eip && a.box == b.box && a.account == b.account && a.app == b.app && a.stamp == b.stamp && |
| + a.platform == b.platform && a.wildcard == b.wildcard |
| +} |
| + |
| +// sameWinner is sameDecision without the classes. |
| +func sameWinner(a, b decision) bool { |
| + a.platform, a.wildcard, b.platform, b.wildcard = false, false, false, false |
| + return sameDecision(a, b) |
| +} |
| + |
| +func describe(set []mc) string { |
| + parts := make([]string, len(set)) |
| + for i, m := range set { |
| + parts[i] = fmt.Sprintf("[%d] %v", i, m) |
| + } |
| + return strings.Join(parts, "; ") |
| +} |
| + |
| +func describeDecision(d decision) string { |
| + switch d.kind { |
| + case decLocal: |
| + return "local " + d.up.route.App |
| + case decRemote: |
| + return fmt.Sprintf("remote %s at %s", d.app, d.box) |
| + case decHolding: |
| + return "holding " + d.hold.App |
| + } |
| + return "none" |
| +} |
| + |
| +// specSuppressed is invariant (d) and (b) as the plan states them: a live |
| +// hold removes exactly the unstamped ready candidates sharing its |
| +// non-empty target with a lower pair. |
| +func specSuppressed(set []mc, i int) bool { |
| + c := set[i] |
| + if c.kind != candReady || c.target == "" || c.stamp != 0 { |
| + return false |
| + } |
| + for _, h := range set { |
| + if h.kind == candHolding && h.live() && h.target == c.target && c.pair < h.pair { |
| + return true |
| + } |
| + } |
| + return false |
| +} |
| + |
| +// specHoldWinner is the holding fold's answer stated without folding |
| +// (invariant (n)): a real hold if there is any, else an alias; of that |
| +// kind, the target of the first by (order, target); of that kind and |
| +// target, the highest pair, the first on a tie. -1 when no hold is live. |
| +func specHoldWinner(set []mc) int { |
| + anyReal := false |
| + for _, h := range set { |
| + if h.kind == candHolding && h.live() && !h.alias { |
| + anyReal = true |
| + } |
| + } |
| + first := -1 |
| + for i, h := range set { |
| + if h.kind != candHolding || !h.live() || h.alias == anyReal { |
| + continue |
| + } |
| + if first < 0 || h.order < set[first].order || (h.order == set[first].order && h.target < set[first].target) { |
| + first = i |
| + } |
| + } |
| + if first < 0 { |
| + return -1 |
| + } |
| + win := first |
| + for i, h := range set { |
| + if h.kind != candHolding || !h.live() || h.alias == anyReal || h.target != set[first].target { |
| + continue |
| + } |
| + w := set[win] |
| + if h.pair > w.pair || (h.pair == w.pair && h.order < w.order) { |
| + win = i |
| + } |
| + } |
| + return win |
| +} |
| + |
| +// foldOracle is foldHomes over the remote ready candidates idx of set, |
| +// each published in its own file: the winning candidate's index, or -1. |
| +func foldOracle(set []mc, cs []candidate, idx []int) int { |
| + files := map[string]edgesFileView{} |
| + for _, i := range idx { |
| + c := cs[i] |
| + h := edgesHostView{Role: edgeRoleHome, Alias: c.alias, Platform: c.platform, Wildcard: c.wildcard, |
| + Account: c.account, App: c.app, Stamp: c.stamp, Incarnation: c.incarnation, ServingSeq: c.servingSeq} |
| + if c.alias { |
| + h.TargetHost = c.targetHost |
| + } |
| + files[fmt.Sprintf("control/edges/%d.json", set[i].order)] = file(c.box, c.eip, map[string]edgesHostView{modelName: h}) |
| + } |
| + hosts, _ := foldHomes(files, "zed", false) |
| + w, ok := hosts[modelName] |
| + if !ok || !w.dial { |
| + return -1 |
| + } |
| + for _, i := range idx { |
| + if cs[i].app == w.app { |
| + return i |
| + } |
| + } |
| + return -1 |
| +} |
| + |
| +// TestRankingProperties runs every candidate set of the small model |
| +// through rank and asserts the scoped invariants: (f) an expired remote |
| +// hold neither suppresses nor decides (the answer is the one without it); |
| +// (d) and (b) suppression removes exactly the spec's candidates, never a |
| +// stamped one; (l) a surviving local ready candidate wins; (a) the remote |
| +// survivors are decided by foldHomes; (h) a real survivor beats an alias |
| +// one; (c) and (e) any ready survivor beats every hold; (n) the holding |
| +// winner is the sorted fold's and the decision is the same in reverse and |
| +// rotated input order; (g) a candidate whose target no other live |
| +// candidate shares (or is empty) decides the same at every pair; (j) the |
| +// classes are OR'd over the live candidates and everything else is the |
| +// winner's, and an alias candidate never contributes platform. |
| +func TestRankingProperties(t *testing.T) { |
| + alphabet := modelAlphabet() |
| + if len(alphabet) != 72 { |
| + t.Fatalf("alphabet = %d candidates, want 72", len(alphabet)) |
| + } |
| + sets, failures := 0, 0 |
| + fail := func(set []mc, format string, args ...any) { |
| + t.Helper() |
| + failures++ |
| + t.Errorf("%s\n set: %s", fmt.Sprintf(format, args...), describe(set)) |
| + if failures >= 10 { |
| + t.FailNow() |
| + } |
| + } |
| + eachSubset(alphabet, 4, func(set []mc) { |
| + if !admissible(set) { |
| + return |
| + } |
| + sets++ |
| + cs := buildSet(set) |
| + d := rank(modelName, cs, rankNow) |
| + |
| + // (f): the answer is the answer without the expired holds. |
| + var liveCs []candidate |
| + for i, m := range set { |
| + if m.live() { |
| + liveCs = append(liveCs, cs[i]) |
| + } |
| + } |
| + if dl := rank(modelName, liveCs, rankNow); !sameDecision(d, dl) { |
| + fail(set, "(f) with the expired holds %s, without %s", describeDecision(d), describeDecision(dl)) |
| + } |
| + |
| + // (j): classes OR'd over the live candidates. |
| + platform, wildcard := false, false |
| + for _, c := range liveCs { |
| + if c.alias && c.platform { |
| + fail(set, "(j) model fixture: an alias carries platform") |
| + } |
| + platform = platform || c.platform |
| + wildcard = wildcard || c.wildcard |
| + } |
| + if d.platform != platform || d.wildcard != wildcard { |
| + fail(set, "(j) classes platform %v wildcard %v, want %v %v", d.platform, d.wildcard, platform, wildcard) |
| + } |
| + |
| + // The spec's survivors. |
| + localWin := -1 |
| + var remote []int |
| + anyReady, anyStamped := false, false |
| + for i, m := range set { |
| + if m.kind != candReady { |
| + continue |
| + } |
| + anyReady = true |
| + if m.stamp != 0 { |
| + anyStamped = true |
| + if specSuppressed(set, i) { |
| + fail(set, "(b) spec: a stamped candidate suppressed") |
| + } |
| + } |
| + if specSuppressed(set, i) { |
| + continue |
| + } |
| + if m.local { |
| + localWin = i |
| + } else { |
| + remote = append(remote, i) |
| + } |
| + } |
| + holdWin := specHoldWinner(set) |
| + |
| + switch { |
| + case localWin >= 0: |
| + // (l) |
| + if d.kind != decLocal || d.up != cs[localWin].up || tablePtr(d.table) != tablePtr(cs[localWin].table) { |
| + fail(set, "(l) decision %s, want local %s", describeDecision(d), cs[localWin].app) |
| + } |
| + case len(remote) > 0: |
| + // (a), (c), (e), (h) and (j)'s owner. |
| + w := foldOracle(set, cs, remote) |
| + if w < 0 { |
| + fail(set, "oracle: foldHomes found no dial target") |
| + return |
| + } |
| + c := cs[w] |
| + if d.kind != decRemote || d.app != c.app || d.box != c.box || d.eip != c.eip || d.account != c.account || d.stamp != c.stamp { |
| + fail(set, "(a) decision %s, want foldHomes's %s at %s", describeDecision(d), c.app, c.box) |
| + } |
| + if d.up != nil || d.hold != nil || d.table != nil { |
| + fail(set, "(j) a remote decision carries a local upstream or a hold") |
| + } |
| + for _, i := range remote { |
| + if !set[i].alias && set[w].alias { |
| + fail(set, "(h) an alias won over a real survivor") |
| + } |
| + } |
| + case holdWin >= 0: |
| + // (n) |
| + if d.kind != decHolding || d.hold != cs[holdWin].hold { |
| + fail(set, "(n) decision %s, want the sorted fold's %s", describeDecision(d), cs[holdWin].app) |
| + } |
| + if anyStamped { |
| + fail(set, "(b) a stamped ready lost to a hold") |
| + } |
| + default: |
| + if d.kind != decNone { |
| + fail(set, "decision %s, want none: no live ready or hold", describeDecision(d)) |
| + } |
| + if anyReady { |
| + fail(set, "spec: a ready candidate and nothing decides") |
| + } |
| + } |
| + if (localWin >= 0 || len(remote) > 0) && d.kind == decHolding { |
| + fail(set, "(c)/(e) a hold beat a surviving ready candidate") |
| + } |
| + |
| + // (d): removing a suppressed candidate changes no winner. |
| + for i := range set { |
| + if !specSuppressed(set, i) { |
| + continue |
| + } |
| + without := append(slices.Clone(cs[:i]), cs[i+1:]...) |
| + if dw := rank(modelName, without, rankNow); !sameWinner(d, dw) { |
| + fail(set, "(d) [%d] is suppressed, yet removing it moves the answer from %s to %s", i, describeDecision(d), describeDecision(dw)) |
| + } |
| + } |
| + |
| + // (g): a candidate of an unshared or empty target decides the same |
| + // at every pair. |
| + for i, m := range set { |
| + if !m.live() { |
| + continue |
| + } |
| + shared := false |
| + for j, o := range set { |
| + if j != i && o.live() && m.target != "" && o.target == m.target { |
| + shared = true |
| + } |
| + } |
| + if shared { |
| + continue |
| + } |
| + for p := range modelPairs { |
| + if p == m.pair { |
| + continue |
| + } |
| + alt := slices.Clone(cs) |
| + alt[i].incarnation, alt[i].servingSeq = modelPairs[p][0], modelPairs[p][1] |
| + if da := rank(modelName, alt, rankNow); !sameDecision(d, da) { |
| + fail(set, "(g) [%d] shares its target with nothing, yet at pair %v the answer is %s, not %s", i, modelPairs[p], describeDecision(da), describeDecision(d)) |
| + } |
| + } |
| + } |
| + |
| + // (n): input order is not an input. |
| + rev := slices.Clone(cs) |
| + slices.Reverse(rev) |
| + if dr := rank(modelName, rev, rankNow); !sameDecision(d, dr) { |
| + fail(set, "(n) reversed input: %s, not %s", describeDecision(dr), describeDecision(d)) |
| + } |
| + if len(cs) > 2 { |
| + rot := append(slices.Clone(cs[1:]), cs[0]) |
| + if dr := rank(modelName, rot, rankNow); !sameDecision(d, dr) { |
| + fail(set, "(n) rotated input: %s, not %s", describeDecision(dr), describeDecision(d)) |
| + } |
| + } |
| + }) |
| + t.Logf("%d candidate sets", sets) |
| +} |
| + |
| +// TestLocalReadyIsUnstamped: this box's own ready candidate is never |
| +// stamped (local first makes stamps moot on the box itself, and a |
| +// claims-on box publishes no holds), so a hold of its target with a higher |
| +// pair suppresses it like any stale publication, and the hold answers. At |
| +// an equal or higher pair it survives and wins over everything: a stamped |
| +// remote ready, a remote real route and every hold. |
| +func TestLocalReadyIsUnstamped(t *testing.T) { |
| + local := func(inc, seq uint64) candidate { |
| + up := &upstream{route: registry.Route{Host: rankHost, App: "local"}} |
| + return candidate{kind: candReady, local: true, order: -1, targetHost: rankHost, incarnation: inc, servingSeq: seq, |
| + up: up, table: map[string]upstream{rankHost: *up}} |
| + } |
| + hold := rhold(1, "betty", rankHost, false, 1, 1) |
| + for _, p := range permutations([]candidate{local(1, 0), hold}) { |
| + wantHolding(t, rank(rankHost, p, rankNow), "mv_betty", "a local ready of the moving target with a lower pair is suppressed") |
| + } |
| + for _, l := range []candidate{local(1, 1), local(1, 2), local(2, 0)} { |
| + others := []candidate{l, hold, rready(0, "annie", rankHost, false, 0, 0, 9), rready(2, "carol", rankHost, false, 3, 3, 0), |
| + lhold(rankHost, false, 1, 1, "mv_local")} |
| + for _, p := range permutations(others) { |
| + if d := rank(rankHost, p, rankNow); d.kind != decLocal || d.up != l.up { |
| + t.Fatalf("local (%d,%d) against a stamped remote, a newer remote and two holds = %v, want decLocal", l.incarnation, l.servingSeq, d.kind) |
| + } |
| + } |
| + } |
| +} |
| + |
| +// holdAlphabet is the holding-only model: holds alone, the real one of |
| +// modelName and alias ones of two primaries, local or in one of two files, |
| +// at the three pairs, fresh or (remote only) expired. 45 of them. |
| +func holdAlphabet() []mc { |
| + var out []mc |
| + targets := []struct { |
| + alias bool |
| + target string |
| + }{{false, modelName}, {true, modelPrimary}, {true, "cart.example.org"}} |
| + for p := range modelPairs { |
| + for _, tg := range targets { |
| + out = append(out, mc{kind: candHolding, local: true, order: -1, alias: tg.alias, target: tg.target, pair: p}) |
| + for order := 0; order < 2; order++ { |
| + for _, expired := range []bool{false, true} { |
| + out = append(out, mc{kind: candHolding, order: order, alias: tg.alias, target: tg.target, pair: p, expired: expired}) |
| + } |
| + } |
| + } |
| + } |
| + return out |
| +} |
| + |
| +// eachPermutation calls fn with every order of cs (Heap's algorithm, over |
| +// one copy permuted in place) until fn returns false. |
| +func eachPermutation(cs []candidate, fn func([]candidate) bool) { |
| + a := slices.Clone(cs) |
| + c := make([]int, len(a)) |
| + if !fn(a) { |
| + return |
| + } |
| + for i := 0; i < len(a); { |
| + if c[i] < i { |
| + if i%2 == 0 { |
| + a[0], a[i] = a[i], a[0] |
| + } else { |
| + a[c[i]], a[i] = a[i], a[c[i]] |
| + } |
| + if !fn(a) { |
| + return |
| + } |
| + c[i]++ |
| + i = 0 |
| + } else { |
| + c[i] = 0 |
| + i++ |
| + } |
| + } |
| +} |
| + |
| +// TestHoldingSortedFold: every holding-only set of the model (at most one |
| +// local real hold and one local alias hold) answers the hold the sorted |
| +// fold names (specHoldWinner: a real hold before an alias, the first by |
| +// order then target, the highest pair of that target), in every one of its |
| +// input orders. Two alias holds of different targets in one file are |
| +// ordered by target (rank's sort), so the page never depends on the order |
| +// the candidates were collected in. |
| +func TestHoldingSortedFold(t *testing.T) { |
| + alphabet := holdAlphabet() |
| + if len(alphabet) != 45 { |
| + t.Fatalf("alphabet = %d holds, want 45", len(alphabet)) |
| + } |
| + sets, failures := 0, 0 |
| + eachSubset(alphabet, 4, func(set []mc) { |
| + realLocal, aliasLocal := 0, 0 |
| + for _, m := range set { |
| + if m.local && m.alias { |
| + aliasLocal++ |
| + } else if m.local { |
| + realLocal++ |
| + } |
| + } |
| + if realLocal > 1 || aliasLocal > 1 { |
| + return |
| + } |
| + sets++ |
| + cs := buildSet(set) |
| + win := specHoldWinner(set) |
| + eachPermutation(cs, func(p []candidate) bool { |
| + d := rank(modelName, p, rankNow) |
| + if (win < 0 && d.kind == decNone) || (win >= 0 && d.kind == decHolding && d.hold == cs[win].hold) { |
| + return true |
| + } |
| + want := "none" |
| + if win >= 0 { |
| + want = cs[win].app |
| + } |
| + failures++ |
| + t.Errorf("decision %s, want %s\n set: %s", describeDecision(d), want, describe(set)) |
| + if failures >= 10 { |
| + t.FailNow() |
| + } |
| + return false |
| + }) |
| + }) |
| + t.Logf("%d holding sets", sets) |
| +} |
| + |
| +// The no-hold differential (invariant (a), Review Focus 2): with no live |
| +// hold, the plan 3 request resolution (Edge.resolve over the table and |
| +// buildCandidates' output, through rank) answers what today's ServeHTTP |
| +// dispatch answered, which was local first, then foldHomes: |
| +// |
| +// - an exact table entry for the host is served locally; |
| +// - a table entry only through the host's "*.<parent>" key loses to |
| +// another box's exact dial target (ExactHome), which an ordinary |
| +// request backhauls to and a hop or a door request does not (they keep |
| +// the local catch-all: the decision's fallback); with none, the local |
| +// catch-all serves; |
| +// - with nothing local, the folded exact entry, else the folded catch-all |
| +// entry, is dialled (Home); |
| +// - an unlabelled box offers no remote answer at all. |
| +// |
| +// diffHost is the request host and diffKey its catch-all key. |
| +const ( |
| + diffHost = "x.example.com" |
| + diffKey = "*.example.com" |
| +) |
| + |
| +// diffWant is today's answer: local names the table key served; remote is |
| +// the folded entry dialled, with fallback when the local catch-all travels |
| +// with it. |
| +type diffWant struct { |
| + local string |
| + remote *homeEntry |
| + fallback bool |
| +} |
| + |
| +func todaysDispatch(table map[string]upstream, files map[string]edgesFileView, self string) diffWant { |
| + hosts, _ := foldHomes(files, self, false) |
| + dial := func(name string) (homeEntry, bool) { |
| + h, ok := hosts[name] |
| + return h, ok && h.dial && self != "" |
| + } |
| + if _, ok := table[diffHost]; ok { |
| + return diffWant{local: diffHost} |
| + } |
| + if _, ok := table[diffKey]; ok { |
| + if h, ok := dial(diffHost); ok { |
| + return diffWant{remote: &h, fallback: true} |
| + } |
| + return diffWant{local: diffKey} |
| + } |
| + if h, ok := dial(diffHost); ok { |
| + return diffWant{remote: &h} |
| + } |
| + if h, ok := dial(diffKey); ok { |
| + return diffWant{remote: &h} |
| + } |
| + return diffWant{} |
| +} |
| + |
| +// diffHostEntry is one file's publication of diffHost: none, a real route |
| +// or an alias (of modelPrimary, or naming no target), at each pair, |
| +// stamped or not; the real one in the first file carries platform. |
| +func diffHostEntries(first bool) []*edgesHostView { |
| + out := []*edgesHostView{nil} |
| + for p, pair := range modelPairs { |
| + for stamp := int64(0); stamp < 2; stamp++ { |
| + out = append(out, |
| + &edgesHostView{Role: edgeRoleHome, Platform: first, Incarnation: pair[0], ServingSeq: pair[1], Stamp: stamp, App: fmt.Sprintf("real%d%d", p, stamp)}, |
| + &edgesHostView{Role: edgeRoleHome, Alias: true, TargetHost: modelPrimary, Incarnation: pair[0], ServingSeq: pair[1], Stamp: stamp, App: fmt.Sprintf("alias%d%d", p, stamp)}, |
| + &edgesHostView{Role: edgeRoleHome, Alias: true, Incarnation: pair[0], ServingSeq: pair[1], Stamp: stamp, App: fmt.Sprintf("legacy%d%d", p, stamp)}) |
| + } |
| + } |
| + return out |
| +} |
| + |
| +// diffKeyEntries is one file's publication of diffKey: none, a dialled |
| +// catch-all at two pairs, or the certificate-only role. |
| +var diffKeyEntries = []*edgesHostView{ |
| + nil, |
| + {Role: edgeRoleHome, Wildcard: true, Incarnation: 1, App: "wild10"}, |
| + {Role: edgeRoleHome, Wildcard: true, Incarnation: 1, ServingSeq: 1, App: "wild11"}, |
| + {Role: edgeRoleWildcard, Wildcard: true}, |
| +} |
| + |
| +// diffFile is one other box's file: its diffHost and diffKey entries and, |
| +// when retained, an expired hold of diffHost at a pair above every ready |
| +// one (it would suppress all of them if it were live) in a file fetched |
| +// five minutes ago. |
| +func diffFile(box string, host, key *edgesHostView, retained bool) edgesFileView { |
| + hosts := map[string]edgesHostView{} |
| + if host != nil { |
| + h := *host |
| + h.Account = "acct" |
| + h.App = box + "-" + h.App |
| + hosts[diffHost] = h |
| + } |
| + if key != nil { |
| + k := *key |
| + if k.App != "" { |
| + k.Account, k.App = "acct", box+"-"+k.App |
| + } |
| + hosts[diffKey] = k |
| + } |
| + f := file(box, box+"-ip", hosts) |
| + f.fetchedAt = rankNow |
| + if retained { |
| + f.fetchedAt = rankNow.Add(-holdingValidity) |
| + f.OfflineMoves = []holdingView{{Host: diffHost, Account: "acct", App: box + "-held", MoveID: "mv_" + box, Incarnation: 9, ServingSeq: 9}} |
| + } |
| + return f |
| +} |
| + |
| +// diffTables is every table: no entry, an exact real route, an exact alias, |
| +// each with and without the local catch-all. |
| +func diffTables() []map[string]upstream { |
| + exact := []*upstream{nil, |
| + {route: registry.Route{Host: diffHost, App: "local-exact"}}, |
| + {alias: true, route: registry.Route{Host: modelPrimary, App: "local-alias"}}} |
| + var out []map[string]upstream |
| + for _, x := range exact { |
| + for _, wild := range []bool{false, true} { |
| + table := map[string]upstream{} |
| + if x != nil { |
| + table[diffHost] = *x |
| + } |
| + if wild { |
| + table[diffKey] = upstream{alias: true, route: registry.Route{Host: "parent.example.com", App: "local-wild"}} |
| + } |
| + out = append(out, table) |
| + } |
| + } |
| + return out |
| +} |
| + |
| +// resolveOn is Edge.resolve on an edge labelled self with table and the |
| +// home map's candidates built from files. |
| +func resolveOn(table map[string]upstream, files map[string]edgesFileView, self string) decision { |
| + e := &Edge{} |
| + e.upstreams.Store(&table) |
| + hosts, _ := foldHomes(files, self, false) |
| + m := &homeMap{self: self, snap: &homeSnapshot{files: files, hosts: hosts, cands: buildCandidates(files, self, false)}} |
| + e.homes.Store(m) |
| + return e.resolve(diffHost, rankNow) |
| +} |
| + |
| +// diffAgrees reports whether the decision is today's answer: the same |
| +// table entry served from the same table, or the same dial target, owner, |
| +// stamp and classes, with the local catch-all as its fallback exactly when |
| +// today kept it. |
| +func diffAgrees(got decision, want diffWant, table map[string]upstream) bool { |
| + switch { |
| + case want.local != "": |
| + return got.kind == decLocal && got.up != nil && got.up.route.App == table[want.local].route.App && |
| + tablePtr(got.table) == tablePtr(table) && got.fallback == nil |
| + case want.remote != nil: |
| + h := want.remote |
| + if got.kind != decRemote || got.eip != h.eip || got.box != h.box || got.account != h.account || got.app != h.app || got.stamp != h.stamp || |
| + got.platform != h.platform || got.wildcard != h.wildcard { |
| + return false |
| + } |
| + if want.fallback { |
| + return got.fallback != nil && got.fallback.up.route.App == "local-wild" && tablePtr(got.fallback.table) == tablePtr(table) |
| + } |
| + return got.fallback == nil |
| + } |
| + return got.kind == decNone |
| +} |
| + |
| +func describeWant(w diffWant) string { |
| + switch { |
| + case w.local != "": |
| + return "local " + w.local |
| + case w.remote != nil: |
| + s := fmt.Sprintf("remote %s at %s (platform %v wildcard %v)", w.remote.app, w.remote.box, w.remote.platform, w.remote.wildcard) |
| + if w.fallback { |
| + s += " with the local catch-all as fallback" |
| + } |
| + return s |
| + } |
| + return "none" |
| +} |
| + |
| +func describeGot(d decision) string { |
| + s := describeDecision(d) |
| + if d.kind == decRemote { |
| + s += fmt.Sprintf(" (platform %v wildcard %v)", d.platform, d.wildcard) |
| + } |
| + if d.fallback != nil { |
| + s += " with fallback " + d.fallback.up.route.App |
| + } |
| + return s |
| +} |
| + |
| +// TestRankEqualsFoldWithoutHolds: every set of the no-hold model (two other |
| +// boxes' files, each publishing the host as nothing, a real route or an |
| +// alias of a target or of none, at three pairs, stamped or not; the |
| +// catch-all as nothing, two dialled entries or the certificate-only role; |
| +// fresh, or retained with an expired hold that would suppress everything; |
| +// every table; labelled, labelled as one of the publishers, and unlabelled) |
| +// resolves as today's dispatch did. Two named sets first: the empty-target |
| +// exception (two legacy aliases never compare pairs, so the first file's |
| +// lower one keeps the dial) and a stamped publication against a retained |
| +// file's higher unstamped pair (the stamp wins). |
| +func TestRankEqualsFoldWithoutHolds(t *testing.T) { |
| + check := func(t *testing.T, table map[string]upstream, files map[string]edgesFileView, self string) bool { |
| + t.Helper() |
| + want := todaysDispatch(table, files, self) |
| + got := resolveOn(table, files, self) |
| + if !diffAgrees(got, want, table) { |
| + keys := make([]string, 0, len(files)) |
| + for k := range files { |
| + keys = append(keys, k) |
| + } |
| + slices.Sort(keys) |
| + var b strings.Builder |
| + for _, k := range keys { |
| + f := files[k] |
| + fmt.Fprintf(&b, "\n %s (fetched %s): hosts %+v holds %d", k, rankNow.Sub(f.fetchedAt), f.Hosts, len(f.OfflineMoves)) |
| + } |
| + tk := slices.Sorted(func(yield func(string) bool) { |
| + for k := range table { |
| + if !yield(k) { |
| + return |
| + } |
| + } |
| + }) |
| + t.Errorf("self %q table %v: resolve = %s, today = %s%s", self, tk, describeGot(got), describeWant(want), b.String()) |
| + return false |
| + } |
| + return true |
| + } |
| + t.Run("the empty-target exception", func(t *testing.T) { |
| + files := map[string]edgesFileView{ |
| + "control/edges/a.json": diffFile("annie", &edgesHostView{Role: edgeRoleHome, Alias: true, Incarnation: 1, App: "low"}, nil, false), |
| + "control/edges/b.json": diffFile("betty", &edgesHostView{Role: edgeRoleHome, Alias: true, Incarnation: 2, App: "high"}, nil, false), |
| + } |
| + check(t, map[string]upstream{}, files, "zed") |
| + if d := resolveOn(map[string]upstream{}, files, "zed"); d.box != "annie" { |
| + t.Fatalf("resolve = %s, want annie: aliases naming no target compare no pairs", describeGot(d)) |
| + } |
| + }) |
| + t.Run("stamped against retained", func(t *testing.T) { |
| + files := map[string]edgesFileView{ |
| + "control/edges/a.json": diffFile("annie", &edgesHostView{Role: edgeRoleHome, Incarnation: 1, ServingSeq: 2, App: "retained"}, nil, true), |
| + "control/edges/b.json": diffFile("betty", &edgesHostView{Role: edgeRoleHome, Stamp: 7, App: "stamped"}, nil, false), |
| + } |
| + check(t, map[string]upstream{}, files, "zed") |
| + if d := resolveOn(map[string]upstream{}, files, "zed"); d.box != "betty" { |
| + t.Fatalf("resolve = %s, want betty: a stamp outranks any pair, and an expired hold suppresses nothing", describeGot(d)) |
| + } |
| + }) |
| + t.Run("the model", func(t *testing.T) { |
| + tables := diffTables() |
| + aHosts, bHosts := diffHostEntries(true), diffHostEntries(false) |
| + cases, failures := 0, 0 |
| + for _, ah := range aHosts { |
| + for _, ak := range diffKeyEntries { |
| + for _, ar := range []bool{false, true} { |
| + for _, bh := range bHosts { |
| + for _, bk := range diffKeyEntries { |
| + for _, br := range []bool{false, true} { |
| + files := map[string]edgesFileView{ |
| + "control/edges/a.json": diffFile("annie", ah, ak, ar), |
| + "control/edges/b.json": diffFile("betty", bh, bk, br), |
| + } |
| + for _, table := range tables { |
| + for _, self := range []string{"zed", "annie", ""} { |
| + cases++ |
| + if !check(t, table, files, self) { |
| + if failures++; failures >= 10 { |
| + t.FailNow() |
| + } |
| + } |
| + } |
| + } |
| + } |
| + } |
| + } |
| + } |
| + } |
| + } |
| + t.Logf("%d resolutions", cases) |
| + }) |
| +} |