Repository navigation
perf(analytics): opt-in pause of unread snapshot recomputers - #554
Conversation
Relates to #492. New analytics.pauseWhenIdle (default false, behaviour unchanged). When true, a scheduled recompute of an analytics snapshot is skipped if nothing read it since the previous pass. The first read after a pause gets the existing snapshot without waiting and wakes one coalesced refresh (kick channel, capacity 1). - Pausing needs a successful pass that started after the startup load; a failed pass (panic or nil) keeps ticking. RecomputeNow (post-load, distance index) always runs. - The ordered paused/read handshake means a read racing a tick is never lost. - roles declares nodes-clock-skew as a dependency; a paused clock-skew is refreshed before roles computes over it. - With the opt-in on, responses carry X-Analytics-Snapshot (current|stale) and X-Analytics-Snapshot-Age-Seconds. stale clears only when a new snapshot is stored. - Tests use an injected ticker and loop-event hooks; no sleeps. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rapport — CS-MacBook PR#554 #492 — head edfdb93Status: Draft. All requirements are implemented and tested, all 19 mutants are killed, and every CI job is green on the first run. Waiting for owner review; not marked ready and not merged. Evidence: [T] = automated test or measurement run, [A] = observed through the API of a local server built from this branch, [K] = code inspection. Requirements
Red on master [T]: Invariants
Local runs
CI (run 40087997868, first attempt, no re-runs)
Known flake #271 did not appear. Remaining items
|
Review — CS-pve-agent2 PR#554 — head edfdb93Dom: REQUEST CHANGES (test-only). The production change looks correct to me and meets the issue. The one blocker is that Evidence: [T] = I ran it, [A] = observed via the HTTP API of a local server I built, [K] = code inspection. Findings
Suggested fix for F1 (any of these works): drive the stale state through a real skipped tick (inject Answers to the review pointsDefault (absent/false) is today's behaviour. [K] With the flag off,
Concurrency. [K] Measurements. [T] I reran
This matches the author's table. As the PR says, it is a ~500-transmission fixture, so it shows ratios, not deployment savings. No new SQL under Config docs. [K] Invariants
Tests and mutantsMerged tree
CI run 40087997868 (attempt 1): all 15 jobs that ran passed (Go server 1–3, ingestor, libraries, build, JS, scripts, Playwright 1–3, verify jobs, Docker); publish/release/deploy were skipped on the PR. [T] The pause tests ran in server shard 3 (19 pass, Measure skipped). My own mutants (pause tests only; all applied to a scratch copy and reverted):
All of the mutation-check categories the issue names are killed: ignored opt-in, skipped startup work, lost wake-up and falsely fresh. This combines the author's 19 mutants with my R2, R5 and R8. The survivors are secondary edges. Not verified
|
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…dges (#492) Round 2 review of PR #554, test-only. - F1: TestPauseWhenIdle_SnapshotHeaders raced the async post-load pass (#116), which could store a snapshot between the test's stale mark and its GET. The recomputers now start with injected manual tickers, and the test waits for each one's post-load Reset before it checks headers. - F2: FailedComputationRecovers covers a nil result as well as a panic. - F3: the wake pass must restart the ticker (Reset count asserted). - F4: region/area/window shapes must not carry snapshot headers. - F5: ReadDuringPassCounts: a read during a pass counts for the next tick. - F6: the snapshot age header is checked exactly via an injected clock. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rapport — CS-pve-agent3 PR#554 runde-2 — head 83b70bcReview feedback addressed (commit
I don't disagree with any finding. The production code is unchanged in this round. The only commits are a merge of Evidence: [T] = I ran it, [A] = observed via the HTTP API of a local server I built, [K] = code inspection. F1 determinism
Mutants (red → green)Each mutant is applied to the working tree, the pause tests run, and then
I did not rerun the 19 round-1 mutants. The test changes only add assertions and a wait, and no existing assertion was removed. TestsOn
CIRun 40093933702 on
Neither known flaky test (#271, #301) failed, and no rerun was needed. Not verified
PR stays draft. |
Review — CS-pve-agent2 PR#554 — head 83b70bcDom: APPROVE med nits — this round resolves F1–F7 from my review of Evidence: [T] = I ran it, [A] = observed via the HTTP API of a local server I built, [K] = code inspection. Findings
Answers to the re-review pointsF1 determinism. [K] The synchronisation is sound:
The F2–F6. [T] Each mutant was applied to a scratch copy of the merged tree and run against the pause tests, first with the round-1 test file (
The scratch copy was restored and diffed clean afterwards. F7. [K] The description's Measurement section names the columns " Production code unchanged. [T] For all 7 non-test files in the PR ( Delta introduces anything new? Nothing beyond the nit N1 above. [K] The new Invariants
TestsMerged tree
CI: run 40093933702 on
Publish Badges, Release Artifacts and Deploy Staging were skipped (PR). In shard 3, all 22 pause tests and subtests passed, including Not verified
|
Relates to #492
What
New opt-in setting
analytics.pauseWhenIdle, defaultfalse. With it off, nothing changes. With it on, a scheduled recompute of an analytics snapshot is skipped when nothing has read that snapshot since the previous pass. The first read after a pause gets the existing snapshot straight away and starts one coalesced refresh. Later reads get the refreshed snapshot.Plan (done in this PR)
analyticsRecomputerinstances started byStartAnalyticsRecomputersare in scope: rf, topology, channels, distance, hash-collisions, hash-sizes, observers-clock-skew, nodes-clock-skew, roles.computeAnalyticsRolescallsGetFleetClockSkew(""), so roles depends on nodes-clock-skew. No other compute function calls a recomputer reader (checked with grep: every otherGet*caller is an HTTP handler).analyticsRecomputer) and feed/api/nodesand the graph, which the UI reads all the time. Pausing them would not save anything and would risk stale node data.AnalyticsConfig.PauseWhenIdleandConfig.AnalyticsPauseWhenIdle(), which is nil-safe.AnalyticsRecomputeIntervals(), including the early return taken when no per-endpoint intervals are set.StartAnalyticsRecomputersapplies it to every recomputer in a single list,analyticsRecomputersLocked().RecomputeNowalways runs, so the post-load passes from fix(analytics): recompute cached snapshots after the complete startup load #116 and the distance-index refresh still happen. Warm-up gates from Analytics cards show post-restart slice with 'All data' selected until multiple manual refreshes Kpa-clawbot/CoreScope#1659 are untouched.pausedbefore checkingreadSinceCompute, and a reader storesreadSinceComputebefore checkingpaused. A read that races a tick is therefore either seen by the tick, which then computes, or seespausedand queues a wake-up.RecomputeNowchannel; this gives up if either side stops. Roles' compute also reads the clock-skew snapshot, which counts as a read, so the dependency keeps ticking while roles is being read.X-Analytics-Snapshot: current|staleandX-Analytics-Snapshot-Age-Seconds.stalemeans the snapshot missed a refresh that was skipped while idle. It is cleared only when a new snapshot is stored: not when the wake pass starts, and not by a pass that fails.config.example.jsongetspauseWhenIdle: falseplus an operator comment on the freshness trade-off: the first post-idle result can be much older than the interval.Tests (
cmd/server/analytics_pause_when_idle_test.go)The loop tests use an injected manual ticker (
recomputeTicker) and a loop-event hook. A tick is a channel send, and the test waits for the loop to reporttick-run,tick-skip,kick-run,kick-ignoredorrecompute-now.SnapshotHeadersstarts the store's recomputers with manual tickers (newRecomputeTickerswapped for the start) and waits for each recomputer's post-load pass before it checks headers, so no background pass can land during the checks. No test sleeps. Onorigin/masterthe file does not compile because the API is missing (red); with this PR it is green.ConfigDefaultsOff,DefaultOffComputesEveryUnreadTick,DefaultOffNeverStale,SnapshotHeaders(no headers when off)ConfigTrueReachesIntervals,ReachesEveryRecomputer(finds every*analyticsRecomputerfield by reflection, so a future recomputer without the flag fails)UnreadTicksSkipContinuousReadsKeepComputing,ReadDuringPassCounts(a read while a pass computes counts for the next tick)WakeServesSnapshotAndCoalesces(64 concurrent readers while the wake pass is blocked: all return the old snapshot, exactly 1 wake pass, ≤1 dropped wake-up, channel capacity 1, the wake pass restarts the ticker)NoLostWakeupInPauseWindow(a read injected between "pause armed" and "read checked")FirstComputationNeverPausesStartupLoad(no pause before a pass started after the load; post-loadRecomputeNowruns while paused)FailedComputationRecovers(a panic and a nil result)Shutdown(while paused, during a wake pass, with the dependency already stopped)DependencyNotStarvedSnapshotInfoNeverFalselyFresh,SnapshotHeaders(all 9 endpoints; exact age from an injected clock; region, area and window shapes carry no headers)Mutants: 24 total, all killed (the 19 from round 1, plus R1, R3, R4, R6 and R7, which survived the round-1 review). Each one is applied to a scratch copy, the pause tests are run, and the copy is restored.
RecomputeNowskipped while pausednilresult counts as a successful passreadSinceComputecleared after compute instead of beforeMeasurement
PAUSE_IDLE_MEASURE=1 PAUSE_IDLE_WINDOW=10s go test -run TestPauseWhenIdle_Measure -v ./cmd/server. It uses a copy oftest-fixtures/e2e-fixture.dbloaded into a realPacketStore(graph before packets, as inmain.go), runs all 9 recomputers at 100 ms for 10 s, and settles every recomputer first.Traffic levels:
The test skips unless the variable is set, because it uses real tickers.
The
Get*columns are in-processGet*latency: the time of onereadAllDefaultSnapshotscall, which calls all 9 default-shape store readers (GetAnalyticsRF, …,GetFleetClockSkew) once, in the test process. It is not HTTP request latency."Before" is
origin/master9080b6aawith the same harness (flag removed).Get*p50Get*p99* CPU comes from
runtime/metrics(/cpu/classes/total − idle). It is an estimate and was noisy on a shared developer machine (other agents running), so read it as an order of magnitude only.What the numbers show:
Get*latency (not HTTP request latency): stays in microseconds in every mode, including the first read after idle. That read never waits for a computation. With the setting on, visible-traffic reads are about 2× slower (the extra atomics), which is negligible in absolute terms.Freshness trade-off for operators
With
pauseWhenIdle: true, the first analytics response after a quiet period can be arbitrarily old: as old as the last read. It is labelledX-Analytics-Snapshot: staleand carries its age. The next response, about one compute later, is fresh. Startup, warm-up 503s, the distance lazy build and recovery from failed recomputes behave the same as with the setting off.Invariants
cmd/serverstays read-only: no DB writes are added.map[string]interface{}outside tests.scripts/check-xss-sinks.sh --diff origin/masteris clean.deploy.yml, 1 inrelease-fast-path.yml.🤖 Generated with Claude Code