carlos / platform Public

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.
Paul Campbell pushed by paul@keymail.dev 84a55c3e9aea38404bc606713e1470ebfcf42c55 parent b028b56
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) {