Repository navigation
test(server): deflake reach_rank hook race and the #1008 not-ready window (#224, #227) - #234
Conversation
…224) TestReachRank_ExpiredSnapshotAlwaysRefreshes hammers the expired snapshot from 16 goroutines. A caller that read the expired snapshot just before the refresh published can register one more singleflight refresh before the callers return. That refresh runs after the test, finds the snapshot cleared by resetReachState's cleanup, and calls onDegreeSnapshotLoad with no happens-before edge to the next test, which swaps the hook: the -race report in TestReach Rank_RefreshHonoursBackoffAfterQueuedFailure. awaitDegreeRefresh joins the "degree" key once more at the end of the test and receives its result: it waits for any refresh still in flight (or runs one that returns the fresh snapshot without a load), which orders that refresh before the test returns. Test-only: an atomic or per-server hook would hide the report but still let the leftover refresh call the next test's hook. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
503 test (#227) TestIssue1008_HandlerReturns503WhileSubpathIndexLoading checked the not-ready window right after Load(), racing the background subpath index build of a 6-transmission fixture; under CPU load the build won and the handler answered 200. PacketStore gets subpathBuildGate, a func field that is nil in production. startBackgroundIndexBuilds reads it before starting the subpath builder goroutine, which calls it first when it is set. The test sets it before Load() to block the build, asserts the 503, then releases it, waits for WaitIndexesReady and asserts 200. A per-store field rather than a package variable: background builds of other tests' stores never read it, so it cannot race the way #224's package hook did. Production cost is one nil check per Load(); readiness behaviour is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rapport — CS-pve-agent3 PR#234 #224+#227 — head 7862f67Status: Both flakes were reproduced under load and fixed in the test design. The fixes are Evidence tags: [T] test or run output, [K] checked in code, diff or CI log, [A] assessment or inference. Load for all "under load" numbers: a second process looping the full Failure rate, before and after
Design and reasons#224.
#227.
Mutants
Other checks
CI (run 37206392371, head
|
| Job | Result |
|---|---|
| Go Build & Test | pass [T]; 0 DATA RACE lines in its log [K] |
| Playwright E2E Tests | pass [T] |
| Build & Publish Docker Image | pass [T]; local image build only, no push [K] |
| Release Artifacts / Deploy Staging / Publish Badges & Summary | skipped (PR) [T] |
Residuals
- [T][A] A separate flake in the same test. Under load,
TestReachRank_ExpiredSnapshotAlwaysRefreshessometimes fails withround N: expired snapshot never refreshed: one round exceeds the test's 3 s per-round deadline, while a normal round takes about 13 ms. It is not a race report.- It was seen before the fix (1/200 processes), in run 1 of the reach_rank tests: onDegreeSnapshotLoad written while a previous test's singleflight refresh still reads it (-race flake) #224 acceptance command (1 of 200 iterations), and in the reach_rank tests: onDegreeSnapshotLoad written while a previous test's singleflight refresh still reads it (-race flake) #224 mutant run (3/200). It did not occur in the 200 processes after the fix, or in runs 2 and 3 of the acceptance command.
- It is not changed here. Proposed follow-up issue:
- Title:
test(server): TestReachRank_ExpiredSnapshotAlwaysRefreshes misses its 3s round deadline under load - Text: under CPU load (alongside another
-racerun) one round in roughly 100–600 does not see the refresh publish within 3 s, although a round normally takes ~13 ms. Check first whether a round is starved or whether a refresh can be delayed by the previous round's leftover flight before raising the deadline. Acceptance:-count=200 -run TestReachRank_ExpiredSnapshotAlwaysRefreshespasses under load.
- Title:
- [K] The reach_rank tests: onDegreeSnapshotLoad written while a previous test's singleflight refresh still reads it (-race flake) #224 commit message misspells the test name ("TestReach Rank_…"). The code is not affected; it was left as is, because amend is not allowed.
Review — CS-MacBook PR#234 race-flakes — head 7862f67Dom: APPROVE med nits Evidence tags: [T] run or test output, [A] assessment or inference, [K] checked in code, diff, CI or git. Scope: independent read-only review. Head Findings
No blocking finding. Answers to points 1–51. #224
2. #227
3. Reproduction My host differs from the author's: macOS arm64 with 10 cores, and other builds running on it. The ambient load was uncontrolled, so I added 16 CPU burner loops for the heavy-load rows. Both load levels always ran alongside three looping full-suite
4. Mutants [T]
5. Rules [K]
Tests
Not verified
|
Relates to #224, #227
Two
-racetest flakes incmd/server. Both are fixed in the test design, one commit per issue. The only production change is a nil func field thatstartBackgroundIndexBuildschecks once perLoad(); it is unset outside the #227 test.#224: reach_rank hook written while a previous test's refresh reads it
Cause. The race report (reproduced under load) shows the reader is a singleflight refresh created by
getDegreeSnapshotinTestReachRank_ExpiredSnapshotAlwaysRefreshes, already finished when the next test writesonDegreeSnapshotLoad. The two accesses never overlap in time; they lack a happens-before edge. This is how it happens:resetReachState's cleanup (degreeSnap = nil), and calls the hook.Fix (test only). At the end of the test,
awaitDegreeRefreshjoins the"degree"singleflight key once more and receives the result.wg.Waitreturns, so this join either waits for it or, with none left, runs one that returns the fresh snapshot without a load.Why not an atomic or per-server hook. Either would silence the race report. But the leftover refresh would still run after the test and call the next test's hook: an atomic hook would be the next test's load counter, which is what
RefreshHonoursBackoffAfterQueuedFailureasserts on. Making the test wait fixes the cause, and it is the smallest change: one helper, no production change.#227: the Kpa-clawbot#1008 503 test races the background subpath index build
Cause. The test asserted the not-ready window right after
Load(). The background build of the 6-transmission fixture can finish first, and then the handler answers 200.Fix.
PacketStoregetssubpathBuildGate func(), nil in production.startBackgroundIndexBuildsreads it before starting the subpath builder goroutine, which calls it first when it is set.Load(), waits until the build is held, and asserts the 503 (status,Retry-After: 5, body). It then releases the gate, waits forWaitIndexesReady, and asserts 200.t.Cleanupreleases the gate if an assertion fails, so the builder is never stranded.Why a per-store field, not a package variable. Many tests call
Load(), and their background builders would read a package variable while this test writes it: the #224 pattern again. A field set beforeLoad()is ordered by thegostatement, and other stores never read it.Production cost and behaviour. One nil check per
Load(), outside any request or ingest path. The gate is never set outside the test, and readiness behaviour (flags, 503, ready channel) is unchanged.Failure rate under load
The load was a second process looping the full
cmd/server-racetest binary (on tmpfs), on a 4-core host with load average 3–8.-count=1(the race detector reports a race once per process)go test -race -count=200 -run 'TestReachRank'FAILon the deadline flake below; the other two passed)-count=600in one processstatus = 200, want 503)go test -race -count=200 -run TestIssue1008_HandlerReturns503WhileSubpathIndexLoadingok, 0/200Mutants
awaitDegreeRefreshcall removedAlso seen, not fixed here
TestReachRank_ExpiredSnapshotAlwaysRefresheshas a second, separate flake under load:round N: expired snapshot never refreshed. One round exceeds the test's 3 s per-round deadline. It appeared before the fix too, in 1/200 processes; it also appeared in 1 of 200 iterations of one acceptance run, and in 3/200 processes of the #224 mutant run. It is not a race report, and it is not touched here. A follow-up issue is proposed in the report.Checks
cd cmd/server && go test -race -count=1 -timeout 20m ./...(TMPDIRon tmpfs, unloaded):ok(665 s), 0 race reports.go vetpasses.gofmt -lflagsindex_ready_1008.goandindex_ready_1008_test.go, but only for their existing doc-comment lists, which are unformatted on master too; the new hunks are formatted.cmd/serverstays read-only. No newmap[string]interface{}. Fork guards are unchanged:deploy.yml9,release-fast-path.yml1.🤖 Generated with Claude Code