Repository navigation
test(server): replace distance lock timing threshold with a deterministic check - #79
Merged
Merged
Conversation
…ng writers The distance lock test gated on average writer Lock/Unlock latency, first at 150µs, then 5000µs. Neither limit holds across machines. Healthy code measures 156-402µs on CI runners. With the RLock deliberately held across the compute, the 5000µs limit let the regression pass 4/30 runs at the default GOMAXPROCS and 9/30 at GOMAXPROCS=8 on a 10-core Mac (4.5-6.9ms). The test now checks the lock state directly. It parks computeAnalyticsDistance on a tx.decodedOnce the test holds, in the area filter that runs right after the snapshot, and confirms the park from the goroutine's stack. s.mu.TryLock() must then succeed. Holding the write lock, the test swaps distHops/distPaths as ingest would, then checks that the result comes from the snapshot. Nothing is timed; the deadlines only turn a broken setup into a failure instead of a hang. The writer-latency measurement stays as an unasserted benchmark. There are no production code changes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The independent review found an escape. A compute that releases s.mu at the snapshot but takes RLock again around the sort/dedupe passed the TryLock check at the park point. The test now keeps the write lock while it releases the park. From then on the compute either returns without touching s.mu or blocks in RLock, which its goroutine header shows as [sync.RWMutex.RLock]. That mutant now fails 250/250 normal and 100/100 under -race. The review also flagged three smaller points: - Doc scope. The comment now says only the region+area path is driven, and that step 4 assigns fresh slices rather than modelling in-place compaction. - The benchmark now waits until every reader has finished one compute before its timer starts. - The benchmark comment no longer claims to be the exact old measurement. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tuck case These are the re-review's two low-severity suggestions. The stack matcher now also requires that this test created the goroutine, so a compute goroutine leaked by another test cannot trigger a false failure. The step-4 timeout message now names the likely cause: s.mu taken some other way, such as Lock(). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-2039-lock-threshold
This was referenced Sep 23, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This replaces the timing threshold in
cmd/server/distance_lock_contention_test.gowith a deterministic check of the lock invariant from upstream issue 1239. The PR started as a port of upstream PR 2039 (Kpa-clawbot/CoreScope), which raised the limit from 150 µs to 5000 µs. That port turned out not to catch the regression it was meant to guard against.Only the test file changes. There are no production, workflow or configuration changes.
Why the threshold had to go
The old test timed bare
s.mu.Lock()/Unlock()cycles while 8 goroutines rancomputeAnalyticsDistance, and compared the average with a fixed limit. How long a cycle takes depends on core count, scheduler,-raceand machine speed, so no single number separates healthy code from a regression on every machine.Healthy runs measured 156–402 µs on CI runners (upstream readings), which is why 150 µs was flaky.
Measured at
ee008091on a 10-core Mac (Go 1.27) against the 5000 µs limit, with the regression being the pre-fixs.mu.RLock(); defer s.mu.RUnlock()held across the whole compute:-race-raceThe regression passed the 5 ms limit on the fast default configuration. Healthy CI readings reach 402 µs, while the regression drops toward 4.5 ms as cores are added. That gap keeps shrinking on faster hardware, so any fixed limit would eventually either flake or miss.
New test method
TestComputeAnalyticsDistanceReleasesLockBeforeComputechecks lock state directly instead of measuring time:tx.decodedOnce.Dofor one transmission and blocks inside it. With a region and an area set, the compute'stx.ParsedDecoded()call in the area filter must wait. That filter is the first step afters.mu.RUnlock().runtime.Stackshows it parked there. The goroutine must be one this test created, insidecomputeAnalyticsDistance,ParsedDecodedandsync.(*Once).doSlow. At that point the compute is provably past its snapshot. This follows the existing pattern inget_store_stats_cache_test.go.s.mu.TryLock()must succeed.NewPacketStorestarts no goroutines, so a failure can only mean the compute still holds the read lock.distHops/distPathsand releases the park. The compute must then either return, or show up parked in[sync.RWMutex.RLock], which means it took the lock again. The result must come from the snapshot.Nothing in the pass condition depends on time. The 10 s deadlines only turn a broken setup into a failure instead of a hang. The old measurement is kept as
BenchmarkComputeAnalyticsDistanceWriterCycle, which is never asserted and does not run in CI.Scope limitation: only the region+area path can be parked. The default path (
region="",area="") shares the lock acquisition, so a change to that shared code is caught, but a lock added only on the default path is not. The test documents this.Verification
All numbers are from the merge result
27b404fd, on macOS arm64 with 10 cores and Go 1.27. The mutants were applied mechanically to fresh worktrees at that commit.-race(GOMAXPROCS 10/1/2/4/8)RLock+defer RUnlockacross the whole compute (the pre-fix shape)RUnlockmoved to just after the area filterRUnlockkept,RLocktaken again for sort/dedupeNo mutant passed a single run. There were no data races, panics or retries, and no failures came from an unexpected cause.
Other checks:
cd cmd/server && go test -timeout 20m -count=1 ./...: ok (22.8 s)cd cmd/server && go test -timeout 20m -race -count=1 ./...: ok (183.6 s), 0DATA RACEwarningsgo vet ./...: cleangofmt -lon the changed file: cleangit diff --check: cleanIndependent review
A separate reviewer agent that did not write the change reviewed it twice.
s.musome other way (writeLock(), orRLockin a spawned goroutine) fail on the timeout and never pass.9d817e40: match only this test's goroutine, and name the stuck case.Out of scope: pre-existing production issues
Both are tracked for separate follow-up and are not changed here:
region="",areaset),computeAnalyticsDistancereadss.distHops/s.distPathsafterRUnlock, not the snapshot. This is a data race under-race, and it can index past the end if ingest shrinks the slice.updateDistanceIndexForTxsand eviction compactdistHops/distPathsin place in the backing array a snapshot may still be reading. The "append-only" safety comment incomputeAnalyticsDistanceis therefore inaccurate. Step 4 of this test assigns fresh slices and does not model this.Commits
9b122b5etest(server): prove the lock release instead of timing writersc717d6fbreviewfix(test): catch a lock re-taken later in the distance compute9d817e40reviewfix(test): match only this test's compute goroutine, name the stuck case27b404fdregular merge ofmaster(6334c427); no conflicts, and nothing undercmd/serverchanged on master🤖 Generated with Claude Code