Repository navigation
port/audit upstream 2284: distance invalidation during active build - #636
Conversation
…chunk merge (#622) Audit of upstream pull request 2284 ("preserve distance invalidation during an active build"). Our fork already replaced the reset sync.Once with a dataset generation in #149, so the upstream defect cannot occur here. The gate state is covered exhaustively by #149 and #558, but every one of those scenarios runs the background loader with an empty chunk loop, so no test ever merged a transmission during an invalidation and none asserted what the rebuilt index then contains. Three tests close that gap with the production chunk loader: - a fill that merges transmissions the hot window did not have invalidates a just-built index even though the observations it adds stay below the 5 % Delta-obs debounce threshold; ten concurrent callers share one rebuild and the 200 that follows carries the merged paths; - the build that read the pre-fill dataset publishes after the fill completed and must not consume the invalidation; - a build whose snapshot lands between a chunk being scanned and merged records the pre-fill generation, so its index is stale once the fill ends. Test-only; no production code changes.
Rapport — CS-Macmini PR#636 #622 — head 140a445Status: Audit done — upstream 2284's defect cannot occur here (our #149 fix is strictly stronger); one real coverage gap found and closed with 3 tests, 5 mutants run, no production code changed. Requirements
Fixture premise is pinned in code: CI per job
All 14 jobs green on the first attempt, no re-run needed. The known flake #271 did not appear. (The earlier run on Local validation
Remainders (not filed)
|
Review — CS-Minimax PR#636 — head 140a445Verdict: APPROVE with nits Independent, read-only review. Merged tree Findings
No blocking findings. The audit conclusion is correct. Answers to the review points
Tests and mutants (all run locally on the merged tree)
Mutants (single edits to
The mutant copies lacked the repo-root compose files, so Not verified
|
Relates to #622
Audit of upstream pull request 2284 ("fix(analytics): preserve distance invalidation during an active build", upstream issue 2281; read-only, nothing was written there).
Verdict: the upstream defect cannot occur in our fork. Our #149 already replaced the reset
sync.Oncewith a dataset generation, which is strictly stronger than upstream's fix. This PR therefore adds tests only and changes no production code. It does close a real coverage gap that the audit found.Plan
Audit: upstream 2284 against our store
distLazyOncereset in the background-fill completion; a reset of an in-usesync.Oncefatally unlocks an unlocked mutexsync.Onceanywhere in the gate since #149. The fill bumpsdistDataGen(anatomic.Uint64) unders.muafter the last chunk mergedTestDistanceBuild149_LoadCompletionDuringBuildDoesNotCrash(runs in a child process)runDistanceIndexBuildreadsdistDataGeninside its owns.musection, records it asdistLazyBuiltGen, anddistIndexCurrentLocked()reports the index current only whiledistLazyBuiltGen == distDataGen. A pass that went stale repeats instead of returningTestDistanceBuild149_LoadAfterBuildSnapshotRebuildsOnce,TestDistanceBuild149_TriggersDuringStaleRepassStartNoBuild,TestDistanceLoadChunkedLoadCompletionDuringBuild558distLazyBuilding = truebefore the goroutine starts)startDistanceBuildLockedsets it underdistLazyMuand then starts the goroutineTestDistanceBuild149_BuildingIsSetBeforeTheBuildGoroutineRuns,TestDistanceBuild149_ConcurrentTriggersStartOneBuilddistLazyMuat all — the atomic generation makes it unnecessary — and the trigger readstotalObsbetween twodistLazyMusections. #149's documented rule:s.muis never acquired whiledistLazyMuis heldTestDistLazyMuNeverHeldWhileTakingStoreMu(static AST guard),TestDistanceBuild149_TriggerAndLoadCompletionDoNotDeadlockUpstream's open question in issue 2281 ("worth fixing given the 5 minute bound?") does not apply either: our gate does not rely on the debounce at all.
TriggerDistanceIndexBuildchecksdistIndexCurrentLocked()first and always builds a stale index; the Δobs/5-minute policy only governs a current index.The gap the audit found
Every #149 scenario, and #558's, runs the background loader with an empty chunk loop —
newDistBuildStore/ the 558 helper setoldestLoadedpast the retention cutoff so the loop exits on its first check. So:That blind spot is not theoretical. M1, which moves the generation bump from after the chunk merge to before the loop, leaves the entire
cmd/serverpackage green (one 76 s run, 0 failures). On that mutant a build whose snapshot lands part-way through the fill records the already-bumped generation, so the gate reports an index that is missing the chunks merged after the snapshot as current — exactly what upstream issue 2281 describes.Tests added
cmd/server/distance_fill_invalidation_622_test.go, test-only, driving the productionLoadChunked+loadBackgroundChunks+ the real/api/analytics/distancehandler.Fixture: 60 transmissions inside the 2 h hot startup window and 2 older ones inside the 48 h retention window that only the chunk loader reaches, one observation each, over GPS-bearing nodes in the
nodestable so each transmission yields a distance path record with its own content hash.dist622RequireBelowDebouncepins the premise that 2 observations on top of 60 (3.3 %) do not reach the 5 % debounce threshold, so a rebuild can only come from the generation invalidation — upstream's "two cold observations stay below the normal 5% rebuild threshold".TestDistanceFill622RebuildBelowTheDebounceThresholdRetry-After; the hot-window 200 lacks the cold paths and the rebuilt 200 carries themTestDistanceFill622StaleCompletionKeepsTheInvalidationTestDistanceFill622MidFillSnapshotIsNotCurrentloadChunkScannedHook) records the pre-fill generation, so its index is stale the moment the fill endsMutants
Each mutant is a single edit to
cmd/server/store.go; the production tree is unchanged.distDataGen.Add(1)moved from after the chunk merge to before the loopdistIndexCurrentLocked()→s.distLazyBuiltinTriggerDistanceIndexBuild(gate on the flag, ignore the generation)TestDistanceBuild149_HandlerContractAndLoadInvalidationFAILdistLazyBuiltGen = gen→distLazyBuiltGen = s.distDataGen.Load()(record the live generation, not the one this pass read)TestDistanceBuild149_TriggersDuringStaleRepassStartNoBuild,TestDistanceLoadChunkedLoadCompletionDuringBuild558FAILdistLazyBuilding = truemoved fromstartDistanceBuildLockedinto the build goroutineTestDistanceBuild149_BuildingIsSetBeforeTheBuildGoroutineRuns,TestDistanceBuild149_ConcurrentTriggersStartOneBuild,TestDistanceLoadChunkedFirstRequestBuildsOnce558FAILtotalObswhiledistLazyMuis held and the fill touches the gate unders.muTestDistLazyMuNeverHeldWhileTakingStoreMuFAIL,TestDistanceBuild149_TriggerAndLoadCompletionDoNotDeadlockFAIL (deadlock, 10 s timeout)M4 and M5 cover the two upstream changes that need no new test: existing tests already kill them.
Validation
cmd/server: full packagego test .green (76.9 s). New tests-count=10 -racegreen, and-count=3 -cpu 1green (deterministic — no sleeps, no polling for a race).go build ./...andgo test ./...green.sh test-all.sh: 274 passed, 0 failed.node test-frontend-helpers.js: 731 passed, 0 failed./api/analytics/distanceanswered 202 +Retry-After: 5, then 200 with 1808 hops / 357 paths / 20 top paths.test-issue-1648-m4-icons-e2e.js(the one E2E that visits the distance tab) 9 passed, 0 failed. No E2E and no frontend file is affected: the diff is a single Go test file.scripts/check-xss-sinks.sh --diff origin/master: clean (nopublic/**changes). Fork guards unchanged: 9 indeploy.yml, 1 inrelease-fast-path.yml. No newmap[string]interface{}. No SQL underPacketStore.mu, no write helper added —cmd/serverstays read-only.Performance
Test-only. No production code, no hot path, no schema, settings or API change. The three new tests add about 0.1 s to the package (1.6 s under
-race).Limits
loadChunkScannedHookseam, which fires per chunk that has rows to merge. An empty chunk is not a park point; the test asserts that nothing was merged yet when it parks, so the ordering it needs holds either way.distDataGendocuments itself as counting only dataset changes the incremental maintenance does not cover, and today the background fill's completion is the only such writer; ingest, eviction and the hash migration all maintaindistHops/distPathsdirectly. Nothing enforces that statically, so a future bulk mutator that forgets both would go unnoticed. Not filed.