TestAutoSaveOnSignalRacesTurnLoop is load-flaky, and it guards the #24 data race #36
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Problem
TestAutoSaveOnSignalRacesTurnLoopfails intermittently under load. It is thetest that proves the autosave path no longer gob-encodes live game state from
the signal goroutine — the data race fixed in #24. A guard test that fails
randomly is worse than useless: it either erodes trust in the suite, or gets
mentally filtered out, and then it cannot do the one job it exists for.
Evidence
Three independent observations, in order of strength:
inside the verbose re-run that
make testperforms after a failure —i.e. the suite running a second time while the first run's processes are
still winding down, at roughly doubled load. It passed all 9 clean gate
runs, including three back-to-back. It never executes a line of
sticks.go, so the sticks work is not implicated.at host load ~99 on 48 cores. Did not recur in five clean runs.
further mutated runs at load ~60 clean.
Taken together: it is load-sensitive, not random. The common factor is
concurrent pressure, and the most reliable trigger is the conditional verbose
re-run introduced by #2, which doubles the load precisely when the suite is
already failing.
Why this is worth fixing rather than tolerating
The failure mode is self-obscuring. The re-run exists to give diagnostics on a
failure; if the re-run itself induces an unrelated failure, every genuine
red build now carries a spurious second failure that has nothing to do with
the change under test. That is exactly what happened three times in PR #35 —
the author had to reason past it to trust their own mutation results.
Definition of done
rather than assumed: a real timing assumption in the test (a deadline, a
sleep, a bounded number of loop iterations before the signal is expected to
land), or a genuine residual race the detector only wins the scheduling
lottery on under pressure.
than widening the timeout — a test that passes because a constant got
bigger has not been fixed. Make it wait on a condition, not a duration.
reported as such before any test change. Say so and stop; do not paper over
it by relaxing the test.
"it failed", and nobody has recorded the assertion text or a race
report. Get that first; it likely settles item 1 immediately.
guard). Prove it by mutation, as PR #26 originally did — reverting
AutoSaveOnSignalto encode on the calling goroutine produced 73-113DATA RACEreports.triggers it) to demonstrate the fix, not just once on an idle box.
make checkgreen;TODO.mdCompleted Steps entry; do not rotate"Next Step"; commit title ends
(closes #N).Implementation requirements
destroy a player's save file.
pendingSaver, service-point, oratomic-write logic settled in #23/#26 unless item 3 applies — and if it
does, report before changing anything.
concurrent agent sessions. Export
GOLANGCI_LINT_CACHEto a fresh emptyprivate directory AND retry on
parallel golangci-lint is running; aprivate cache does not prevent the lock collision. Treat any result naming
paths outside your worktree as void.
maketargets only. Do NOT modify.golangci.yml. No Dockerfile/CI/script/.Priority
Medium-high. No user-facing defect — but it degrades the signal quality of
every future red build in this repo, and it sits on top of the most
consequential bug fixed so far.
Failure text captured at last — it is not a data race
Fired during review of #38 (head
6f409bd),on the verbose rerun
make testdoes after a failure, under heavy parallel load(a mutation to
showMapwas in the tree at the time — unrelated to this test,and it is what triggered the rerun). Full output was written to a file and read
whole, not filtered.
The assertion, from the
=== NAMEblock:and the summary line:
There is no
WARNING: DATA RACEanywhere in the output. The 1400-line logcontains exactly two
--- FAILlines: this one and the deliberateshowMapmutation. So this flake is not the race the test exists to catch.
What it actually is.
autosave_test.go:63is thet.Fatalat the bottom ofdriveUntilDone:TestAutoSaveOnSignalRacesTurnLoopasks forwantSaves = 25saves while theturn loop runs, capped at 1000 turns. The cap was hit before the saving
goroutine finished its 25 requests. That is a turn-budget exhaustion, i.e.
the handoff was working but too slowly under load, not a broken handoff and not
a race — each
AutoSaveOnSignalcosts the game goroutine a full state encodeplus a file write, all under
-race, and 18 concurrent agent sessions on thishost is exactly the load that makes 1000 turns insufficient.
Note the deadline knob that exists (
autoSaveWait = 10 * time.Second, commented"long enough that a loaded machine cannot turn a working handoff into a spurious
failure") guards the waiting side only. The driving side is a fixed turn
count with no such headroom, which is the asymmetry.
Suggested direction, not filed as a fix here: bound
driveUntilDoneby walltime (or raise/derive
maxTurnsfromwantSaves) rather than by a fixed turncount, so a slow machine cannot convert a working handoff into a failure — the
same reasoning
autoSaveWaitalready documents.Not a finding against #38: the test is
pre-existing, that PR touches no non-test code, and four other full
GOFLAGS=-count=1 make testruns on the same head were clean.