Repository navigation
Run the checkpoint shadow check off the project write lock - #59
Merged
Merged
Conversation
A shadowed checkpoint cut used to run the full O(history) computation inside the project write transaction, so on Postgres every write to the project waited for it (up to the 1 s budget). It now runs in three steps: 1. The write lock is held only for the incremental cut and the shadow decision, as for every cut. A due shadow rolls that transaction back (errShadowDue) having written nothing. 2. The incremental and full bodies are computed and compared on one repeatable-read snapshot outside the lock (shadowOffLock). A mismatch returns here: no lock is taken and nothing is signed. 3. The lock is taken again for a constant-time check that the latest checkpoint is still the base the cut used, then the exact bytes that were compared are signed. A per-project cut gate makes other cuts of the project in this process wait during the shadow; writes do not wait. If another writer process moves the base, the cut returns an error and signs nothing (new counter averin_checkpoint_shadow_raced_total). A shadowed checkpoint covers the records committed when its snapshot was taken. Default shadow budget 1 s -> 20 s (it no longer bounds a write stall). New gauge averin_checkpoint_shadow_last_duration_seconds. The hourly rate limit, backoff, latch, the three existing metrics and the ALERT log texts are unchanged. Tests: off-lock race tests (writes continue while the shadow is paused), mismatch, raced-by-another-writer, same-process gate, cancel, in-tx sentinel, on Mem and Postgres. Formal claim checkpoint-cut-incremental- equals-full restated, covers relocked, 6 mutants re-cut or added. Co-Authored-By: Claude Opus 5.5 <[email protected]>
The new off-lock tests deadlocked instead of failing when a mutant broke the lock or the gate: the pause is now released in t.Cleanup and the cuts that could block run under a deadline. Add a Postgres client-cancel test, a created_ts ordering test, and two mutants for them. A phase 3 race now clears the project's first-cut verified mark so the next cut is shadowed again. API, LIMITATIONS and CONFIGURATION state the snapshot coverage, latency, raced 500, the process-wide latch cost and the statement timeout override. The kit detector gets an explicit 300 s go test timeout. Co-Authored-By: Claude Opus 5.5 <[email protected]>
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.
Post-release 1: do not merge before the release-1 cut.
Closes the averin part of FeirAI/feir-ops#24 (wave-1 item averin-shadow-offlock). Design: ops-log/designs/wave1-averin-shadow-offlock.md, lead decisions: ops-log/designs/wave1-lead-decisions.md.
Problem
A shadowed checkpoint cut ran the full O(history) computation inside the project write transaction. On Postgres every write to the project waited for it, up to the shadow budget (1 s since #55). The budget was cut to 1 s to bound that stall, which also made the shadow stop fitting after about an hour of history.
Design
A cut that needs a shadow now runs in three steps.
errShadowDueand rolls back having written nothing.shadowOffLockopens one repeatable-read snapshot S, reads the latest checkpoint first (a changed base returnserrShadowRaced), takescreated_tsafter that read, and computes the incremental body and the full body at S with the same timestamp. They are compared byte for byte. The classification, counters, latch and ALERT texts are the old block moved unchanged (compareShadow). A mismatch returns here, before any lock or signature.errShadowRacedand nothing is signed. Then the exact bytes compared in step 2 are sealed and stored. They are never recomputed.A per-project cut gate (one slot, context-cancellable, ref-counted) is held across all steps of a cut, so cuts of one project in one process do not run two shadows at once. Writers never take it. Lock order is gate, then write guard. It is released before witness and TSA.
What is compared against what: both bodies are deterministic functions of S. Records and checkpoints are append-only and every write commits under the guard, so S is a prefix of the guard-serialized write sequence. The checkpoint signed is the full body at S.
Could a divergent checkpoint be signed before the comparison finishes?
averin_checkpoint_shadow_raced_total).Invariants
created_tsis taken after the snapshot's first read.cut.incremental/cut.fullmove once per cut;shadow_checks_totalonce per attempt that reaches the full computation; mismatch and failed keep their meaning; ALERT texts byte-identical.Decisions taken (lead)
averin_checkpoint_shadow_raced_totaland gaugeaverin_checkpoint_shadow_last_duration_seconds. Note for the lead: the gauge is process-wide (the most recent shadow of any project), not per project. The metrics registry has no labelled gauge and a project label would be unbounded in a multi-tenant deployment. Say if per-project is wanted.What this does not fix (stated in the docs)
Tests
New
checkpoint_cut_offlock_test.go: writes continue while the shadow is paused and nothing is stored before the comparison finishes, and the signed body equals the snapshot's body (Mem and Postgres); mismatch with writes continuing refuses and latches; raced by another writer process (Mem and Postgres) signs nothing, one checkpoint per seq; same-process cut waits on the gate and signs seq N+1; gate wait is cancellable; client cancel is not a failed shadow and stores nothing; in-transaction phase 1 returns the sentinel and writes nothing; a concurrent-writers hammer for the race detector. Existing tests keep their assertions; only thecutOncehelper changed (it calls the real cut), plus the one budget assertion (1 s to 20 s). The Postgres run proves a read-only repeatable-read transaction accepts the budget savepoint andSET LOCAL.Formal:
checkpoint-cut-incremental-equals-fullrestated in both registers (off-lock, base check, error on a moved base, snapshot coverage, 20 s, and the new does-not-establish items). OnlycheckpointBodyCheckedchanged among the existing covers (relocked: the statement still holds, the change is that the shadow block moved out of it); added covers forshadowOffLock,compareShadow,createCheckpointTx,cutCheckpoint,acquireCutGate.cutBodyIncremental,parseCutBase,checkGaplessGrantLogAfter,checkpointBody, the min-interval functions andcutBodyFullare byte-unchanged. Mutants re-cut:shadow-cannot-run-refuses,shadow-ignores-backoff. New:shadow-under-write-lock,shadow-skips-base-check,shadow-signs-current-state,shadow-no-cut-gate. All 21 mutants of the claim killed.Gate output
All under
nice -n 10 taskset -c 0-11; this host needsCGO_LDFLAGS=-ldlfor the cgo link (old glibc), unrelated to the change.No dependency or core change, so no make test-core or supply-chain run.
Risks
Database load and pool occupancy for up to 20 s per shadow (R1, R3), cut latency up to the budget (R4), mutant patch churn. The govder four-plane e2e was not run (it cuts without concurrent writes).
🤖 Generated with Claude Code