schedule: a race-safe clock for the tests that advance time mid-delivery
The runner reads its Now seam from inside every host goroutine, so a test that parks one delivery and then advances a plain `now` variable is a data race — TestBusyHostDoesNotStarveAnotherHostsTimer was one, and -race said so. testClock guards the instant behind a mutex; the tests that only set the clock before any goroutine exists are left alone.
1 file changed,
+25
−3
internal/schedule/runner_test.go+25 −3
diff --git a/internal/schedule/runner_test.go b/internal/schedule/runner_test.go| index 01056be33..2f887c642 100644 |
| --- a/internal/schedule/runner_test.go |
| +++ b/internal/schedule/runner_test.go |
| @@ -30,6 +30,27 @@ func (f *fakeDeliverer) Deliver(_ context.Context, rt registry.Route, req *http. |
| return f.status, f.err |
| } |
| +// testClock is the Now seam for the tests that advance time WHILE a delivery |
| +// goroutine is running: the plain `now := …; r.Now = func() …` shape those |
| +// tests would otherwise use is a data race under -race, and the runner reads |
| +// its clock from inside every host goroutine. |
| +type testClock struct { |
| + mu sync.Mutex |
| + t time.Time |
| +} |
| + |
| +func (c *testClock) now() time.Time { |
| + c.mu.Lock() |
| + defer c.mu.Unlock() |
| + return c.t |
| +} |
| + |
| +func (c *testClock) advance(d time.Duration) { |
| + c.mu.Lock() |
| + defer c.mu.Unlock() |
| + c.t = c.t.Add(d) |
| +} |
| + |
| // firedHosts counts deliveries per host — the counting assertion the |
| // parallelism and exactly-once tests need. |
| func (f *fakeDeliverer) firedHosts() map[string]int { |
| @@ -443,8 +464,9 @@ func TestRunnerPrunesWithoutAConverger(t *testing.T) { |
| func TestBusyHostDoesNotStarveAnotherHostsTimer(t *testing.T) { |
| d := &blockingDeliverer{fakeDeliverer: fakeDeliverer{status: 204}, slow: "slow.bab.oncarlos.com", gate: make(chan struct{})} |
| reg, r := runnerFixture(t, d) |
| - now := at("2026-08-23T13:07:00Z") |
| - r.Now = func() time.Time { return now } |
| + clk := &testClock{t: at("2026-08-23T13:07:00Z")} |
| + now := clk.now() |
| + r.Now = clk.now |
| for _, h := range []string{d.slow, "quiet.bab.oncarlos.com"} { |
| if err := reg.Upsert(registry.Route{Host: h, Kind: "instance", Account: "bab", App: "app", Status: "ready"}); err != nil { |
| t.Fatal(err) |
| @@ -466,7 +488,7 @@ func TestBusyHostDoesNotStarveAnotherHostsTimer(t *testing.T) { |
| // Second tick, while it is still parked: the quiet host's row is due and |
| // must be delivered and recorded. |
| - now = now.Add(TickEvery) |
| + clk.advance(TickEvery) |
| r.Tick(context.Background()) |
| deadline := time.Now().Add(10 * time.Second) |
| for time.Now().Before(deadline) { |