Repository navigation
Conversation
|
@archang19 has exported this pull request. If you are a Meta employee, you can view the originating Diff in D126882379. |
✅ clang-tidy: No findings on changed linesCompleted in 383.5s. |
Summary: **Bug:** with WAL recycling on (`recycle_log_file_num > 0`), RocksDB can lose writes that were already acknowledged, including `sync` writes. **Why:** `FindObsoleteFiles()` puts an old WAL A on the recycle list and only closes A's writer later, after releasing the mutexes. If a memtable switch happens in that window, A is renamed and reused as the new WAL C, so A's still-open writer and C point at the same file. When the old writer finally closes, `PosixWritableFile::Close()` truncates the file to A's old size and wipes out what was written to C. The next reopen recovers from the truncated WAL, and those writes are gone. In the test, C went from 65,589 bytes to 32. With `background_close_inactive_wals = true`, two more paths (`FindObsoleteFiles()` and `MarkLogsSynced()`) take A out of `logs_` before its writer is closed, so the same reuse can happen. **Fix:** never reuse a WAL whose writer may still be open. - `SwitchMemtable()` skips recycling a WAL that is still in `logs_` (`IsWalInLogs()`) and creates a fresh WAL that time. - With recycling on, `FindObsoleteFiles()` and `MarkLogsSynced()` close a WAL's writer before removing it from `logs_`. Differential Revision: D126882379
62ba902 to
b8fce73
Compare
Claude Code Review - OBSOLETESuperseded by a newer AI review. Expand to see the original review.✅ Claude Code ReviewAuto-triggered after CI passed — reviewing commit b8fce73 SummaryThis PR correctly diagnoses and fixes a real data-loss race condition where WAL recycling can truncate acknowledged sync writes. The fix is well-scoped, the approach is sound, and the locking is correct. The test exercises the key race windows for both High-severity findings (0): No high-severity findings. Full review (click to expand)Findings🔴 HIGHNone. 🟡 MEDIUMM1. Stalled recycling when
|
| Context | Affected? | Analysis |
|---|---|---|
two_write_queues |
Yes | SwitchMemtableImpl acquires/releases wal_write_mutex_ for wal_empty_ (lines 3286-3292), then IsWalInLogs acquires it again. These are independent lock regions with no state dependency — safe. |
background_close_inactive_wals=true |
Yes (primary bug path) | Both MarkLogsSynced and FindObsoleteFiles fixes apply. The MarkLogsSynced fix prevents opportunistic removal from logs_ with open writer. FindObsoleteFiles fix ensures writer is closed before removal. |
background_close_inactive_wals=false |
Yes (secondary bug path) | FindObsoleteFiles already closed writer before removal (original behavior unchanged). SwitchMemtableImpl IsWalInLogs check adds defense-in-depth. |
| ReadOnly/Secondary DB | No | No WAL writing or recycling. |
| Recovery | No | wal_recycle_files_ is empty during recovery. |
| Concurrent flushes | Safe | Serialized by mutex_. |
TOCTOU Analysis
The IsWalInLogs() check at line 3298 runs under mutex_. Between this check and CreateWAL() at line 3356, mutex_ is released (line 3335). Two scenarios:
- IsWalInLogs returns true → recycling skipped: Safe.
recycle_log_number = 0, fresh WAL created. - IsWalInLogs returns false → recycling proceeds: The WAL's writer is closed and removed from
logs_. Between the check and the actual rename inCreateWAL, could the writer be re-opened? No — WAL numbers are monotonically increasing and never re-enterlogs_. Could the file be deleted? No — it's inwal_recycle_files_which protects it from purge. Safe.
DB Mutex Impact
- Before:
SwitchMemtableImpldid not acquirewal_write_mutex_at line 3298. - After: One additional
wal_write_mutex_acquire/release + linear scan oflogs_(typically 1-3 entries). - Assessment: Negligible. The function already acquires
wal_write_mutex_at lines 3286 and 3338. The added work is O(1) in practice and occurs only when recycling is enabled and the recycle queue is non-empty.
Lock Ordering Verification
db_impl.h:3561-3562: "if needed to acquire both wal_write_mutex_ and mutex_, the order should be first mutex_ and then wal_write_mutex_." IsWalInLogs is called with mutex_ held and acquires wal_write_mutex_ internally — correct order.
Positive Observations
- The fix is minimal and well-targeted — three small changes that close the race from both directions (prevent premature removal from
logs_, and checklogs_before recycling). - Defense-in-depth: the
IsWalInLogscheck inSwitchMemtableImplis a safety net even if theFindObsoleteFilesandMarkLogsSyncedchanges handle the common paths. - The test uses sync points and
std::promise/std::futurefor deterministic synchronization — no sleeps. - The test covers 4 combinations of
background_close_inactive_wals×sync_old_wal_before_flushwithSCOPED_TRACE. - The release note is clear, concise, and accurately describes the user impact.
ℹ️ About this response
Generated by Claude Code.
Review methodology: claude_md/ci_review_prompt.md
Limitations:
- Claude may miss context from files not in the diff
- Large PRs may be truncated
- Always apply human judgment to AI suggestions
Commands:
/claude-review [context]— Request a code review/claude-query <question>— Ask about the PR or codebase
Summary: X-link: meta#15458 **Bug:** with WAL recycling on (`recycle_log_file_num > 0`), RocksDB can lose writes that were already acknowledged, including `sync` writes. **Why:** `FindObsoleteFiles()` puts an old WAL A on the recycle list and only closes A's writer later, after releasing the mutexes. If a memtable switch happens in that window, A is renamed and reused as the new WAL C, so A's still-open writer and C point at the same file. When the old writer finally closes, `PosixWritableFile::Close()` truncates the file to A's old size and wipes out what was written to C. The next reopen recovers from the truncated WAL, and those writes are gone. In the test, C went from 65,589 bytes to 32. With `background_close_inactive_wals = true`, two more paths (`FindObsoleteFiles()` and `MarkLogsSynced()`) take A out of `logs_` before its writer is closed, so the same reuse can happen. **Fix:** never reuse a WAL whose writer may still be open. - `SwitchMemtable()` skips recycling a WAL that is still in `logs_` (`IsWalInLogs()`) and creates a fresh WAL that time. - With recycling on, `FindObsoleteFiles()` and `MarkLogsSynced()` close a WAL's writer before removing it from `logs_`. Reviewed By: xingbowang Differential Revision: D126882379
b8fce73 to
f8920da
Compare
✅ Claude Code ReviewAuto-triggered after CI passed — reviewing commit f8920da SummaryThis PR correctly identifies and fixes a real data-loss race condition in WAL recycling. The fix is well-targeted: three coordinated changes ensure a WAL's writer is closed before the WAL can be recycled. The approach is sound, the locking is correct, and the test coverage is good. No high-severity issues found. High-severity findings (0): Full review (click to expand)Findings🟡 MEDIUMM1.
|
| Context | Code executes? | Assumptions hold? | Action needed? |
|---|---|---|---|
| Normal (single write queue) | YES | YES — mutex_ held when checking wal_recycle_files_ and calling IsWalInLogs |
None |
two_write_queues_ |
YES | YES — wal_write_mutex_ briefly held/released at 3300-3307, not held when IsWalInLogs called at 3313 |
None |
background_close_inactive_wals=true + recycling |
YES | YES — both MarkLogsSynced and FindObsoleteFiles changes prevent premature removal from logs_ |
None |
background_close_inactive_wals=false + recycling |
YES | YES — FindObsoleteFiles already closed writers before this fix; the new condition preserves this behavior |
None |
recycle_log_file_num=0 (no recycling) |
Code partially executes | YES — IsWalInLogs never called (outer condition at 3312 checks recycle_log_file_num); MarkLogsSynced/FindObsoleteFiles changes are no-ops when recycling is off |
None |
| WAL error recovery | Not for recycling | Safe — failed WALs are recent, not in wal_recycle_files_ |
None |
| ReadOnly/Secondary DB | NO | N/A | None |
| 2PC | YES | YES — same WAL lifecycle | None |
Assumption Stress Test
Claim: "A WAL still in logs_ means its writer may still be open"
- Precondition 1: WAL is in
logs_→ writer object exists (viaReleaseWritercontract) - Precondition 2: Writer's file may still be open (
file() != nullptr) - Counterexample: WAL is in
logs_but writer was already closed viaFindObsoleteFiles's close path, followed byPublishIfClosed()→file()returns nullptr. In this case,IsWalInLogsreturns true but the writer is already closed. This is conservative but safe — we skip recycling unnecessarily, creating a fresh WAL. The nextFindObsoleteFilesremoves it fromlogs_, and subsequent recycling attempts succeed. - Verdict: The claim is intentionally conservative. No bug.
Claim: "Skipping recycling when IsWalInLogs returns true prevents data loss"
- Precondition: The WAL's writer close (with ftruncate) must happen BEFORE recycling
- The fix ensures either: (a)
IsWalInLogsreturns true → skip recycling → safe, or (b)IsWalInLogsreturns false → WAL already removed fromlogs_→ its writer already freed → Close/ftruncate already happened → safe to recycle - TOCTOU analysis: Between
IsWalInLogsreturning false (line 3313,mutex_held) andCreateWAL(line 3371,mutex_released at 3350): Could the WAL's writer somehow not have been closed? No — if WAL is not inlogs_, its writer was transferred towals_to_free_. For theFindObsoleteFilespath,Close({})is called explicitly at line 454 BEFORE removing fromlogs_. For theMarkLogsSyncedpath with the fix, the WAL stays inlogs_untilFindObsoleteFileshandles it. So by the timeIsWalInLogsreturns false,Closehas already been called. - Verdict: The TOCTOU is safe.
Positive Observations
- Root cause analysis is excellent. The PR description clearly identifies the race window and the mechanism (
ftruncateinPosixWritableFile::Close). - Three-pronged fix is comprehensive. The fix addresses all three paths where a WAL could leave
logs_with an open writer:SwitchMemtableImpl(defense at consumption),FindObsoleteFiles(close before removal), andMarkLogsSynced(prevent premature removal). - Test covers all relevant dimensions. The new test iterates over pause points,
background_close_inactive_walssettings, and sync-before-flush scenarios. It verifies data survives reopen. - Existing test fixes are correct. The
TEST_WaitForBackgroundWork()additions address latent timing assumptions exposed by the fix. - Release note is appropriately concise and describes the user-facing impact.
- Lock ordering is correct throughout all changes.
ℹ️ About this response
Generated by Claude Code.
Review methodology: claude_md/ci_review_prompt.md
Limitations:
- Claude may miss context from files not in the diff
- Large PRs may be truncated
- Always apply human judgment to AI suggestions
Commands:
/claude-review [context]— Request a code review/claude-query <question>— Ask about the PR or codebase
|
This pull request has been merged in 02815ee. |
Summary:
X-link: #15458
Bug: with WAL recycling on (
recycle_log_file_num > 0), RocksDB can lose writes that were already acknowledged, includingsyncwrites.Why:
FindObsoleteFiles()puts an old WAL A on the recycle list and only closes A's writer later, after releasing the mutexes. If a memtable switch happens in that window, A is renamed and reused as the new WAL C, so A's still-open writer and C point at the same file. When the old writer finally closes,PosixWritableFile::Close()truncates the file to A's old size and wipes out what was written to C. The next reopen recovers from the truncated WAL, and those writes are gone. In the test, C went from 65,589 bytes to 32.With
background_close_inactive_wals = true, two more paths (FindObsoleteFiles()andMarkLogsSynced()) take A out oflogs_before its writer is closed, so the same reuse can happen.Fix: never reuse a WAL whose writer may still be open.
SwitchMemtable()skips recycling a WAL that is still inlogs_(IsWalInLogs()) and creates a fresh WAL that time.FindObsoleteFiles()andMarkLogsSynced()close a WAL's writer before removing it fromlogs_.Reviewed By: xingbowang
Differential Revision: D126882379