Skip to content

Clamp post-commit free horizon to the savepoint purge horizon#1296

Merged
cberner merged 1 commit into
masterfrom
claude/writetransaction-race-conditions-lfge2u
Jul 7, 2026
Merged

Clamp post-commit free horizon to the savepoint purge horizon#1296
cberner merged 1 commit into
masterfrom
claude/writetransaction-race-conditions-lfge2u

Conversation

@cberner

@cberner cberner commented Jul 7, 2026

Copy link
Copy Markdown
Owner

Summary

WriteTransaction is Sync, so an ephemeral Savepoint can be dropped from a different
thread than the one committing the transaction that created it. During a durable commit:

  • flush_data_allocated_pages() purges DATA_ALLOCATED_TABLE down to the oldest savepoint's
    transaction, keeping the entries that savepoint pins.
  • The post-commit free epilogue (process_data_freed_pages_after_commit()) frees
    DATA_FREED_TABLE pages up to a horizon derived from the live read transactions.

A savepoint is also a live read, so absent concurrency the two horizons agree. But if the
ephemeral Savepoint::drop lands between the purge and the epilogue, it removes the
savepoint's read reference and the epilogue's horizon advances past the savepoint the purge
accounted for. The epilogue then frees pages that surviving DATA_ALLOCATED_TABLE entries
still name. This is the concurrent-drop sibling of the staged-deletion bug fixed in d098d3b.

Impact

check_integrity() hits the assert! in check_repaired_allocated_pages_table, i.e. a
debug-build panic / false corruption report. No durable corruption is reachable: the stale
entries are always for transactions strictly older than any restorable savepoint (the drop
only advances the horizon when the dropped savepoint was the sole oldest live read, so the
over-freed pages are older than the next oldest read, hence older than every surviving
savepoint), and restore_savepoint() only reads DATA_ALLOCATED_TABLE entries newer than the
restored savepoint. The next durable commit re-purges the stale entries regardless.

Fix

Return the purge horizon from flush_data_allocated_pages() and clamp the epilogue's free
horizon to it. The clamp is a no-op in every non-racy case (a live savepoint always bounds
oldest_live_read_transaction()); a concurrently-dropped savepoint's entries are simply
reclaimed by the next durable commit.

Testing

  • New deterministic regression test check_integrity_ephemeral_savepoint_drop_racing_commit
    uses the existing BlockingSyncBackend to pause the commit at its fsync (between the purge
    and the epilogue), drops the savepoint in that window, then asserts check_integrity(). It
    panics at check_repaired_allocated_pages_table without the fix and passes with it.
  • cargo test --all-features, cargo fmt --check, and
    cargo clippy --all-targets --all-features are all clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_019rKhoFerkDeoUn1DqpJn3t


Generated by Claude Code

An ephemeral Savepoint can be dropped from a different thread than the one
committing the WriteTransaction that created it, since WriteTransaction is
Sync. During a durable commit, flush_data_allocated_pages() purges
DATA_ALLOCATED_TABLE down to the oldest savepoint's transaction, keeping the
entries that savepoint pins. The post-commit epilogue then frees
DATA_FREED_TABLE pages up to a horizon derived from the live read
transactions. A savepoint is also a live read, so absent concurrency the two
horizons agree.

If the ephemeral Savepoint::drop lands between the purge and the epilogue, it
removes the savepoint's read reference, so the epilogue's horizon advances
past the savepoint the purge accounted for. The epilogue then frees pages that
the surviving DATA_ALLOCATED_TABLE entries still name, so check_integrity()
hits the assertion in check_repaired_allocated_pages_table (a debug-build
panic / false corruption report). This is the concurrent-drop sibling of the
staged-deletion bug fixed in d098d3b. No durable corruption is reachable: the
next durable commit re-purges the stale entries.

Capture the purge horizon in flush_data_allocated_pages() and clamp the
epilogue's free horizon to it. The clamp is a no-op in every non-racy case; a
dropped savepoint's entries are simply reclaimed by the next durable commit.

Assisted-by: Claude Code
https://claude.ai/code/session_019rKhoFerkDeoUn1DqpJn3t
@codecov

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.30769% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 90.21%. Comparing base (b2da02d) to head (ebd0b52).
⚠️ Report is 1 commits behind head on master.

Files with missing lines Patch % Lines
src/transactions.rs 92.30% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##           master    #1296      +/-   ##
==========================================
- Coverage   90.21%   90.21%   -0.01%     
==========================================
  Files          36       36              
  Lines       16318    16326       +8     
==========================================
+ Hits        14722    14728       +6     
- Misses       1596     1598       +2     
Files with missing lines Coverage Δ
src/transactions.rs 88.59% <92.30%> (-0.01%) ⬇️

... and 1 file with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@cberner cberner merged commit c8545cf into master Jul 7, 2026
8 checks passed
@cberner cberner deleted the claude/writetransaction-race-conditions-lfge2u branch July 7, 2026 22:53
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant