Unblocks S4 (delete the owned reopen path, no residual): Owned-regime eligible files (compaction images, kill-switched, legacy) reopen INTO the overlay. F5 cannot convert a non-empty Owned WAL (install_prebuilt V-2 check + set_overlay_regime rejects non-empty in-place stamp). Owner chose (A): build the rotation.
Overlay writes log RANKED (CommitRank) records relying on Overlay orphan-DROP at next replay. So the active WAL MUST be durably Overlay-regime BEFORE the first overlay write. "Force-install overlay but keep Owned WAL" is INCOHERENT for a durable file (next reopen replays overlay-ranked records under Owned KEEP -> orphan resurrection = data loss).
open_inner: rank==Overlay -> existing F5 arm; rank==Owned -> NEW
convert_owned_to_overlay_on_reopen (replaces the legacy stay-owned arm). overlay_eligible_v()
is always true post-F2, so there is NO surviving owned-regime reopen arm.
Empty Owned WAL = cheap case: set_overlay_regime (empty OK) + load_root_immutable.
S0. snapshot carried_floor (wal.commit_seq_floor), carried_ckpt_lsn (checkpoint_lsn). S1. rotate Owned tail -> archive (carries regime+floor into the OLD archived header). S2. stamp the fresh active WAL Overlay + re-assert floor + FSYNC == DURABLE COMMIT POINT. S3. load_root_immutable(root_ptr) -> builds + installs the overlay (V-2 now passes). S4. replay the ARCHIVED Owned tail INTO the overlay via replay_records_lww_overlay( rank_regime=Owned) == Owned KEEP semantics (== the old owned reopen's keep-then-LWW).
AsyncWalWriter::rotate_to_archive (async_writer.rs:685) does NOT reset the async
next_lsn (the sync rotate carries the OLD lsn by DG0 design), so post-rotation
is_empty_after_header() is FALSE -> set_overlay_regime() REJECTS. Fix: a dedicated
AsyncWalWriter::rotate_and_restamp_overlay(config) that, under the writer lock:
rotate_to_archive -> reset inner+async next_lsn/synced_lsn to 1/0 (the fresh segment is
genuinely empty) -> set_overlay_regime -> set_commit_seq_floor(carried_floor). Does NOT
change the shared rotate_to_archive contract (checkpoint path needs continue-LSN).
The F5-Overlay reopen arm currently replays only the ACTIVE WAL. It MUST also drain archived segments (collect_wal_segments + per-segment-regime reconcile, like recover_from_archives) so the post-S2-crash window recovers the archived Owned tail (KEEP) and any Overlay tails archived under load (DROP). VERIFY whether the normal F5 arm already does this; if not, EXTEND it (promote replay_records_lww_overlay to accept a per-LSN regime_of, or add a sibling). Most important correctness item.
Switch byte compact() (compaction_impl.rs:331-348) from reopen-Owned-image+reflip to in-memory reestablish_overlay_from_owned on the empty-WAL post-rename file (clean Overlay stamp, no rotation). compact() stops producing an on-disk Owned-regime artifact.
Delete: the legacy Owned reopen arms (mmap+io_uring, byte+char), reestablish_overlay_dispatch
replay_records_lww and replay_records_lww_overlay call the SAME reconcile_lww (regime-aware:
recovery.rs:328-334 Owned KEEP @ lsn / Overlay DROP); only the apply target differs. The
F5 both-loaders correspondence already proves the overlay applier reproduces the owned
final state (byte+char $\times$ V incl. "" and term-only members). So converted-reopen == old
owned-reopen, incl. unranked Owned entries (orphan-KEEP).
\times$ byte/char $\times$ V $\in${(),u64,String} $\times$ empty/non-empty
WAL/image): inject drop at each step, reopen, assert every committed term+value + "" +
unranked Owned entries survive + final regime Overlay; idempotence on double reopen.R1 lazy-on-first-write (read-only reopen sees empty overlay; first-write latency cliff; spread surface). R2 force-install+rotate-at-checkpoint (violates INV-COHERENCE). R3 in-place non-empty stamp (formally forbidden — orphan corruption). R4 truncate Owned WAL (loses the post-checkpoint committed tail). R5 re-log all terms (O(N), breaks LSN/watermark/#41).
Red-team found v1 NEEDS-REVISION: OBLIGATION-A real (all 4 arms active-only); and a COUPLED BLOCKER pair (#2 LSN-reset breaks global monotonicity recovery.rs:286 -> cross-domain (generation,lsn) inversion; #3 BatchIncrement delta DOUBLE-APPLY because a post-conversion checkpoint writes checkpoint_lsn in a NEW low domain so the archive re-drain skip fails).
v1's rotate_and_restamp_overlay reset next_lsn->1. WRONG. The sync rotate_to_archive
ALREADY carries the high next_lsn (DG0) — that IS the global-monotone-LSN invariant
(recovery.rs:286) the LWW sort needs. So:
rotate_and_restamp_overlay (and have
set_overlay_regime accept a header-only-but-high-LSN active). A header-only WAL has NO
records -> stamping Overlay is unambiguous (no Owned records to mis-interpret).tail_lsn <= checkpoint_lsn is TRUE for
any tail record a later checkpoint subsumed -> applied EXACTLY ONCE. [#3 fixed]
Archive pruning after a subsuming checkpoint is a PERF cleanup (correctness rests on the
lsn-skip, not on removal); reuse the existing prune/segment-lifecycle.The drain of archived segments is a SINGLE shared archive-aware overlay reconcile:
collect_wal_segments(config) (writer.rs:594, LSN-ordered) -> reconcile_lww_with_regime
(recovery.rs:290) with a per-SEGMENT regime_of (each archived segment header carries its
own regime: the converted Owned tail -> KEEP; overlay-written archived tails -> DROP) ->
apply winners via the overlay publishers. Promote replay_records_lww_overlay to accept the
per-segment regime closure (or add a sibling replay_segments_lww_overlay). WIRE IT INTO ALL
FOUR reopen arms (byte+char $\times$ mmap+io_uring) — the Overlay F5 arm AND the new converter's S4
both go through it (the converter is just "Owned active -> rotate -> Overlay file whose tail
is the just-archived segment", so after S2 it IS the Overlay arm draining the archive). This
unifies S4 with the Overlay arm: convert = rotate+stamp+(the shared archive-aware F5 reopen).
The S0-S4 sequence (minus the reset), the crash-safety windows (durable commit = S2 fsync; now with continuing LSN the post-S2 reopen drains the archive via FIX B), double-rotation empty-segment (benign; use collect_wal_segments LSN-order), compaction re-point, the clear_owned residual (honest; archive-rebuild conversion deferred), replay equivalence (reconcile_lww regime-parametric), verification (crash proptest + TLA 5-state + correspondence
Round 2: #2 CONFIRMED FIXED (keep high carried next_lsn = DG0 monotonicity). But #3
RESIDUAL: the committed-watermark base at reopen is re-derived by SCANNING the ACTIVE WAL's
records (max_lsn over active records: byte mmap_ctor.rs:499, char :420/:453, io_uring
:181/:212), NOT the writer's next_lsn atom. Post-S2 the active is header-only -> active
max_lsn = 0 -> watermark base = 0 -> first post-conversion checkpoint writes
checkpoint_lsn = 0 < tail_max(12) -> re-drain skip 12 <= 0 FALSE -> BatchIncrement
DOUBLE-APPLIES. The no-WAL drain (apply_recovered_operation_overlay) never mark_committed's.
At any archive-aware reopen (the converter S4 AND the Overlay arm draining archived tails),
after applying the drained records, advance committed_watermark to cover them:
CORRECT = SEED the watermark BASE = max_lsn_in_segments(collect_wal_segments(...))
(writer.rs:507) at reopen, instead of active-only. This matches how a NORMAL reopen seeds
base = active_max (treats ALL records <= max as committed: the image covers <=ckpt_lsn, the
WAL covers the rest — all durable), just extended to include the ARCHIVE segments.
REJECTED = mark_committed(lsn) per drained tail record: WRONG. The watermark is a
CONTIGUOUS-PREFIX value; the image-subsumed records (lsn <= checkpoint_lsn=10) are NEVER
re-applied/marked, so marking only the drained tail (11,12) leaves a GAP at 1..10 -> the
contiguous prefix stays 0 (watermark never advances). The single-step base-seed sets
"all <= max committed" DIRECTLY, correct because image+archive+active are ALL durable (the
archive is the rotated tail = committed Order-A records). Guarantees watermark() >= tail_max BEFORE the first checkpoint, so checkpoint_lsn >= tail_max, so the re-drain skip
tail_lsn <= checkpoint_lsn is TRUE -> BatchIncrement applied EXACTLY ONCE.
Trace (fixed): image 5 @ ckpt 10; tail +3 @ lsn 12; convert -> 8, watermark=12; checkpoint
writes checkpoint_lsn=12; crash+reopen drain skip 12<=12 TRUE -> stays 8. NOT 11.
NOTE: this watermark-base-from-active-only is a UNIFORM gap (all 4 arms); FIX C corrects it
for every archive-draining reopen, not just the converter (normal Overlay files keep
active-only behavior because their archive is already checkpoint-subsumed, so base=max is a
no-op there; only the un-subsumed converted/under-load archive needs the full-segment seed).
No file-length emptiness predicate exists; is_empty_after_header == next_lsn==1
(writer.rs:370, async_writer.rs:604), and writer.rs:367-369 argues against a file-length
check due to BufWriter buffering. Resolution: rotate_and_restamp_overlay holds the writer
lock across rotate->stamp and the fresh active is header-only + fsync'd BEFORE the gate
(no buffered records possible in that window), so a file-length records-empty check (len ==
WalHeader::SIZE) is SOUND there. Add records_empty_on_disk() (file-length based, used ONLY
on the post-rotate fsync'd active) and gate the Overlay stamp on it (admit a header-only,
HIGH-next_lsn active). Also update the OTHER current_lsn()==1 emptiness gates that the
converted file hits: flip.rs:430, :439, :460 (the flip/kill-switch regime-stamp guards) —
they must accept the converted (header-only, high-next_lsn) active too, or the converter's
flip path is rejected.
reconcile_lww_with_regime, all 4 arms + converter S4), the no-reset DG0 monotonicity (#2), crash windows, compaction re-point, clear_owned residual, replay equivalence, verification (crash proptest now MUST assert BatchIncrement applied exactly once across the post-S2 crash
All 3 BLOCKERs closed, no new BLOCKER (code-verified): #1 FIX B (reconcile_lww_with_regime
\notin$ snapshot; no acked
write reclaimed — #41 invariant). FIX A precision NIT: the load-bearing gate is
set_overlay_regime's is_empty_after_header()==(next_lsn==1) (writer.rs:383 / async :612),
which records_empty_on_disk() targets; the convert path's install_prebuilt_overlay_root
(flip.rs:830/835) already admits the high-next_lsn file post-S2-stamp, and flip.rs:430/439/460==1 gates are NOT on the convert path (editing them is
harmless-but-unnecessary). Implementer: target set_overlay_regime only.Round 4 (independent) found a REAL data-loss BLOCKER rounds 1-3 missed: after a crash
AFTER S1 (tail archived) BEFORE S2 (stamp), the fresh active is header-only BUT carries the
high next_lsn (rotate restores next_lsn_after_rotation, writer.rs:582; async never re-syncs).
So is_empty_after_header() (next_lsn==1) is FALSE -> the converter MISCLASSIFIES it as a
non-empty Owned WAL -> RE-ROTATES -> mints an empty archive segment each crash-reopen.
prune_segments_if_needed (writer.rs:654, oldest-first, max_segments=10) then evicts the
OLDEST = the REAL un-subsumed Owned tail after ~10 crash loops -> the FIX-B drain rebuilds an
INCOMPLETE trie SILENTLY (reconcile_lww_with_regime has NO RES-3 prefix-gap guard). The v1
"second rotate archives an EMPTY segment (harmless, pruned)" claim was WRONG.
The converter decides cheap-vs-rotate via records_empty_on_disk(active) (file len ==
WalHeader::SIZE on the fsync'd active), NOT is_empty_after_header() (next_lsn==1):
records_empty_on_disk predicate gates BOTH the cheap-vs-rotate decision AND the Overlay
stamp.The FIX-B archive-aware drain must FAIL LOUD (not silently rebuild incomplete) if a committed
prefix is missing: if the min surviving record lsn across (image-frontier, archive, active)
leaves a gap below it that checkpoint_lsn does not cover (min_surviving_lsn > checkpoint_lsn+1),
return a corruption error (the RES-3 guard that today lives ONLY in
rebuild_from_wal_segments_regime_aware recovery.rs:1616 — port it to the FIX-B drain path).
Belt-and-suspenders: exempt un-subsumed segments (first_lsn > checkpoint_lsn) from
prune_segments_if_needed. HIGHEST RESIDUAL RISK (implementer): the FIX-B drain must thread
the REAL (loaded_from_disk=true, checkpoint_lsn) into reconcile_lww_with_regime — do NOT
reuse rebuild_from_wal_segments_regime_aware which hardcodes (false, 0); a wrong
(loaded_from_disk, checkpoint_lsn) reintroduces the FIX-C BatchIncrement double-apply.
max_lsn_in_segments), no-reset DG0 monotonicity, S0-S4 (S1 now conditional on records-non-empty), compaction re-point, clear_owned residual, replay equivalence, verification (the crash proptest MUST include the >=11x crash-before-S2 loop asserting the tail is NEVER pruned/lost, + the RES-3 fail-loud on an injected prefix gap). Round-4 confirmed CLEAN: ranked-tail (regime gate short-circuits recovery.rs:328), empty-image (drain rebuilds from tail), Overlay-arm subsumed-archive skip, compaction re-point, watermark<=synced_frontier (both seed from max_lsn_in_segments).
All 4 BLOCKERs closed (OBLIGATION-A, no-reset monotonicity, FIX-C double-apply, FIX-D crash-loop). Torn-tail SAFE+BOUNDED (records_empty_on_disk non-empty -> rotate once -> next reopen cheap; CRC reader stops at the torn prefix, nothing corrupt applied). Prune-exemption of un-subsumed segments correct (retain un-checkpointed committed data until a checkpoint subsumes+prunes). Convergence reached across two independent deep passes (r3, r5); the once-more (r4) caught + fixed the crash-loop. The crash-proptest + TLA + correspondence are the empirical final gate (test the exact red-team concerns).
rotate_and_restamp_overlay MUST sync_all() the fresh header after the
Overlay stamp (the shared rotate_to_archive only flush()es, writer.rs:579 — file-len is
accurate for records_empty_on_disk regardless, but the EMPTY-Overlay header must be
fsync-durable across power-cut = the S2 durable commit point).checkpoint_lsn — used BOTH for the
RES-3 guard threshold AND the FIX-C lsn <= checkpoint_lsn skip — MUST be sourced from the
LOADED IMAGE/DESCRIPTOR (the dense-image redo frontier), NOT the active-WAL Checkpoint
record (rotate zeroes the fresh header's checkpoint_lsn, WalHeader::new carries only
floor+regime, writer.rs:575; RecoveryManager reads the active record = 0 post-rotate). Wrong
source => RES-3 false-positive on every legitimately-subsumed archive AND FIX-C double-apply.
Do NOT reuse rebuild_from_wal_segments_regime_aware (hardcodes (loaded=false, ckpt=0),
recovery.rs:1633); thread (loaded_from_disk=true, image_checkpoint_lsn) explicitly.Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |