edge: wake witnesses for a losing platform hold beside a ready platform route and a winning hold leaving its file (Task 5 review round 1)
A losing platform hold arriving, expiring, reviving and leaving beside a ready platform route of the same name wakes nothing; a live winning platform hold leaving its file, the ready fold unmoved, wakes the manager and the parent leaves. The matrix's field comment names annie and dolly.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 file changed,
+97
−1
internal/edge/homemap_test.go+97 −1
CI failed — run details
CI log
=== gate === sigwho: pid 2 ppid 1 pgid 0 sid 0; child pid 3 .amadan/test-gate.sh gate: 2026-10-24T04:50:07Z on aarch64, 32 cpus, 62094 MB available, go1.26.8 linux/arm64 gate: +0s start launchctx gate: +0s start vet gate: +0s start gofmt gate: +1s start rest gate: +12s start internal_console@0 gate: +12s start internal_console@1 gate: +12s start internal_console@2 gate: +12s start internal_console@3 gate: +12s start internal_console@4 gate: +12s start internal_console@5 gate: +12s start internal_console@6 gate: +12s start internal_console@7 gate: +12s waiting for internal_console's shards before the next split package gate: +172s start root@0 gate: +172s start root@1 gate: +172s start root@2 gate: +172s waiting for root's shards before the next split package amadan: step gate failed: signal: terminated amadan: job failed at step gate — its output is above, not at the tail of this log
diff --git a/internal/edge/homemap_test.go b/internal/edge/homemap_test.go| index 75d8c2eea..ac532fe72 100644 |
| --- a/internal/edge/homemap_test.go |
| +++ b/internal/edge/homemap_test.go |
| @@ -2120,7 +2120,7 @@ func TestHoldingEntriesExpireFromTheFetchClock(t *testing.T) { |
| // faults runs before the poll at one minute; nil is no poll at all. |
| faults func(fx *freshScene) |
| wantErr bool |
| - // annie and carol are the fetch times the poll leaves on the hold's |
| + // annie and dolly are the fetch times the poll leaves on the hold's |
| // file and the bystander's, after rankNow. |
| annie, dolly time.Duration |
| } |
| @@ -2429,3 +2429,99 @@ func TestExpiredPlatformHoldLeavesTheParentSetAndRevives(t *testing.T) { |
| waitWoke(t, &woke, 3) |
| waitParents(t, w, []string{"bdj." + apps}) |
| } |
| + |
| +// wakeScene is a freshScene on an atomic clock (the wake reads it from its |
| +// own goroutine) with a real wildcard manager woken by its home map. set |
| +// moves the clock to d after rankNow; quiet asserts no further wake. |
| +type wakeScene struct { |
| + *freshScene |
| + w *wildcardManager |
| + woke atomic.Int32 |
| + clock atomic.Int64 |
| +} |
| + |
| +func newWakeScene(t *testing.T, apps string) *wakeScene { |
| + t.Helper() |
| + ws := &wakeScene{freshScene: newFreshScene(t, "carol")} |
| + ws.set(0) |
| + ws.m.now = func() time.Time { return time.Unix(0, ws.clock.Load()).UTC() } |
| + ws.w = newWildcardManager(nil, apps, ws.e.deploymentHosts, nil, "", nil, nil) |
| + ws.m.setHostsWake(func() { ws.w.refresh(); ws.woke.Add(1) }) |
| + return ws |
| +} |
| + |
| +func (ws *wakeScene) set(d time.Duration) { ws.clock.Store(rankNow.Add(d).UnixNano()) } |
| + |
| +func (ws *wakeScene) quiet(want int32, what string) { |
| + ws.t.Helper() |
| + time.Sleep(20 * time.Millisecond) |
| + if n := ws.woke.Load(); n != want { |
| + ws.t.Fatalf("%s: woke %d time(s), want %d", what, n, want) |
| + } |
| +} |
| + |
| +// A platform hold that loses to a ready platform route of the same name is |
| +// no parent input of its own (the route already is one): its arriving, its |
| +// expiry and revival, and its leaving move nothing the manager derives, so |
| +// none of them wakes it. |
| +func TestLosingPlatformHoldBesideAReadyPlatformRouteWakesNothing(t *testing.T) { |
| + const apps = "apps.example.com" |
| + held := "cart.bdj." + apps |
| + ws := newWakeScene(t, apps) |
| + ready := fmt.Sprintf(`%q:{"role":"home","platform":true,"account":"bab","app":"shop","incarnation":1,"serving_seq":2}`, held) |
| + ws.put("betty", "2.2.2.2", ready, "") |
| + ws.mustPoll() |
| + waitWoke(t, &ws.woke, 1) |
| + waitParents(t, ws.w, []string{"bdj." + apps}) |
| + |
| + // The hold (1,1) arrives beside the route (1,2): the route wins. |
| + ws.put("annie", "1.1.1.1", "", holdJSON(held, "mv_a", true, 1, 1)) |
| + ws.mustPoll() |
| + if d := ws.m.resolveExact(held, ws.m.clock()); d.kind != decRemote || d.box != "betty" { |
| + t.Fatalf("test bug: %s = %v %q, want betty's route over the losing hold", held, d.kind, d.box) |
| + } |
| + if !ws.m.snapshot().platformHolds[held] { |
| + t.Fatal("test bug: the losing hold must be a platform hold candidate") |
| + } |
| + ws.quiet(1, "a losing platform hold arriving") |
| + |
| + // Its file goes unread past five minutes, then is read again. |
| + ws.set(holdingValidity) |
| + ws.faults.set(false, false, edgesKey("annie")) |
| + ws.mustPoll() |
| + ws.quiet(1, "a losing platform hold expiring") |
| + ws.set(6 * time.Minute) |
| + ws.faults.set(false, false) |
| + ws.mustPoll() |
| + ws.quiet(1, "a losing platform hold revived") |
| + |
| + // It leaves its file. |
| + ws.put("annie", "1.1.1.1", "", "") |
| + ws.mustPoll() |
| + ws.quiet(1, "a losing platform hold leaving") |
| + waitParents(t, ws.w, []string{"bdj." + apps}) |
| +} |
| + |
| +// A live platform hold that wins its name (nothing ready behind it) is the |
| +// name's parent input: when it leaves its file, the files changed but the |
| +// ready fold did not, and the manager is woken and drops the parent. |
| +func TestWinningPlatformHoldLeavingItsFileWakesTheManager(t *testing.T) { |
| + const apps = "apps.example.com" |
| + held := "cart.bdj." + apps |
| + ws := newWakeScene(t, apps) |
| + bystander := readyJSON("shop.example.net", "bab", "shop", 0, 0) |
| + ws.put("annie", "1.1.1.1", bystander, holdJSON(held, "mv_a", true, 1, 1)) |
| + ws.mustPoll() |
| + waitWoke(t, &ws.woke, 1) |
| + waitParents(t, ws.w, []string{"bdj." + apps}) |
| + readyFold := ws.m.snapshot().hosts |
| + |
| + ws.set(time.Minute) |
| + ws.put("annie", "1.1.1.1", bystander, "") |
| + ws.mustPoll() |
| + if !samePlatformSet(readyFold, ws.m.snapshot().hosts) { |
| + t.Fatal("test bug: the ready fold's platform set must not move") |
| + } |
| + waitWoke(t, &ws.woke, 2) |
| + waitParents(t, ws.w, nil) |
| +} |