Observability for the WriteBufferManager (#15162) - #15162
Conversation
|
@rban1 has exported this pull request. If you are a Meta employee, you can view the originating Diff in D114903210. |
|
| Check | Count |
|---|---|
cert-err58-cpp |
8 |
| Total | 8 |
Details
db/internal_stats.cc (8 warning(s))
db/internal_stats.cc:327:26: warning: initialization of 'write_buffer_manager_memory_usage' with static storage duration may throw an exception that cannot be caught [cert-err58-cpp]
db/internal_stats.cc:329:26: warning: initialization of 'write_buffer_manager_mutable_memory_usage' with static storage duration may throw an exception that cannot be caught [cert-err58-cpp]
db/internal_stats.cc:331:26: warning: initialization of 'write_buffer_manager_buffer_size' with static storage duration may throw an exception that cannot be caught [cert-err58-cpp]
db/internal_stats.cc:333:26: warning: initialization of 'write_buffer_manager_stall_active' with static storage duration may throw an exception that cannot be caught [cert-err58-cpp]
db/internal_stats.cc:449:35: warning: initialization of 'kWriteBufferManagerMemoryUsage' with static storage duration may throw an exception that cannot be caught [cert-err58-cpp]
db/internal_stats.cc:451:35: warning: initialization of 'kWriteBufferManagerMutableMemoryUsage' with static storage duration may throw an exception that cannot be caught [cert-err58-cpp]
db/internal_stats.cc:453:35: warning: initialization of 'kWriteBufferManagerBufferSize' with static storage duration may throw an exception that cannot be caught [cert-err58-cpp]
db/internal_stats.cc:455:35: warning: initialization of 'kWriteBufferManagerStallActive' with static storage duration may throw an exception that cannot be caught [cert-err58-cpp]
Claude Code Review - OBSOLETESuperseded by a newer AI review. Expand to see the original review.✅ Claude Code ReviewAuto-triggered after CI passed — reviewing commit 1877265 SummaryWell-structured PR adding meaningful observability and a cross-DB flush policy to WriteBufferManager. The design is sound with careful attention to liveness, lock ordering, and shutdown safety. One build-breaking bug (missing ticker in TickersNameMap) and a few style/guideline issues. High-severity findings (1):
Full review (click to expand)Findings🔴 HIGHH1. Missing
|
| Context | Does code execute? | Assumptions hold? | Action needed? |
|---|---|---|---|
| WritePreparedTxnDB | YES (shares PreprocessWrite) | YES (flush policy is orthogonal to txn visibility) | Safe |
| ReadOnly DB | NO (MaybeRegisterFlushInitiator returns early) | N/A | Safe |
| User-defined timestamps | YES | YES (flush selection doesn't depend on key format) | Safe |
| MemPurge | YES | YES (mutable memtable memory is still tracked) | Safe |
| BlobDB | YES | YES (memtable memory tracked regardless of value location) | Safe |
| Concurrent writers | YES | YES (write thread join is serialized) | Safe |
| FIFO / Universal compaction | YES | YES (flush policy is orthogonal to compaction style) | Safe |
| Atomic flush | YES (special-cased throughout) | YES (total_mem vs largest_mem distinction) | Safe |
| PauseBackgroundWork | YES (checked in GetFlushableMemUsage and ScheduleWriteBufferManagerFlush) | YES | Safe |
| WaitForCompact | YES (bg_wbm_flush_scheduled_ added to wait condition) | YES | Safe |
Positive Observations
- Liveness argument is carefully constructed: The "self-flush before stall" pattern in
PreprocessWritecorrectly handles the case where a deferred flush to another DB might not complete, preventing global deadlock. - Lock ordering is well-documented: The registry mutex -> DB mutex ordering is stated in comments and enforced by the code structure.
CollectFlushableCFsconsolidation: Extracting the CF scanning logic into a single function ensures the bid (GetFlushableMemUsage) and the action (BackgroundCallWBMFlush) agree on what gets flushed.- Test coverage is excellent: The tests cover key edge cases including stalled DBs, write-stopped DBs, read-only DBs, atomic flush DBs, queued job cancellation on close, and the self-flush-before-stall liveness property.
- Non-blocking write thread join: The
EnterUnbatchedNonBlockingimplementation correctly handles both the "stall already active" case (via LinkOne's no_slowdown check) and the "stall begins while queued" case (via AwaitState with STATE_COMPLETED mask + BeginWriteStall sweep). - Shutdown safety: The
bg_wbm_flush_scheduled_counter is properly included in all wait conditions (CloseHelper, WaitForBackgroundWork, PauseBackgroundWork, WaitForCompact).
ℹ️ 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: Today a `WriteBufferManager` is nearly invisible. Its own state (`memory_usage()`, `mutable_memtable_memory_usage()`, `buffer_size()`, `IsStallActive()`) is exported nowhere -- no DB property, no ticker, no LOG line except a single message emitted when an in-line flush actually fires. Time spent blocked on the WBM limit is likewise unmeasured: `STALL_MICROS`, `rocksdb.db.write.stall`, and `db.user_write_stall_micros` all come from `DelayWrite()`, which only covers `WriteController` (CF-scope) stalls -- see the existing TODO on `kIntStatsWriteStallMicros`. And because every existing memtable counter is per-DB, a manager shared across several DBs cannot be observed as a single entity at all. This diff adds the missing instrumentation. Four DB properties reporting the shared manager's totals, so one DB can report for the whole manager: - `rocksdb.write-buffer-manager-memory-usage` -- all accounted memory, including memtables pinned by in-flight flushes - `rocksdb.write-buffer-manager-mutable-memory-usage` -- the mutable portion; the gap between the two is flush-in-flight memory - `rocksdb.write-buffer-manager-buffer-size` -- the budget (flush at 7/8, stall at 1/1) - `rocksdb.write-buffer-manager-stall-active` They follow the `rocksdb.block-cache-*` pattern exactly (`GetWriteBufferManagerForStats()` mirrors `GetBlockCacheForStats()`), and return false -- i.e. the property is unavailable -- when no `WriteBufferManager` is configured. Stall duration is now measured in `WriteBufferManagerStallWrites()` and recorded two ways: a new `WRITE_BUFFER_MANAGER_STALL_MICROS` ticker (`rocksdb.write_buffer_manager.stall.micros`) and a new `kIntStatsWriteBufferManagerStallMicros` DB stat (`db.write_buffer_manager_stall_micros`). The `InternalStats` copy matters because it lands in `rocksdb.dbstats` and the periodic LOG dump, so it is visible without a `Statistics` object -- which is the common case, since the fb_rocksdb wrapper does not export tickers unless `--rocksdb_fb303_enable_stats=true`. It is deliberately kept separate from `STALL_MICROS` so WBM and `WriteController` stalls stay individually attributable. Finally, the cross-DB flush path added in D112396106 now logs which column family it picked, matching the log line the in-line path already had. Note on Java: the `TickerType` byte enum is exhausted (`FILE_SUBMIT_ASYNC_READ_FALLBACK` occupies `-0x80`, the last slot, and `portal.h` carries a TODO that the ticker count no longer fits in a `jbyte`), so the new ticker is intentionally not mirrored there. This is safe: `toJavaTickerType()` -- the only place an unmapped ticker could alias to `0x0` -- has no callers, and Java clients can only reach tickers they can name. Differential Revision: D114903210
1877265 to
ccbfa44
Compare
Summary: Today a `WriteBufferManager` is nearly invisible. Its own state (`memory_usage()`, `mutable_memtable_memory_usage()`, `buffer_size()`, `IsStallActive()`) is exported nowhere -- no DB property, no ticker, no LOG line except a single message emitted when an in-line flush actually fires. Time spent blocked on the WBM limit is likewise unmeasured: `STALL_MICROS`, `rocksdb.db.write.stall`, and `db.user_write_stall_micros` all come from `DelayWrite()`, which only covers `WriteController` (CF-scope) stalls -- see the existing TODO on `kIntStatsWriteStallMicros`. And because every existing memtable counter is per-DB, a manager shared across several DBs cannot be observed as a single entity at all. This diff adds the missing instrumentation. Four DB properties reporting the shared manager's totals, so one DB can report for the whole manager: - `rocksdb.write-buffer-manager-memory-usage` -- all accounted memory, including memtables pinned by in-flight flushes - `rocksdb.write-buffer-manager-mutable-memory-usage` -- the mutable portion; the gap between the two is flush-in-flight memory - `rocksdb.write-buffer-manager-buffer-size` -- the budget (flush at 7/8, stall at 1/1) - `rocksdb.write-buffer-manager-stall-active` They follow the `rocksdb.block-cache-*` pattern exactly (`GetWriteBufferManagerForStats()` mirrors `GetBlockCacheForStats()`), and return false -- i.e. the property is unavailable -- when no `WriteBufferManager` is configured. Stall duration is now measured in `WriteBufferManagerStallWrites()` and recorded two ways: a new `WRITE_BUFFER_MANAGER_STALL_MICROS` ticker (`rocksdb.write_buffer_manager.stall.micros`) and a new `kIntStatsWriteBufferManagerStallMicros` DB stat (`db.write_buffer_manager_stall_micros`). The `InternalStats` copy matters because it lands in `rocksdb.dbstats` and the periodic LOG dump, so it is visible without a `Statistics` object -- which is the common case, since the fb_rocksdb wrapper does not export tickers unless `--rocksdb_fb303_enable_stats=true`. It is deliberately kept separate from `STALL_MICROS` so WBM and `WriteController` stalls stay individually attributable. Finally, the cross-DB flush path added in D112396106 now logs which column family it picked, matching the log line the in-line path already had. Note on Java: the `TickerType` byte enum is exhausted (`FILE_SUBMIT_ASYNC_READ_FALLBACK` occupies `-0x80`, the last slot, and `portal.h` carries a TODO that the ticker count no longer fits in a `jbyte`), so the new ticker is intentionally not mirrored there. This is safe: `toJavaTickerType()` -- the only place an unmapped ticker could alias to `0x0` -- has no callers, and Java clients can only reach tickers they can name. Differential Revision: D114903210
ccbfa44 to
d69dee0
Compare
facebook#15047) Summary: Adds configurable WriteBufferManager flush policies to reclaim memory more efficiently. The existing oldest-memtable policy remains the default; new policies flush the largest memtable within one DB or across all DBs sharing the manager. Cross-DB flushes run asynchronously and avoid blocking on stalled write threads. Differential Revision: D112396106
Summary: Today a `WriteBufferManager` is nearly invisible. Its own state (`memory_usage()`, `mutable_memtable_memory_usage()`, `buffer_size()`, `IsStallActive()`) is exported nowhere -- no DB property, no ticker, no LOG line except a single message emitted when an in-line flush actually fires. Time spent blocked on the WBM limit is likewise unmeasured: `STALL_MICROS`, `rocksdb.db.write.stall`, and `db.user_write_stall_micros` all come from `DelayWrite()`, which only covers `WriteController` (CF-scope) stalls -- see the existing TODO on `kIntStatsWriteStallMicros`. And because every existing memtable counter is per-DB, a manager shared across several DBs cannot be observed as a single entity at all. This diff adds the missing instrumentation. Four DB properties reporting the shared manager's totals, so one DB can report for the whole manager: - `rocksdb.write-buffer-manager-memory-usage` -- all accounted memory, including memtables pinned by in-flight flushes - `rocksdb.write-buffer-manager-mutable-memory-usage` -- the mutable portion; the gap between the two is flush-in-flight memory - `rocksdb.write-buffer-manager-buffer-size` -- the budget (flush at 7/8, stall at 1/1) - `rocksdb.write-buffer-manager-stall-active` They follow the `rocksdb.block-cache-*` pattern exactly (`GetWriteBufferManagerForStats()` mirrors `GetBlockCacheForStats()`), and return false -- i.e. the property is unavailable -- when no `WriteBufferManager` is configured. Stall duration is now measured in `WriteBufferManagerStallWrites()` and recorded two ways: a new `WRITE_BUFFER_MANAGER_STALL_MICROS` ticker (`rocksdb.write_buffer_manager.stall.micros`) and a new `kIntStatsWriteBufferManagerStallMicros` DB stat (`db.write_buffer_manager_stall_micros`). The `InternalStats` copy matters because it lands in `rocksdb.dbstats` and the periodic LOG dump, so it is visible without a `Statistics` object -- which is the common case, since the fb_rocksdb wrapper does not export tickers unless `--rocksdb_fb303_enable_stats=true`. It is deliberately kept separate from `STALL_MICROS` so WBM and `WriteController` stalls stay individually attributable. Finally, the cross-DB flush path added in D112396106 now logs which column family it picked, matching the log line the in-line path already had. Note on Java: the `TickerType` byte enum is exhausted (`FILE_SUBMIT_ASYNC_READ_FALLBACK` occupies `-0x80`, the last slot, and `portal.h` carries a TODO that the ticker count no longer fits in a `jbyte`), so the new ticker is intentionally not mirrored there. This is safe: `toJavaTickerType()` -- the only place an unmapped ticker could alias to `0x0` -- has no callers, and Java clients can only reach tickers they can name. Differential Revision: D114903210
d69dee0 to
0e40ee5
Compare
✅ Claude Code ReviewAuto-triggered after CI passed — reviewing commit 0e40ee5 SummaryWell-structured PR that adds meaningful observability to the WriteBufferManager and introduces a cross-DB flush registry. The non-blocking write thread join design correctly avoids the deadlock scenario. Lock ordering (registry -> DB mutex) is consistently maintained. The flush policy as a WBM-level (not DBOptions) setting is the right design for a shared manager. High-severity findings (1):
Full review (click to expand)Findings🔴 HIGHH1. UnscheduleWBMFlushCallback asserts mutex_ held, but unschedule callbacks are not guaranteed to run under mutex_ --
|
| Context | Does code execute? | Assumptions hold? | Action needed? |
|---|---|---|---|
| ReadOnly DB | YES (GetFlushableMemUsage returns 0) | YES | Safe -- MaybeRegisterFlushInitiator correctly skips |
| WritePreparedTxnDB | YES (via PreprocessWrite) | YES | No visibility interaction |
| Atomic flush DB | YES | YES | BackgroundCallWBMFlush handles both paths |
| Pipelined write | YES | YES | EnterUnbatchedNonBlocking calls WaitForMemTableWriters |
| two_write_queues_ | YES | YES (but reversal path untested) | See M4 |
| CompactedDBImpl | NO (read-only, never hits PreprocessWrite) | N/A | Safe |
| Shutdown during WBM flush | YES | YES | bg_wbm_flush_scheduled_ properly drained |
| CF dropped during WBM flush | YES | YES | BackgroundCallWBMFlush Refs the CF |
Positive Observations
- The three-layer eligibility checking (
GetFlushableMemUsage->ScheduleWriteBufferManagerFlush->BackgroundCallWBMFlush) provides thorough defense-in-depth against races. - The
unref_generated_candidateslambda inAtomicFlushMemTablesImplcorrectly reduces code duplication from the original three separate unref blocks. - Lock ordering (
flush_initiators_mu_->mutex_) is documented and consistently followed throughout. - Test coverage is comprehensive with good use of sync points for deterministic concurrency testing.
- The
FlushLargestAcrossDBsSelfFlushesBeforeStalltest specifically validates the deadlock-prevention claim. - The
EnterUnbatchedNonBlockingSweptOutByLaterStalltest validates the tricky swept-writer edge case. - Using LOW priority for WBM flushes correctly avoids competing with the flush pool (HIGH), meaning WBM flushes proceed even when the HIGH pool is saturated.
- The Java ticker exhaustion issue is handled correctly:
toJavaTickerTypehas no callers, so the unmapped ticker is safe.
ℹ️ 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:
Today a
WriteBufferManageris nearly invisible. Its own state (memory_usage(),mutable_memtable_memory_usage(),buffer_size(),IsStallActive()) is exported nowhere -- no DB property, no ticker, no LOG line except a single message emitted when an in-line flush actually fires. Time spent blocked on the WBM limit is likewise unmeasured:STALL_MICROS,rocksdb.db.write.stall, anddb.user_write_stall_microsall come fromDelayWrite(), which only coversWriteController(CF-scope) stalls -- see the existing TODO onkIntStatsWriteStallMicros. And because every existing memtable counter is per-DB, a manager shared across several DBs cannot be observed as a single entity at all.This diff adds the missing instrumentation.
Four DB properties reporting the shared manager's totals, so one DB can report for the whole manager:
rocksdb.write-buffer-manager-memory-usage-- all accounted memory, including memtables pinned by in-flight flushesrocksdb.write-buffer-manager-mutable-memory-usage-- the mutable portion; the gap between the two is flush-in-flight memoryrocksdb.write-buffer-manager-buffer-size-- the budget (flush at 7/8, stall at 1/1)rocksdb.write-buffer-manager-stall-activeThey follow the
rocksdb.block-cache-*pattern exactly (GetWriteBufferManagerForStats()mirrorsGetBlockCacheForStats()), and return false -- i.e. the property is unavailable -- when noWriteBufferManageris configured.Stall duration is now measured in
WriteBufferManagerStallWrites()and recorded two ways: a newWRITE_BUFFER_MANAGER_STALL_MICROSticker (rocksdb.write_buffer_manager.stall.micros) and a newkIntStatsWriteBufferManagerStallMicrosDB stat (db.write_buffer_manager_stall_micros). TheInternalStatscopy matters because it lands inrocksdb.dbstatsand the periodic LOG dump, so it is visible without aStatisticsobject -- which is the common case, since the fb_rocksdb wrapper does not export tickers unless--rocksdb_fb303_enable_stats=true. It is deliberately kept separate fromSTALL_MICROSso WBM andWriteControllerstalls stay individually attributable.Finally, the cross-DB flush path added in D112396106 now logs which column family it picked, matching the log line the in-line path already had.
Note on Java: the
TickerTypebyte enum is exhausted (FILE_SUBMIT_ASYNC_READ_FALLBACKoccupies-0x80, the last slot, andportal.hcarries a TODO that the ticker count no longer fits in ajbyte), so the new ticker is intentionally not mirrored there. This is safe:toJavaTickerType()-- the only place an unmapped ticker could alias to0x0-- has no callers, and Java clients can only reach tickers they can name.Differential Revision: D114903210