Repository navigation
fix(wal): read the log and replay refs through the caller's manifest - #107
Open
cyharyanto wants to merge 1 commit into
Open
cyharyanto wants to merge 1 commit into
cyharyanto wants to merge 1 commit into
Conversation
catch_up took head_seq from one manifest while read_log resolved a fresher one; a checkpoint in between trims that one's segments, so the bridge emitted nothing and advanced its cursor past the push. read_log, refs_at_seq, refs_as_of and the checkpoint-time helpers now take the caller's manifest, and fresh_manifest() is the explicit way to get the newest one. Round trips: the bridge catch-up, the settings history and policy dry-run, the repository overview and `walgit settings history` each made two conditional manifest GETs (the sync, then read_log's own) and now make one; `walgit wal ls` and `wal show` still make one. No budget in docs/ROUNDTRIPS.md changes.
This was referenced Oct 19, 2026
Closed
Author
|
I've just done a check across upstream, its open PRs and issues, and the 23 forks pushed since
This follows bf65c01, which added |
This branch has not been deployed
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
docs/EVENTS.md's "No lost events".catch_uptakeshead_seqandmin_seqfrom one manifest, thenRepoHandle::read_logresolved a fresher one on its own and read segments through it. A checkpoint in between trims those segments from the fresher manifest, so the read returns nothing.read_log,refs_at_seq,refs_as_ofand the checkpoint-time helpers now take the caller's&Manifest;RepoHandle::fresh_manifest()is the explicit way to get the freshest one. A caller can no longer read through a different manifest than the one its bounds came from.Problem
This is a TOCTOU race:
Bridge::catch_uptakes its bounds from one manifest, andread_logread the log through a newer one, which a checkpoint written in between has trimmed.sequenceDiagram autonumber participant B as Bridge (catch_up) participant S as Store participant C as Another instance Note over S: manifest M1: head_seq 2, segment for seq 2 B->>S: sync_refs: read M1 Note over B: from = cursor 1, head = 2, no gap C->>S: write checkpoint folding seq 2 Note over S: manifest M2: min_seq 3, segment for seq 2 trimmed rect rgba(220, 38, 38, 0.18) Note over B,S: Bug: read_log resolves the manifest again B->>S: read_log(2..=2): freshness_check S-->>B: M2, not the M1 the bounds came from Note over B: M2 lists no segment for seq 2, so 0 entries end B->>S: CAS cursor to 2 Note over B,S: seq 2 never published, no gap countedNothing records the loss:
publishedonly fires whenemitted > 0, and the gap warning only when the cursor is behindmin_seq, which it is not. Every later wake-up, the sweep included, seescursor == head.An age-triggered checkpoint makes the window common on a quiet repository: a push that finds the last checkpoint older than
checkpoint_intervalis followed by a checkpoint almost at once. In one deployment, two of a repository's three pushes in a day were lost this way, each followed by an age checkpoint folding it 290 to 340 ms later; the one that got through came 17 minutes after a checkpoint.The same pattern, bounds from one manifest and the read through another, was in:
refs_at_seq, which validatedseqagainst onemanifest()and replayed through another (replay_refs): a checkpoint in between turns itsCorruptinto an empty ref snapshot;refs_as_of, whosereplay_refsjudged the checkpoint bycheckpoint_times(), which read its ownmanifest(): a newer checkpoint's time can reject the older one and silently drop refs;pin_coverage_refs, which checked coverage on one manifest and fell back torefs_at_seq, which re-resolved;apply_delta, which learned checkpoint times from the held manifest rather than the one it was about to adopt;GET …/settings, the policy dry-run,walgit settings historyandwalgit wal rewind, which tookmin_seqor a cut from one manifest and read throughread_log's.Objective
Every read of the log or of replayed refs must go through the manifest its caller took its bounds from, so a checkpoint written in between can no longer make a read return less than the caller saw committed.
Changes
RepoHandle::read_log&Manifestand reads through it;read_log_implis removedhead_seqRepoHandle::fresh_manifestread_log_implrefs_at_seq,refs_as_of,replay_refshandle.manifest()&Manifestcheckpoint_times,learn_checkpoint_timesself.manifest()&Manifest;apply_deltapasses the manifest it adoptsbridge.rs,settings.rs,web/ui.rs,settings_cmd.rs,wal_cmd.rs,snapshots.rsread_log/refs_at_seqwith bounds from another manifesttests/events.rsbridge_publishes_entries_a_concurrent_checkpoint_foldsread_log(1, None)read_log(&handle.manifest(), 1, None), orfresh_manifest()where another handle wrotePrinciples
sync_refsorfresh_manifest(), and now reads the log through that one snapshot rather than a second, later one.walgit settings historyeach made two conditional manifest GETs (the sync, thenread_log's own) and now make one;walgit wal lsandwal showstill make one. No budget indocs/ROUNDTRIPS.mdchanges.Testing
wal.freshness_ttlso it keeps the manifest it holds; it publishes seq 1, takes a push at seq 2, and a sibling instance on the same store writes a checkpoint folding seq 2 beforecatch_up. Before the fixcatch_upreports(from 1, head 2, emitted 0, gap None); with it, seq 2 is published and the cursor reaches 2. It failed 10 of 10 runs against the oldread_logand passed 20 of 20 with the fix; no sleeps or timing.just warnings,just clippyandjust sim(19 passed) are clean.just test: 429 passed; the 3 failures areapi_v1::v1_surface_and_browser_lane,static_http::ui_assets_etag_304_and_precompressedandweb_ui::assets_have_content_type_and_immutable_cache, which needweb/distfrompnpm run buildand fail the same way onmainwithout it.cargo fmt --checkis clean.refs_at_seq,refs_as_ofandapply_deltahave no added test: their two reads happened inside one call, so a test cannot interleave them; with one manifest passed in, the window no longer exists.Not in this PR
catch_upstarts) is still counted and warned rather than published.walgit wal rewindalready reads folded segments directly aslog/<seq>.pb, so the bridge could backfill the same way.sync_objects, andremote_reader'sAlreadyRunningpath returning a reader without checking its revision), andcompact_repoplanning from a config read separately from the manifest it compacts.