Skip to content

Serialize ephemeral_savepoint() against the first table-open#1297

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

Serialize ephemeral_savepoint() against the first table-open#1297
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 ephemeral_savepoint() and the first table-open of the same
transaction can run on different threads. ephemeral_savepoint() checks the dirty flag and then
registers the savepoint, while TableNamespace::set_dirty() sets the dirty flag and then, if no
savepoint exists, disables allocation tracking. Their opposite-order accesses to the dirty flag
and the savepoint set can interleave:

  1. ephemeral_savepoint() reads dirty == false
  2. set_dirty() stores dirty = true, observes no savepoint, and sets allocation tracking to Ignore
  3. ephemeral_savepoint() registers the savepoint

That leaves a live savepoint with allocation tracking disabled.

Impact

A later restore_savepoint() relies on that tracking to free the pages allocated since the
savepoint; with it Ignored, those pages are leaked and reclaimed only by a full repair
(quick-repair does not recover them). A leak, not corruption. This is the concurrent sibling of
the drop-vs-commit race fixed in #1296.

Fix

set_dirty() always runs under the tables lock, so take that same lock across the dirty check and
savepoint registration in ephemeral_savepoint(). This orders the two operations: either the
savepoint is registered before set_dirty() checks for it (tracking stays enabled), or
set_dirty() has already set the dirty flag and the savepoint call returns InvalidSavepoint.

Testing

  • New regression test ephemeral_savepoint_racing_first_open_no_leak races several
    savepoint-creating threads against one table-opening thread across many iterations, then
    restores a savepoint; check_integrity()'s allocator rebuild reports any leaked (allocated but
    unreachable) page. It reliably fails 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

ephemeral_savepoint() checks the dirty flag and then registers the savepoint,
while TableNamespace::set_dirty() sets the dirty flag and then, if no savepoint
exists, disables allocation tracking. Because WriteTransaction is Sync, the two
can run on different threads, and their opposite-order accesses to the dirty
flag and the savepoint set can interleave: the savepoint call reads
dirty == false, set_dirty() then stores dirty, observes no savepoint, and sets
allocation tracking to Ignore, and only then does the savepoint call register
the savepoint.

That leaves a live savepoint with allocation tracking disabled. A later
restore_savepoint() relies on that tracking to free the pages allocated since
the savepoint; with it Ignored, those pages are leaked and are reclaimed only
by a full repair (quick-repair does not recover them).

set_dirty() always runs under the tables lock, so take that same lock across
the dirty check and savepoint registration in ephemeral_savepoint(). This
orders the two operations: either the savepoint is registered before
set_dirty() checks for it (tracking stays enabled), or set_dirty() has already
set the dirty flag and the savepoint call returns InvalidSavepoint.

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

codecov Bot commented Jul 7, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.21%. Comparing base (c8545cf) to head (2d28bb7).

Additional details and impacted files
@@           Coverage Diff           @@
##           master    #1297   +/-   ##
=======================================
  Coverage   90.20%   90.21%           
=======================================
  Files          36       36           
  Lines       16328    16330    +2     
=======================================
+ Hits        14729    14732    +3     
+ Misses       1599     1598    -1     
Files with missing lines Coverage Δ
src/transactions.rs 88.66% <100.00%> (+0.07%) ⬆️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@cberner cberner merged commit b6b56aa into master Jul 7, 2026
8 checks passed
@cberner cberner deleted the claude/writetransaction-race-conditions-lfge2u branch July 7, 2026 23:31
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