package host import ( "context" "errors" "fmt" "slices" "sync" "testing" "time" "github.com/carlosframework/platform/internal/registry" "github.com/carlosframework/platform/internal/release" ) // wakeSidecarRoute is sidecarRoute (restart_test.go) with Slug set — the // field a sidecar route carries its short name on (registry.Route.Slug's // doc comment; provision's reconciler stamps it from the sidecar record). // sidecarWakeChanges keys entirely off it, so a fixture that omits it is // useless here even though restart_test.go's sidecarRoute never needed it. func wakeSidecarRoute(host, acct, app, slug string) registry.Route { rt := sidecarRoute(host, acct, app) rt.Slug = slug return rt } // touchSidecarWake bumps name's wake generation through the real writer // (release.TouchSidecarWake) — the same door the console's wake handler // (Task 12) will call, so these tests exercise the actual signal a box // sees rather than a hand-rolled stand-in for it. func touchSidecarWake(t *testing.T, ctx context.Context, s release.Store, acct, app, name string) { t.Helper() if err := release.TouchSidecarWake(ctx, s, acct, app, name, "member:bdm", "test wake"); err != nil { t.Fatalf("TouchSidecarWake %s/%s/%s: %v", acct, app, name, err) } } // ---- sidecarWakeChanges: the synchronous etag comparison ------------------ func TestSidecarWakeChangesEtagMoveReportsExactlyThatSidecar(t *testing.T) { ctx, s := genKit(t) routes := []registry.Route{ wakeSidecarRoute("bot.eleven.sidecar.internal", "bdm", "eleven", "bot"), wakeSidecarRoute("other.eleven.sidecar.internal", "bdm", "eleven", "other"), } mem := newSidecarWakeMemory(false) sidecarWakeChanges(ctx, routes, s, mem) // seed: first sight of both, silently touchSidecarWake(t, ctx, s, "bdm", "eleven", "bot") got, errs := sidecarWakeChanges(ctx, routes, s, mem) if len(errs) != 0 { t.Fatalf("unexpected read errors: %v", errs) } if len(got) != 1 || got[0].Host != "bot.eleven.sidecar.internal" { t.Fatalf("sidecarWakeChanges = %v, want exactly the moved sidecar's route", got) } } func TestSidecarWakeChangesUnchangedEtagReportsNothing(t *testing.T) { ctx, s := genKit(t) routes := []registry.Route{wakeSidecarRoute("bot.eleven.sidecar.internal", "bdm", "eleven", "bot")} touchSidecarWake(t, ctx, s, "bdm", "eleven", "bot") mem := newSidecarWakeMemory(false) sidecarWakeChanges(ctx, routes, s, mem) // seed got, errs := sidecarWakeChanges(ctx, routes, s, mem) if len(errs) != 0 { t.Fatalf("unexpected read errors: %v", errs) } if len(got) != 0 { t.Fatalf("unchanged etag reported %v, want none", got) } } // TestSidecarWakeChangesBaselinePassDoesNotReplay is the no-replay rule at // the point it applies: the BASELINE pass — arming, i.e. an agent restart — // must not treat "I have never looked at this before" as movement, or every // restart would replay every wake this sidecar has ever received, a // thundering herd on boot. (First sight AFTER the baseline is a different // fact entirely — a route minted under a running agent — and is woken; see // TestSidecarWakeChangesRouteMintedAfterTheBaselineIsWoken.) func TestSidecarWakeChangesBaselinePassDoesNotReplay(t *testing.T) { ctx, s := genKit(t) // Wake object already exists BEFORE this box ever looks — the shape a // long-lived wake request or a box that just (re)started sees. touchSidecarWake(t, ctx, s, "bdm", "eleven", "bot") routes := []registry.Route{wakeSidecarRoute("bot.eleven.sidecar.internal", "bdm", "eleven", "bot")} mem := newSidecarWakeMemory(false) got, errs := sidecarWakeChanges(ctx, routes, s, mem) if len(errs) != 0 { t.Fatalf("unexpected read errors: %v", errs) } if len(got) != 0 { t.Fatalf("first sight reported %v — a box that just started must not replay every past wake", got) } want := sidecarWakeStoreEtag(t, ctx, s, "bdm", "eleven", "bot") if got := mem.etags[[3]string{"bdm", "eleven", "bot"}]; got != want { t.Fatalf("cache etag = %q, want the store's %q", got, want) } // A SECOND wake request after the seed IS movement. touchSidecarWake(t, ctx, s, "bdm", "eleven", "bot") got, errs = sidecarWakeChanges(ctx, routes, s, mem) if len(errs) != 0 { t.Fatalf("unexpected read errors: %v", errs) } if len(got) != 1 || got[0].Host != "bot.eleven.sidecar.internal" { t.Fatalf("second request reported %v, want exactly the sidecar", got) } } // TestSidecarWakeChangesRouteMintedAfterTheBaselineIsWoken is the // create-then-wake flow, which is the feature's PRIMARY one: an operator // wake is a hibernating sidecar's only first-run mechanism, and // `carlos sidecars create` + `carlos sidecars wake` routinely land in the // same tick — the tick that mints the route. Seeding that route's etag // silently (the baseline rule, over-applied) loses the wake outright while // the CLI reports success: a bot that never runs. func TestSidecarWakeChangesRouteMintedAfterTheBaselineIsWoken(t *testing.T) { ctx, s := genKit(t) existing := wakeSidecarRoute("old.eleven.sidecar.internal", "bdm", "eleven", "old") mem := newSidecarWakeMemory(false) sidecarWakeChanges(ctx, []registry.Route{existing}, s, mem) // arming: this box's baseline // The console creates a sidecar and wakes it; this box's next tick mints // the route AND sees the wake object for the first time, together. minted := wakeSidecarRoute("bot.eleven.sidecar.internal", "bdm", "eleven", "bot") touchSidecarWake(t, ctx, s, "bdm", "eleven", "bot") got, errs := sidecarWakeChanges(ctx, []registry.Route{existing, minted}, s, mem) if len(errs) != 0 { t.Fatalf("unexpected read errors: %v", errs) } if len(got) != 1 || got[0].Host != "bot.eleven.sidecar.internal" { t.Fatalf("reported %v, want the just-minted sidecar — a wake standing at a route this pass minted is a wake somebody already asked for", got) } } // A route minted after the baseline with NO wake object is silent: only a // PRESENT object makes a first sight a wake, so ordinary provisioning never // wakes a hibernating sidecar nobody asked to run. func TestSidecarWakeChangesRouteMintedWithNoWakeObjectIsSilent(t *testing.T) { ctx, s := genKit(t) mem := newSidecarWakeMemory(false) sidecarWakeChanges(ctx, nil, s, mem) // arming on a box with no sidecars minted := wakeSidecarRoute("bot.eleven.sidecar.internal", "bdm", "eleven", "bot") got, errs := sidecarWakeChanges(ctx, []registry.Route{minted}, s, mem) if len(errs) != 0 { t.Fatalf("unexpected read errors: %v", errs) } if len(got) != 0 { t.Fatalf("reported %v — a newly minted sidecar nobody has ever asked to wake must stay asleep", got) } // …and it behaves as an ordinary member from then on: the first real // request moves the etag and wakes it. touchSidecarWake(t, ctx, s, "bdm", "eleven", "bot") got, _ = sidecarWakeChanges(ctx, []registry.Route{minted}, s, mem) if len(got) != 1 || got[0].Host != "bot.eleven.sidecar.internal" { t.Fatalf("reported %v, want the sidecar on its first real wake request", got) } } // An arming pass whose route LIST failed must not leave the box thinking // every sidecar it serves is brand new: the memory stays unseeded, so the // first leg pass establishes the baseline itself instead of replaying every // historical wake at once. func TestSidecarWakeSeedListFailureLeavesTheBaselineToTheFirstPass(t *testing.T) { ctx, s := genKit(t) touchSidecarWake(t, ctx, s, "bdm", "eleven", "bot") // history, from before this agent routes := []registry.Route{wakeSidecarRoute("bot.eleven.sidecar.internal", "bdm", "eleven", "bot")} mem := newSidecarWakeMemory(false) // arming never completed: no List, nothing seeded got, errs := sidecarWakeChanges(ctx, routes, s, mem) if len(errs) != 0 { t.Fatalf("unexpected read errors: %v", errs) } if len(got) != 0 { t.Fatalf("reported %v — with no baseline established, the first pass IS the baseline, not a replay of every past wake", got) } } func sidecarWakeStoreEtag(t *testing.T, ctx context.Context, s release.Store, acct, app, name string) string { t.Helper() _, etag, err := s.Get(ctx, release.SidecarWakeKey(acct, app, name)) if err != nil { t.Fatalf("read sidecar wake: %v", err) } return etag } // TestSidecarWakeChangesReadErrorDoesNotSuppressOthers: a transient read // failure against one sidecar's wake object must not blind the pass to a // real move on a different one in the same tick. func TestSidecarWakeChangesReadErrorDoesNotSuppressOthers(t *testing.T) { ctx, s0 := genKit(t) routes := []registry.Route{ wakeSidecarRoute("bot.eleven.sidecar.internal", "bdm", "eleven", "bot"), wakeSidecarRoute("worker.titogo.sidecar.internal", "bdm", "titogo", "worker"), } mem := newSidecarWakeMemory(false) sidecarWakeChanges(ctx, routes, s0, mem) // seed both, absent touchSidecarWake(t, ctx, s0, "bdm", "eleven", "bot") touchSidecarWake(t, ctx, s0, "bdm", "titogo", "worker") failing := &failingKeyStore{Store: s0, failKey: release.SidecarWakeKey("bdm", "eleven", "bot"), err: errors.New("bucket blip")} got, errs := sidecarWakeChanges(ctx, routes, failing, mem) if len(errs) != 1 { t.Fatalf("errs = %v, want exactly one (the failing key)", errs) } if len(got) != 1 || got[0].Host != "worker.titogo.sidecar.internal" { t.Fatalf("reported %v, want only the unaffected sidecar moved", got) } } // TestSidecarWakeChangesIgnoresUnservedSidecar: a wake object naming a // sidecar this box does not serve is never even read, let alone reported — // the pass only ever asks about routes this box actually has. func TestSidecarWakeChangesIgnoresUnservedSidecar(t *testing.T) { ctx, s := genKit(t) touchSidecarWake(t, ctx, s, "bdm", "eleven", "ghost") // no route for "ghost" at all routes := []registry.Route{wakeSidecarRoute("bot.eleven.sidecar.internal", "bdm", "eleven", "bot")} mem := newSidecarWakeMemory(false) sidecarWakeChanges(ctx, routes, s, mem) // seed touchSidecarWake(t, ctx, s, "bdm", "eleven", "ghost") // "ghost" moves again got, errs := sidecarWakeChanges(ctx, routes, s, mem) if len(errs) != 0 { t.Fatalf("unexpected read errors: %v", errs) } if len(got) != 0 { t.Fatalf("an unserved sidecar's wake object was reported: %v, want none", got) } if _, ok := mem.etags[[3]string{"bdm", "eleven", "ghost"}]; ok { t.Fatal("a sidecar this box does not serve must never appear in its cache") } } // TestSidecarWakeChangesIgnoresNonSidecarRouteWithMatchingSlug: Slug is not // a sidecar-only field (registry.Route.Slug's doc comment — it is the // app-account short name usable on a root domain, stamped on ordinary // routes too), so an ordinary instance route that happens to carry the // same Slug as a real sidecar name must never be mistaken for that // sidecar. func TestSidecarWakeChangesIgnoresNonSidecarRouteWithMatchingSlug(t *testing.T) { ctx, s := genKit(t) touchSidecarWake(t, ctx, s, "bdm", "eleven", "bot") notSidecar := execRoute("app.example.com", "bdm", "eleven") notSidecar.Slug = "bot" // same name a sidecar could carry, wrong Kind routes := []registry.Route{notSidecar} mem := newSidecarWakeMemory(false) got, errs := sidecarWakeChanges(ctx, routes, s, mem) if len(errs) != 0 { t.Fatalf("unexpected read errors: %v", errs) } if len(got) != 0 { t.Fatalf("a non-sidecar route with a matching Slug was reported: %v, want none", got) } if len(mem.etags) != 0 { t.Fatalf("cache = %v, want empty — a non-sidecar route must never seed a sidecar cache entry", mem.etags) } } // ---- dispatchSidecarWakes: the bounded-concurrency, non-blocking half ---- // TestDispatchSidecarWakesReturnsBeforeASlowWakeCompletes pins fix round 1's // finding directly: WakeWorker can block up to 30s per sidecar (the settle // window), and dispatchSidecarWakes' only caller (sidecarWakeLeg) runs // synchronously on the box's single per-tick convergence goroutine — so // this function must never wait on a wake, or one slow sidecar delays every // other app's turn on that tick. Asserted against a deliberately slow fake // wake, the same elapsed-time shape Task 8's boot pass and Task 11's // TestCycleWorkersReturnsBeforeASlowWakeCompletes both use. func TestDispatchSidecarWakesReturnsBeforeASlowWakeCompletes(t *testing.T) { moved := []registry.Route{wakeSidecarRoute("bot.eleven.sidecar.internal", "bdm", "eleven", "bot")} slow := make(chan struct{}) wake := func(rt registry.Route) error { <-slow; return nil } // Run under a deadline rather than measuring elapsed time on the test's // own goroutine (fix round 3): under a REGRESSION dispatchSidecarWakes // itself never returns, so a synchronous call plus an elapsed-time // check never fails — it hangs until Go's 10-minute default test // timeout. Racing a goroutine against time.After (the same shape // TestDispatchSidecarWakesBoundsConcurrency already uses) makes a // regression here fail cleanly and promptly, in this test alone. done := make(chan struct{}) go func() { dispatchSidecarWakes(moved, wake) close(done) }() select { case <-done: close(slow) // release the parked wake goroutine so the test can exit cleanly case <-time.After(time.Second): close(slow) t.Fatal("dispatchSidecarWakes did not return within 1s with a wake blocked indefinitely — wakes must be dispatched asynchronously, not awaited") } } // TestDispatchSidecarWakesWakesEveryMovedSidecarEventually: dispatching // asynchronously must not mean dropping any of them — every moved sidecar // still gets woken, just not before this function returns. func TestDispatchSidecarWakesWakesEveryMovedSidecarEventually(t *testing.T) { moved := []registry.Route{ wakeSidecarRoute("bot.a.sidecar.internal", "bdm", "eleven", "a"), wakeSidecarRoute("bot.b.sidecar.internal", "bdm", "eleven", "b"), } var mu sync.Mutex var woken []string wake := func(rt registry.Route) error { mu.Lock() woken = append(woken, rt.Host) mu.Unlock() return nil } dispatchSidecarWakes(moved, wake) waitForCond(t, "both sidecars to be woken", func() bool { mu.Lock() defer mu.Unlock() slices.Sort(woken) return slices.Equal(woken, []string{"bot.a.sidecar.internal", "bot.b.sidecar.internal"}) }) } // TestDispatchSidecarWakesBoundsConcurrency pins the carried requirement // that wakes must not fan out unbounded: N sidecars strictly more than // workerBootConcurrency drive more in-flight wakes than the bound and prove // it is actually reached (not stuck at 1, which a serial dispatch would // show) and never exceeded (which an unbounded one would show) — the same // shape TestWorkerBootPassWakesConcurrently pins for Task 8's boot pass. func TestDispatchSidecarWakesBoundsConcurrency(t *testing.T) { t.Parallel() n := workerBootConcurrency + 3 moved := make([]registry.Route, n) for i := range moved { moved[i] = wakeSidecarRoute("s"+string(rune('a'+i))+".x.sidecar.internal", "bdm", "eleven", "s"+string(rune('a'+i))) } var mu sync.Mutex inFlight, maxInFlight := 0, 0 release := make(chan struct{}) wake := func(registry.Route) error { mu.Lock() inFlight++ if inFlight > maxInFlight { maxInFlight = inFlight } mu.Unlock() <-release mu.Lock() inFlight-- mu.Unlock() return nil } // dispatchSidecarWakes' own dispatch loop blocks on sem once the bound // saturates (it is a bounded fan-out, not a fire-and-forget launcher) — // exactly runWorkerBootPass's own shape, and its test // (TestWorkerBootPassWakesConcurrently) runs the call in a goroutine for // the same reason: the test needs to observe the saturated state and // then release it, which it cannot do if the call under test is itself // the thing blocking the test's only goroutine. done := make(chan struct{}) go func() { dispatchSidecarWakes(moved, wake) close(done) }() waitForCond(t, "wakes to saturate the concurrency bound", func() bool { mu.Lock() defer mu.Unlock() return inFlight == workerBootConcurrency }) // A beat with the bound already saturated: an unbounded implementation // would keep climbing past it here rather than holding still. time.Sleep(50 * time.Millisecond) mu.Lock() got := maxInFlight mu.Unlock() if got != workerBootConcurrency { t.Fatalf("max concurrent wakes = %d, want exactly the bound %d", got, workerBootConcurrency) } close(release) select { case <-done: case <-time.After(5 * time.Second): t.Fatal("dispatch loop did not finish after its wakes were released") } } // TestDispatchSidecarWakesWithoutAnActivatorIsANoop: a box with no // activator armed gets nil for wake — a working no-op, not a panic. func TestDispatchSidecarWakesWithoutAnActivatorIsANoop(t *testing.T) { moved := []registry.Route{wakeSidecarRoute("bot.eleven.sidecar.internal", "bdm", "eleven", "bot")} dispatchSidecarWakes(moved, nil) // must not panic } // ---- sidecarWakeLeg: the wired combination fastConvergeLoop calls -------- // TestSidecarWakeLegActsOnBumpedGeneration is the "wired into the // convergence path" pin: a real TouchSidecarWake write, run through the // seed-then-leg shape fastConvergeLoop actually uses (sidecarWakeSeed at // arming, sidecarWakeLeg on every serial move), must reach WakeWorker for // exactly the sidecar the console asked for. func TestSidecarWakeLegActsOnBumpedGeneration(t *testing.T) { t.Parallel() ctx, s := genKit(t) routes := []registry.Route{ wakeSidecarRoute("bot.eleven.sidecar.internal", "bdm", "eleven", "bot"), wakeSidecarRoute("other.eleven.sidecar.internal", "bdm", "eleven", "other"), } reg := restartTestRegistry(t) for _, rt := range routes { if err := reg.Upsert(rt); err != nil { t.Fatalf("upsert %s: %v", rt.Host, err) } } mem := newSidecarWakeMemory(false) sidecarWakeSeed(ctx, reg, s, mem) touchSidecarWake(t, ctx, s, "bdm", "eleven", "bot") var mu sync.Mutex var woken []string wake := func(rt registry.Route) error { mu.Lock() woken = append(woken, rt.Host) mu.Unlock() return nil } moved, errs := sidecarWakeLeg(ctx, routes, s, mem, wake) if len(errs) != 0 { t.Fatalf("unexpected read errors: %v", errs) } if len(moved) != 1 || moved[0].Host != "bot.eleven.sidecar.internal" { t.Fatalf("sidecarWakeLeg reported %v, want exactly the bumped sidecar", moved) } waitForCond(t, "the bumped sidecar to be woken", func() bool { mu.Lock() defer mu.Unlock() return slices.Equal(woken, []string{"bot.eleven.sidecar.internal"}) }) } // TestSidecarWakeLegCreateThenWakeInOneTickStillWakesIt is the blocker as a // member would meet it, through the leg fastConvergeLoop actually calls: the // agent arms with no sidecars, then one serial move carries BOTH the record // (so this tick mints the route) and the wake object. The wake must land. func TestSidecarWakeLegCreateThenWakeInOneTickStillWakesIt(t *testing.T) { t.Parallel() ctx, s := genKit(t) reg := restartTestRegistry(t) mem := newSidecarWakeMemory(false) sidecarWakeSeed(ctx, reg, s, mem) // arming: no sidecar routes yet // create + wake, both before this box's next tick. minted := wakeSidecarRoute("bot.eleven.sidecar.internal", "bdm", "eleven", "bot") if err := reg.Upsert(minted); err != nil { t.Fatalf("upsert: %v", err) } touchSidecarWake(t, ctx, s, "bdm", "eleven", "bot") var mu sync.Mutex var woken []string wake := func(rt registry.Route) error { mu.Lock() woken = append(woken, rt.Host) mu.Unlock() return nil } moved, errs := sidecarWakeLeg(ctx, []registry.Route{minted}, s, mem, wake) if len(errs) != 0 { t.Fatalf("unexpected read errors: %v", errs) } if len(moved) != 1 || moved[0].Host != "bot.eleven.sidecar.internal" { t.Fatalf("leg reported %v, want the just-created sidecar — the CLI already told the member this wake was accepted", moved) } waitForCond(t, "the just-created sidecar to be woken", func() bool { mu.Lock() defer mu.Unlock() return slices.Equal(woken, []string{"bot.eleven.sidecar.internal"}) }) } // The other side of the same coin, end to end: an agent RESTART on a box // whose sidecars have been woken many times before wakes nothing at all. func TestSidecarWakeLegAfterAnAgentRestartReplaysNothing(t *testing.T) { t.Parallel() ctx, s := genKit(t) reg := restartTestRegistry(t) routes := []registry.Route{ wakeSidecarRoute("bot.eleven.sidecar.internal", "bdm", "eleven", "bot"), wakeSidecarRoute("other.eleven.sidecar.internal", "bdm", "eleven", "other"), } for _, rt := range routes { if err := reg.Upsert(rt); err != nil { t.Fatalf("upsert %s: %v", rt.Host, err) } } touchSidecarWake(t, ctx, s, "bdm", "eleven", "bot") // history touchSidecarWake(t, ctx, s, "bdm", "eleven", "other") // history mem := newSidecarWakeMemory(false) // a fresh process sidecarWakeSeed(ctx, reg, s, mem) if len(mem.etags) != 2 { t.Fatalf("the arming pass recorded %d etags, want 2 — a baseline that records nothing is no baseline at all", len(mem.etags)) } var mu sync.Mutex var woken []string wake := func(rt registry.Route) error { mu.Lock() woken = append(woken, rt.Host) mu.Unlock() return nil } moved, errs := sidecarWakeLeg(ctx, routes, s, mem, wake) if len(errs) != 0 { t.Fatalf("unexpected read errors: %v", errs) } if len(moved) != 0 { t.Fatalf("an agent restart replayed %v — every wake these sidecars ever received would fire at once", moved) } time.Sleep(50 * time.Millisecond) // the dispatch is asynchronous; give it a chance to be wrong mu.Lock() defer mu.Unlock() if len(woken) != 0 { t.Fatalf("woke %v on an agent restart, want none", woken) } } // TestSidecarWakeLegReturnsBeforeASlowWakeCompletes: the glued leg itself // must not block on WakeWorker either — the property dispatchSidecarWakes // provides must survive being called from inside sidecarWakeLeg. func TestSidecarWakeLegReturnsBeforeASlowWakeCompletes(t *testing.T) { ctx, s := genKit(t) routes := []registry.Route{wakeSidecarRoute("bot.eleven.sidecar.internal", "bdm", "eleven", "bot")} mem := newSidecarWakeMemory(false) sidecarWakeChanges(ctx, routes, s, mem) // seed touchSidecarWake(t, ctx, s, "bdm", "eleven", "bot") slow := make(chan struct{}) wake := func(rt registry.Route) error { <-slow; return nil } // Deadline-vs-elapsed-time correction, same as // TestDispatchSidecarWakesReturnsBeforeASlowWakeCompletes (fix round 3): // a synchronous call on the test's own goroutine never fails under a // regression, it hangs. var moved []registry.Route done := make(chan struct{}) go func() { moved, _ = sidecarWakeLeg(ctx, routes, s, mem, wake) close(done) }() select { case <-done: close(slow) case <-time.After(time.Second): close(slow) t.Fatal("sidecarWakeLeg did not return within 1s with a wake blocked indefinitely — the wake must be dispatched asynchronously") } if len(moved) != 1 { t.Fatalf("moved = %v, want exactly one (detection is synchronous)", moved) } } // ---- wired into fastTick: rides the same serial move as the restart leg -- // TestFastTickCallsWakeLegOnSerialChange pins the wiring itself: fastTick // must call fastDeps.wakeLeg exactly when it calls restartLeg — on a serial // move, after fullPass — or `carlos sidecars wake` writes a generation // object no box ever reads. Same shape as TestFastTickActsOnSerialChangeNotBoot, // with a call-counting wakeLeg spy standing in for the production closure. func TestFastTickCallsWakeLegOnSerialChange(t *testing.T) { env := newFastFixture(t) var calls int restart := func(string) error { return nil } d := &fastDeps{ reg: env.reg, store: env.store, pub: env.pub, optDir: env.optDir, restart: restart, passOpts: fastPassOpts(env.dataDir, func(string, string, error) {}, restart, func(string) error { return nil }), fullPass: func(context.Context) {}, wakeLeg: func(context.Context) { calls++ }, } now := time.Unix(1754640000, 0) fastTick(context.Background(), d, now) // no serial object at all: seeds at zero, runs nothing if calls != 0 { t.Fatalf("wakeLeg ran on the boot seed, want 0 calls, got %d", calls) } if err := release.BumpSerial(context.Background(), env.store, now); err != nil { t.Fatalf("BumpSerial: %v", err) } fastTick(context.Background(), d, now.Add(2*time.Second)) if calls != 1 { t.Fatalf("wakeLeg calls = %d, want exactly 1 on a serial move", calls) } fastTick(context.Background(), d, now.Add(10*time.Second)) // past clamp, unchanged serial if calls != 1 { t.Fatalf("wakeLeg calls = %d after an unchanged serial, want still 1", calls) } } // TestFastTickToleratesANilWakeLeg: a box built before this feature (or a // test fixture that never sets one) must not panic — wakeLeg is optional // exactly like restartLeg. func TestFastTickToleratesANilWakeLeg(t *testing.T) { env := newFastFixture(t) d := env.deps(nil) // deps() never sets wakeLeg now := time.Unix(1754640000, 0) if err := release.BumpSerial(context.Background(), env.store, now); err != nil { t.Fatalf("BumpSerial: %v", err) } fastTick(context.Background(), d, now) // must not panic } // TestDispatchSidecarWakesReturnsPromptlyWithManySlowWakesQueued is // dispatchSidecarWakes' half of review round 2's regression pin (see // restart_test.go's TestCycleWorkersReturnsPromptlyWithManySlowWakesQueued // for the full shape of the bug this catches): strictly more than // workerBootConcurrency sidecars, every wake deliberately parked, must // still return this function immediately — the whole bounded fan-out runs // inside dispatchBoundedWakes' own goroutine now, not just each individual // wake dispatched from a loop that can itself stall once the bound fills. func TestDispatchSidecarWakesReturnsPromptlyWithManySlowWakesQueued(t *testing.T) { n := workerBootConcurrency + 5 moved := make([]registry.Route, n) for i := range moved { moved[i] = wakeSidecarRoute(fmt.Sprintf("s%d.x.sidecar.internal", i), "bdm", "eleven", fmt.Sprintf("s%d", i)) } slow := make(chan struct{}) var mu sync.Mutex woken := map[string]bool{} wake := func(rt registry.Route) error { <-slow mu.Lock() woken[rt.Host] = true mu.Unlock() return nil } // Same deadline-vs-elapsed-time correction as // TestDispatchSidecarWakesReturnsBeforeASlowWakeCompletes (fix round 3). done := make(chan struct{}) go func() { dispatchSidecarWakes(moved, wake) close(done) }() select { case <-done: case <-time.After(time.Second): close(slow) t.Fatalf("dispatchSidecarWakes did not return within 1s with %d wakes blocked (bound %d) — the whole bounded fan-out must run off the caller's goroutine", n, workerBootConcurrency) } close(slow) waitForCond(t, "every queued sidecar to be woken eventually", func() bool { mu.Lock() defer mu.Unlock() return len(woken) == n }) }