fix(blob-store): keep undurable frees pending on write rollback - #39
Merged
Conversation
`release_reserved_slots` returns the slots a failed write reserved, but it did so through `Manifest::trim_trailing_free_slots`, whose first statement drains `pending_free_slots` into the reusable pool. Those slots belong to other guids' superseded generations: the last durable manifest still references them and no reader has been drained off them. Publishing them there bypasses both fences `flush_locked` applies — the manifest-delta durability fence and the `enter_slot_write` reader drain. A later write can then land on a slot the durable manifest still maps to a live guid, so a crash before the next successful flush resolves that guid to another blob's frame. Nothing detects it: there is no frame checksum, and the header's `blob_guid` is written but never read back. Split the trim in two. `trim_trailing_reusable_slots` lowers the high-water mark over already-reusable slots only; `trim_trailing_free_slots` keeps its publish-then-trim behaviour for `flush_locked` and `vacuum`. The rollback path uses the former. Only the I/O-error path changes: superseded slots now stay pending until the next successful flush, which is the only place both fences hold. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Signed-off-by: wchwawa <wch19961116@gmail.com>
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.
What changed
release_reserved_slotsreturns the slots a failed write reserved. It did thatthrough
Manifest::trim_trailing_free_slots, whose first statement ispublish_pending_free_slots()— so a single failed write also promoted theentire
pending_free_slotsqueue into the reusable pool.Those slots are not the caller's to publish. They belong to other guids'
superseded generations: the last durable manifest still references them, and no
reader has been drained off them. Publishing them there bypasses both fences
flush_lockedapplies (persist_pending_deltas, thenenter_slot_write()).Consequence, with no concurrency required:
write_blob(A, v1);flush()→ durable manifest mapsA → slot 0write_blob(A, v2)→ shadow slot 1 published in memory,pending_free = [0], no flushpwrite/ensure_data_capacityfails → rollback drains[0]into the allocatorA → slot 0, andread_blob(A)returns the other blob's frameNothing downstream detects it: there is no frame checksum, and
BlobHeader::blob_guidis written (
set_frame_blob_guid) but never read back anywhere in the crate.delete_blobalso feedspending_free_slots, so a delete plus one failed write isenough — the trigger is any genuine device error inside the store (ENOSPC, EIO, EDQUOT).
The fix splits the trim in two.
trim_trailing_reusable_slotslowers the high-watermark over already-reusable slots only;
trim_trailing_free_slotskeeps itspublish-then-trim behaviour for
flush_lockedandvacuum. The rollback path usesthe former.
Only the I/O-error path changes: superseded slots stay pending until the next
successful flush, which is the only place both fences hold.
Test plan
New regression test
rolling_back_reserved_slots_keeps_undurable_frees_pending,the error-path sibling of the existing
replaced_slot_is_reused_only_after_manifest_flush.Verified it fails on the parent commit and passes here:
It drives
release_reserved_slotsdirectly — the same call thepwriteand capacityerror arms make — because the failure it rolls back is a device error raised inside
FileBlobStore, below the injectableBlobStoreboundary thattests/checkpoint_failpoint.rsuses.Full local run on macOS (
aarch64-apple-darwin):cargo fmt --all --check— passcargo clippy --workspace --all-targets --all-features --locked -- -D warnings— passcargo test --workspace --all-features --lib --tests --locked— 659 passed, 0 failedcargo test --workspace --all-features --doc --locked— passRUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps— passNo on-disk layout change, no public API change, no
unsafetouched.Related
Follows #38 / a97630c ("Make blob rewrites crash-safe"), which introduced shadow
paging and the
pending_free_slots/reusable_slotssplit. This closes therollback path that the success path already pinned in
replaced_slot_is_reused_only_after_manifest_flushandmanifest_flush_drains_old_slot_readers_before_reuse.Also relevant to TESTING.md's "Known Gaps" entry on host-level disk-full / ENOSPC
coverage: that is exactly the class of failure that reaches this path.
🤖 Generated with Claude Code