Compare commits

..
72 Commits
Author SHA1 Message Date
Josh Kang 3b44608914 Update HISTORY.md and version for 11.1.2 2026-06-24 16:52:00 -07:00
Josh Kang 83a5b52eda Do not allow read only DBs to delete obsolete files (#14881)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14881

Read-only DB instances can share a directory with a live writer, so their view of live files is only a snapshot from the time they opened. After read-only DB open began setting `opened_successfully_` for its intended open-success semantics, that state had an unintended side effect: the common close path treated a successful read-only open like a read-write open and ran obsolete-file cleanup. If the writer created or made files live after the read-only handle opened, that cleanup could use the stale read-only live set and delete files still needed by the writer.

This change records whether a `DBImpl` was opened read-only and skips close-time obsolete-file cleanup for read-only DBs instead of simply keeping `opened_successfully_=false`, which could be confusing and easily mistaken in the future again.

For completeness I also updated other callsites of `opened_successfully_`, but those should not be real bugs as ReadOnly DBs do not run flushes/compactions or write to WALs.

Reviewed By: xingbowang

Differential Revision: D109622445

fbshipit-source-id: d7be4b20fce86ccb218a63ac6f5b707b316aac79
2026-06-24 16:51:01 -07:00
anand76 6cdeb9d9d0 Update HISTORY.md and version for 11.1.1 2026-04-10 15:00:01 -07:00
Josh Kang ea72ee89da Support Pinnable Reads In SstFileReader (#14500)
Summary:
Add `SstFileReader::Get` (single-key) and `SstFileReader::MultiGet` (PinnableSlice) overloads to enable zero-copy point lookups directly from SST files. The existing `MultiGet(std::string*)` is refactored to delegate to the new `MultiGet(PinnableSlice*)`, which writes results directly into caller-provided `PinnableSlice` values instead of copying through an intermediate buffer. The single-key `Get` uses `TableReader::Get` with a `GetContext` for efficient single-key lookups without the overhead of MultiGet's sorting and batching machinery.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14500

Test Plan: - New unit tests

Reviewed By: xingbowang

Differential Revision: D97825648

Pulled By: joshkang97

fbshipit-source-id: 17f3edd59bbf4747d17309c44ef12f0d952ea4eb
2026-04-10 12:34:59 -07:00
Josh Kang 37c3a6de09 Support ExternalTable PinnableSlice Get (#14497)
Summary:
The ExternalTableReader `Get` API has been modified to use PinnableSlice instead of std::string, this will allow implementations to utilize zero-copy Gets (e.g. reading from mmap or a cache). This is not a compatible change, but the API is marked as experimental, so should be allowed.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14497

Test Plan: - New unit tests

Reviewed By: xingbowang

Differential Revision: D97782011

Pulled By: joshkang97

fbshipit-source-id: 8a9e8c5bc5ff5e8dee6c0f2ee745521f09042cef
2026-04-10 12:34:54 -07:00
Anand Ananthabhotla ede2cbd67b Fix MultiScanIndexIterator crash on reseek after exhaustion (#14581)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14581

In `MultiScanIndexIterator::Seek()` Case 3, when re-entering a scan range
after all ranges were exhausted, `block_idx = std::max(cur_scan_start_idx,
cur_idx_)` could produce an out-of-bounds value because `cur_idx_` was left
at `block_handles_.size()` from previous exhaustion. `SeekToBlockIdx()`
unconditionally set `valid_ = true` without checking bounds, causing the
subsequent `value()` call to hit the assertion
`cur_idx_ < block_handles_.size()`.

Added bounds check before `SeekToBlockIdx()` in Case 3 to correctly report
exhaustion instead of crashing.

Reviewed By: joshkang97

Differential Revision: D99604049

fbshipit-source-id: 9d5d91afde7c0984a7b4c2f62604f27f19b07922
2026-04-09 22:44:06 -07:00
Anand Ananthabhotla b21eaa91c2 Fix memory accounting leak in IODispatcher ReadIndex() (#14569)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14569

ReadSet::ReadIndex() moves block values out of pinned_blocks_ via std::move,
but never releases the associated prefetch memory accounting. This causes
ReleaseBlock() and the destructor to skip ReleaseMemory() since they check
pinned_blocks_.GetValue() which returns null after the move. Over time, the
memory budget is exhausted and no further prefetches can be dispatched when
max_prefetch_memory_bytes is set. The bug was introduced in https://github.com/facebook/rocksdb/pull/14401.

The fix releases memory accounting in ReadIndex() when moving values out
(both for Case 1: block already available, and Case 2: after async IO
polling), and zeros block_sizes_ to prevent double-release.

Also adds multiscan_max_prefetch_memory_bytes option to db_stress/crashtest
for stress testing this code path.

Reviewed By: hx235

Differential Revision: D99488961

fbshipit-source-id: 5ddd1f50e2f6ebb357f86e013d781a790e7e558a
2026-04-09 22:43:39 -07:00
Jialiang Tan 405e1a4ac6 Add block_decompress_count to PerfContext (#14557)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14557

RocksDB's PerfContext tracks block_decompress_time but has no counter for the
number of block decompressions. The global Statistics ticker
NUMBER_BLOCK_DECOMPRESSED exists but is not accessible through PerfContext,
which is the thread-local, per-operation metric system used by benchmarks.

Add block_decompress_count as a new PerfContext counter (gated at
kEnableCount level) incremented in DecompressBlockData alongside the existing
NUMBER_BLOCK_DECOMPRESSED ticker. This enables benchmarks and applications to
observe how many blocks required decompression per operation, complementing
the existing block_read_count.

Reviewed By: anand1976

Differential Revision: D99233514

fbshipit-source-id: 40a68c1d9321f560cebdb7c30a544a0c62ae64f0
2026-04-09 22:43:16 -07:00
anand76 a201a3c61a Update HISTORY.md for 11.1.0 2026-03-25 10:49:10 -07:00
Xingbo Wang 77a0e347bb Add Claude code review CI workflow (#14480)
Summary:
Add GitHub Actions workflows for AI-powered code review on PRs using Claude (Anthropic). Follows the clang-tidy security pattern with two separate workflows for privilege separation.

## Trigger Modes

**1. Auto** — Runs after `pr-jobs` workflow completes successfully via `workflow_run`. Safe for fork PRs (runs from default branch, never executes PR code).

**2. Manual** — Maintainers comment `/claude-review [focus area]` or `/claude-query <question>` on any PR. Restricted to 15 authorized team members.

**3. workflow_dispatch** — For manual testing.

## Security Model (Two-Workflow Separation)

Same pattern as clang-tidy:

**`claude-review.yml`** (analysis):
- Runs Claude with `ANTHROPIC_API_KEY`
- Has ONLY `contents: read` — no PR write, no issue write
- Saves review markdown + metadata as artifact

**`claude-review-comment.yml`** (posting):
- Triggers on `workflow_run` completion
- Downloads artifact and posts/updates PR comment
- Has `pull-requests: write` but never runs AI

This separation prevents a crafted PR from tricking Claude into exfiltrating write tokens.

## Review Methodology

Review prompt in `claude_md/code_review.md` (shared with local Claude Code reviews). Five perspectives:
- Call-chain analysis (3-5 levels up/down)
- Correctness & edge cases
- Cross-component & adversarial (10 execution contexts)
- Performance
- API compatibility & test coverage

## Shared Scripts

- `.github/scripts/post-pr-comment.js` — Create-or-update PR comment with marker-based dedup. Now used by both clang-tidy and Claude review.
- `.github/scripts/parse-claude-review.js` — Parses `claude-code-base-action` execution log into markdown.

## Files Changed

| File | Description |
|------|-------------|
| `.github/workflows/claude-review.yml` | Analysis workflow (476 lines) |
| `.github/workflows/claude-review-comment.yml` | Comment posting workflow (146 lines) |
| `.github/scripts/post-pr-comment.js` | Shared PR comment utility (57 lines) |
| `.github/scripts/parse-claude-review.js` | Execution log parser (78 lines) |
| `.github/workflows/clang-tidy-comment.yml` | Updated to use shared script |
| `claude_md/code_review.md` | Review methodology (104 lines) |

## Setup Required

Add `ANTHROPIC_API_KEY` secret to the repo settings.

## Testing

Tested end-to-end on `xingbowang/rocksdb` fork — both auto and manual triggers, artifact upload/download, comment posting, and duplicate detection all verified working.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14480

Reviewed By: omkarhgawde

Differential Revision: D97832666

Pulled By: xingbowang

fbshipit-source-id: f80c7d8683ac980614dc4ca66c1e545deb3be504
2026-03-23 16:20:01 -07:00
Andrew Chang 89e384ca6f Support GetLiveFiles on secondary DB instances (#14475)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14475

GetLiveFiles was previously blocked on secondary instances with
Status::NotSupported, even though the operation is safe to perform.

The only reason GetLiveFiles was originally blocked is that it defaults
to flushing the memtable (flush_memtable=true), which is a write
operation. However, the actual implementation in DBImpl::GetLiveFiles
(db_filesnapshot.cc) does two things:

1. Optionally flush the memtable — which we skip by passing
   flush_memtable=false. The secondary already overrides
   FlushForGetLiveFiles() as a no-op, so even if true were passed
   it would not actually flush.

2. Read live file state from versions_ under the mutex — this is
   purely read-only. It iterates the ColumnFamilySet to collect live
   table and blob file numbers, builds relative file paths for SST
   files, blob files, CURRENT, MANIFEST, and OPTIONS, and reads the
   manifest file size.

The secondary maintains its own VersionSet which it keeps up to date
via MANIFEST replay in TryCatchUpWithPrimary(). So all of this state
is valid and accurate — it reflects exactly which files the secondary
considers live at its current replay point.

This is the same approach used by DBImplReadOnly and CompactedDBImpl,
which both delegate to DBImpl::GetLiveFiles with flush_memtable=false.

Reviewed By: xingbowang

Differential Revision: D97563143

fbshipit-source-id: 8d5b52e26a478ef190eba598819de6527817bcfc
2026-03-23 12:09:10 -07:00
Josh Kang 3d99378791 Fix table handle leak (#14469)
Summary:
Fix leaked table cache entries that cause `TEST_VerifyNoObsoleteFilesCached` assertion failure during `DB::Close()` in ASAN crash test builds (T258745630):
```
File 126519 is not live nor quarantined
Assertion `cached_file_is_live_or_quar' failed.
```

When a compaction fails *after* `VerifyOutputFiles` succeeds (e.g., at `VerifyCompactionRecordCounts`), the overall `compact_->status` is set to error but each subcompaction's individual `status` remains OK. `SubcompactionState::Cleanup` only checked the individual subcompaction status, so it skipped calling `ReleaseObsolete` on the output files' table cache entries — leaking them.

Normally, `Close()`'s backstop (`FindObsoleteFiles(force=true)` + `PurgeObsoleteFiles`) would catch this by finding the orphan file on disk, evicting the cache entry, and deleting the file. However, `FindObsoleteFiles` calls `FaultInjectionTestFS::GetChildren` which can fail under metadata read fault injection (`--open_metadata_read_fault_one_in=8`). The error is silently ignored (`s.PermitUncheckedError()` in db_impl_files.cc:199), so the orphan file is never found and the leaked cache entry is never evicted.

The `Cleanup` bug is latent and predates recent changes. PR https://github.com/facebook/rocksdb/issues/14433 added `verify_output_flags` randomization to the crash test, and PR https://github.com/facebook/rocksdb/issues/14456 fixed false-positive corruptions that https://github.com/facebook/rocksdb/issues/14433 caused. Before https://github.com/facebook/rocksdb/issues/14456, `VerifyOutputFiles` would produce false corruption errors that *accidentally prevented the leak* by setting the subcompaction status to non-OK.

### How it triggers

**Step 1 — VerifyOutputFiles adds cache entries for compaction output files:**
```
CompactionJob::Run()
  RunSubcompactions()              // all subcompactions succeed
                                   // output file 12 written to disk
                                   // each sub_compact.status = OK
  SyncOutputDirectories()          // status = OK
  VerifyOutputFiles()
    for each output_file:
      table_cache()->NewIterator(output_file.meta)
        FindTable()
          cache->Insert(file_number=12)   // <<< ENTRY ADDED TO TABLE CACHE
        return iterator holding handle
      delete iter                          // releases handle, entry stays in LRU
    // status = OK
```

**Step 2 — A post-verification step fails, overall status set but NOT subcompaction status:**
```
CompactionJob::Run()
  VerifyCompactionRecordCounts()   // returns Status::Corruption(...)
  FinalizeCompactionRun(status=Corruption)
    compact_->status = Corruption  // <<< OVERALL status = error
    // each sub_compact.status is still OK!
```

**Step 3 — Install skips InstallCompactionResults (file 12 never enters a Version):**
```
CompactionJob::Install()
  status = compact_->status        // Corruption
  if (status.ok())                 // FALSE
    InstallCompactionResults()     // <<< SKIPPED — file 12 not in any version
```

**Step 4 — CleanupCompaction skips ReleaseObsolete (THE BUG):**
```
CompactionJob::CleanupCompaction()
  for each sub_compact:
    sub_compact.Cleanup(table_cache)
      if (!status.ok())            // checks sub_compact.status = OK
                                   // <<< FALSE — individual status is OK!
        ReleaseObsolete(...)       // <<< NEVER CALLED — cache entry leaked!
```

**Step 5 — Metadata read fault prevents backstop from finding the orphan:**

`FindObsoleteFiles(force=true)` calls `GetChildren()` to scan the DB directory.
`FaultInjectionTestFS::GetChildren` injects a metadata read error
(`--open_metadata_read_fault_one_in=8`). The error is silently ignored
(`s.PermitUncheckedError()`), so the directory listing is empty and the orphan
file is never found. This happens both in the post-compaction cleanup
(`BackgroundCallCompaction`) and in `Close()`'s backstop:
```
FindObsoleteFiles(force=true)
  fs->GetChildren(path, ...)       // returns IOError (fault injected)
  s.PermitUncheckedError();        // error silently ignored
  // files vector is empty — orphan file 12 not found
PurgeObsoleteFiles()               // nothing to do — no Evict called
```

**Step 6 — Assertion fires during Close():**
```
CloseHelper()
  FindObsoleteFiles(force=true)    // GetChildren fails again → orphan missed
  PurgeObsoleteFiles()             // nothing to evict
  TEST_VerifyNoObsoleteFilesCached()
    for each cache entry:
      file_number = 12
      live_and_quar_files.find(12) == end()  // NOT in any version!
      >>> assert(cached_file_is_live_or_quar) FAILS <<<
```

**Crash test call stack:**
```
frame https://github.com/facebook/rocksdb/issues/9:  __assert_fail_base("cached_file_is_live_or_quar", "db_impl_debug.cc", 389)
frame https://github.com/facebook/rocksdb/issues/11: DBImpl::TEST_VerifyNoObsoleteFilesCached()::lambda  // finds leaked entry
frame https://github.com/facebook/rocksdb/issues/18: LRUCacheShard::ApplyToSomeEntries(...)              // iterating cache shard
frame https://github.com/facebook/rocksdb/issues/19: ShardedCache::ApplyToAllEntries(...)                // iterating all shards
frame https://github.com/facebook/rocksdb/issues/20: DBImpl::TEST_VerifyNoObsoleteFilesCached()          // the verification
frame https://github.com/facebook/rocksdb/issues/21: DBImpl::CloseHelper()                               // during Close
frame https://github.com/facebook/rocksdb/issues/22: DBImpl::CloseImpl()
frame https://github.com/facebook/rocksdb/issues/23: DBImpl::Close()
frame https://github.com/facebook/rocksdb/issues/24: StressTest::Reopen()                                // crash test reopen
frame https://github.com/facebook/rocksdb/issues/25: StressTest::OperateDb()                             // worker thread
```

### Fix

Pass the overall `compact_->status` to `SubcompactionState::Cleanup` and call
`ReleaseObsolete` when *either* the subcompaction status or the overall status
is non-OK:
```cpp
// Before:
if (!status.ok()) { ReleaseObsolete(...); }

// After:
if (!status.ok() || !overall_status.ok()) { ReleaseObsolete(...); }
```

## Key changes

- **`SubcompactionState::Cleanup`**: Now takes an `overall_status` parameter and calls `ReleaseObsolete` when *either* the subcompaction status or the overall compaction status is non-OK.
- **`CompactionJob::CleanupCompaction`**: Passes `compact_->status` (the overall status) to each subcompaction's `Cleanup`.
- **Sync point**: Added `CompactionJob::Run():AfterVerifyOutputFiles` for error injection in tests.
- **Unit test**: `DBCompactionTest.LeakedTableCacheEntryOnCompactionFailure` uses `FaultInjectionTestFS` to reproduce the crash test scenario — injects error after `VerifyOutputFiles` and deactivates the filesystem so `GetChildren` fails in `FindObsoleteFiles`, preventing the backstop from evicting the leaked cache entry.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14469

Test Plan:
- `DBCompactionTest.LeakedTableCacheEntryOnCompactionFailure`:
  - **Without fix (ASAN)**: assertion fires during `Close()` — `File 12 is not live nor quarantined`
  - **With fix (ASAN)**: passes — `ReleaseObsolete` properly cleans up the cache entry
  - **With fix (non-ASAN)**: passes
```
$ COMPILE_WITH_ASAN=1 make -j db_compaction_test
$ ./db_compaction_test --gtest_filter="DBCompactionTest.LeakedTableCacheEntryOnCompactionFailure"
[  PASSED  ] 1 test.
```

Reviewed By: xingbowang

Differential Revision: D97190944

Pulled By: joshkang97

fbshipit-source-id: fdfd481cc1e192803cfb7d64052ccb9162c21b94
2026-03-23 10:50:43 -07:00
Hui Xiao f455ab7bd6 Fix flaky BackgroundJobPressure test (#14476)
Summary:
**Context/Summary:**
Remove assertions on flush_running/flush_scheduled from the BackgroundJobPressure listener test. These checks are inherently racy because Flush() can return before the background thread fires the pressure callback.

The race: when BackgroundCallFlush re-acquires the DB mutex after cleanup (line 3661), COERCE_CONTEXT_SWITCH=1 or others may call bg_cv_->SignalAll() and sleep before actually acquiring the lock. This spurious signal wakes WaitForFlushMemTables(), which sees the flush is already installed and returns — so Flush() "completes" seen as foreground in test. The test then starts the next Put()+Flush(), scheduling new flush work. When the previous background thread finally wakes and captures the pressure snapshot, it sees the newly scheduled flush (flush_scheduled > 0). If the race happens on the last flush, its callback may not have fired at all by the time the test reads GetSnapshots(), so snapshots.back() can also show stale data.

The remaining assertions (compaction counts, speedup, write stall proximity) are not affected: compaction is blocked by sleeping_task in Phase 1-2, and Phase 3 uses TEST_WaitForCompact() whose wait condition (bg_*_scheduled_ counts) is only satisfied after the mutex is re-acquired and callbacks have fired.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14476

Test Plan:
COERCE_CONTEXT_SWITCH=1 make -j56 listener_test
  Before: ~22/50 failures at line 1715 (flush_running != 0)
  After:  50/50 pass

Reviewed By: xingbowang

Differential Revision: D97572545

Pulled By: hx235

fbshipit-source-id: e8a57a7a00e41d17bf47ef8e04cea977225a5909
2026-03-20 18:58:34 -07:00
Hui Xiao 63c86160cd Add OnBackgroundJobPressureChanged listener callback (#14474)
Summary:
**Summary:**
Add a new EventListener callback `OnBackgroundJobPressureChanged` that fires after every flush or compaction background job completes. The callback delivers a `BackgroundJobPressure` snapshot containing:
- Compaction scheduling counters (scheduled/running, combined and per-priority LOW/BOTTOM breakdown)
- Flush scheduling counters (scheduled/running)
- Write stall proximity percentage (0=healthy, 100=at stall threshold, can exceed 100 when stalling)
- Whether compaction speedup is active

`CaptureBackgroundJobPressure()` reads scheduling counters and computes write stall proximity from L0 sorted run count and pending compaction bytes (same inputs as `RecalculateWriteStallConditions()`). TODO: add memory-related write stall triggers later.

Introduces `num_running_bottom_compactions_` counter to track BOTTOM- priority compactions separately from LOW, enabling per-pool breakdown in the pressure snapshot.

The callback fires on the background thread after counter decrements and `MaybeScheduleFlushOrCompaction()`, so the snapshot reflects post- completion state. Uses the same mutex unlock/lock pattern as `NotifyOnFlushCompleted`. A `bg_pressure_callback_in_progress_` counter ensures destructor safety since the callback fires after `bg_flush_scheduled_`/`bg_compaction_scheduled_` are decremented.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14474

Test Plan:
- listener_test BackgroundJobPressure: 3-phase test verifying no pressure, pressure build-up (speedup, scheduling, proximity), and pressure relief after compaction completes
- db_compaction_test CompactRangeBottomPri: verifies num_running_bottom_compactions_ via sync point
- db_stress_tool exercises the callback with RandomSleep()

Reviewed By: xingbowang

Differential Revision: D97423623

Pulled By: hx235

fbshipit-source-id: 07003c8de226ec29d32b8a88e2d86e5de85cd2cc
2026-03-20 15:02:53 -07:00
Jay Huh 89322fdd9e Skip Wal Recovery on SecondaryDB Open if for Remote Compaction (#14462)
Summary:
Skip WAL recovery when opening a secondary DB instance in OpenAndCompact() for remote compaction. WAL replay is unnecessary in this flow since only LSM state from MANIFEST is needed.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14462

Test Plan:
- make -j db_secondary_test && ./db_secondary_test — 35/35 passed
- make -j compaction_service_test && ./compaction_service_test — 43/43 passed (includes new SkipWALRecoveryInOpenAndCompact test)
- make -j options_settable_test && ./options_settable_test --gtest_filter="*DBOptionsAllFieldsSettable*" — 1/1 passed
- Removed temporary hack in stress test that disables WAL

Reviewed By: hx235

Differential Revision: D96788211

Pulled By: jaykorean

fbshipit-source-id: f91a2f861f2450ebc83423ed4c6f5b70da7d9e8b
2026-03-19 15:48:16 -07:00
Yoshinori Matsunobu b23fc77aca Add per-block-type block read byte perf counters (#14473)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14473

Add separate PerfContext byte counters for data, index, filter, compression-dictionary, and metadata block reads while preserving block_read_byte as the aggregate. Wire the new counters through the block fetch and multi-read data block paths, expose them via the C perfcontext API, and extend table tests to verify byte attribution and that the classified counters sum back to block_read_byte.

Reviewed By: xingbowang

Differential Revision: D97333746

fbshipit-source-id: 3411844d7fa9c76c9ff28af477b3a72a5d6e5d9b
2026-03-19 14:54:24 -07:00
Danny Chen b6f498b2c9 Add verify_manifest_content_on_close option (#14451)
Summary:
Add a new mutable DB option `verify_manifest_content_on_close` (default: false).
When enabled, on DB close the MANIFEST file is read back and all records are
validated (CRC checksums via log::Reader and logical content via
VersionEdit::DecodeFrom). If corruption is detected, a fresh MANIFEST is written
from in-memory state using the existing LogAndApply recovery path.

This complements the existing size validation in VersionSet::Close() with content
validation, reusing the same manifest reading pattern as VersionSet::Recover().

Implementation plan:

## Part 1: New DB Option — verify_manifest_content_on_close
- A new mutable bool DB option (default: false) that can be dynamically toggled
  via SetDBOptions() at runtime, following the pattern of other mutable manifest
  options like max_manifest_file_size.
- Propagation: SetDBOptions() -> DBImpl::mutable_db_options_ ->
  versions_->UpdatedMutableDbOptions() -> VersionSet::verify_manifest_content_on_close_

## Part 2: Core Implementation — Content Validation in VersionSet::Close()
- Inserted after existing size check, before closed_ = true
- Opens manifest as SequentialFileReader, creates log::Reader with checksum=true
- Loops ReadRecord with WALRecoveryMode::kAbsoluteConsistency, decodes each
  record as VersionEdit
- On corruption: fires OnIOError listeners, logs error, calls LogAndApply with
  empty edit to trigger manifest rewrite from in-memory state
- If manifest can't be opened for reading: logs warning, doesn't fail close

## Part 3: Unit Tests (in version_set_test.cc)
- ManifestContentValidationOnClose_Clean: enable option, normal close, verify
  no manifest rotation
- ManifestContentValidationOnClose_CorruptRecord: enable option, corrupt manifest
  via SyncPoint, verify rotation occurs and DB reopens cleanly
- ManifestContentValidationOnClose_Disabled: default off, verify content
  validation does not run
- ManifestContentValidationOnClose_SizeCheckFails: truncate manifest so size
  check fails first, verify recovery via size-check path

## What Happens If a Corruption is Detected
If corruption was detected, four things happen:
1. **Notify listeners** — Fires `OnIOError` on all registered event listeners
   (from db_options_->listeners) so monitoring/alerting systems can observe
   the corruption event. Uses `FileOperationType::kVerify` to categorize it.
2. **Permit unchecked errors** — `PermitUncheckedError()` silences RocksDB's
   debug-mode assertion that every `IOStatus` must be inspected. These statuses
   are informational-only here; the real recovery is via `LogAndApply`.
3. **Log the error** — Writes a `ROCKS_LOG_ERROR` message with the filename
   for operational visibility (grep-able in production logs).
4. **Rewrite the manifest via `LogAndApply`** — This is the actual recovery.
   `LogAndApply` is called with an empty `VersionEdit` (no changes). Internally,
   `LogAndApply` detects that the current `descriptor_log_` is null (it was
   reset at line 5551, or by the previous `LogAndApply` in the size-check
   path) and creates a brand-new MANIFEST file. It serializes the entire
   current in-memory LSM state — all column families, all levels, all file
   metadata, sequence numbers, etc. — into this new file. It then atomically
   updates the `CURRENT` file pointer to reference the new MANIFEST.
   This works because the in-memory state was built from the original manifest
   during `DB::Open()` and has been kept fully up to date through all
   subsequent operations (flushes, compactions, etc.) during the DB's lifetime.
   The on-disk manifest is essentially a journal of changes; `LogAndApply`
   with an empty edit produces a fresh, compacted snapshot of that state.

## Flow Diagram of Manifest Content Validation

VersionSet::Close()
│
├─ Close descriptor_log_ and check size
│  └─ Size mismatch? → LogAndApply (rewrite manifest)
│
├─ Content validation (if s.ok() && option enabled)
│  ├─ Open manifest for sequential reading
│  │  └─ Can't open? → WARN log, continue
│  │
│  ├─ For each record:
│  │  ├─ ReadRecord (CRC32 check, kAbsoluteConsistency)
│  │  └─ DecodeFrom (VersionEdit logical check)
│  │
│  └─ Corruption detected?
│     ├─ Notify OnIOError listeners
│     ├─ LOG_ERROR
│     └─ LogAndApply (rewrite manifest from in-memory state)
│
└─ closed_ = true; return s;

## How This Relates to the Existing Size Check
The existing size check (lines 5556-5582) and the new content validation are
complementary:
| Check          | What it catches                         | How it checks              |
|----------------|-----------------------------------------|----------------------------|
| Size check     | Truncation, partial writes, extra bytes | Compare expected vs actual file size |
| Content check  | Bit-rot, silent corruption, bad records | CRC32 + VersionEdit decode |
The size check catches gross corruption (file too short or too long). The
content check catches subtle corruption where the file is the right size but
individual bytes have been flipped (e.g., storage media bit-rot, buggy
filesystem, incomplete block write).
Both recovery paths use the same mechanism: `LogAndApply` with an empty
`VersionEdit` to rewrite the manifest from in-memory state.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14451

Reviewed By: xingbowang

Differential Revision: D96004906

Pulled By: dannyhchen

fbshipit-source-id: 0b0ecdada3a74e97d2cadbba2091b8b577f1d684
2026-03-19 12:01:23 -07:00
Xingbo Wang bcaf2794dc Fix TSAN data race in InjectedErrorLog by suppressing benign races (#14467)
Summary:
InjectedErrorLog is a lock-free circular ring buffer designed to be safe to call from signal handlers (which cannot use locks). It has an intentional benign data race between Record() (called by worker threads) and PrintAll() (called by the main thread from a signal/termination handler). The code documents this trade-off in comments, but was missing TSAN suppression annotations.

Simply adding TSAN_SUPPRESSION (__attribute__((no_sanitize("thread")))) is insufficient because TSAN still intercepts libc functions like vsnprintf/snprintf -- accesses through these interceptors are still tracked even when the calling function is annotated.

The fix:
1. Add TSAN_SUPPRESSION to both Record() and PrintAll() to suppress direct field reads/writes in the function body.
2. Restructure both functions to use local stack buffers for vsnprintf/snprintf operations instead of operating directly on shared entry data. This avoids passing shared memory through TSAN-intercepted libc functions.

Also adds fault_injection_fs_test with a ConcurrentRecordAndPrintAll test that exercises the concurrent Record() + PrintAll() pattern and verifies no TSAN race is reported.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14467

Test Plan:
- fault_injection_fs_test passes under TSAN (buck2 test fbcode//mode/dbg-tsan)
- Reverted fix, re-ran: Fatal (6 TSAN warnings) -- round-trip confirmed
- fault_injection_fs_test passes under debug mode (no regression)

Reviewed By: mszeszko-meta

Differential Revision: D96948483

Pulled By: xingbowang

fbshipit-source-id: efdd5eafa12a5a82f973e40aa327901cc5f95033
2026-03-19 09:40:02 -07:00
Andrew Chang 1cb7594b87 Set correct file_type in BackupInfo::file_details (#14464)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14464

SetBackupInfoFromBackupMeta passes file_ptr->filename (which may contain
directory components like "private/1/000008.log") directly to
ParseFileName. ParseFileName expects a bare filename, so it fails and
file_type stays at the default kTempFile for all files in file_details.

Fix by extracting the basename before calling ParseFileName.

Reviewed By: mszeszko-meta

Differential Revision: D96793040

fbshipit-source-id: 7bb6b633eb07bb7ebda06edbc039a96b9b77b410
2026-03-18 12:01:29 -07:00
Dmitry Vinnik 166b4a4487 Remove Support Ukraine banner from RocksDB website (#14463)
Summary:
Remove the Support Ukraine socialBanner div from the RocksDB docs homepage layout.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14463

Reviewed By: hx235

Differential Revision: D96559640

Pulled By: xingbowang

fbshipit-source-id: 508c7bd660760e39cc73977c5e5c1bda204efedd
2026-03-17 10:15:24 -07:00
anand76 03bded03b4 Fix finger.prev_[0] assertion failure in MultiGet finger search (#14465)
Summary:
The callback loop in InlineSkipList::MultiGet updated finger.prev_[0] as it walked forward through entries (e.g., merge operands). When the MultiGet batch contained duplicate user keys, the next lookup for the same key would find finger.prev_[0] pointing to an entry that sorts AFTER the lookup key in internal key order (because the lookup key has a high sequence number which sorts first), violating the FindSpliceForLevel precondition: before == head_ || KeyIsAfterNode(key, before).

Fix: stop updating finger.prev_[0] in the callback loop. Only finger.next_[0] needs advancing to track the walk-forward position. The prev_[0] from FindGreaterOrEqualWithFinger is always a valid lower bound for any subsequent key, whether it uses kMaxSequenceNumber or a snapshot sequence number.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14465

Reviewed By: xingbowang

Differential Revision: D96882549

Pulled By: anand1976

fbshipit-source-id: a733fa9d4f23f8b55a027c257a114c4cf35abe2b
2026-03-17 09:57:24 -07:00
zaidoon ec22903914 Support all operation types in User Defined Index (UDI) interface (#14399)
Summary:
Remove the restriction that limited UDI to ingest-only, Puts-only use cases. This enables UDI plugins (including the trie index from https://github.com/facebook/rocksdb/issues/14310) to work with all operation types: Put, Delete, Merge, SingleDelete, PutEntity, etc.

Part of https://github.com/facebook/rocksdb/issues/12396

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14399

Reviewed By: pdillinger

Differential Revision: D96392636

Pulled By: xingbowang

fbshipit-source-id: 0f1e6c38531fa72539a0e2c6a3dffff333392b4c
2026-03-16 15:25:05 -07:00
Josh Kang 0c6f741422 Rename write_prepared_transaction_test_seqno to write_prepared_transaction_seqno_test (#14453)
Summary:
write_prepared_transaction_test_seqno keeps showing up in git changes

test binaries need to end with _test so they are ignored by .gitignore

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14453

Reviewed By: xingbowang

Differential Revision: D96202671

Pulled By: joshkang97

fbshipit-source-id: 91642226bedde37ca72c6af03f300990dca8ff3a
2026-03-16 14:33:13 -07:00
anand76 c66c14258f Add memtable MultiGet finger search optimization (#14428)
Summary:
Add memtable batch lookup optimization with finger search

Optimize memtable MultiGet by using a finger search on the skip list. After finding key[i], the search path (Splice) is retained as a "finger" for key[i+1]. The next search walks up the finger until the forward pointer overshoots, then descends -- costing O(log d) where d is the distance between consecutive sorted keys, rather than O(log N) from the head each time.

Controlled by the new `memtable_batch_lookup_optimization` column family option (default: false).

## Changes

- Add `FindGreaterOrEqualWithFinger()` and `MultiGet()` to `InlineSkipList` with optional paranoid validation support (key ordering checks and per-key checksum verification via default parameters)
- Add virtual `MultiGet()` to `MemTableRep`, override in `SkipListRep`
- Add `memtable_batch_lookup_optimization` CF option
- Integrate finger search into `MemTable::MultiGet` -- sorts keys, performs batched finger search, then unscrambles results
- Add `db_bench`, `db_stress`, and crash test support
- Add unit tests for `InlineSkipList::MultiGet` (5 tests) and integration tests at the DB level (25 `BatchLookup` tests), including paranoid validation tests

## Benchmark Results

Setup: 2M keys in memtable, release build (`DEBUG_LEVEL=0`).

### Single-threaded `multireadrandom`

| Batch Size | Baseline | BatchOpt | vs Base |
|:---:|---:|---:|---:|
| 2 | 363,593 | 376,562 | **+3.6%** |
| 8 | 385,204 | 386,082 | **+0.2%** |
| 32 | 360,339 | 375,105 | **+4.1%** |
| 64 | 352,696 | 378,497 | **+7.3%** |

### Multithreaded `multireadrandom` (batch_size=64)

| Threads | Baseline | BatchOpt | vs Base |
|:---:|---:|---:|---:|
| 4 | 171,356 | 185,633 | **+8.3%** |
| 8 | 161,478 | 171,392 | **+6.1%** |

### `multireadwhilewriting` (batch_size=64)

| Threads | Baseline | BatchOpt | vs Base |
|:---:|---:|---:|---:|
| 1 | 163,938 | 175,068 | **+6.8%** |
| 4 | 109,698 | 120,790 | **+10.1%** |
| 8 | 116,623 | 123,658 | **+6.0%** |

No regression at any batch size or thread count. The finger is stack-allocated per `MultiGet` call, so there is no shared state or cache-line contention between threads. Concurrent writes don't invalidate the finger's bracket since it only reads via acquire-load `Next()` pointers.

Small Batch Size Regression Check: memtable_batch_lookup_optimization
=====================================================================

All values are ops/sec. Measured with db_bench (release build).
2M keys in memtable, value_size=100, duration=30s per run.

Multithreaded multireadrandom — small batch sizes
--------------------------------------------------
Threads | Batch Size | Baseline   | BatchOpt   | Change
   4    |     2      |   532,302  |   540,682  |  +1.6%
   8    |     2      | 1,044,046  | 1,046,920  |  +0.3%
   4    |     8      |   633,733  |   630,039  |  -0.6%
   8    |     8      | 1,260,818  | 1,241,792  |  -1.5%

multireadwhilewriting — small batch sizes
-----------------------------------------
Threads | Batch Size | Baseline   | BatchOpt   | Change
   1    |     2      |   107,284  |   105,217  |  -1.9%
   4    |     2      |   428,508  |   423,747  |  -1.1%
   8    |     2      |   854,654  |   864,923  |  +1.2%
   1    |     8      |   114,788  |   118,496  |  +3.2%
   4    |     8      |   467,995  |   473,437  |  +1.2%
   8    |     8      |   935,636  |   960,791  |  +2.7%

No regression at small batch sizes. Variations at batch_size=2 are
within noise (~1-2%). At batch_size=8, modest positive trend (+1-3%
in read-while-writing).

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14428

Test Plan:
- `make check` -- all tests pass
- `ASSERT_STATUS_CHECKED=1 make check` -- all tests pass
- 2-hour `db_crashtest.py blackbox` with `--memtable_batch_lookup_optimization=1` -- passed
- 2-hour `db_crashtest.py blackbox` with `--memtable_batch_lookup_optimization=1 --paranoid_memory_checks=1` -- passed

Reviewed By: pdillinger

Differential Revision: D95813786

Pulled By: anand1976

fbshipit-source-id: b182a023e1026021f1b9682d1cc024c7729326c7
2026-03-16 10:45:49 -07:00
Omkar Gawde e43171d6ca Fix memory leak in ExportColumnFamily for empty column families (#14458)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14458

In `CheckpointImpl::ExportColumnFamily()`, the assignment
`*metadata = result_metadata` is inside the `for` loop over
`db_metadata.levels`. If the column family has no levels, the loop body
never executes, so the `new ExportImportFilesMetaData()` is leaked and
the caller receives a nullptr despite a success status.

Fix: Move `*metadata = result_metadata` outside the loop.

Reviewed By: anand1976

Differential Revision: D95303457

fbshipit-source-id: 6a9be47bcca257803969eb3daac7b91e95143ebf
2026-03-13 21:14:48 -07:00
Omkar Gawde 1e1097d8d3 Fix close(-1) on failed open in btrfs rename fsync path (#14443)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14443

In `PosixDirectory::FsyncWithDirOptions()`, when handling btrfs file rename
syncing, if `open()` fails and `fd` is -1, the code unconditionally calls
`close(fd)`. Calling `close(-1)` is undefined behavior per POSIX (returns
EBADF on Linux), and overwrites the original meaningful open error with a
misleading "While closing file after fsync" error message.

Fix: Guard the `close()` call with `fd >= 0`.

Reviewed By: xingbowang

Differential Revision: D95303407

fbshipit-source-id: 9b64b45a09e6ba41d87d164a1c094b4d22f7c186
2026-03-13 12:26:44 -07:00
Xingbo Wang e44a99f560 Fix false positive corruption in compaction output verification (#14456)
Summary:
Fix a bug where `VerifyOutputFiles()` produces false positive "Key-value checksum of compaction output doesn't match what was computed when written" errors when `verify_output_flags` includes `kVerifyIteration` but `paranoid_file_checks` is false.

The root cause is a hash enable flag mismatch between writing and verification:

- During compaction writing (`OpenCompactionOutputFile`), the `OutputValidator` hash computation was gated solely by `paranoid_file_checks_`. When false, `enable_hash=false` and the hash stays at 0.
- During verification (`VerifyOutputFiles`), a new `OutputValidator` is always created with `enable_hash=true`, computing a non-zero hash.
- `CompareValidator()` then compares 0 vs non-zero, producing a false positive corruption.

This was exposed by the crash test randomization of `verify_output_flags` added in https://github.com/facebook/rocksdb/issues/14433. Before that change, `verify_output_flags` was always 0, so `kVerifyIteration` was only exercised via `paranoid_file_checks=true` (which correctly enabled the hash during writing).

The fix ensures hash computation is enabled during writing whenever either `paranoid_file_checks_` is true OR `verify_output_flags` includes `kVerifyIteration`.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14456

Test Plan:
- Added `DBCompactionTest.VerifyIterationWithoutParanoidFileChecks`
- Added `DBCompactionTest.VerifyAllOutputFlagsWithoutParanoidFileChecks`
- Round-trip verified: tests FAIL without fix, PASS with fix

Reviewed By: jaykorean

Differential Revision: D96371769

Pulled By: xingbowang

fbshipit-source-id: 2f7406327496c7b541e4fa2668894df89eb813e8
2026-03-12 16:26:30 -07:00
Jay Huh 5494bc1d76 Add option to verify file checksum of output files (#14433)
Summary:
One of the follow ups from https://github.com/facebook/rocksdb/pull/14103. Users will have the option to verify file checksums for all compaction output files before they are installed. This feature helps prevent corrupted SST files from being added to the LSM tree.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14433

Test Plan: Unit test added

Reviewed By: archang19

Differential Revision: D95648000

Pulled By: jaykorean

fbshipit-source-id: 512f3c1a7449b96a660865531f3537624c89a9cc
2026-03-11 21:14:28 -07:00
Xingbo Wang 91f227d9be Add injected error log ring buffer for fault injection diagnostics (#14431)
Summary:
Add a circular ring buffer (InjectedErrorLog) to FaultInjectionTestFS that records the last 1000 injected errors. Each entry captures the timestamp, thread ID, FS API name with all arguments (file path, offset, buffer size, first 8 bytes of data in hex), and the injected error status. For example:

  Append("/path/035354.sst", size=4096, head=[1a 2b 3c ...]) -> IOError (Retryable): injected write error
  RenameFile("/path/tmp.sst", "/path/035355.sst") -> IOError: injected metadata write error
  Read(offset=16384, size=4096) -> IOError (Retryable): injected read error

The ring buffer is printed to a file automatically:
- On any fatal signal (SIGABRT, SIGSEGV, SIGTERM, SIGINT, SIGHUP, SIGFPE, SIGBUS, SIGILL, SIGQUIT, SIGXCPU, SIGXFSZ, SIGSYS) via a registered crash callback
- At the end of db_stress main(), for diagnostic visibility even when the test completes normally

This addresses a key debugging gap: when write fault injection causes secondary failures (e.g., the builder error propagation issue in T257612259), the injected errors were previously completely silent with no logging trail. The ring buffer provides the missing diagnostic context to correlate fault injection with downstream failures.

Changes:
- port/stack_trace.h/.cc: Add RegisterCrashCallback() API; extend InstallStackTraceHandler() to catch all catchable termination signals
- utilities/fault_injection_fs.h: Add InjectedErrorLog class with printf-style Record(), HexHead() for data bytes, and signal-safe PrintAll()
- utilities/fault_injection_fs.cc: Record full API arguments and error status at all 31 fault injection call sites
- db_stress_tool/db_stress_tool.cc: Register crash callback and print ring buffer at end of main()

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14431

Reviewed By: hx235

Differential Revision: D95435430

Pulled By: xingbowang

fbshipit-source-id: 6c18e1b072044575d6c8c3f198070127b0f80608
2026-03-11 18:15:04 -07:00
Omkar Gawde cb8bc56d14 Fix memory leak on error in C API create_column_family (#14447)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14447

`rocksdb_create_column_family()` and
`rocksdb_transactiondb_create_column_family()` allocate a
`rocksdb_column_family_handle_t` but always return it even when
`CreateColumnFamily()` fails. This leaks the handle and returns an object
with an indeterminate `rep` pointer. Other similar functions like
`rocksdb_create_column_family_with_import()` correctly delete the handle
and return nullptr on error.

Fix: Initialize `handle->rep = nullptr`, check `SaveError()` return value,
and on error delete the handle and return nullptr.

Reviewed By: anand1976

Differential Revision: D95303444

fbshipit-source-id: 5fde7a3ed588794d78429d5cb3d9f621f0fb6388
2026-03-10 22:56:54 -07:00
Maciej Szeszko 1ed5052486 Prepopulate block cache during compaction (#14445)
Summary:
When RocksDB operates with tiered or remote storage (e.g., Warm Storage, HDFS, S3), reading recently compacted data incurs high-latency remote reads because compaction output files are not present in the block cache. The existing `prepopulate_block_cache = kFlushOnly` avoids this for flush output but leaves compaction output cold until first access.

Add a new `PrepopulateBlockCache::kFlushAndCompaction` enum value that warms all block types (data, index, filter, compression dict) into the block cache during both flush and compaction. Flush-warmed blocks use `LOW` priority (unchanged from kFlushOnly behavior), while compaction-warmed blocks use `BOTTOM` priority — compaction data is less temporally local than freshly flushed data, so it should be the first to be evicted when the cache is full. This gives the remote-read avoidance benefit without risking cache thrashing.

The enum uses `kFlushAndCompaction` rather than separate `kCompactionOnly` + `kFlushAndCompaction` values because there is no practical use case for warming compaction output without also warming flush output. Flush output is by definition the hottest data (just written by the user), so if a workload benefits from warming the colder compaction output, it would always benefit from warming flush output too.

The implementation reuses the existing `InsertBlockInCacheHelper` / `WarmInCache` infrastructure in `BlockBasedTableBuilder`. The only internal change is adding a `warm_cache_priority` field to `Rep` alongside the existing `warm_cache` bool, and plumbing it through to the `WarmInCache` call instead of the previously hardcoded `Cache::Priority::LOW`.

### Key changes
- New `PrepopulateBlockCache::kFlushAndCompaction` enum value in table.h
- `Rep::warm_cache_priority` field in BlockBasedTableBuilder for per-reason priority control
- Serialization support ("kFlushAndCompaction" in string map)
- db_bench support (--prepopulate_block_cache=2)
- Crash test coverage (random choice includes new value)

**NOTE:** Unlike flush output (which is inherently hot — just written by the user), it is hard to distinguish hot from cold blocks in compaction output. Warming all compaction output therefore risks polluting the block cache and evicting genuinely hot entries. The kFlushAndCompaction mode is recommended only for use cases where most or all of the database is expected to reside in cache (e.g., the working set fits in cache). For workloads where only a fraction of the data is hot, kFlushOnly remains the safer choice.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14445

Test Plan:
- New `WarmCacheWithDataBlocksDuringCompaction` test: verifies data blocks from compaction output are present in the block cache and served without misses
- Extended `DynamicOptions` test: verifies dynamic switching through kDisable -> kFlushAndCompaction -> kFlushOnly -> kDisable via SetOptions
- Existing `WarmCacheWithDataBlocksDuringFlush` and parameterized `WarmCacheWithBlocksDuringFlush` tests continue to pass (kFlushOnly behavior unchanged)
- db_block_cache_test: 81/81 passed
- options_test: 74/74 passed
- table_test: 6910/6910 passed

Reviewed By: xingbowang

Differential Revision: D95997952

Pulled By: mszeszko-meta

fbshipit-source-id: 4ad568264992532053947df298e63e343821ddb5
2026-03-10 16:19:33 -07:00
Peter Dillinger db9449a154 Fix crash test failures when use_trie_index conflicts with txn options (#14446)
Summary:
The trie UDI sanitization in db_crashtest.py disables use_txn because TransactionDB ROLLBACK writes DELETE entries that violate UDI's Put-only restriction. However, it was not clearing use_optimistic_txn or test_multi_ops_txns, which both require use_txn to be true.

Since use_trie_index is randomly enabled with 1/8 probability, the optimistic_txn and multiops_txn crash tests would intermittently fail with:
- "You cannot set use_optimistic_txn true while use_txn is false"
- "-use_txn must be true if -test_multi_ops_txns"

Fix by also setting use_optimistic_txn=0 and test_multi_ops_txns=0 in the trie UDI sanitization block alongside the existing use_txn=0.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14446

Test Plan: Watch crash test CI results

Reviewed By: xingbowang

Differential Revision: D95986370

Pulled By: pdillinger

fbshipit-source-id: 0751dcb4e99425ba9eb9aa34c2a99efa8eb3a194
2026-03-10 14:35:28 -07:00
Xingbo Wang 26aff32db3 Rename IsExpectedTxnLockTimeout to IsExpectedTxnError (#14441)
Summary:
The function handles more than just lock timeouts — it also covers TryAgain from optimistic transactions. Rename it and update the comment to reflect its broader scope.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14441

Reviewed By: hx235

Differential Revision: D95877799

Pulled By: xingbowang

fbshipit-source-id: 0651ffd39ac9d3a7979750be6dfe5b293eab7a64
2026-03-10 13:33:31 -07:00
Omkar Gawde 2afb387917 Fix missed condition variable signal in DeleteScheduler (#14442)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14442

In `BackgroundEmptyTrash()`, the bucket counter uses post-decrement
(`iter->second--`), which assigns the pre-decrement value to
`pending_files_in_bucket`. When a bucket transitions from 1 to 0 files,
`pending_files_in_bucket` receives the value 1 (not 0), so the check
`pending_files_in_bucket == 0` fails. This means `WaitForEmptyTrashBucket()`
is never woken up when a specific bucket empties, unless all global pending
files are also zero.

Fix: Use pre-decrement (`--iter->second`) so the decremented value correctly
triggers the condition variable signal.

Reviewed By: xingbowang

Differential Revision: D95303385

fbshipit-source-id: 3c5f0978ff33600acaf406b3f839cf13d9983055
2026-03-10 10:26:45 -07:00
Omkar Gawde a066318a31 Fix out-of-bounds access in BlobFileReader::MultiGetBlob (#14427)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14427

In `MultiGetBlob()`, the `adjustments` autovector is populated only for blob
requests that pass validation. Requests that fail validation are skipped via
`continue`, so `adjustments` has fewer entries than `blob_reqs`. When consuming
results, `adjustments[i]` uses the `blob_reqs` index instead of the
`read_reqs`/`adjustments` index, causing an out-of-bounds read when any request
fails validation.

Fix: Replace `adjustments[i]` with `adjustments[j - 1]`, since `j` tracks the
position in `read_reqs`/`adjustments` and has already been post-incremented.

Reviewed By: hx235

Differential Revision: D95303356

fbshipit-source-id: a264ae6481f74ce33f64e40624441d666135bcd0
2026-03-09 16:57:24 -07:00
Josh Kang 42eff8b632 Add new heauristic 'num_collapsible_entry_reads_sampled' (#14434)
Summary:
Add per-file sampling of "collapsible" entry reads (single deletions, merges, and kNotFound results) that may later be used to help inform read-triggered compactions. This is a better metric than `num_reads_sampled` as it is more targeted towards reads that could be avoided via compaction.

The existing behavior of `num_reads_sampled` is that reads only gets sampled on iterator creation for a file. It is problematic because next/prev() calls are not sampled, nor are additional seeks().

This PR moves sampling to per-seek/next granularity within `LevelIterator` and adds a new `num_collapsible_entry_reads_sampled` counter that tracks how often a file serves entries that could be eliminated by compaction.

 Note only L1+ files have iterator seeks/nexts/prevs sampled. Introducing this at L0 would require wrapping table reader iterators, introducing a performance cost.

## Key changes

- **New counter `num_collapsible_entry_reads_sampled`** in `FileSampledStats` tracks sampled reads that encounter deletions, single deletions, merges, or kNotFound results in both Get and Iterator paths.
- **Moved sampling from file-open to per-operation** in `LevelIterator`: sampling now happens in `SampleRead()` called from `Seek()`, `SeekForPrev()`, `SeekToFirst()`, `SeekToLast()`, `Next()`, `NextAndGetResult()`, and `Prev()`. The `should_sample` parameter was removed from `LevelIterator`'s constructor.
- **Differentiated sampling rate for Next() vs Seek()**: `should_sample_file_read_next()` uses a 64x lower sampling rate (`kFileReadSampleRate * 64`) since Next() is cheaper than Seek() and called more frequently.
- **Collapsible tracking in Get path**: `Version::Get()` now increments the collapsible counter when `GetContext::State()` is `kNotFound`, `kMerge`, or `kDeleted`.
- **Collapsible tracking in MultiGet path**: `MultiGetFromSST` also increments the collapsible counter for the same states.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14434

Test Plan:
- Added new DB tests for both num_reads_sampled and num_collapsible_entry_reads_sampled

### Benchmark results (readrandom, readseq)

Setup: 1M keys, 16-byte keys, 100-byte values, no compression, fillrandom+compact

| Benchmark  | Params             | ops/s (main) | ops/s (feature) | % change |
|------------|--------------------|-------------|--------------------------|----------|
| readrandom | seed=1, threads=1  | 387,194     | 389,449                  | +0.6%    |
| readseq    | seed=1, threads=1  | 5,598,371   | 5,572,975                | -0.5%    |

No meaningful performance regression observed — differences are within run-to-run noise.

Reviewed By: xingbowang

Differential Revision: D95613793

Pulled By: joshkang97

fbshipit-source-id: 9dd09c9b7527b148424bde5686f4157c7a9e1214
2026-03-09 16:42:41 -07:00
Hui Xiao cb43abb1f1 Fix incorrect input file expansion in SetupOtherFilesWithRoundRobinExpansion (#14436)
Summary:
### Context/Summary:
**_See below for an example of the bug:_**

L1 has 5 SST files at indices [0, 1, 2, 3, 4] where files at indices 1/2/3
share user key boundaries (e.g., file[1].largest and file[2].smallest have
the same user key).

Round-robin picks file[1] at start_index=1.

Before fix:
  1. ExpandInputsToCleanCut expands {file[1]} → {file[1], file[2], file[3]}
  2. Loop starts at i=2 (start_index+1), adds file[2] → duplicate!
  3. start_level_inputs_ = {file[1], file[2], file[3], file[2]}
  4. GetRange uses back()=file[2], returns [file[1].smallest, file[2].largest]
  5. ExpandInputsToCleanCut converges on {file[1], file[2]}, missing file[3]
  6. AssertCleanCut crashes (debug) or data corruption (release)

After fix:
  1. ExpandInputsToCleanCut expands {file[1]} → {file[1], file[2], file[3]}
  2. Find last file = file[3] at index 3, loop starts at i=4
  3. start_level_inputs_ = {file[1], file[2], file[3], file[4]} (no duplicates)
  4. GetRange correctly returns [file[1].smallest, file[4].largest]

_**More details:**_

When round-robin compaction picks a file, PickFileToCompact calls ExpandInputsToCleanCut which may expand start_level_inputs_ from 1 file to N files (when adjacent files share user key boundaries). Then SetupOtherFilesWithRoundRobinExpansion loops from start_index + 1 to add more files, not knowing the expansion already happened. This re-adds files already in start_level_inputs_, creating duplicates.

The duplicates corrupt GetRange, which trusts inputs.back()->largest for non-L0 levels. With a duplicate earlier file at back(), GetRange returns a truncated range. ExpandInputsToCleanCut then converges on an incomplete file set, violating the clean-cut invariant.

In debug builds this crashes at AssertCleanCut. In release builds the compaction proceeds with a non-clean-cut input, causing data corruption (newer data in a later level while older data remains in an earlier level).

The fix finds the position of the last file already in start_level_inputs_ and starts the loop after it, avoiding duplicates. When start_level_inputs_ has only 1 file (no expansion happened), the behavior is unchanged.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14436

Test Plan:
New test RoundRobinCleanCutWithSharedBoundary:
- Without fix: crashes at AssertCleanCut in SetupOtherFilesWithRoundRobinExpansion
```
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from DBCompactionTest
[ RUN      ] DBCompactionTest.RoundRobinCleanCutWithSharedBoundary
db_compaction_test: db/compaction/compaction_picker.cc:81: void rocksdb::AssertCleanCut(const rocksdb::InternalKeyComparator*, rocksdb::VersionStorageInfo*, rocksdb::CompactionInputFiles*, int, rocksdb::Logger*): Assertion `false' failed.
Received signal 6 (Aborted)
Invoking GDB for stack trace...
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/local/fbcode/platform010/lib/libthread_db.so.1".
0x00007fd9ba8f0e13 in __GI___wait4 (pid=1004850, stat_loc=0x7fd9b8bfb2ac, options=0, usage=0x0) at ../sysdeps/unix/sysv/linux/wait4.c:30
30      ../sysdeps/unix/sysv/linux/wait4.c: No such file or directory.
https://github.com/facebook/rocksdb/issues/4  __pthread_kill_internal (signo=6, threadid=<optimized out>) at pthread_kill.c:45
45      pthread_kill.c: No such file or directory.
https://github.com/facebook/rocksdb/issues/5  __GI___pthread_kill (threadid=<optimized out>, signo=6) at pthread_kill.c:62
62      in pthread_kill.c
https://github.com/facebook/rocksdb/issues/6  0x00007fd9ba8444ad in __GI_raise (sig=6) at ../sysdeps/posix/raise.c:26
26      ../sysdeps/posix/raise.c: No such file or directory.
https://github.com/facebook/rocksdb/issues/7  0x00007fd9ba82c433 in __GI_abort () at abort.c:79
79      abort.c: No such file or directory.
https://github.com/facebook/rocksdb/issues/8  0x00007fd9ba83bc28 in __assert_fail_base (fmt=0x7fd9ba9e11d8 "%s%s%s:%u: %s%sAssertion `%s' failed.\n%n", assertion=0x7fd9bcc04819 "false", file=0x7fd9bcc04700 "db/compaction/compaction_picker.cc", line=81, function=<optimized out>) at assert.c:92
92      assert.c: No such file or directory.
https://github.com/facebook/rocksdb/issues/9  0x00007fd9ba83bc93 in __GI___assert_fail (assertion=0x7fd9bcc04819 "false", file=0x7fd9bcc04700 "db/compaction/compaction_picker.cc", line=81, function=0x7fd9bcc04780 "void rocksdb::AssertCleanCut(const rocksdb::InternalKeyComparator*, rocksdb::VersionStorageInfo*, rocksdb::CompactionInputFiles*, int, rocksdb::Logger*)") at assert.c:101
101     in assert.c
https://github.com/facebook/rocksdb/issues/10 0x00007fd9bc2f8181 in rocksdb::AssertCleanCut (icmp=0x7fd9b9a7a840, vstorage=0x7fd9b9b6d040, inputs=0x7fd9b8bfc690, level=1, logger=0x7fd9b9aa2790) at db/compaction/compaction_picker.cc:81
81            assert(false);
https://github.com/facebook/rocksdb/issues/11 0x00007fd9bc2f8fa6 in rocksdb::CompactionPicker::ExpandInputsToCleanCut (this=0x7fd9b9b32900, vstorage=0x7fd9b9b6d040, inputs=0x7fd9b8bfc690, next_smallest=0x0) at db/compaction/compaction_picker.cc:310
310       AssertCleanCut(icmp_, vstorage, inputs, level, ioptions_.logger);
https://github.com/facebook/rocksdb/issues/12 0x00007fd9bc30cbfb in rocksdb::(anonymous namespace)::LevelCompactionBuilder::SetupOtherFilesWithRoundRobinExpansion (this=0x7fd9b8bfc910) at db/compaction/compaction_picker_level.cc:423
```
- With fix: compaction completes, data correctness verified

Reviewed By: joshkang97

Differential Revision: D95718659

Pulled By: hx235

fbshipit-source-id: 6ef125455ef5ae8a07c323835ff25588dbbb3634
2026-03-09 16:04:17 -07:00
Jay Huh e27114b135 Skip flush during recovery if read_only (#14440)
Summary:
The whitebox_crash_test crashes with Assertion `checking_set_.count(cfd) == 0` failed in `FlushScheduler::ScheduleWork()` during a read-only DB open (e.g., StressTest::TestBackupRestore).

## Root cause:

Commit 3aa706c2b ("Enforce WriteBufferManager during WAL recovery") added logic to schedule flushes when `WriteBufferManager::ShouldFlush()` is true during WAL recovery. However, the drain logic in `MaybeWriteLevel0TableForRecovery()` was gated by !read_only, so in read-only mode the flush
scheduler queue was never cleared. On subsequent WAL records, `ScheduleWork()` is called again for the same CFD still in the queue, triggering the duplicate assertion.

## Fix:

Add a `read_only` parameter to `InsertLogRecordToMemtable()` and skip WBM flush scheduling entirely in read-only mode. Flushes cannot be performed during read-only recovery, so scheduling them is pointless and causes the assertion failure when the scheduler is never drained.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14440

Test Plan:
- flush_job_test — all 15 tests pass
  - db_basic_test --gtest_filter="*ReadOnly*:*Recovery*:*WAL*" — all 9 tests pass
  - db_write_buffer_manager_test — all 18 tests pass
  - `python3 tools/db_crashtest.py whitebox --simple --duration=600 --interval=30`

Reviewed By: xingbowang

Differential Revision: D95829493

Pulled By: jaykorean

fbshipit-source-id: bdfef9ce66d507a1169381064344afd612a4b318
2026-03-09 15:17:31 -07:00
Josh Kang 3ad23b2d94 Support automated interpolation search (#14383)
Summary:
Add automatic per-block interpolation search selection (`kAuto` mode) for index blocks. During SST construction, each index block's key distribution is analyzed using the coefficient of variation (CV) of gaps between restart-point keys. Blocks with uniformly distributed keys are flagged via a new bit in the data block footer, and at read time, `kAuto` resolves to interpolation search for uniform blocks and binary search otherwise.

## Key changes

- **New `BlockSearchType::kAuto` enum value**: Resolves per-block at read time to either `kInterpolation` or `kBinary` based on the block's uniformity flag. Falls back to `kBinary` on older versions that don't recognize it.
- **Write-path uniformity analysis**: `BlockBuilder::ScanForUniformity()` uses Welford's online algorithm to incrementally compute the CV of key gaps at restart points. The result is stored in a new bit (bit 30) of the data block footer's packed restart count.
- **New table option `uniform_cv_threshold`** (default: -1 `disabled`): Controls how strict the uniformity check is. Set to negative to disable. Exposed in C++, Java (JNI), and `db_bench`.
- **Code reorganization**: Block entry decode helpers (`DecodeEntry`, `DecodeKey`, `DecodeKeyV4`, `ReadBe64FromKey`) moved from `block.cc` to a new shared header `block_util.h` so they can be reused by `BlockBuilder` on the write path.
- **New histogram `BLOCK_KEY_DISTRIBUTION_CV`**: Records the CV (scaled by 10000) of each index block's key distribution for observability.
- **Java bindings**: `IndexSearchType.kAuto`, `uniformCvThreshold` getter/setter, JNI portal constructor signature updated, and `HistogramType.BLOCK_KEY_DISTRIBUTION_CV` added.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14383

Test Plan:
- `IndexBlockTest.IndexValueEncodingTest` parameterized to include `kAuto` search type alongside `kBinary` and `kInterpolation`, verifying correct seek/iteration behavior across all combinations of key distributions, restart intervals, and key lengths.
- Uniformity detection validated: blocks with uniform key distribution correctly set `is_uniform = true`, blocks with clustered/non-uniform keys set `is_uniform = false`.
- Stress test coverage
- Updated check_format_compatible to also include a "uniform" dataset. By default using uniform_cv_threshold=-1 does not result in an incompatibility issues. When manually changing the threshold (e.g. `uniform_cv_threshold=1000`), I see `bad block contents`, which is expected

## Benchmark

readrandom with `fillrandom,compact -seed=1 --statistics`:

| Benchmark | Branch | Params | avg ops/s | % change vs main | CV P50 |
|-----------|--------|--------|-----------|------------------|--------|
| readrandom | main | `binary_search, shortening=1` | 335,791 | baseline | N/A |
| readrandom | feature | `binary_search, shortening=1` (default) | 335,749 | -0.0% | 1,500 |
| readrandom | feature | `auto_search, shortening=1` (kAuto) | 366,832 | **+9.2%** | 1,500 |
| readrandom | feature | `interpolation_search, shortening=1` | 366,598 | **+9.2%** | 1,500 |
| readrandom | feature | `auto_search, shortening=2` (kAuto) | 344,631 | **+2.6%** | 1,030,000 |
| readrandom | feature | `interpolation_search, shortening=2` | 201,178 | **-40.1%** | 1,030,000 |

As seen with shortening=2, a non-uniform distribution produces a high CV, which does not use interpolation search.

## Write benchmark

There is a write overhead which scans each restart entry for a block upon Finish. In practice this is very low because currently it is only applied to index blocks.

See cpu profile (https://fburl.com/strobelight/io5hwj9h) here of `-benchmarks=fillseq,compact -compression_type=none -disable_wal=1`. Only 0.08% attributed to `ScanForUniformity`.

Reviewed By: pdillinger

Differential Revision: D94738890

Pulled By: joshkang97

fbshipit-source-id: 9661ac593c5fef89d49f3a8a027f1338a0c96766
2026-03-06 10:13:51 -08:00
Xingbo Wang 8a86620653 Disable more features in stress test when trie UDI is enabled (#14432)
Summary:
Address 2 compatibility issue of UDI:

A:
Trie UDI (UserDefinedIndex) does not support SeekToFirst/SeekToLast. The crash test already disabled prefix scanning (prefixpercent=0) when use_trie_index=1, but iteration (iterpercent) was still enabled.

During iteration, LevelIterator::SkipEmptyFileForward() internally calls file_iter_.SeekToFirst() when Next() crosses SST file boundaries within a level. This propagates to UserDefinedIndexIteratorWrapper::SeekToFirst() which returns NotSupported, causing "Iterator diverged from control iterator" / "VerifyIterator failed" errors across many crash test variants.

B:
BlobDB is incompatible with trie UDI (user-defined index). When BlobDB is enabled (`enable_blob_files=1`), the flush job stores large values in separate blob files and writes `kTypeBlobIndex` entries in the SST instead of `kTypeValue`. The UDI builder (`UserDefinedIndexBuilderWrapper::OnKeyAdded()`) rejects any entry whose type is not `kTypeValue`, causing the flush to fail with `"user_defined_index_factory only supported with Puts"`.

Once the flush fails, the DB enters background error state, and all subsequent `Write()` calls fail with the same error -- printed as the repeated `"multiput error: ..."` messages from
`BatchedOpsStressTest::TestPut`. Eventually, `ProcessStatus` encounters this error and triggers `assert(false)`.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14432

Reviewed By: pdillinger

Differential Revision: D95457462

Pulled By: xingbowang

fbshipit-source-id: bf2bc47bd1ed75926765b2e3ff068f99f89a7793
2026-03-06 08:56:16 -08:00
Maciej Szeszko 071b5eff7a Fix MemPurge memtable ID ordering assertion (#14424)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14424

## Context

`MemPurge` releases db_mutex_ to do its merge work and re-acquires it before adding the output memtable to the immutable list. During this window, concurrent writers can fill the active memtable and trigger a switch, adding a new immutable memtable with a higher ID. `MemPurge` then assigns its output memtable the stale ID from mems_.back() (the newest memtable in the original flush batch), which is now lower than the front of the immutable list, violating the ordering assertion in `MemTableListVersion::AddMemTable `.

## Fix

After re-acquiring db_mutex_, use `std::max(mems_.back()->GetID(), imm()->GetLatestMemTableID())` so the output memtable's ID is never lower than any existing immutable memtable. Two `TEST_SYNC_POINT`s are added to the `MemPurge` success path to enable deterministic repro.

Reviewed By: pdillinger

Differential Revision: D94756638

fbshipit-source-id: 2e9e15b4285dc6b996c8744795228180dbd73ed3
2026-03-05 22:32:47 -08:00
Jay Huh 3aa706c2bf Enforce WriteBufferManager during WAL recovery (#14305)
Summary:
Add a new immutable DB option `enforce_write_buffer_manager_during_recovery` to control whether WriteBufferManager buffer_size is enforced during WAL recovery. When multiple RocksDB instances share a WriteBufferManager, a recovering instance could exceed the global memory limit by replaying large amounts of WAL data into memtables. This can cause OOM, especially when other instances are actively using the shared memory budget. When this option is enabled (and a WriteBufferManager is configured), RocksDB will check `WriteBufferManager::ShouldFlush()` after each batch insertion during WAL recovery and schedule flushes when needed to keep memory bounded.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14305

Test Plan:
Two unit tests added - `WriteBufferManagerLimitDuringWALRecoverySingleDB` and `WriteBufferManagerLimitDuringWALRecoveryMultipleDBs`
```
./db_write_buffer_manager_test --gtest_filter="*WriteBufferManagerLimitDuringWALRecovery*"
```
Basic stress test added with options toggled

## To follow up

Multiple DB scenario in stress test

Reviewed By: hx235

Differential Revision: D92533792

Pulled By: jaykorean

fbshipit-source-id: 35ca70e2300a8bfd6b81a6646a65ef284fb90a9a
2026-03-05 15:56:46 -08:00
Xingbo Wang 2dc6d6f765 Propagate builder error when flush produces empty output (#14418)
Summary:
Propagate builder error when flush produces empty output

When write fault injection causes the table builder to enter an error
state during flush, all subsequent Add() calls return early (ok() is
false), leaving the builder empty (IsEmpty() == true). Previously,
BuildTable() would call builder->Abandon() but not propagate the
builder's error status to 's', leaving it OK. This caused the downstream
key count validation in flush_job.cc to fire a misleading Corruption
error ('Number of keys in flush output SST files does not match...'),
which the stress test harness couldn't identify as a retryable injected
fault error, leading to SafeTerminate().

This started failing recently because ('Separate keys and
values in data blocks', ) introduced a new SST
block format (separate_key_value_in_data_block) that stores keys and
values in separate sections within data blocks. This format requires
additional write operations during Flush() inside the table builder,
increasing the probability that write fault injection
(--write_fault_one_in=128) hits a data block write and puts the builder
into an error state before any entries are committed. The bug in
BuildTable() existed before, but was rarely triggered because the old
interleaved block format had fewer write points susceptible to fault
injection during the critical Add() path.

Fix: After builder->Abandon(), propagate the builder's error status to
's' when the builder is empty due to an internal error. This ensures the
actual IOError from write fault injection is reported, which the stress
test can properly handle via IsErrorInjectedAndRetryable().

The analysis was based on stack trace. However, it would be great
if we could get direct evidence from fault injection.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14418

Test Plan: Unit test

Reviewed By: hx235

Differential Revision: D95121070

Pulled By: xingbowang

fbshipit-source-id: cfb513bd744ac34ac90cda11c1cbe49a9d0a7c6c
2026-03-05 14:18:08 -08:00
Peter Dillinger ef5b145140 Fix ldb dump swallowing errors, but ldb scan for compatibility test (#14422)
Summary:
This fixes a longstanding bug in which `ldb dump` swallows iterator errors. This can affect check_format_compatible.sh test results; if lucky, it will misleadingly look like a data mismatch instead of an outright failure. If unlucky, it could cause a test false negative.

However the compatibility test uses old versions of ldb, so the best way to improve the test (for the foreseeable future) is to replace `ldb dump` with `ldb scan`.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14422

Test Plan: manual

Reviewed By: joshkang97

Differential Revision: D95332577

Pulled By: pdillinger

fbshipit-source-id: bef1b427dd8aaa2cabbd23b7ad9f3cad1f67a349
2026-03-05 09:41:58 -08:00
Anand Ananthabhotla 3b5cb114e3 Refactor MultiScan to use MultiScanIndexIterator (#14401)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14401

Unify the MultiScan and regular iterator codepaths in BlockBasedTableIterator by introducing a MultiScanIndexIterator that implements InternalIteratorBase<IndexValue>. During Prepare(), the original index iterator is swapped out for a MultiScanIndexIterator that wraps the prefetched block handles and scan range metadata. This allows SeekImpl() and FindBlockForward() to use the same code flow for both regular and MultiScan operations, eliminating the need for separate MultiScan-specific methods (SeekMultiScan, FindBlockForwardInMultiScan, MultiScanSeekTargetFromBlock, MultiScanUnexpectedSeekTarget, MultiScanLoadDataBlock, MarkPreparedRangeExhausted).

Key changes:
- New MultiScanIndexIterator class that manages scan range tracking, block handle iteration, forward-only seek enforcement, and wasted block counting
- InitDataBlock() loads blocks from ReadSet when MultiScan is active
- FindBlockForward() detects scan range boundaries via IsScanRangeExhausted() after index_iter_->Next()
- Disabled reseek optimization for MultiScan so MultiScanIndexIterator::Seek() is always called to update scan range tracking state
- Removed MultiScanState struct and all MultiScan-specific methods from BlockBasedTableIterator
- No changes to CheckDataBlockWithinUpperBound or CheckOutOfBound — they work as-is through iterate_upper_bound
- multi_scan_status_ intentionally not checked in Valid() hot path to avoid performance regression; when status is non-OK, block_iter_points_to_real_block_ is already false
- Fixed pre-existing bug in ReadSet::SyncRead() that used the base decompressor without compression dictionary, causing ZSTD data corruption when blocks with dictionary compression needed synchronous fallback reads

Reviewed By: xingbowang

Differential Revision: D93300655

fbshipit-source-id: 231059208e0cc512bc2ec43ff7055fcb2a2dc72d
2026-03-04 21:09:54 -08:00
zaidoon 16c81a27e9 Extend UDI trie index with seqno side-table for same-user-key block boundaries (#14412)
Summary:
When the same user key spans adjacent data blocks with different sequence
numbers, the trie index cannot distinguish them because it only stores
user keys. This adds a side-table that stores per-leaf sequence numbers
and overflow block metadata, enabling correct post-seek correction using
seqno comparison.

Design:
- Trie always stores user-key-only separators (unchanged)
- Duplicate separators from same-key boundaries are de-duplicated
- Side-table stores leaf_seqnos[], leaf_block_counts[], and overflow
  arrays (offsets, sizes, seqnos) serialized after the trie data
- Post-seek correction: after trie Seek lands on a leaf, compare
  target_seq vs leaf_seqno to decide whether to advance through
  overflow blocks or to the next trie leaf
- Zero overhead when no same-user-key boundaries exist (no side-table
  serialized, no seqno checks at seek time)

Public API changes (user_defined_index.h):
- AddIndexEntry: single pure virtual with IndexEntryContext parameter
  (replaces old 4-arg version). Context carries last_key_seq and
  first_key_seq for block boundaries.
- SeekAndGetResult: single pure virtual with SeekContext parameter
  (replaces old 2-arg version). Context carries target_seq.
- Fix trailing semicolons on NewBuilder/NewReader default overrides.

Wrapper layer (user_defined_index_wrapper.h):
- Extract sequence numbers from parsed internal keys and pass via
  context structs to UDI builder/iterator.
- Fix CurrentIndexSizeEstimate() to delegate to internal builder
  (was returning 0).
- Fix ApproximateMemoryUsage() to include UDI reader memory.
- Fix typos: lof_err_key -> log_err_key, COuld -> Could,
  "Bad index name" -> "Bad index name: ".

Trie builder (trie_index_factory.cc):
- Buffer separator entries during building; at Finish(), detect whether
  any same-user-key boundary was seen (sticky flag, same strategy as
  ShortenedIndexBuilder::must_use_separator_with_seq_).
- When seqno encoding is needed, re-encode all separators with seqno
  side-table metadata in the trie.
- Default null comparator to BytewiseComparator() to prevent crash.

Trie iterator (trie_index_factory.cc):
- Post-seek correction: compare target_seq vs leaf_seqno to advance
  through overflow runs.
- Overflow run state tracking: overflow_run_index_, overflow_run_size_,
  overflow_base_idx_ for O(1) access to overflow block handles.
- NextAndGetResult advances within overflow runs before moving to next
  trie leaf.
- Return kOutOfBound instead of kUnknown when Seek/Next finds no blocks.

LOUDS trie (louds_trie.h, louds_trie.cc):
- Seqno side-table: builder AddKeyWithSeqno/AddOverflowBlock methods,
  BFS reordering of seqno/block_count arrays, serialization/deserialization
  of per-leaf seqnos, block counts, overflow handles/seqnos, and
  overflow_base_ prefix sum for O(1) access.
- AppendKeySlot() helper with debug bounds assert on all key-append sites.

Dead code removal:
- LoudsTrieBuilder::NumKeys() (never called externally)
- sparse_leaf_count_ (serialized but never read by the reader)
- DenseChildNodeNum(pos), DenseLeafIndex(pos), DenseNodeNum(pos)
  (superseded by FromRank variants that avoid redundant Rank1 calls)

Deserialization hardening (all in InitFromData, cold path only):
- Move num_keys_ validation before any dependent arithmetic.
- Validate dense bitvector sizes match dense_node_count_ (d_labels
  must be node_count*256, d_has_child must equal d_labels.NumOnes(),
  d_is_prefix_key must equal node_count).
- Validate sparse bitvector sizes match s_labels_size_ (s_has_child
  and s_louds must have the same number of bits as the label array).
- Validate child position table values within s_labels_size_ bounds.
- Validate chain suffix offset+length within suffix data blob.
- Validate chain end child indices < num_internal or == UINT32_MAX.
- EliasFano: add count_ upper-bound check (<=2^30) to prevent
  count_*low_bits_ integer overflow.
- Bitvector: validate select hint values < num_rank_samples_ to
  prevent OOB in FindNthOneBit/FindNthZeroBit.
- EliasFano: custom move ctor/assignment to re-seat low_words_
  after owned_low_data_ move (fixes dangling pointer for SSO strings).

https://github.com/facebook/rocksdb/issues/14406

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14412

Reviewed By: anand1976

Differential Revision: D95160933

Pulled By: xingbowang

fbshipit-source-id: f2c3681c1059c03d540ce1cb9cc14cc79cd9730c
2026-03-04 15:31:17 -08:00
Xingbo Wang 3b66de3fac Fix out of disk unit test issue (#14425)
Summary:
It contains 3 commits.

  Commit 1: Fix /dev/shm exhaustion during make check

  Fix /dev/shm exhaustion during make check

  1. Add space-heavy tests to NON_PARALLEL_TEST: perf_context_test (1GB
     write_buffer_size), obsolete_files_test (~1GB), backup_engine_test
     (1GB), prefetch_test (1GB), and db_io_failure_test (256MB).

  2. Add per-test cleanup on success: each parallel test script now
     removes its test directory after passing. Failed test directories
     are preserved for debugging.

  3. Add age-based stale directory cleanup: at the start of make check,
     remove /dev/shm/rocksdb.* directories older than 3 hours from
     previous failed runs, using age-based filtering to avoid disturbing
     concurrent runs.

  Commit 2: Fix SIGSEGV in prefetch_test due to stale SyncPoint callbacks

  Both FilePrefetchBufferTest and FSBufferPrefetchTest fixtures set up
  SyncPoint callbacks capturing local variables by reference and enable
  processing, but neither fixture's TearDown() clears them. When a
  subsequent test runs, the stale callbacks fire with dangling
  references, causing memory corruption and SIGSEGV.

  Fixed by adding DisableProcessing() and ClearAllCallBacks() to both
  fixtures' TearDown() methods.

  Commit 3: Fix OpenFilesAsyncTest using excessive disk space

  Fix OpenFilesAsyncTest using excessive disk space in /dev/shm

  OpenFilesAsyncTest::SetupData() set write_buffer_size to SIZE_MAX to
  prevent automatic flushes. This caused each of its ~20 parallel
  instances to consume 6-43 GB in /dev/shm (validated: a single
  Shutdown/3 instance used 43 GB), totaling ~233 GB and filling the
  disk. This was the primary cause of "No space left on device" errors
  in make check. The sst flushed is small, but it causes issue in WAL
  space reservation, which bloated the disk usage.

  The SIZE_MAX is unnecessary — the test writes only 4 tiny key-value
  pairs and does explicit Flush() after each. The default 64 MB
  write_buffer_size will never auto-flush for such small writes.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14425

Test Plan: Unit Test

Reviewed By: joshkang97

Differential Revision: D95253518

Pulled By: xingbowang

fbshipit-source-id: 03907df59b3d89d90413a0e33996ec205944d48b
2026-03-04 13:48:48 -08:00
Xingbo Wang 9a9cc1e2ef Fix uninitialized wal_in_db_path_ in read-only and compacted DB open paths (#14419)
Summary:
wal_in_db_path_ was declared without an in-class initializer in DBImpl. While DB::Open, OpenAsSecondary, and OpenAsFollower all explicitly set this field, OpenForReadOnly and CompactedDBImpl::Open did not.

This was harmless until recently because CloseHelper() only calls PurgeObsoleteFiles when opened_successfully_ is true, and read-only DBs previously did not set opened_successfully_. After https://github.com/facebook/rocksdb/issues/14322 added opened_successfully_ = true to the read-only path, CloseHelper now executes PurgeObsoleteFiles -> DeleteObsoleteFileImpl, which reads wal_in_db_path_ to decide whether WAL files should be deleted in the foreground. Reading an uninitialized bool is undefined behavior, caught by UBSan as "invalid-bool-load" in the secondary_cache_crash_test.

Fixed by adding an in-class initializer (= false) to wal_in_db_path_. The default of false means WAL deletions use foreground deletion, which is safe for read-only DBs that don't write WALs. The existing DB::Open path continues to set the correct value explicitly.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14419

Test Plan:
- Added regression test ReadOnlyDBWalInDbPathInitialized in db_test2
- Ran test 5 times without failure
- Ran all OpenForReadOnly tests (3 tests pass)

Task:
T257988006

Reviewed By: joshkang97

Differential Revision: D95122268

Pulled By: xingbowang

fbshipit-source-id: 7f65fe1f09e7e0b42ba68f44f246615abc0757d4
2026-03-04 13:26:19 -08:00
Xingbo Wang ea2f33c81a Disable prefix scanning when use_trie_index is enabled in crash test (#14421)
Summary:
The trie-based User Defined Index (UDI) has a bug in its iterator implementation that causes "Not implemented: SeekToFirst not supported" errors during prefix scanning. This causes assertion failures in BatchedOpsStressTest::TestPrefixScan and may cause silent wrong results in other prefix scan tests.

Until the trie index is fixed, disable prefix scanning when use_trie_index is enabled by setting prefixpercent=0 and redistributing the percentage to reads.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14421

Test Plan:
- Reproduced: db_stress with --use_trie_index=1 --prefixpercent=5 crashes with assertion failure. Instrumentation revealed iterator status "Not implemented: SeekToFirst not supported" on prefix 0x35.
- Verified: db_stress with --use_trie_index=1 --prefixpercent=0 completes cleanly.

Reviewed By: anand1976

Differential Revision: D95135250

Pulled By: xingbowang

fbshipit-source-id: 81540b2426aa1a855a58e6ca9e68a035d53aa2d8
2026-03-04 13:11:33 -08:00
Xingbo Wang 15f2f2bf6f Relax option sanitization for kv ratio compaction (#14397)
Summary:
The kv ratio compaction option sanitization is too strict. Sometimes, it blocks engine start up. Relax the validation and convert it to runtime check.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14397

Test Plan: Unit test

Reviewed By: pdillinger

Differential Revision: D94709471

Pulled By: xingbowang

fbshipit-source-id: cc076c397b3acfa426112063224771a196684798
2026-03-03 21:40:36 -08:00
Xingbo Wang 9d3d3c4b61 Handle TryAgain from optimistic transactions in stress test (#14410)
Summary:
When using optimistic transactions (--use_optimistic_txn=1), the transaction commit can fail with Status::TryAgain() if the memtable history is insufficient for conflict detection (controlled by max_write_buffer_size_to_maintain). ExecuteTransaction retries up to 10 times, and if all retries fail, it returns TryAgain.

Previously, this TryAgain status was not handled in TestPut, TestDelete, and TestSingleDelete, causing the stress test to call SafeTerminate() and crash with "put or merge error" / "delete error".

This is an expected condition with optimistic transactions and should be handled gracefully by rolling back the pending expected value.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14410

Test Plan:
- make -j db_stress && run crash_test_with_optimistic_txn
- Stress test with --use_optimistic_txn=1 --use_txn=1 with small max_write_buffer_size_to_maintain to verify no crash on TryAgain.

Reviewed By: hx235

Differential Revision: D95077227

Pulled By: xingbowang

fbshipit-source-id: 15c343cc1c5f888fb79e95a87cfe46145f98b3a1
2026-03-03 17:16:17 -08:00
Xingbo Wang cc19bd7555 Disable mmap_read with trie index to fix SIGSEGV (#14409)
Summary:
The trie-based UDI (LoudsTrie/Bitvector) stores zero-copy reinterpret_cast pointers into the serialized UDI block data. When mmap_read is enabled, the block data resides in memory-mapped file pages. If the SST file's mmap mapping is later invalidated (e.g. through table cache eviction or DB reopen), the trie's internal pointers become dangling, causing SIGSEGV.

For now, disable mmap_read when use_trie_index is enabled:
- db_crashtest.py: force mmap_read=0 when use_trie_index=1
- db_stress_tool.cc: reject the combination with an error message

The proper fix, involve loading index then fix the pointer address.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14409

Reviewed By: pdillinger

Differential Revision: D94971965

Pulled By: xingbowang

fbshipit-source-id: 6abbd4bfbfd26e7ba8725a93a6c68beb26f6393e
2026-03-03 08:50:44 -08:00
Xingbo Wang c2580e2b7a Fix use-after-free in db_stress with UDI factory (#14411)
Summary:
In StressTest::OperateDb(), read_opts.table_index_factory is set to udi_factory_.get() (a raw pointer) once at function entry. When Reopen() is called during the stress test loop, Open() recreates udi_factory_ via std::make_shared<TrieIndexFactory>(), destroying the old factory. But read_opts.table_index_factory still holds the dangling pointer.

When MultiGet subsequently calls UserDefinedIndexReaderWrapper::NewIterator() and accesses read_options.table_index_factory->Name(), it dereferences a dangling pointer, causing a segmentation fault.

Fix: After each reopen, update read_opts.table_index_factory to point to the newly created udi_factory_ instance.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14411

Test Plan: - Ran db_stress with --use_trie_index=1 --reopen=20 --use_multiget=1 4 times without failure (previously crashed with SIGSEGV)

Reviewed By: pdillinger

Differential Revision: D95064144

Pulled By: xingbowang

fbshipit-source-id: 7139baacda659d8a3cb84fdcc4822a428542bf0c
2026-03-03 08:43:40 -08:00
Josh Kang f25fb41da6 Add option to validate sst files in the background on DB open (#14322)
Summary:
Add `open_files_async` option for faster DB startup. When enabled, SST file opening and validation is deferred to a background thread after `DB::Open` returns, reducing startup latency for databases with many SST files. WAL recovery remains synchronous.

To support this, `FindTable` is extended with a pinning mechanism that stores the cache handle directly on `FileMetaData` via a new `PinnedTableReader` class, and sets the table reader atomically so subsequent reads skip cache lookups. `FileDescriptor::table_reader` is replaced with `PinnedTableReader pinned_reader` which wraps a `std::atomic<TableReader*>` with acquire/release ordering to safely handle concurrent access between the background opener and read threads.

Should validations fail, the background opener sets a `kAsyncFileOpen` background error. Future read requests will look up the table reader again via the cache, and if any validations fail there it will get propagated to the user (existing behavior when `max_open_files > 0`).

This feature is most useful when `max_open_files=-1`, because otherwise file opening is already capped at 16 files and DB open should be fast.

## Restrictions
- This feature also is incompatible with fifo compaction because fifo compaction requires reading table properties under DB mutex. When table reader is unpinned, this may cause a DB hang.
- This feature is also incompatible with `skip_stats_update_on_db_open=false` because it will result in even longer DB open

## Key changes

- New `open_files_async` DB option with C, Java, and `db_bench` bindings
- `BGWorkAsyncFileOpen` background worker that opens all SST files post-`DB::Open`, with shutdown awareness via `shutting_down_` flag
- New `PinnedTableReader` class in `version_edit.h` — thread-safe wrapper holding `std::atomic<TableReader*>` and `Cache::Handle*` with proper acquire/release ordering. Replaces the old `FileDescriptor::table_reader` raw pointer and `FileMetaData::table_reader_handle`
- Extract `LoadTableHandlersHelper` into `db/version_util.cc` — shared between `VersionBuilder::LoadTableHandlers` (for version edits during recovery) and `BGWorkAsyncFileOpen` (for base storage post-open)
- `FindTable` extended with `pin_table_handle` and `out_table_reader` params — when pinning is enabled, the table reader is stored on `FileMetaData` so Get/MultiGet/Iterator skip redundant cache lookups. `FindTable` now performs the pinned-reader fast-path check internally instead of requiring callers to check `fd.table_reader` beforehand
  - Note: pinning is explicit (not default) because some callers create temporary `FileMetaData`s that would need to properly clean up table handles
- `CompactedDBImpl` updated to use `FindTable` + pinning instead of raw `fd.table_reader` access for Get/MultiGet
- New `kAsyncFileOpen` background error reason in `listener.h` and `error_handler.cc`
- Add a check in ~DBImpl to ensure async file open task has not been forgotten to be scheduled in (future) subclasses of DBImpl. Certain subclasses that never use it will need to explicitly mark it.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14322

Test Plan:
- `OpenFilesAsyncTest` parameterized over `num_flushes` (1, 20), `ReadType` (Get, MultiGet, Iterator), `max_open_files` (-1, 10), and `read_only` (true, false)
  - **ConcurrentFileAccess**: concurrent reads and compactions race with async opener
  - **AfterRead**: reads happen before async opener, verifying lazy open and that the opener sees already-pinned readers
  - **BeforeRead**: async opener completes first, verifying reads use pre-loaded table readers
  - **Shutdown**: DB closes before async opener starts, verifying clean cancellation with 0 file opens
  - **Error**: corrupted SST files, verifying `kAsyncFileOpen` background error is set and reads return corruption
  - **DropColumnFamily**: CF dropped before async opener runs, verifying the opener gracefully skips dropped CFs
- Added to crash test

### Benchmark

To simulate a high-latency remote filesystem, I set up a virtual filesystem with dm-delay using 10ms reads, 0 ms writes.

```
# Generate a DB with many L0 files

TEST_TMPDIR=/data/users/jkangs/dm-delay-test/mnt ./db_bench -benchmarks=fillseq -disable_auto_compactions=true -write_buffer_size=1000 -num=1000000
```

```
./db_bench -use_existing_db=true -db=/data/users/jkangs/dm-delay-test/mnt/dbbench -benchmarks=readrandom -reads=1 -report_open_timing=true -open_files_async=true -use_direct_reads -file_opening_threads=1 -skip_stats_update_on_db_open

OpenDb:     25.1419 milliseconds
```

```
./db_bench -use_existing_db=true -db=/data/users/jkangs/dm-delay-test/mnt/dbbench -benchmarks=readrandom -reads=1 -report_open_timing=true -open_files_async=false -use_direct_reads -file_opening_threads=1 -skip_stats_update_on_db_open

OpenDb:     23109.4 milliseconds
```

### No read regressions

On main branch
```
./db_bench -use_existing_db=true -db=/dev/shm/dbbench -benchmarks=readrandom -seed=1 -threads=8 -duration=30

readrandom   :       4.827 micros/op 1657100 ops/sec 30.005 seconds 49720992 operations;  183.3 MB/s (6198999 of 6198999 found)
```

On this branch
```
./db_bench -use_existing_db=true -db=/dev/shm/dbbench -benchmarks=readrandom -seed=1 -threads=8 -duration=30

readrandom   :       4.863 micros/op 1644808 ops/sec 30.007 seconds 49354992 operations;  182.0 MB/s (6099999 of 6099999 found)

./db_bench -use_existing_db=true -db=/dev/shm/dbbench -benchmarks=readrandom -seed=1 -threads=8 -duration=30 -open_files_async=true

readrandom   :       4.803 micros/op 1665392 ops/sec 30.004 seconds 49968992 operations;  184.2 MB/s (6222999 of 6222999 found)
```

Reviewed By: pdillinger, xingbowang

Differential Revision: D93538033

Pulled By: joshkang97

fbshipit-source-id: 32ac70c112cd733b7c1e1c1e2e7ce6422318a5ae
2026-03-02 16:18:14 -08:00
Xingbo Wang b48d6f2c12 Block TransactionDB when UDI (use_trie_index) is enabled (#14408)
Summary:
User-Defined Index (UDI) only supports kTypeValue (Put) entries. TransactionDB is incompatible because transaction ROLLBACK operations write DELETE entries to the WAL to undo uncommitted changes.

Evidence from crash DB analysis (T257683723):
- All SST files had 0 deletions, 0 merges, 0 range deletions
- WAL file 012653.log contained DELETE entries from ROLLBACK: 322982,DELETE(2) ROLLBACK(0x7869643334333837) 322987,DELETE(8) ROLLBACK(0x7869643334333935) 322989,DELETE(8) ROLLBACK(0x7869643334343031) 322993,DELETE(9) ROLLBACK(0x7869643334343035) 322996,DELETE(4) ROLLBACK(0x7869643334333939)

Root cause chain:
1. TransactionDB ROLLBACK writes DELETE entries to WAL
2. During recovery/Open(), WAL entries are replayed into memtable
3. During flush, memtable (containing DELETEs) triggers UDI builder
4. UDI builder rejects DELETE: pkey.type != kTypeValue
5. Error: "user_defined_index_factory only supported with Puts"

This change adds:
1. Validation in db_stress_tool.cc to reject use_trie_index + use_txn
2. Sanitization in db_crashtest.py to set use_txn=0 when use_trie_index=1

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14408

Test Plan:
./db_stress --use_trie_index=1 --use_txn=1 --delpercent=0 \
      --delrangepercent=0 --readpercent=30 --prefixpercent=20 \
      --writepercent=40 --iterpercent=10 --customopspercent=0
      # Now correctly rejects with clear error message

Reviewed By: joshkang97

Differential Revision: D94928704

Pulled By: xingbowang

fbshipit-source-id: bdaa1e719f158e4a3fd76c64cf097ac3f41c8512
2026-03-02 15:54:27 -08:00
Xingbo Wang aa9160e8d7 Fix stress test failure with trie index (#14407)
Summary:
Fix stress test to check UDI usage for unsupported iterator ops

    UserDefinedIndexIterator only supports Seek(target) + Next() - it requires
    a target key for all seeks. The following operations are not supported:
    - SeekToFirst() - no target key
    - SeekToLast() - no target key
    - SeekForPrev() - not in UDI interface
    - Prev() - not in UDI interface

    This fix checks for UDI usage at both levels:
    1. ReadOptions level: ro.table_index_factory != nullptr
    2. CF/table level: udi_factory_ != nullptr (BlockBasedTableOptions)

    This is future-proof for any UDI implementation, not just trie index.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14407

Reviewed By: joshkang97

Differential Revision: D94925001

Pulled By: xingbowang

fbshipit-source-id: bf2ceacc7acebbc2347be445e2f208820a97b817
2026-03-02 11:25:57 -08:00
Peter Dillinger da2c3c0ee6 Fix & improve compaction trigger on a "quiet" DB (#14396)
Summary:
As a follow-up to https://github.com/facebook/rocksdb/issues/13736, allow a "quiet" DB to react much sooner to time-based compaction triggers. For details see DBImpl::ComputeTriggerCompactionPeriod() implementation.

Also based on review feedback, fixing a bug where only column families setting periodic compaction would be triggered, rather than any time-based compaction.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14396

Test Plan: extended+added unit tests to cover much of the logic

Reviewed By: xingbowang

Differential Revision: D94626166

Pulled By: pdillinger

fbshipit-source-id: de9ca19e46bdba5d9715474efbc61805354d730d
2026-02-28 13:43:55 -08:00
zaidoon 8707abfc11 Fix crash in UserDefinedIndexBuilderWrapper with buffered data blocks (#14404)
Summary:
When the UDI wrapper's OnKeyAdded() encountered a non-Put key type (e.g. Delete or Merge), it set its internal status_ to non-OK and stopped forwarding OnKeyAdded() to the wrapped internal index builder. However, AddIndexEntry() was always forwarded unconditionally. This asymmetry left the internal ShortenedIndexBuilder's current_block_first_internal_key_ empty, triggering an assertion failure in GetFirstInternalKey() during the buffered-block replay in MaybeEnterUnbuffered().

The crash required three conditions to co-occur:
1. UDI enabled (use_trie_index=1)
2. Compression dictionary enabled (triggering kBuffered mode)
3. Non-Put entries in the data (Delete, Merge, etc.)

Fix: move the internal_index_builder_->OnKeyAdded() call before the status_ guard so the internal builder always receives every key, matching the unconditional forwarding in AddIndexEntry().

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14404

Reviewed By: archang19

Differential Revision: D94791493

Pulled By: xingbowang

fbshipit-source-id: 882e409f61ae9aca084e9511794ca32ba4ff090f
2026-02-28 12:58:21 -08:00
zaidoon ce0fff9f41 Add trie-based User Defined Index (UDI) plugin (#14310)
Summary:
Implement a Fast Succinct Trie (FST) index based on LOUDS encoding as a User Defined Index plugin for RocksDB's block-based tables. First step toward trie-based indexing per https://github.com/facebook/rocksdb/issues/12396.
The trie uses hybrid LOUDS-Dense (upper levels, 256-bit bitmaps) + LOUDS-Sparse (lower levels, label arrays) encoding inspired by the [SuRF paper](https://www.pdl.cmu.edu/PDL-FTP/Storage/surf_sigmod18.pdf) (Zhang et al., SIGMOD 2018). The boundary between dense and sparse levels is automatically chosen to minimize total space.

Key Components
- **Bitvector** with O(1) rank and O(log n) select using a rank LUT sampled every 256 bits with popcount intrinsics. Uses uint32_t rank LUT entries (halving memory vs uint64_t). Includes word-level `AppendWord()` for efficient dense bitmap construction and `AppendMultiple()` optimized at word granularity for bulk bit fills.
- **Streaming trie builder** using flat per-level arrays with deferred internal marking, handle migration for prefix keys, and lazy node creation. Infers trie structure directly from sorted keys via LCP analysis in a single pass (no intermediate tree).
- **LoudsTrie** for immutable querying with BFS-ordered handle reordering built into single-pass level-by-level serialization. Move-only semantics with correct pointer re-seating after `std::string` move.
- **LoudsTrieIterator** with rank-based traversal, key reconstruction from trie path, and stack-based backtracking for `Next()`. Uses packed 8-byte `LevelPos` (is_dense flag in bit 63) and `autovector<LevelPos, 24>` to avoid heap allocation. Key reconstruction uses a raw char buffer allocated once to `MaxDepth()+1` bytes.
- **TrieIndexFactory/Builder/Reader/Iterator** implementing the `UserDefinedIndexFactory` interface.
- **Zero-copy block handle loading** using two fixed-width uint64_t arrays (offsets + sizes) with 8-byte alignment, enabling O(1) initialization via direct pointer assignment.

Seek Hot Path Optimizations
- **Fanout-1 sparse fast path**: Most sparse nodes in tries built from zero-padded numeric keys have exactly one child. Detected via `start_pos + 1 == end_pos` and inlined as a single byte comparison, avoiding the full `SparseSeekLabel` call.
- **Linear scan for small sparse nodes**: `SparseSeekLabel` uses sequential scan for nodes with ≤16 labels instead of binary search. Faster for common 10-child digit nodes where branch misprediction cost outweighs linear scan cost.
- **Rank reuse**: `DenseLeafIndexFromRankAndHasChildRank` and `SparseLeafIndexFromHasChildRank` overloads accept pre-computed `has_child_rank` from the Seek descent, avoiding redundant `Rank1` calls.
 General Performance Optimizations
- **Select-free sparse traversal**: Precomputed child position lookup tables (`s_child_start_pos_`/`s_child_end_pos_`) eliminate `Select1` calls during Seek. Sparse traversal tracks `(start_pos, end_pos)` directly, using only `Rank1` (O(1)) + array lookup (O(1)) for child descent.
- **Cached label_rank pattern**: Eliminates redundant `Rank1` calls in hot paths (Seek, Next, Advance all cache and reuse the label_rank computed for has_child checking).
- **Leaf index fast path**: When no prefix keys exist (common case), `SparseLeafIndex` and `DenseLeafIndexFromRank` skip the prefix-key `Rank1` calls entirely, reducing from 3 to 1 `Rank1` call.
- **Popcount-based Select64** via 6-step binary search within 64-bit words.
- MSVC portability using RocksDB's `BitsSetToOne`/`CountTrailingZeroBits`.

Benchmark Results

Trie Seek at 32K keys (16-byte keys, 5M lookups, median of 5 runs):
| Configuration | ns/op |
|---|---:|
| Trie (optimized) | **118** |
| Binary search (native) | 134 |
Trie Seek is **~12% faster** than native binary search index at 32K keys per block.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14310

Reviewed By: anand1976

Differential Revision: D93921511

Pulled By: xingbowang

fbshipit-source-id: ba18604bc6bd4f575311a1ae3047a541274869b6
2026-02-26 21:20:48 -08:00
Hui Xiao 372995470a Pass statistics and fix null clock in SstFileReader::MultiGet (#14393)
Summary:
**Summary:**
This is to sync an internal change of passing `Statistics*` to the GetContext constructor and collecting more stats as well as fix a bug this change created.

SstFileReader::MultiGet was passing nullptr for both `SystemClock*` and `Statistics*` to the GetContext constructor.  After  `Statistics*` was passed to the GetContext constructor (the internal change), this caused a segfault when a merge operation was triggered with statistics enabled, because the merge helper's StopWatchNano attempted to dereference the null clock pointer. Fix by passing `r->ioptions.clock` from the reader's options.

Additionally, add `assert(clock_)` guards to `StopWatchNano::Start()` and `ElapsedNanos()` to catch null clock bugs in debug builds. Can't do so in release build because it's on hot path.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14393

Test Plan:
- `./sst_file_reader_test --gtest_filter='SstFileReaderTableMultiGetTest.Basic'` exercises merge with statistics enabled, previously segfaulted without the fix with the clock, now passes.
- `./sst_file_reader_test` - all tests pass.

Reviewed By: xingbowang

Differential Revision: D94599343

Pulled By: hx235

fbshipit-source-id: 0a748bb00ee27bb202d01d410b52657101c05de0
2026-02-26 19:16:12 -08:00
xingbowang 1a8471b17e stop using portable folly build for nightly job (#14391)
Summary:
The nightly build had a flag for portable build on folly, but rocksdb does not. This causes link error. Fix this by removing portable build flag in folly build in nightly run. Nightly run will always build without cache using native flag. Only PR jobs uses cache.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14391

Test Plan: nightly run

Reviewed By: joshkang97

Differential Revision: D94520182

Pulled By: xingbowang

fbshipit-source-id: 7c7d3b089744c7e2e8ea073158f1b5b80db420d4
2026-02-26 12:43:28 -08:00
Xingbo Wang c311362ab6 Reapply "Fix flaky unit test in mempurge. (#14377)" (#14381) (#14385)
Summary:
This reverts commit 4213f9e14a.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14385

Reviewed By: archang19

Differential Revision: D94367585

Pulled By: xingbowang

fbshipit-source-id: 6c125b23ffa74e4113ef04847ccdefa015d7db35
2026-02-25 12:39:45 -08:00
xingbowang 5cb88bae30 fix portable build for cmake pr job (#14388)
Summary:
-march=native in cmake builds causes SIGILL after cache hits
Issue: PORTABLE=1 env var only works for Makefile builds. cmake ignores it and injects -march=native via its own CPU detection. Since ccache hashes the flag string literally, a cache compiled on an AVX-512 runner and restored on a non-AVX-512 runner produces a SIGILL crash.
Fix: Added -DPORTABLE=ON to build-linux-cmake-with-benchmark-no-thread-status and build-linux-cmake-with-folly-coroutines cmake commands.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14388

Test Plan: Github CI

Reviewed By: joshkang97

Differential Revision: D94367557

Pulled By: xingbowang

fbshipit-source-id: 5aa39cc36fc004d0ee636cd3fa197880d6cb773a
2026-02-25 10:47:20 -08:00
Hui Xiao 300fb8cace Add claude md file for deprecated option removal (#14360)
Summary:
**Context/Summary:**

It's very easy to make mistake in removing deprecated option such as deleting the corresponding entry in type info that breaks backward compatibility or missing tests to test backward compatibility. Example: https://github.com/facebook/rocksdb/pull/14350#discussion_r2834554287. Added a claude md file for that.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14360

Test Plan: In claude code, prompted for deprecated option removal with this claude md file in a repo separated from my previous deprecation efforts as much as possible and received correct change at one try.

Reviewed By: pdillinger

Differential Revision: D93920127

Pulled By: hx235

fbshipit-source-id: 1977ce7404b0188f84258552b4843e70394a5aea
2026-02-24 23:04:22 -08:00
Josh Kang 5db2eb0f3c Fix missing corruption check for key overflows (#14384)
Summary:
The refactoring in https://github.com/facebook/rocksdb/pull/14287/changes#diff-9f58e06172d3c8b15496fb4e8216a9ceee07ea2648e4c93d6a105d578697aa07L101-L103 unintentionally removed a corruption check for a key existing past its limit, but does account for a value existing past its limit.

This fix adds an explicit key check as well

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14384

Test Plan: CI

Reviewed By: pdillinger

Differential Revision: D94275062

Pulled By: joshkang97

fbshipit-source-id: ba793bbb966f0bae33df4c98e498e3d6a2b04087
2026-02-24 22:34:28 -08:00
Peter Dillinger 1ba60ee9ee Restore blob_dump_tool compression support (partially revert PR #14266) (#14382)
Summary:
PR https://github.com/facebook/rocksdb/issues/14266 ("Remove compression support") removed compression-related functionality from blob_dump_tool and ldb commands. While this was valid for the legacy stacked BlobDB (which no longer supports compression), the integrated blob storage (`enable_blob_files`) uses the same blob file format and fully supports compression via `blob_compression_type`.

This partial revert restores the ability to view uncompressed blob data from compressed blob files, which is essential for debugging and analysis of integrated blob storage.

Restored functionality:
- `blob_dump --show_uncompressed_blob` option
- `ldb dump --dump_uncompressed_blobs` option
- `ldb dump_live_files --dump_uncompressed_blobs` option
- Related ldb_test.py test coverage

The decompression implementation in utilities/blob_db/blob_dump_tool.cc has been updated to use the modern
`GetBuiltinV2CompressionManager()->GetDecompressorOptimizeFor()` API.

Suggested follow-up:
* Tests for blob_dump (or integrate it into sst_dump/ldb?)

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14382

Test Plan:
Restored ldb_test.py
Some manual testing

Reviewed By: mszeszko-meta

Differential Revision: D94257588

Pulled By: pdillinger

fbshipit-source-id: c6d8a556a51ec9422df208ae161806ccc1f20b36
2026-02-24 14:10:13 -08:00
xingbowang e95f3759e9 Fix clang tidy workflow (#14380)
Summary:
Fix clang tidy workflow

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14380

Test Plan: Unit test

Reviewed By: joshkang97

Differential Revision: D94237335

Pulled By: xingbowang

fbshipit-source-id: ed794cc13e684b3d6d11e20ce33edb4d6de79c9b
2026-02-24 13:16:18 -08:00
Hui Xiao de3616dc3b Prepare for 11.1.0 development (#14365)
Summary:
**Context/Summary:** history, version.h, format checking script, folly hash update

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14365

Test Plan: CI

Reviewed By: pdillinger

Differential Revision: D93952349

Pulled By: hx235

fbshipit-source-id: deeb7237cb51bbe3f3255f8eaa6a4ddda65248c2
2026-02-24 12:09:55 -08:00
xingbowang 4213f9e14a Revert "Fix flaky unit test in mempurge. (#14377)" (#14381)
Summary:
This reverts commit e7c3391640.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14381

Reviewed By: archang19

Differential Revision: D94244056

Pulled By: xingbowang

fbshipit-source-id: 0fffa8ce15dab4ece7b37e3839368fb399cd5734
2026-02-24 10:56:10 -08:00
Xingbo Wang 49a4165e3a Fix deadlock error false positive in stress test (#14376)
Summary:
Fix deadlock error false positive in stress test. TestDelete could trigger similar deadlock issue found earlier.

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14376

Test Plan: Stress test

Reviewed By: hx235

Differential Revision: D94149457

Pulled By: xingbowang

fbshipit-source-id: 53f19c3816bcaaba0b7c01fe4e3f5f3e09a5dd6e
2026-02-24 07:14:30 -08:00
xingbowang bd5b299b92 2026 02 21 accelerate ci (#14368)
Summary:
Accelerate CI: ccache integration, replace scan-build, upgrade runners

    1. ccache integration via reusable composite action (setup-ccache)
       - Created .github/actions/setup-ccache/action.yml
       - Applied to 13+ Linux compilation jobs in pr-jobs.yml
       - PORTABLE=1 by default to avoid illegal instruction errors on
         heterogeneous runners; disabled for jobs linking pre-built Folly
       - On cache hit, reducing compilation from 20+ min to ~2-5 min per job
       - ccache placed before Folly build so Folly compilation also
         benefits from cache on Folly cache misses

    2. Replace scan-build with clang-tidy (30+ min -> seconds)
       - Removed build-linux-clang18-clang-analyze job from pr-jobs.yml
       - Expanded .clang-tidy to enable all clang-analyzer-* checks,
         matching scan-build's full coverage
       - Existing clang-tidy-comment.yml workflow now handles both
         clang-tidy and clang-analyzer checks on changed files only

    3. Upgrade Folly job runners for faster builds and tests
       - build-linux-make-with-folly: 16-core -> 32-core, -j32 -> -j64
       - build-linux-cmake-with-folly-coroutines: 16-core -> 32-core,
         -j20 -> -j64 (both make and ctest)

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14368

Reviewed By: joshkang97

Differential Revision: D94166095

Pulled By: xingbowang

fbshipit-source-id: 3dfbd71aace3ba9cb2e790873bdbedba20215657
2026-02-24 04:47:32 -08:00
Xingbo Wang e7c3391640 Fix flaky unit test in mempurge. (#14377)
Summary:
Add proper synchronization in mempurge unit test to fix flaky test

Pull Request resolved: https://github.com/facebook/rocksdb/pull/14377

Test Plan: Unit test

Reviewed By: nmk70

Differential Revision: D94153290

Pulled By: xingbowang

fbshipit-source-id: 2a2cdcc6f8b5500bb5e68d2d223255cd1a5660b3
2026-02-23 22:55:04 -08:00
181 changed files with 23514 additions and 1753 deletions

No files matched your search

+1 -3
View File
@@ -17,9 +17,7 @@ Checks: "-*,\
cert-err58-cpp,\
cert-msc30-c,\
cert-msc50-cpp,\
clang-analyzer-core.NullDereference,\
clang-analyzer-core.StackAddressEscape,\
clang-analyzer-deadcode.DeadStores,\
clang-analyzer-*,\
clang-diagnostic-*,\
-clang-diagnostic-missing-designated-field-initializers,\
concurrency-mt-unsafe,\
+1 -1
View File
@@ -30,4 +30,4 @@ runs:
# - The docker image, which may not always be specified/known
# - Hash of folly.mk, which includes the folly repository commit hash
# NOTE: this is still only intended for DEBUG folly builds
key: folly-build-${{ runner.os }}-${{ runner.arch }}-${{ github.job_container.image }}-${{ steps.extract-folly-hash.outputs.hash }}
key: folly-build-${{ runner.os }}-${{ runner.arch }}-${{ github.job_container.image }}-${{ steps.extract-folly-hash.outputs.hash }}-${{ env.PORTABLE == '1' && 'portable' || 'native' }}
+49
View File
@@ -0,0 +1,49 @@
name: setup-ccache
description: Setup ccache for faster C++ compilation caching
inputs:
cache-key-prefix:
description: Unique prefix for the cache key (e.g., 'build-linux')
required: true
portable:
description: Set PORTABLE=1 to disable -march=native (set to "false" for jobs linking pre-built Folly)
required: false
default: "true"
runs:
using: composite
steps:
- name: Set ccache environment variables
run: |
echo "CCACHE_DIR=${{ github.workspace }}/.ccache" >> $GITHUB_ENV
echo "CCACHE_BASEDIR=${{ github.workspace }}" >> $GITHUB_ENV
echo "CCACHE_NOHASHDIR=true" >> $GITHUB_ENV
if [ "${{ inputs.portable }}" = "true" ]; then
echo "PORTABLE=1" >> $GITHUB_ENV
fi
shell: bash
- name: Restore ccache
uses: actions/cache@v4
with:
path: ${{ github.workspace }}/.ccache
key: ccache-${{ inputs.cache-key-prefix }}-${{ github.head_ref || github.ref }}-${{ github.sha }}
restore-keys: |-
ccache-${{ inputs.cache-key-prefix }}-${{ github.head_ref || github.ref }}-
ccache-${{ inputs.cache-key-prefix }}-refs/heads/main-
- name: Install ccache
run: |
if [ "$RUNNER_OS" = "macOS" ]; then
which ccache || brew install ccache
else
which ccache || (apt-get update && apt-get install -y ccache)
fi
shell: bash
- name: Add ccache to PATH
run: |
if [ "$RUNNER_OS" = "macOS" ]; then
echo "$(brew --prefix ccache)/libexec" >> $GITHUB_PATH
else
echo "/usr/lib/ccache" >> $GITHUB_PATH
fi
shell: bash
- name: Zero ccache stats
run: ccache -z
shell: bash
@@ -4,16 +4,15 @@ runs:
steps:
- name: Add msbuild to PATH
uses: microsoft/setup-msbuild@v1.3.1
- name: Cache ccache directory
id: ccache-cache
uses: actions/cache@v4
with:
path: C:\a\rocksdb\rocksdb\.ccache
key: rocksdb-build-${{ runner.os }}-${{ runner.arch }}-ccache-${{ hashFiles('CMakeLists.txt', 'cmake/**/*.cmake') }}-v1
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2
with:
max-size: "10GB"
- name: Configure ccache
shell: pwsh
run: |
ccache --set-config=base_dir=C:\a\rocksdb\rocksdb
ccache --set-config=hash_dir=false
- name: Custom steps
env:
THIRDPARTY_HOME: ${{ github.workspace }}/thirdparty
+78
View File
@@ -0,0 +1,78 @@
// Parse Claude Code execution log and produce a markdown review comment.
//
// Usage from actions/github-script:
// const parse = require('./.github/scripts/parse-claude-review.js');
// const markdown = parse({ executionFile, conclusion, meta });
//
// Parameters:
// executionFile - path to the JSON execution log from claude-code-base-action
// conclusion - 'success' or 'failure' from the action output
// meta - { trigger, headSha, reviewer, isQuery }
const fs = require('fs');
module.exports = function parseClaude({executionFile, conclusion, meta}) {
let responseBody = '';
try {
const executionLog = JSON.parse(fs.readFileSync(executionFile, 'utf8'));
if (!Array.isArray(executionLog)) {
throw new Error('Expected array format from claude-code-base-action');
}
const resultMessage = executionLog.find(m => m.type === 'result');
if (!resultMessage) {
responseBody = '⚠️ No result message found in execution log.';
} else if (resultMessage.subtype === 'success' && resultMessage.result) {
responseBody = resultMessage.result;
} else if (resultMessage.is_error || resultMessage.subtype === 'error') {
const errorInfo =
resultMessage.result || resultMessage.error || 'Unknown error';
responseBody = `❌ **Claude encountered an error:**\n\n${errorInfo}`;
} else if (resultMessage.result) {
responseBody = `⚠️ **Completed with status: ${
resultMessage.subtype}**\n\n${resultMessage.result}`;
} else {
responseBody = '⚠️ Claude completed but produced no output.';
}
} catch (error) {
responseBody = `❌ Error parsing Claude response: ${error.message}`;
}
const icon = conclusion === 'success' ? '✅' : '⚠️';
const headerTitle = meta.isQuery ? 'Claude Response' : 'Claude Code Review';
const triggerLine = meta.trigger === 'auto' ?
`*Auto-triggered after CI passed — reviewing commit ${
meta.headSha.substring(0, 7)}*` :
`*Requested by @${meta.reviewer}*`;
return [
`## ${icon} ${headerTitle}`,
'',
triggerLine,
'',
'---',
'',
responseBody,
'',
'---',
'',
'<details>',
'<summary>ℹ️ About this response</summary>',
'',
'Generated by [Claude Code](https://github.com/anthropics/claude-code-action).',
'Review methodology: `claude_md/code_review.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',
'</details>',
].join('\n');
};
+57
View File
@@ -0,0 +1,57 @@
// Shared PR comment posting utility.
// Used by both clang-tidy-comment.yml and claude-review-comment.yml.
//
// Usage from actions/github-script:
// const post = require('./.github/scripts/post-pr-comment.js');
// await post({ github, context, core, prNumber, body, marker });
//
// Parameters:
// github - octokit instance from actions/github-script
// context - GitHub Actions context
// core - @actions/core for logging
// prNumber - PR number to comment on
// body - comment body (markdown string)
// marker - HTML comment marker for dedup (e.g. '<!-- claude-review -->')
// If an existing comment with this marker is found, it is updated.
// If not found, a new comment is created.
module.exports = async function postPrComment(
{github, context, core, prNumber, body, marker}) {
if (!prNumber || !body) {
core.warning('Missing prNumber or body; skipping comment.');
return;
}
const owner = context.repo.owner;
const repo = context.repo.repo;
// Ensure marker is embedded in the body
const markedBody = body.includes(marker) ? body : `${marker}\n${body}`;
// Search for existing comment with this marker
const {data: comments} = await github.rest.issues.listComments({
owner,
repo,
issue_number: prNumber,
per_page: 100,
});
const existing = comments.find(c => c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner,
repo,
comment_id: existing.id,
body: markedBody,
});
core.info(`Updated existing comment ${existing.id}`);
} else {
await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body: markedBody,
});
core.info('Created new PR comment');
}
};
+16 -23
View File
@@ -12,6 +12,12 @@ jobs:
if: github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Checkout scripts
uses: actions/checkout@v4
with:
sparse-checkout: .github/scripts
sparse-checkout-cone-mode: true
- name: Download clang-tidy results
id: download
uses: actions/download-artifact@v4.0.0
@@ -20,6 +26,7 @@ jobs:
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
continue-on-error: true
- name: Post or update PR comment
if: steps.download.outcome == 'success'
uses: actions/github-script@v7
@@ -32,27 +39,13 @@ jobs:
}
const body = fs.readFileSync('clang-tidy-comment.md', 'utf8');
const prNumber = parseInt(fs.readFileSync('pr_number.txt', 'utf8').trim());
const marker = '<!-- clang-tidy-bot -->';
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
const post = require('./.github/scripts/post-pr-comment.js');
await post({
github,
context,
core,
prNumber,
body,
marker: '<!-- clang-tidy-bot -->',
});
const existing = comments.find(c => c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
core.info(`Updated existing comment ${existing.id}`);
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body,
});
core.info('Created new PR comment');
}
+146
View File
@@ -0,0 +1,146 @@
# Claude Code Review — Comment Posting Workflow
#
# Companion to claude-review.yml. Downloads the review artifact and posts
# it as a PR comment. This workflow has write permissions but never runs
# AI or processes untrusted code.
#
# Security: Separating analysis (has ANTHROPIC_API_KEY, no write perms)
# from posting (has GITHUB_TOKEN write perms, no AI) prevents a crafted
# PR from tricking Claude into exfiltrating tokens.
name: Post Claude Review Comment
on:
workflow_run:
workflows: ["Claude Code Review"]
types: [completed]
permissions:
pull-requests: write
issues: write
jobs:
comment:
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
steps:
- name: Checkout scripts
uses: actions/checkout@v4
with:
sparse-checkout: .github/scripts
sparse-checkout-cone-mode: true
- name: Download review artifact
id: download
uses: actions/download-artifact@v4
with:
name: claude-review-result
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
continue-on-error: true
- name: Post or update PR comment
if: steps.download.outcome == 'success'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
if (!fs.existsSync('claude-review-comment.md') || !fs.existsSync('pr_number.txt')) {
core.info('No review results found; skipping.');
return;
}
const body = fs.readFileSync('claude-review-comment.md', 'utf8');
const prNumber = parseInt(fs.readFileSync('pr_number.txt', 'utf8').trim());
const trigger = fs.existsSync('trigger_type.txt')
? fs.readFileSync('trigger_type.txt', 'utf8').trim()
: 'auto';
// Use different markers for auto vs manual so they don't
// overwrite each other. Auto-reviews update in place (one
// per PR). Manual reviews always create new comments.
const marker = trigger === 'auto'
? '<!-- claude-review-auto -->'
: `<!-- claude-review-manual-${Date.now()} -->`;
const post = require('./.github/scripts/post-pr-comment.js');
await post({
github,
context,
core,
prNumber,
body,
marker,
});
- name: Add reaction to trigger comment
if: steps.download.outcome == 'success'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
if (!fs.existsSync('trigger_type.txt')) return;
const trigger = fs.readFileSync('trigger_type.txt', 'utf8').trim();
if (trigger !== 'manual') return;
if (!fs.existsSync('comment_id.txt')) return;
const commentId = parseInt(fs.readFileSync('comment_id.txt', 'utf8').trim());
if (!commentId) return;
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: commentId,
content: 'rocket'
});
# Notify on failure (for manual triggers)
failure-notice:
if: github.event.workflow_run.conclusion == 'failure'
runs-on: ubuntu-latest
steps:
- name: Download artifact for PR number
id: download
uses: actions/download-artifact@v4
with:
name: claude-review-result
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
continue-on-error: true
- name: React with failure
if: steps.download.outcome == 'success'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
if (!fs.existsSync('comment_id.txt')) return;
const commentId = parseInt(fs.readFileSync('comment_id.txt', 'utf8').trim());
if (!commentId) return;
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: commentId,
content: 'confused'
});
# Unauthorized user notice
unauthorized-notice:
runs-on: ubuntu-latest
if: >-
github.event.workflow_run.event == 'issue_comment' &&
github.event.workflow_run.conclusion == 'skipped'
steps:
- name: Check if unauthorized
id: check
uses: actions/github-script@v7
with:
script: |
// This job only runs when the analysis workflow was skipped,
// which happens when the user is not in the authorized list.
// We cannot easily get the original comment from workflow_run,
// so this is a best-effort notice via workflow logs.
core.info('Analysis workflow was skipped — likely unauthorized user.');
+476
View File
@@ -0,0 +1,476 @@
# Claude Code Review — Analysis Workflow
#
# This workflow runs Claude to review PRs. It produces a markdown review
# as an artifact but does NOT post comments (no write permissions).
# The companion workflow (claude-review-comment.yml) handles posting.
#
# Triggers:
# 1. workflow_run: Auto-review after pr-jobs passes
# 2. issue_comment: Manual /claude-review or /claude-query by maintainers
# 3. workflow_dispatch: Manual testing
#
# Security:
# - This job has contents:read ONLY (no PR write, no issue write)
# - ANTHROPIC_API_KEY is used here but never coexists with write tokens
# - Claude uses read-only tools (View, GlobTool, GrepTool)
# - Review methodology: claude_md/code_review.md
name: Claude Code Review
on:
workflow_run:
workflows: ["facebook/rocksdb/pr-jobs"]
types: [completed]
issue_comment:
types: [created]
workflow_dispatch:
inputs:
pr_number:
description: PR number to review
required: true
type: number
model:
description: Claude model to use
required: false
type: choice
options:
- claude-opus-4-6
- claude-sonnet-4-20250514
default: claude-opus-4-6
permissions:
contents: read
jobs:
# ======================== Auto Review ======================== #
auto-review:
name: Auto Claude Review
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
if: >-
github.event_name == 'workflow_run' &&
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'pull_request'
steps:
- name: Get PR info
id: pr_info
uses: actions/github-script@v7
with:
script: |
const headSha = context.payload.workflow_run.head_sha;
let prNumber = null;
const prs = context.payload.workflow_run.pull_requests;
if (prs && prs.length > 0) {
prNumber = prs[0].number;
} else {
const { data: pulls } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
sort: 'updated',
direction: 'desc',
per_page: 30
});
const match = pulls.find(p => p.head.sha === headSha);
if (match) prNumber = match.number;
}
if (!prNumber) {
core.setFailed(`Could not find PR for SHA ${headSha}`);
return;
}
const pr = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber
});
core.setOutput('pr_number', prNumber.toString());
core.setOutput('head_repo', pr.data.head.repo.clone_url);
core.setOutput('head_ref', pr.data.head.ref);
core.setOutput('head_sha', pr.data.head.sha);
core.setOutput('base_sha', pr.data.base.sha);
core.setOutput('base_ref', pr.data.base.ref);
core.setOutput('pr_title', pr.data.title);
core.setOutput('pr_body', pr.data.body || '');
core.setOutput('pr_author', pr.data.user.login);
- name: Check for existing review
id: check_existing
uses: actions/github-script@v7
env:
PR_NUMBER: ${{ steps.pr_info.outputs.pr_number }}
HEAD_SHA: ${{ steps.pr_info.outputs.head_sha }}
with:
script: |
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: parseInt(process.env.PR_NUMBER),
per_page: 100
});
const shortSha = process.env.HEAD_SHA.substring(0, 7);
const existing = comments.data.find(c =>
c.body.includes('<!-- claude-review -->') &&
c.body.includes(shortSha)
);
core.setOutput('skip', existing ? 'true' : 'false');
- name: Checkout base repository
if: steps.check_existing.outputs.skip != 'true'
uses: actions/checkout@v4
with:
ref: ${{ steps.pr_info.outputs.base_ref }}
fetch-depth: 0
persist-credentials: false
- name: Generate diff and prompt
if: steps.check_existing.outputs.skip != 'true'
env:
HEAD_REPO: ${{ steps.pr_info.outputs.head_repo }}
HEAD_REF: ${{ steps.pr_info.outputs.head_ref }}
HEAD_SHA: ${{ steps.pr_info.outputs.head_sha }}
BASE_SHA: ${{ steps.pr_info.outputs.base_sha }}
PR_TITLE: ${{ steps.pr_info.outputs.pr_title }}
PR_BODY: ${{ steps.pr_info.outputs.pr_body }}
PR_AUTHOR: ${{ steps.pr_info.outputs.pr_author }}
run: |
git remote add fork "${HEAD_REPO}" || true
git fetch fork "${HEAD_REF}" --depth=100
git diff "${BASE_SHA}...${HEAD_SHA}" > /tmp/pr.diff
DIFF_STATS=$(git diff --stat "${BASE_SHA}...${HEAD_SHA}" | tail -1)
DIFF_SIZE=$(wc -c < /tmp/pr.diff)
IS_TRUNCATED=false
if [ "$DIFF_SIZE" -gt 100000 ]; then
head -c 100000 /tmp/pr.diff > /tmp/pr_truncated.diff
mv /tmp/pr_truncated.diff /tmp/pr.diff
IS_TRUNCATED=true
fi
cat > /tmp/prompt.txt << 'PROMPT_EOF'
Read claude_md/code_review.md for the full review methodology, then
review the pull request described below. Follow all perspectives and
the output format defined in that document.
PROMPT_EOF
cat >> /tmp/prompt.txt << PROMPT_EOF
## Pull Request Information
- **Title:** ${PR_TITLE}
- **Author:** ${PR_AUTHOR}
- **Changes:** ${DIFF_STATS}
PROMPT_EOF
if [ "${IS_TRUNCATED}" = "true" ]; then
echo "- **Note:** Diff truncated due to size." >> /tmp/prompt.txt
fi
printf '\n## PR Description\n' >> /tmp/prompt.txt
printf '%s\n' "${PR_BODY}" >> /tmp/prompt.txt
printf '\n---\n\n## Diff to Review\n\n' >> /tmp/prompt.txt
cat /tmp/pr.diff >> /tmp/prompt.txt
- name: Run Claude
if: steps.check_existing.outputs.skip != 'true'
id: claude
uses: anthropics/claude-code-base-action@beta
with:
prompt_file: /tmp/prompt.txt
model: claude-opus-4-6
max_turns: "30"
allowed_tools: "View,GlobTool,GrepTool"
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
- name: Build review comment
if: steps.check_existing.outputs.skip != 'true'
uses: actions/github-script@v7
env:
EXECUTION_FILE: ${{ steps.claude.outputs.execution_file }}
CONCLUSION: ${{ steps.claude.outputs.conclusion }}
HEAD_SHA: ${{ steps.pr_info.outputs.head_sha }}
with:
script: |
const parse = require('./.github/scripts/parse-claude-review.js');
const body = parse({
executionFile: process.env.EXECUTION_FILE,
conclusion: process.env.CONCLUSION,
meta: {
trigger: 'auto',
headSha: process.env.HEAD_SHA,
reviewer: '',
}
});
const fs = require('fs');
fs.writeFileSync('claude-review-comment.md', body);
- name: Save PR number
if: steps.check_existing.outputs.skip != 'true'
run: echo "${{ steps.pr_info.outputs.pr_number }}" > pr_number.txt
- name: Save trigger metadata
if: steps.check_existing.outputs.skip != 'true'
run: echo "auto" > trigger_type.txt
- name: Upload review artifact
if: always() && steps.check_existing.outputs.skip != 'true'
uses: actions/upload-artifact@v4
with:
name: claude-review-result
path: |
claude-review-comment.md
pr_number.txt
trigger_type.txt
if-no-files-found: ignore
retention-days: 1
- name: Upload execution log
if: always() && steps.check_existing.outputs.skip != 'true'
uses: actions/upload-artifact@v4
with:
name: claude-review-execution-log
path: ${{ steps.claude.outputs.execution_file }}
retention-days: 7
if-no-files-found: warn
# ======================== Manual Review ======================== #
manual-review:
name: Manual Claude Review
runs-on: ubuntu-latest
permissions:
contents: read
if: >-
github.event_name == 'workflow_dispatch' ||
(
github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
(contains(github.event.comment.body, '/claude-review') || contains(github.event.comment.body, '/claude-query')) &&
contains(fromJSON('["pdillinger", "jaykorean", "hx235", "dannyhchen", "joshkang97", "zaidoon1", "omkarhgawde", "xingbowang", "anand1976", "virajthakur", "nmk70", "archang19", "mszeszko-meta", "yoshinorim", "cbi42"]'), github.event.comment.user.login)
)
steps:
- name: Detect command type
id: command
uses: actions/github-script@v7
with:
script: |
if (context.eventName === 'workflow_dispatch') {
core.setOutput('type', 'review');
core.setOutput('query', '');
return;
}
const body = context.payload.comment.body;
if (body.includes('/claude-review')) {
core.setOutput('type', 'review');
const match = body.match(/\/claude-review\s+([\s\S]*)/);
core.setOutput('query', match ? match[1].trim() : '');
} else if (body.includes('/claude-query')) {
const match = body.match(/\/claude-query\s+([\s\S]*)/);
const query = match ? match[1].trim() : '';
if (!query) {
core.setFailed('No query provided. Usage: /claude-query <your question>');
return;
}
core.setOutput('type', 'query');
core.setOutput('query', query);
} else {
core.setFailed('Unknown command');
}
- name: Get PR details
id: pr_info
uses: actions/github-script@v7
env:
INPUT_PR_NUMBER: ${{ inputs.pr_number }}
with:
script: |
const prNumber = context.eventName === 'workflow_dispatch'
? parseInt(process.env.INPUT_PR_NUMBER, 10)
: context.issue.number;
const pr = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber
});
core.setOutput('pr_number', prNumber.toString());
core.setOutput('head_repo', pr.data.head.repo.clone_url);
core.setOutput('head_ref', pr.data.head.ref);
core.setOutput('head_sha', pr.data.head.sha);
core.setOutput('base_sha', pr.data.base.sha);
core.setOutput('base_ref', pr.data.base.ref);
core.setOutput('pr_title', pr.data.title);
core.setOutput('pr_body', pr.data.body || '');
core.setOutput('pr_author', pr.data.user.login);
- name: Checkout base repository
uses: actions/checkout@v4
with:
ref: ${{ steps.pr_info.outputs.base_ref }}
fetch-depth: 0
persist-credentials: false
- name: Generate diff and prompt
env:
HEAD_REPO: ${{ steps.pr_info.outputs.head_repo }}
HEAD_REF: ${{ steps.pr_info.outputs.head_ref }}
HEAD_SHA: ${{ steps.pr_info.outputs.head_sha }}
BASE_SHA: ${{ steps.pr_info.outputs.base_sha }}
PR_TITLE: ${{ steps.pr_info.outputs.pr_title }}
PR_BODY: ${{ steps.pr_info.outputs.pr_body }}
PR_AUTHOR: ${{ steps.pr_info.outputs.pr_author }}
DIFF_STATS: ""
COMMAND_TYPE: ${{ steps.command.outputs.type }}
ADDITIONAL_CONTEXT: ${{ steps.command.outputs.query }}
USER_QUERY: ${{ steps.command.outputs.query }}
run: |
git remote add fork "${HEAD_REPO}" || true
git fetch fork "${HEAD_REF}" --depth=100
git diff "${BASE_SHA}...${HEAD_SHA}" > /tmp/pr.diff
DIFF_STATS=$(git diff --stat "${BASE_SHA}...${HEAD_SHA}" | tail -1)
DIFF_SIZE=$(wc -c < /tmp/pr.diff)
IS_TRUNCATED=false
if [ "$DIFF_SIZE" -gt 100000 ]; then
head -c 100000 /tmp/pr.diff > /tmp/pr_truncated.diff
mv /tmp/pr_truncated.diff /tmp/pr.diff
IS_TRUNCATED=true
fi
if [ "${COMMAND_TYPE}" = "query" ]; then
# Query prompt
cat > /tmp/prompt.txt << 'PROMPT_EOF'
You are an expert C++ engineer for RocksDB.
Read CLAUDE.md in the repo root and claude_md/ files for project context.
Answer thoroughly with exact file paths and line numbers.
---
PROMPT_EOF
cat >> /tmp/prompt.txt << PROMPT_EOF
## Pull Request Context
- **Title:** ${PR_TITLE}
- **Author:** ${PR_AUTHOR}
- **Changes:** ${DIFF_STATS}
## PR Description
PROMPT_EOF
printf '%s\n' "${PR_BODY}" >> /tmp/prompt.txt
printf '\n## Question\n' >> /tmp/prompt.txt
printf '%s\n' "${USER_QUERY}" >> /tmp/prompt.txt
printf '\n---\n## PR Diff (for reference)\n' >> /tmp/prompt.txt
cat /tmp/pr.diff >> /tmp/prompt.txt
else
# Review prompt
cat > /tmp/prompt.txt << 'PROMPT_EOF'
Read claude_md/code_review.md for the full review methodology, then
review the pull request described below. Follow all perspectives and
the output format defined in that document.
PROMPT_EOF
cat >> /tmp/prompt.txt << PROMPT_EOF
## Pull Request Information
- **Title:** ${PR_TITLE}
- **Author:** ${PR_AUTHOR}
- **Changes:** ${DIFF_STATS}
PROMPT_EOF
if [ "${IS_TRUNCATED}" = "true" ]; then
echo "- **Note:** Diff truncated due to size." >> /tmp/prompt.txt
fi
printf '\n## PR Description\n' >> /tmp/prompt.txt
printf '%s\n' "${PR_BODY}" >> /tmp/prompt.txt
if [ -n "${ADDITIONAL_CONTEXT}" ]; then
printf '\n## Additional Instructions from Reviewer\n' >> /tmp/prompt.txt
printf '%s\n' "${ADDITIONAL_CONTEXT}" >> /tmp/prompt.txt
fi
printf '\n---\n\n## Diff to Review\n\n' >> /tmp/prompt.txt
cat /tmp/pr.diff >> /tmp/prompt.txt
fi
- name: Run Claude
id: claude
uses: anthropics/claude-code-base-action@beta
with:
prompt_file: /tmp/prompt.txt
model: ${{ github.event_name == 'workflow_dispatch' && inputs.model || 'claude-opus-4-6' }}
max_turns: "30"
allowed_tools: "View,GlobTool,GrepTool"
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
- name: Build review comment
uses: actions/github-script@v7
env:
EXECUTION_FILE: ${{ steps.claude.outputs.execution_file }}
CONCLUSION: ${{ steps.claude.outputs.conclusion }}
HEAD_SHA: ${{ steps.pr_info.outputs.head_sha }}
REVIEWER: ${{ github.event_name == 'workflow_dispatch' && github.actor || github.event.comment.user.login }}
COMMAND_TYPE: ${{ steps.command.outputs.type }}
with:
script: |
const parse = require('./.github/scripts/parse-claude-review.js');
const isReview = process.env.COMMAND_TYPE === 'review';
const body = parse({
executionFile: process.env.EXECUTION_FILE,
conclusion: process.env.CONCLUSION,
meta: {
trigger: 'manual',
headSha: process.env.HEAD_SHA,
reviewer: process.env.REVIEWER,
isQuery: !isReview,
}
});
const fs = require('fs');
fs.writeFileSync('claude-review-comment.md', body);
- name: Save PR number
run: echo "${{ steps.pr_info.outputs.pr_number }}" > pr_number.txt
- name: Save trigger metadata
env:
REVIEWER: ${{ github.event_name == 'workflow_dispatch' && github.actor || github.event.comment.user.login }}
COMMENT_ID: ${{ github.event.comment.id }}
run: |
echo "manual" > trigger_type.txt
echo "${REVIEWER}" > reviewer.txt
echo "${COMMENT_ID}" > comment_id.txt
- name: Upload review artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: claude-review-result
path: |
claude-review-comment.md
pr_number.txt
trigger_type.txt
reviewer.txt
comment_id.txt
if-no-files-found: ignore
retention-days: 1
- name: Upload execution log
if: always()
uses: actions/upload-artifact@v4
with:
name: claude-review-execution-log
path: ${{ steps.claude.outputs.execution_file }}
retention-days: 7
if-no-files-found: warn
+176 -56
View File
@@ -92,7 +92,14 @@ jobs:
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: build-linux
- run: make V=1 J=32 -j32 check
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- uses: "./.github/actions/post-steps"
build-linux-cmake-mingw:
if: needs.config.outputs.only_job == 'build-linux-cmake-mingw' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
@@ -105,6 +112,9 @@ jobs:
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: cmake-mingw
- run: update-alternatives --set x86_64-w64-mingw32-g++ /usr/bin/x86_64-w64-mingw32-g++-posix
- name: Build cmake-mingw
run: |-
@@ -113,12 +123,16 @@ jobs:
which java && java -version
which javac && javac -version
mkdir build && cd build && cmake -DJNI=1 -DWITH_GFLAGS=OFF .. -DCMAKE_C_COMPILER=x86_64-w64-mingw32-gcc -DCMAKE_CXX_COMPILER=x86_64-w64-mingw32-g++ -DCMAKE_SYSTEM_NAME=Windows && make -j4 rocksdb rocksdbjni
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- uses: "./.github/actions/post-steps"
build-linux-make-with-folly:
if: needs.config.outputs.only_job == 'build-linux-make-with-folly' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
runs-on:
labels: 16-core-ubuntu
labels: 32-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
options: --shm-size=16gb
@@ -127,12 +141,19 @@ jobs:
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/cache-getdeps-downloads"
- uses: "./.github/actions/setup-folly"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: make-with-folly
- uses: "./.github/actions/cache-folly"
id: cache-folly
- uses: "./.github/actions/build-folly"
with:
cache-hit: ${{ steps.cache-folly.outputs.cache-hit }}
- run: USE_FOLLY=1 LIB_MODE=static V=1 make -j32 check
- run: USE_FOLLY=1 LIB_MODE=static V=1 make -j64 check
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- uses: "./.github/actions/post-steps"
build-linux-make-with-folly-lite-no-test:
if: needs.config.outputs.only_job == 'build-linux-make-with-folly-lite-no-test' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
@@ -147,13 +168,20 @@ jobs:
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/cache-getdeps-downloads"
- uses: "./.github/actions/setup-folly"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: make-folly-lite
- run: USE_FOLLY_LITE=1 EXTRA_CXXFLAGS=-DGLOG_USE_GLOG_EXPORT V=1 make -j32 all
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- uses: "./.github/actions/post-steps"
build-linux-cmake-with-folly-coroutines:
if: needs.config.outputs.only_job == 'build-linux-cmake-with-folly-coroutines' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
runs-on:
labels: 16-core-ubuntu
labels: 32-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
options: --shm-size=16gb
@@ -162,12 +190,19 @@ jobs:
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/cache-getdeps-downloads"
- uses: "./.github/actions/setup-folly"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: cmake-folly-coroutines
- uses: "./.github/actions/cache-folly"
id: cache-folly
- uses: "./.github/actions/build-folly"
with:
cache-hit: ${{ steps.cache-folly.outputs.cache-hit }}
- run: "(mkdir build && cd build && cmake -DUSE_COROUTINES=1 -DWITH_GFLAGS=1 -DROCKSDB_BUILD_SHARED=0 .. && make VERBOSE=1 -j20 && ctest -j20)"
- run: "(mkdir build && cd build && cmake -DUSE_COROUTINES=1 -DWITH_GFLAGS=1 -DROCKSDB_BUILD_SHARED=0 -DPORTABLE=ON .. && make VERBOSE=1 -j64 && ctest -j64)"
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- uses: "./.github/actions/post-steps"
build-linux-cmake-with-benchmark-no-thread-status:
if: needs.config.outputs.only_job == 'build-linux-cmake-with-benchmark-no-thread-status' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
@@ -180,7 +215,14 @@ jobs:
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- run: mkdir build && cd build && cmake -DWITH_GFLAGS=1 -DWITH_BENCHMARK=1 -DCMAKE_CXX_FLAGS=-DNROCKSDB_THREAD_STATUS .. && make VERBOSE=1 -j20 && ctest -j20
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: cmake-benchmark
- run: mkdir build && cd build && cmake -DWITH_GFLAGS=1 -DWITH_BENCHMARK=1 -DPORTABLE=ON -DCMAKE_CXX_FLAGS=-DNROCKSDB_THREAD_STATUS .. && make VERBOSE=1 -j20 && ctest -j20
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- uses: "./.github/actions/post-steps"
build-linux-encrypted_env-no_compression:
if: needs.config.outputs.only_job == 'build-linux-encrypted_env-no_compression' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
@@ -193,8 +235,15 @@ jobs:
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: encrypted-env-no-compression
- run: ENCRYPTED_ENV=1 ROCKSDB_DISABLE_SNAPPY=1 ROCKSDB_DISABLE_ZLIB=1 ROCKSDB_DISABLE_BZIP=1 ROCKSDB_DISABLE_LZ4=1 ROCKSDB_DISABLE_ZSTD=1 make V=1 J=32 -j32 check
- run: "./sst_dump --help | grep -E -q 'Supported built-in compression types: kNoCompression$' # Verify no compiled in compression\n"
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- uses: "./.github/actions/post-steps"
# ======================== Linux No Test Runs ======================= #
build-linux-release:
@@ -207,6 +256,9 @@ jobs:
options: --shm-size=16gb
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: release
- run: make V=1 -j32 LIB_MODE=shared release
- run: ls librocksdb.so
- run: "./trace_analyzer --version" # A tool dependent on gflags that can run in release build
@@ -223,6 +275,10 @@ jobs:
- run: USE_RTTI=1 make V=1 -j32 release
- run: ls librocksdb.a
- run: if ./trace_analyzer --version; then false; else true; fi
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- uses: "./.github/actions/post-steps"
build-linux-clang-13-no_test_run:
if: needs.config.outputs.only_job == 'build-linux-clang-13-no_test_run' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
@@ -235,11 +291,18 @@ jobs:
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: clang-13
# FIXME: get back to "all microbench" targets
- run: CC=clang-13 CXX=clang++-13 USE_CLANG=1 EXTRA_CXXFLAGS=-stdlib=libc++ EXTRA_LDFLAGS=-stdlib=libc++ make -j32 shared_lib
- run: make clean
# FIXME: get back to "release" target
- run: CC=clang-13 CXX=clang++-13 USE_CLANG=1 EXTRA_CXXFLAGS=-stdlib=libc++ EXTRA_LDFLAGS=-stdlib=libc++ DEBUG_LEVEL=0 make -j32 shared_lib
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- uses: "./.github/actions/post-steps"
build-linux-clang-18-no_test_run:
if: needs.config.outputs.only_job == 'build-linux-clang-18-no_test_run' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
@@ -252,9 +315,16 @@ jobs:
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: clang-18
- run: CC=clang-18 CXX=clang++-18 USE_CLANG=1 make -j32 all microbench
- run: make clean
- run: CC=clang-18 CXX=clang++-18 USE_CLANG=1 DEBUG_LEVEL=0 make -j32 release
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- uses: "./.github/actions/post-steps"
build-linux-gcc-14-no_test_run:
if: needs.config.outputs.only_job == 'build-linux-gcc-14-no_test_run' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
@@ -267,30 +337,17 @@ jobs:
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: gcc-14
- run: CC=gcc-14 CXX=g++-14 V=1 make -j32 all microbench
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- uses: "./.github/actions/post-steps"
# ======================== Linux Other Checks ======================= #
build-linux-clang18-clang-analyze:
if: needs.config.outputs.only_job == 'build-linux-clang18-clang-analyze' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
runs-on:
labels: 16-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
options: --shm-size=16gb
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- run: CC=clang-18 CXX=clang++-18 ROCKSDB_DISABLE_ALIGNED_NEW=1 CLANG_ANALYZER="/usr/bin/clang++-18" CLANG_SCAN_BUILD=scan-build-18 USE_CLANG=1 make V=1 -j32 analyze
- uses: "./.github/actions/post-steps"
- name: compress test report
run: tar -cvzf scan_build_report.tar.gz scan_build_report
if: failure()
- uses: actions/upload-artifact@v4.0.0
with:
name: scan-build-report
path: scan_build_report.tar.gz
build-linux-unity-and-headers:
if: needs.config.outputs.only_job == 'build-linux-unity-and-headers' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
@@ -303,9 +360,16 @@ jobs:
steps:
- uses: actions/checkout@v4.1.0
- run: apt-get update -y && apt-get install -y libgflags-dev
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: unity-headers
- name: Unity build
run: make V=1 -j8 unity_test
- run: make V=1 -j8 -k check-headers
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- uses: "./.github/actions/post-steps"
build-linux-mini-crashtest:
if: needs.config.outputs.only_job == 'build-linux-mini-crashtest' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
@@ -318,7 +382,14 @@ jobs:
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: mini-crashtest
- run: ulimit -S -n `ulimit -H -n` && make V=1 -j8 CRASH_TEST_EXT_ARGS='--duration=960 --max_key=2500000' blackbox_crash_test_with_atomic_flush
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- uses: "./.github/actions/post-steps"
# ======================= Linux with Sanitizers ===================== #
build-linux-clang18-asan-ubsan:
@@ -332,7 +403,14 @@ jobs:
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: clang18-asan-ubsan
- run: COMPILE_WITH_ASAN=1 COMPILE_WITH_UBSAN=1 CC=clang-18 CXX=clang++-18 ROCKSDB_DISABLE_ALIGNED_NEW=1 USE_CLANG=1 make V=1 -j40 check
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- uses: "./.github/actions/post-steps"
build-linux-clang18-mini-tsan:
if: needs.config.outputs.only_job == 'build-linux-clang18-mini-tsan' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
@@ -345,7 +423,14 @@ jobs:
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: clang18-tsan
- run: COMPILE_WITH_TSAN=1 CC=clang-18 CXX=clang++-18 ROCKSDB_DISABLE_ALIGNED_NEW=1 USE_CLANG=1 make V=1 -j32 check
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- uses: "./.github/actions/post-steps"
build-linux-static_lib-alt_namespace-status_checked:
if: needs.config.outputs.only_job == 'build-linux-static_lib-alt_namespace-status_checked' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
@@ -358,7 +443,14 @@ jobs:
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: static-alt-namespace
- run: ASSERT_STATUS_CHECKED=1 TEST_UINT128_COMPAT=1 ROCKSDB_MODIFY_NPHASH=1 LIB_MODE=static OPT="-DROCKSDB_USE_STD_SEMAPHORES -DROCKSDB_NAMESPACE=alternative_rocksdb_ns" make V=1 -j24 check
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- uses: "./.github/actions/post-steps"
# ========================= MacOS build only ======================== #
build-macos:
@@ -372,11 +464,18 @@ jobs:
- uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: 16.4.0
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: macos
- uses: "./.github/actions/increase-max-open-files-on-macos"
- uses: "./.github/actions/install-gflags-on-macos"
- uses: "./.github/actions/pre-steps-macos"
- name: Build
run: ulimit -S -n `ulimit -H -n` && make V=1 J=16 -j8 all
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- uses: "./.github/actions/post-steps"
# ========================= MacOS with Tests ======================== #
build-macos-cmake:
@@ -391,6 +490,9 @@ jobs:
- uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: 16.4.0
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: macos-cmake
- uses: "./.github/actions/increase-max-open-files-on-macos"
- uses: "./.github/actions/install-gflags-on-macos"
- uses: "./.github/actions/pre-steps-macos"
@@ -410,6 +512,10 @@ jobs:
- name: Run shard 3 out of 4 test shards
run: ulimit -S -n `ulimit -H -n` && cd build && ctest -j8 -I 3,,4
if: ${{ matrix.run_sharded_tests == 3 }}
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- uses: "./.github/actions/post-steps"
# ======================== Windows with Tests ======================= #
# NOTE: some windows jobs are in "nightly" to save resources
@@ -433,29 +539,22 @@ jobs:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
options: --shm-size=16gb
steps:
# The docker image is intentionally based on an OS that has an older GLIBC version.
# That GLIBC is incompatibile with GitHub's actions/checkout. Thus we implement a manual checkout step.
# NOTE: replaced evolvedbinary/rocksjava:centos7_x64-be with ghcr.io/facebook/rocksdb_ubuntu:22.1
# until a more appropriate docker image with C++20 support is made.
- name: Checkout
env:
GH_TOKEN: ${{ github.token }}
run: |
chown `whoami` . || true
git clone --no-checkout https://oath2:$GH_TOKEN@github.com/${{ github.repository }}.git .
git -c protocol.version=2 fetch --update-head-ok --no-tags --prune --no-recurse-submodules --depth=1 origin +${{ github.sha }}:${{ github.ref }}
git checkout --progress --force ${{ github.ref }}
git log -1 --format='%H'
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: java
- name: Set Java Environment
run: |-
echo "JAVA_HOME=${JAVA_HOME}"
which java && java -version
which javac && javac -version
- name: Test RocksDBJava
# NOTE: replaced scl enable devtoolset-7 'make V=1 J=8 -j8 jtest'
run: make V=1 J=8 -j8 jtest
# post-steps skipped because of compatibility issues with docker image
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
build-linux-java-static:
if: needs.config.outputs.only_job == 'build-linux-java-static' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
@@ -465,29 +564,22 @@ jobs:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
options: --shm-size=16gb
steps:
# The docker image is intentionally based on an OS that has an older GLIBC version.
# That GLIBC is incompatibile with GitHub's actions/checkout. Thus we implement a manual checkout step.
# NOTE: replaced evolvedbinary/rocksjava:centos7_x64-be with ghcr.io/facebook/rocksdb_ubuntu:22.1
# until a more appropriate docker image with C++20 support is made.
- name: Checkout
env:
GH_TOKEN: ${{ github.token }}
run: |
chown `whoami` . || true
git clone --no-checkout https://oath2:$GH_TOKEN@github.com/${{ github.repository }}.git .
git -c protocol.version=2 fetch --update-head-ok --no-tags --prune --no-recurse-submodules --depth=1 origin +${{ github.sha }}:${{ github.ref }}
git checkout --progress --force ${{ github.ref }}
git log -1 --format='%H'
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: java-static
- name: Set Java Environment
run: |-
echo "JAVA_HOME=${JAVA_HOME}"
which java && java -version
which javac && javac -version
- name: Build RocksDBJava Static Library
# NOTE: replaced scl enable devtoolset-7 'make V=1 J=8 -j8 rocksdbjavastatic'
run: make V=1 J=8 -j8 rocksdbjavastatic
# post-steps skipped because of compatibility issues with docker image
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
build-macos-java:
if: needs.config.outputs.only_job == 'build-macos-java' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
@@ -500,6 +592,9 @@ jobs:
- uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: 16.4.0
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: macos-java
- uses: "./.github/actions/increase-max-open-files-on-macos"
- uses: "./.github/actions/install-gflags-on-macos"
- uses: "./.github/actions/install-jdk8-on-macos"
@@ -511,6 +606,10 @@ jobs:
which javac && javac -version
- name: Test RocksDBJava
run: make V=1 J=16 -j16 jtest
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- uses: "./.github/actions/post-steps"
build-macos-java-static:
if: needs.config.outputs.only_job == 'build-macos-java-static' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
@@ -523,6 +622,9 @@ jobs:
- uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: 16.4.0
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: macos-java-static
- uses: "./.github/actions/increase-max-open-files-on-macos"
- uses: "./.github/actions/install-gflags-on-macos"
- uses: "./.github/actions/install-jdk8-on-macos"
@@ -534,6 +636,10 @@ jobs:
which javac && javac -version
- name: Build RocksDBJava x86 and ARM Static Libraries
run: make V=1 J=16 -j16 rocksdbjavastaticosx
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- uses: "./.github/actions/post-steps"
build-macos-java-static-universal:
if: needs.config.outputs.only_job == 'build-macos-java-static-universal' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
@@ -546,6 +652,9 @@ jobs:
- uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: 16.4.0
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: macos-java-static-universal
- uses: "./.github/actions/increase-max-open-files-on-macos"
- uses: "./.github/actions/install-gflags-on-macos"
- uses: "./.github/actions/install-jdk8-on-macos"
@@ -557,6 +666,10 @@ jobs:
which javac && javac -version
- name: Build RocksDBJava Universal Binary Static Library
run: make V=1 J=16 -j16 rocksdbjavastaticosx_ub
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- uses: "./.github/actions/post-steps"
build-linux-java-pmd:
if: needs.config.outputs.only_job == 'build-linux-java-pmd' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
@@ -593,6 +706,13 @@ jobs:
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- run: sudo apt-get update && sudo apt-get install -y build-essential
- run: sudo apt-get update && sudo apt-get install -y build-essential ccache
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: arm
- run: ROCKSDBTESTS_PLATFORM_DEPENDENT=only make V=1 J=4 -j4 all_but_some_tests check_some
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- uses: "./.github/actions/post-steps"
+27 -4
View File
@@ -106,6 +106,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
"db/version_edit.cc",
"db/version_edit_handler.cc",
"db/version_set.cc",
"db/version_util.cc",
"db/wal_edit.cc",
"db/wal_manager.cc",
"db/wide/wide_column_serialization.cc",
@@ -207,6 +208,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
"table/block_based/hash_index_reader.cc",
"table/block_based/index_builder.cc",
"table/block_based/index_reader_common.cc",
"table/block_based/multi_scan_index_iterator.cc",
"table/block_based/parsed_full_filter_block.cc",
"table/block_based/partitioned_filter_block.cc",
"table/block_based/partitioned_index_iterator.cc",
@@ -362,6 +364,9 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
"utilities/transactions/write_prepared_txn_db.cc",
"utilities/transactions/write_unprepared_txn.cc",
"utilities/transactions/write_unprepared_txn_db.cc",
"utilities/trie_index/bitvector.cc",
"utilities/trie_index/louds_trie.cc",
"utilities/trie_index/trie_index_factory.cc",
"utilities/ttl/db_ttl_impl.cc",
"utilities/types_util.cc",
"utilities/wal_filter.cc",
@@ -5137,6 +5142,12 @@ cpp_unittest_wrapper(name="faiss_ivf_index_test",
extra_compiler_flags=[])
cpp_unittest_wrapper(name="fault_injection_fs_test",
srcs=["utilities/fault_injection_fs_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="fault_injection_test",
srcs=["db/fault_injection_test.cc"],
deps=[":rocksdb_test_lib"],
@@ -5623,6 +5634,18 @@ cpp_unittest_wrapper(name="transaction_test",
extra_compiler_flags=[])
cpp_unittest_wrapper(name="trie_index_db_test",
srcs=["utilities/trie_index/trie_index_db_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="trie_index_test",
srcs=["utilities/trie_index/trie_index_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="ttl_test",
srcs=["utilities/ttl/ttl_test.cc"],
deps=[":rocksdb_test_lib"],
@@ -5725,14 +5748,14 @@ cpp_unittest_wrapper(name="write_controller_test",
extra_compiler_flags=[])
cpp_unittest_wrapper(name="write_prepared_transaction_test",
srcs=["utilities/transactions/write_prepared_transaction_test.cc"],
cpp_unittest_wrapper(name="write_prepared_transaction_seqno_test",
srcs=["utilities/transactions/write_prepared_transaction_seqno_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="write_prepared_transaction_test_seqno",
srcs=["utilities/transactions/write_prepared_transaction_test_seqno.cc"],
cpp_unittest_wrapper(name="write_prepared_transaction_test",
srcs=["utilities/transactions/write_prepared_transaction_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
+3
View File
@@ -250,6 +250,9 @@ The following patterns emerged as frequent sources of review feedback:
### Adding new option
Refer to claude_md/add_option.md
### Removing deprecated option
Refer to claude_md/remove_option.md
### Metrics
* When adding a new feature, evaluate whether there is opportunity to add
metrics. Try to avoid causing performance regression on hot path when adding
+9 -1
View File
@@ -766,6 +766,7 @@ set(SOURCES
db/version_edit.cc
db/version_edit_handler.cc
db/version_set.cc
db/version_util.cc
db/wal_edit.cc
db/wal_manager.cc
db/wide/wide_column_serialization.cc
@@ -842,6 +843,7 @@ set(SOURCES
table/block_based/block_based_table_builder.cc
table/block_based/block_based_table_factory.cc
table/block_based/block_based_table_iterator.cc
table/block_based/multi_scan_index_iterator.cc
table/block_based/block_based_table_reader.cc
table/block_based/block_builder.cc
table/block_based/block_cache.cc
@@ -999,6 +1001,9 @@ set(SOURCES
utilities/transactions/write_prepared_txn_db.cc
utilities/transactions/write_unprepared_txn.cc
utilities/transactions/write_unprepared_txn_db.cc
utilities/trie_index/bitvector.cc
utilities/trie_index/louds_trie.cc
utilities/trie_index/trie_index_factory.cc
utilities/types_util.cc
utilities/ttl/db_ttl_impl.cc
utilities/wal_filter.cc
@@ -1531,6 +1536,7 @@ if(WITH_TESTS)
utilities/cassandra/cassandra_serialize_test.cc
utilities/checkpoint/checkpoint_test.cc
utilities/env_timed_test.cc
utilities/fault_injection_fs_test.cc
utilities/memory/memory_test.cc
utilities/merge_operators/string_append/stringappend_test.cc
utilities/object_registry_test.cc
@@ -1548,10 +1554,12 @@ if(WITH_TESTS)
utilities/transactions/lock/point/point_lock_manager_stress_test.cc
utilities/transactions/write_committed_transaction_ts_test.cc
utilities/transactions/write_prepared_transaction_test.cc
utilities/transactions/write_prepared_transaction_test_seqno.cc
utilities/transactions/write_prepared_transaction_seqno_test.cc
utilities/transactions/write_unprepared_transaction_test.cc
utilities/transactions/lock/range/range_locking_test.cc
utilities/transactions/timestamped_snapshot_test.cc
utilities/trie_index/trie_index_db_test.cc
utilities/trie_index/trie_index_test.cc
utilities/ttl/ttl_test.cc
utilities/types_util_test.cc
utilities/util_merge_operators_test.cc
+25 -8
View File
@@ -1,17 +1,34 @@
# Rocksdb Change Log
> NOTE: Entries for next release do not go here. Follow instructions in `unreleased_history/README.txt`
## 11.0.4 (03/11/2026)
### Public API Changes
* Re-remove deprecated DB::Open raw pointer variants in db.h
## 11.1.2 (06/24/2026)
### Bug Fixes
* Fixed a bug where closing a read-only DB instance could delete live SST files created by a concurrent read-write DB sharing the same directory.
## 11.0.3 (03/03/2026)
## 11.1.1 (04/10/2026)
### Public API Changes
* Relax option sanitization for kv ratio compaction
* Changed experimental feature `ExternalTableReader::Get` and `ExternalTableReader::MultiGet` to use `PinnableSlice` instead of `std::string` for output values, enabling zero-copy pinning. This will break existing implementations.
* Added `SstFileReader::Get` and `SstFileReader::MultiGet` overloads that accept `PinnableSlice`/`std::vector<PinnableSlice>*`, enabling zero-copy reads when the underlying `TableReader` supports pinning.
* Added block_decompress_count to PerfContext
## 11.0.2 (02/26/2026)
### Public API Changes
* Revert removing deprecated DB::Open raw pointer variants in db.h
### Bug Fixes
* Fix a memory accounting leak in IODispatcher where ReadIndex() moved block values out of ReadSet without releasing the associated prefetch memory, causing subsequent prefetches to be blocked when max_prefetch_memory_bytes was set.
## 11.1.0 (03/23/2026)
### New Features
* Add a new option `open_files_async`. The existing behavior is on DB open, we open all sst files and do basic validations. For very large DBs on remote filesystems with many ssts, this may take very long. This option performs these validations instead in the background. Open errors found by this async background task are surfaced as a new background error kAsyncFileOpen.
* Added `BlockBasedTableOptions::kAuto` index block search type that automatically selects between binary and interpolation search on a per-index-block basis. During SST construction, each index block's key distribution uniformity is analyzed using the coefficient of variation of key gaps, and index blocks with uniform keys use interpolation search while others fall back to binary search. The uniformity threshold is configurable via `BlockBasedTableOptions::uniform_cv_threshold` (default: 0.2).
* Introduced enforce_write_buffer_manager_during_recovery option to allow WriteBufferManager to be enforced during WAL recovery. (Default: true)
* Add `memtable_batch_lookup_optimization` option to use batch lookup optimization for memtable MultiGet. For skip list memtables, after each key lookup, the search path is cached and reused for the next key, reducing per-key cost from O(log N) to O(log d) where d is the distance between consecutive keys. Benchmarks show ~7% improvement in memtable-resident MultiGet throughput.
* Added `BlockBasedTableOptions::PrepopulateBlockCache::kFlushAndCompaction` to prepopulate the block cache during both flush and compaction. Compaction-warmed blocks are inserted at `BOTTOM` priority (vs `LOW` for flush) so they are evicted first under cache pressure. Recommended only for use cases where most or all of the database is expected to reside in cache (e.g., tiered or remote storage where the working set fits in cache).
* Added new mutable DB option `verify_manifest_content_on_close` (default: false). When enabled, on DB close the MANIFEST file is read back and all records are validated (CRC checksums and logical content). If corruption is detected, a fresh MANIFEST is written from in-memory state.
### Behavior Changes
* num_reads_sampled now factors in re-seeks and next/prev() on file iterators for files in L1+. next/prev() is discounted by 64x compared to seek due to being a much cheaper call.
* Remote compaction workers now skip WAL recovery when opening the secondary DB instance, since only the LSM state from MANIFEST is needed for compaction. This reduces I/O and speeds up remote compaction startup.
### Bug Fixes
* Fix a bug in round-robin compaction that missed selecting input files that are needed to guarantee data correctness and cause crashing in debug builds or silent data corruption in release builds for Get().
## 11.0.0 (02/23/2026)
### New Features
+29 -1
View File
@@ -627,11 +627,22 @@ ROCKSDBTESTS_SUBSET ?= $(TESTS)
# its various tests. Parallel can fill up your /dev/shm
# db_bloom_filter_test - serial because excessive space usage by instances
# of DBFilterConstructionReserveMemoryTestWithParam can fill up /dev/shm
# perf_context_test - 1GB write_buffer_size default, parallel can fill /dev/shm
# obsolete_files_test - ~1GB write_buffer_size/target_file_size_base
# backup_engine_test - 1GB write_buffer_size in FailOverwritingBackups
# prefetch_test - 1GB write_buffer_size in PrefetchWhenReseek parameterized test
# db_io_failure_test - 256MB write_buffer_size in FlushSstRangeSyncError and
# CompactionSstRangeSyncError
NON_PARALLEL_TEST = \
c_test \
env_test \
deletefile_test \
db_bloom_filter_test \
perf_context_test \
obsolete_files_test \
backup_engine_test \
prefetch_test \
db_io_failure_test \
$(PLUGIN_TESTS) \
PARALLEL_TEST = $(filter-out $(NON_PARALLEL_TEST), $(TESTS))
@@ -884,6 +895,9 @@ $(parallel_tests):
"d=\$(TEST_TMPDIR)$$TEST_SCRIPT" \
'mkdir -p $$d' \
"TEST_TMPDIR=\$$d $(DRIVER) ./$$TEST_BINARY --gtest_filter=$$TEST_NAME" \
'test_retcode=$$?' \
'[ $$test_retcode -eq 0 ] && rm -rf $$d' \
'exit $$test_retcode' \
> $$TEST_SCRIPT; \
chmod a=rx $$TEST_SCRIPT; \
done
@@ -1001,6 +1015,11 @@ check-progress:
# If J != 1 and GNU parallel is installed, run the tests in parallel,
# via the check_0 rule above. Otherwise, run them sequentially.
check: all
$(AM_V_at)echo "Cleaning up stale test directories older than 3 hours..."; \
test_tmpdir_parent=$$(dirname $(TEST_TMPDIR)); \
find $$test_tmpdir_parent -maxdepth 1 -name 'rocksdb.*' -type d \
-mmin +180 -exec rm -rf {} + 2>/dev/null; \
true
$(MAKE) gen_parallel_tests
$(AM_V_GEN)if test "$(J)" != 1 \
&& (build_tools/gnu_parallel --gnu --help 2>/dev/null) | \
@@ -1589,6 +1608,12 @@ object_registry_test: $(OBJ_DIR)/utilities/object_registry_test.o $(TEST_LIBRARY
ttl_test: $(OBJ_DIR)/utilities/ttl/ttl_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
trie_index_db_test: $(OBJ_DIR)/utilities/trie_index/trie_index_db_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
trie_index_test: $(OBJ_DIR)/utilities/trie_index/trie_index_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
types_util_test: $(OBJ_DIR)/utilities/types_util_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
@@ -1640,6 +1665,9 @@ io_posix_test: $(OBJ_DIR)/env/io_posix_test.o $(TEST_LIBRARY) $(LIBRARY)
fault_injection_test: $(OBJ_DIR)/db/fault_injection_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
fault_injection_fs_test: $(OBJ_DIR)/utilities/fault_injection_fs_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
rate_limiter_test: $(OBJ_DIR)/util/rate_limiter_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
@@ -1838,7 +1866,7 @@ write_committed_transaction_ts_test: $(OBJ_DIR)/utilities/transactions/write_com
write_prepared_transaction_test: $(OBJ_DIR)/utilities/transactions/write_prepared_transaction_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
write_prepared_transaction_test_seqno: $(OBJ_DIR)/utilities/transactions/write_prepared_transaction_test_seqno.o $(TEST_LIBRARY) $(LIBRARY)
write_prepared_transaction_seqno_test: $(OBJ_DIR)/utilities/transactions/write_prepared_transaction_seqno_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
write_unprepared_transaction_test: $(OBJ_DIR)/utilities/transactions/write_unprepared_transaction_test.o $(TEST_LIBRARY) $(LIBRARY)
+104
View File
@@ -0,0 +1,104 @@
# Code Review Skill
This document defines the methodology for performing thorough, multi-perspective
code reviews on RocksDB changes. It is used both by CI (GitHub Actions) and
local review workflows.
## Prerequisites
Before starting a review:
- Read `CLAUDE.md` in the repository root for project-specific guidelines
- Read any files in `claude_md/` for architecture context
- Identify the commit/diff/PR to review and parse the changed files
## Review Methodology
Conduct the review from multiple perspectives, then synthesize findings into a
single report. Each perspective catches different classes of bugs.
### Perspective 1: Codebase Context & Call-Chain Analysis
Before reviewing the diff itself, build deep context:
- For each changed function/method, trace the call chain UPWARD 3-5 levels.
Who consumes the changed behavior? What invariants do callers rely on?
- Trace DOWNWARD into callees. What side effects do they have? What shared
state do they mutate (counters, sequence numbers, flags, metadata)?
- Identify the "sibling" or "reference" implementation that handles the same
scenario the standard way. Compare behaviors.
- For data written to shared structures (memtable, SST block, cache entry),
find ALL readers and verify their visibility rules match the writer.
### Perspective 2: Correctness & Edge Cases
- Thread safety and concurrency (lock ordering, data races)
- Error handling and Status propagation
- Edge cases: empty inputs, overflow, boundary conditions
- Data corruption scenarios
- Behavioral contract changes: do return value semantics match callers?
### Perspective 3: Cross-Component & Adversarial Analysis
Check the change in ALL execution contexts:
| Context | Key difference |
|---------|---------------|
| WritePreparedTxnDB | read_callback_ controls visibility |
| ReadOnly DB / SecondaryInstance | No mutable memtable |
| CompactionService / Remote compaction | Different process |
| User-defined timestamps | Extra key comparison dimension |
| MemPurge | Memtable-to-memtable path |
| BlobDB | Values in blob files, not inline |
| Old snapshots | Seqno far behind current |
| Concurrent writers | Lock-free vs locked paths |
| FIFO / Universal compaction | Different invariants |
| Prefix seek / total order seek | Different iterator behavior |
When the change claims a property ("logically redundant", "safe because X"),
systematically try to break it: list preconditions, construct counterexamples.
### Perspective 4: Performance
RocksDB is a high-performance storage engine.
- Memory allocation on hot paths
- Unnecessary copies (prefer move semantics, Slice, string_view)
- Cache efficiency and data locality
- Loop optimization, branch prediction (LIKELY/UNLIKELY)
### Perspective 5: API, Compatibility & Testing
- Public API backwards compatibility
- Serialization format correctness and versioning
- Test coverage for edge cases, failure modes, and system-level interactions
## How to Explore the Codebase
Use available tools to explore deeply — do NOT just review the diff in isolation.
The most critical bugs hide at component boundaries.
- Read the header files for changed implementations
- Search for callers of changed functions (3-5 levels up)
- Read existing tests to understand guaranteed behaviors
- Check related implementations for conventions and patterns
## Output Format
### Summary
Brief overall assessment (1-2 sentences).
### Issues Found
Categorize by severity:
- :red_circle: **Critical**: Must fix (correctness, data corruption, security)
- :yellow_circle: **Suggestion**: Should consider (performance, edge cases)
- :green_circle: **Nitpick**: Minor style issues
For each issue include:
- File and line reference
- Which review perspective found it
- Description with root cause analysis
- Suggested fix
### Cross-Component Analysis
Execution context checks and assumption stress-test results.
### Positive Observations
Good patterns, clever optimizations, or improvements.
+7
View File
@@ -0,0 +1,7 @@
# Removing Deprecated Options from RocksDB
### Keep in these files:
- [ ] **KEEP** entry in type_info (`options/cf_options.cc` or `options/db_options.cc`) with `OptionVerificationType::kDeprecated` for loading old option files
- [ ] **KEEP** or add test in `options/options_test.cc` `OptionsOldApiTest::GetOptionsFromMapTest` for loading old option files
### Documentation:
- [ ] Add release note to `HISTORY.md` and `unreleased_history/`
+1 -1
View File
@@ -533,7 +533,7 @@ void BlobFileReader::MultiGetBlob(
}
// Uncompress blob if needed
Slice value_slice(record_slice.data() + adjustments[i], req->len);
Slice value_slice(record_slice.data() + adjustments[j - 1], req->len);
*req->status = UncompressBlobIfNeeded(
value_slice, compression_type_, decompressor_.get(), allocator, clock_,
statistics_, &blob_reqs[i].second);
+86
View File
@@ -1011,6 +1011,92 @@ TEST_P(BlobFileReaderDecodingErrorTest, DecodingError) {
SyncPoint::GetInstance()->ClearAllCallBacks();
}
TEST_F(BlobFileReaderTest, MultiGetBlobWithFailedValidation) {
// Test that MultiGetBlob correctly handles the case where some requests
// fail validation. The adjustments vector is only populated for requests
// that pass validation, so the indices must track correctly.
Options options;
options.env = mock_env_.get();
options.cf_paths.emplace_back(
test::PerThreadDBPath(
mock_env_.get(),
"BlobFileReaderTest_MultiGetBlobWithFailedValidation"),
0);
options.enable_blob_files = true;
ImmutableOptions immutable_options(options);
constexpr uint32_t column_family_id = 1;
constexpr bool has_ttl = false;
constexpr ExpirationRange expiration_range;
constexpr uint64_t blob_file_number = 1;
constexpr size_t num_blobs = 3;
const std::vector<std::string> key_strs = {"key1", "key2", "key3"};
const std::vector<std::string> blob_strs = {"blob1", "blob2", "blob3"};
const std::vector<Slice> keys = {key_strs[0], key_strs[1], key_strs[2]};
const std::vector<Slice> blobs = {blob_strs[0], blob_strs[1], blob_strs[2]};
std::vector<uint64_t> blob_offsets(keys.size());
std::vector<uint64_t> blob_sizes(keys.size());
WriteBlobFile(immutable_options, column_family_id, has_ttl, expiration_range,
expiration_range, blob_file_number, keys, blobs, kNoCompression,
blob_offsets, blob_sizes);
constexpr HistogramImpl* blob_file_read_hist = nullptr;
std::unique_ptr<BlobFileReader> reader;
ReadOptions read_options;
ASSERT_OK(BlobFileReader::Create(
immutable_options, read_options, FileOptions(), column_family_id,
blob_file_read_hist, blob_file_number, nullptr /*IOTracer*/, &reader));
// Enable checksum verification so adjustments are non-zero
read_options.verify_checksums = true;
constexpr MemoryAllocator* allocator = nullptr;
// Fail the first request by giving it an invalid offset (too small).
// This causes the first request to be skipped in the adjustments vector,
// so adjustments has only 2 entries (for blobs 1 and 2), while blob_reqs
// has 3 entries. Without the fix, adjustments[i] would use the wrong
// index for the remaining valid requests.
std::array<Status, num_blobs> statuses_buf;
std::array<BlobReadRequest, num_blobs> requests_buf;
autovector<std::pair<BlobReadRequest*, std::unique_ptr<BlobContents>>>
blob_reqs;
// First request: invalid offset (too small, will fail validation)
requests_buf[0] = BlobReadRequest(keys[0], /*offset=*/0, blob_sizes[0],
kNoCompression, nullptr, &statuses_buf[0]);
// Second and third requests: valid
requests_buf[1] = BlobReadRequest(keys[1], blob_offsets[1], blob_sizes[1],
kNoCompression, nullptr, &statuses_buf[1]);
requests_buf[2] = BlobReadRequest(keys[2], blob_offsets[2], blob_sizes[2],
kNoCompression, nullptr, &statuses_buf[2]);
for (size_t i = 0; i < num_blobs; ++i) {
blob_reqs.emplace_back(&requests_buf[i], std::unique_ptr<BlobContents>());
}
uint64_t bytes_read = 0;
reader->MultiGetBlob(read_options, allocator, blob_reqs, &bytes_read);
// First request should fail validation
ASSERT_TRUE(statuses_buf[0].IsCorruption());
ASSERT_EQ(blob_reqs[0].second, nullptr);
// Second and third requests should succeed with correct data
ASSERT_OK(statuses_buf[1]);
ASSERT_NE(blob_reqs[1].second, nullptr);
ASSERT_EQ(blob_reqs[1].second->data(), blobs[1]);
ASSERT_OK(statuses_buf[2]);
ASSERT_NE(blob_reqs[2].second, nullptr);
ASSERT_EQ(blob_reqs[2].second->data(), blobs[2]);
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
+10
View File
@@ -322,6 +322,8 @@ Status BuildTable(
}
TEST_SYNC_POINT("BuildTable:BeforeFinishBuildTable");
TEST_SYNC_POINT_CALLBACK("BuildTable:BeforeCheckEmpty",
static_cast<TableBuilder*>(builder));
const bool empty = builder->IsEmpty();
if (flush_stats) {
assert(c_iter.HasNumInputEntryScanned());
@@ -330,6 +332,14 @@ Status BuildTable(
}
if (!s.ok() || empty) {
builder->Abandon();
// Propagate the builder's error when the builder is empty due to an
// internal error (e.g., write fault injection causing all Add() calls to
// return early). Without this, `s` would remain OK and the downstream
// key count validation in flush_job.cc would produce a misleading
// Corruption error instead of the actual builder error.
if (s.ok() && !builder->status().ok()) {
s = builder->status();
}
} else {
SeqnoToTimeMapping relevant_mapping;
if (seqno_to_time_mapping) {
+33 -6
View File
@@ -1759,9 +1759,13 @@ rocksdb_column_family_handle_t* rocksdb_create_column_family(
rocksdb_t* db, const rocksdb_options_t* column_family_options,
const char* column_family_name, char** errptr) {
rocksdb_column_family_handle_t* handle = new rocksdb_column_family_handle_t;
SaveError(errptr, db->rep->CreateColumnFamily(
ColumnFamilyOptions(column_family_options->rep),
std::string(column_family_name), &(handle->rep)));
handle->rep = nullptr;
if (SaveError(errptr, db->rep->CreateColumnFamily(
ColumnFamilyOptions(column_family_options->rep),
std::string(column_family_name), &(handle->rep)))) {
delete handle;
return nullptr;
}
handle->immortal = false;
return handle;
}
@@ -4351,6 +4355,15 @@ unsigned char rocksdb_options_get_paranoid_checks(rocksdb_options_t* opt) {
return opt->rep.paranoid_checks;
}
void rocksdb_options_set_open_files_async(rocksdb_options_t* opt,
unsigned char v) {
opt->rep.open_files_async = v;
}
unsigned char rocksdb_options_get_open_files_async(rocksdb_options_t* opt) {
return opt->rep.open_files_async;
}
void rocksdb_options_set_db_paths(rocksdb_options_t* opt,
const rocksdb_dbpath_t** dbpath_values,
size_t num_paths) {
@@ -5789,6 +5802,16 @@ uint64_t rocksdb_perfcontext_metric(rocksdb_perfcontext_t* context,
return rep->internal_range_del_reseek_count;
case rocksdb_block_read_cpu_time:
return rep->block_read_cpu_time;
case rocksdb_data_block_read_byte:
return rep->data_block_read_byte;
case rocksdb_index_block_read_byte:
return rep->index_block_read_byte;
case rocksdb_filter_block_read_byte:
return rep->filter_block_read_byte;
case rocksdb_compression_dict_block_read_byte:
return rep->compression_dict_block_read_byte;
case rocksdb_metadata_block_read_byte:
return rep->metadata_block_read_byte;
default:
break;
}
@@ -7525,9 +7548,13 @@ rocksdb_column_family_handle_t* rocksdb_transactiondb_create_column_family(
const rocksdb_options_t* column_family_options,
const char* column_family_name, char** errptr) {
rocksdb_column_family_handle_t* handle = new rocksdb_column_family_handle_t;
SaveError(errptr, txn_db->rep->CreateColumnFamily(
ColumnFamilyOptions(column_family_options->rep),
std::string(column_family_name), &(handle->rep)));
handle->rep = nullptr;
if (SaveError(errptr, txn_db->rep->CreateColumnFamily(
ColumnFamilyOptions(column_family_options->rep),
std::string(column_family_name), &(handle->rep)))) {
delete handle;
return nullptr;
}
handle->immortal = false;
return handle;
}
+15
View File
@@ -4880,6 +4880,21 @@ int main(int argc, char** argv) {
rocksdb_sst_file_manager_destroy(sst_file_manager);
}
StartPhase("create_column_family_error_returns_null");
{
// Creating a column family with a name that already exists should fail
// and return NULL. Without the fix, the handle is leaked and a non-NULL
// pointer with an indeterminate rep field is returned.
char* cf_err = NULL;
rocksdb_column_family_handle_t* cf_handle =
rocksdb_create_column_family(db, options, "default", &cf_err);
// Should have an error since "default" already exists
CheckCondition(cf_err != NULL);
// The handle should be NULL on error (this is the bug fix)
CheckCondition(cf_handle == NULL);
free(cf_err);
}
StartPhase("cancel_all_background_work");
rocksdb_cancel_all_background_work(db, 1);
+22
View File
@@ -1569,6 +1569,28 @@ Status ColumnFamilyData::ValidateOptions(
"FIFO compaction only supported with max_open_files = -1.");
}
if (db_options.open_files_async) {
// FIFO TTL picker relies on reading the files table properties inside a
// DB mutex. This can be slow if files are opened asynchronously, so we
// disable it.
//
// TODO: consider blocking fifo compaction until async file open task
// completes.
if (cf_options.compaction_style == kCompactionStyleFIFO) {
return Status::NotSupported(
"FIFO compaction is not supported with open_files_async = true.");
}
// Open files async is not useful if skip_stats_update_on_db_open=true
// because DB open will still block on IO.
//
// TODO: consider moving stats update inside async file open background
// task.
if (!db_options.skip_stats_update_on_db_open) {
return Status::InvalidArgument(
"open_files_async requires skip_stats_update_on_db_open = true.");
}
}
std::vector<uint32_t> supported{0, 1, 2, 4, 8};
if (std::find(supported.begin(), supported.end(),
cf_options.memtable_protection_bytes_per_key) ==
+49 -9
View File
@@ -33,6 +33,7 @@
#include "db/range_del_aggregator.h"
#include "db/version_edit.h"
#include "db/version_set.h"
#include "file/file_util.h"
#include "file/filename.h"
#include "file/read_write_util.h"
#include "file/sst_file_manager_impl.h"
@@ -903,17 +904,17 @@ Status CompactionJob::VerifyOutputFiles() {
// use_direct_io_for_flush_and_compaction is true, we will regard this
// verification as user reads since the goal is to cache it here for
// further user reads
ReadOptions verify_table_read_options(Env::IOActivity::kCompaction);
verify_table_read_options.verify_checksums = true;
verify_table_read_options.readahead_size =
ReadOptions verification_read_options(Env::IOActivity::kCompaction);
verification_read_options.verify_checksums = true;
verification_read_options.readahead_size =
file_options_for_read_.compaction_readahead_size;
std::unique_ptr<TableReader> table_reader_guard;
TableReader* table_reader_ptr = table_reader_guard.get();
verify_table_read_options.rate_limiter_priority =
verification_read_options.rate_limiter_priority =
GetRateLimiterPriority();
InternalIterator* iter = cfd->table_cache()->NewIterator(
verify_table_read_options, file_options_, cfd->internal_comparator(),
verification_read_options, file_options_, cfd->internal_comparator(),
output_file.meta,
/*range_del_agg=*/nullptr, compact_->compaction->mutable_cf_options(),
/*table_reader_ptr=*/&table_reader_ptr,
@@ -941,12 +942,17 @@ Status CompactionJob::VerifyOutputFiles() {
!!(verify_output_flags & VerifyOutputFlags::kVerifyBlockChecksum);
const bool should_verify_iteration =
!!(verify_output_flags & VerifyOutputFlags::kVerifyIteration);
const bool should_verify_file_checksum =
!!(verify_output_flags &
VerifyOutputFlags::kVerifyFileChecksum) &&
db_options_.file_checksum_gen_factory != nullptr &&
output_file.meta.file_checksum != kUnknownFileChecksum;
if (should_verify_block_checksum) {
assert(table_reader_ptr != nullptr);
// If verifying iteration as well, verify meta blocks here only to
// avoid redundant checks on data blocks
s = table_reader_ptr->VerifyChecksum(
verify_table_read_options, TableReaderCaller::kCompaction,
verification_read_options, TableReaderCaller::kCompaction,
/*meta_blocks_only=*/should_verify_iteration);
}
if (s.ok() && should_verify_iteration) {
@@ -967,6 +973,24 @@ Status CompactionJob::VerifyOutputFiles() {
"was computed when written");
}
}
if (s.ok() && should_verify_file_checksum) {
std::string file_checksum;
std::string file_checksum_func_name;
std::string fname =
GetTableFileName(output_file.meta.fd.GetNumber());
s = GenerateOneFileChecksum(
fs_.get(), fname, db_options_.file_checksum_gen_factory.get(),
output_file.meta.file_checksum_func_name, &file_checksum,
&file_checksum_func_name,
verification_read_options.readahead_size,
db_options_.allow_mmap_reads, io_tracer_,
db_options_.rate_limiter.get(), verification_read_options,
stats_, db_options_.clock, file_options_for_read_);
if (s.ok() && file_checksum != output_file.meta.file_checksum) {
s = Status::Corruption(
"File checksum mismatch for compaction output file " + fname);
}
}
}
}
@@ -1092,6 +1116,9 @@ Status CompactionJob::Run() {
status = VerifyOutputFiles();
}
TEST_SYNC_POINT_CALLBACK("CompactionJob::Run():AfterVerifyOutputFiles",
&status);
if (status.ok()) {
SetOutputTableProperties();
}
@@ -2470,8 +2497,16 @@ Status CompactionJob::OpenCompactionOutputFile(SubcompactionState* sub_compact,
return s;
}
// Enable hash computation if paranoid_file_checks is on or if
// verify_output_flags includes kVerifyIteration, so that
// VerifyOutputFiles() can compare the hash of the written data
// against a re-read of the output file.
bool enable_output_hash =
paranoid_file_checks_ ||
!!(sub_compact->compaction->mutable_cf_options().verify_output_flags &
VerifyOutputFlags::kVerifyIteration);
outputs.AddOutput(std::move(meta), cfd->internal_comparator(),
paranoid_file_checks_);
enable_output_hash);
}
writable_file->SetIOPriority(GetRateLimiterPriority());
@@ -2512,7 +2547,7 @@ Status CompactionJob::OpenCompactionOutputFile(SubcompactionState* sub_compact,
void CompactionJob::CleanupCompaction() {
for (SubcompactionState& sub_compact : compact_->sub_compact_states) {
sub_compact.Cleanup(table_cache_.get());
sub_compact.Cleanup(table_cache_.get(), compact_->status);
}
delete compact_;
compact_ = nullptr;
@@ -2904,12 +2939,17 @@ void CompactionJob::RestoreCompactionOutputs(
const auto& output_files = subcompaction_progress_per_level.GetOutputFiles();
const bool enable_output_hash =
paranoid_file_checks_ ||
!!(compact_->compaction->mutable_cf_options().verify_output_flags &
VerifyOutputFlags::kVerifyIteration);
for (size_t i = 0; i < output_files.size(); i++) {
FileMetaData file_copy = output_files[i];
outputs_to_restore->AddOutput(std::move(file_copy),
cfd->internal_comparator(),
paranoid_file_checks_, true /* finished */);
enable_output_hash, true /* finished */);
outputs_to_restore->UpdateTableProperties(
*output_files_table_properties[i]);
+6 -5
View File
@@ -102,10 +102,10 @@ Compaction* FIFOCompactionPicker::PickTTLCompaction(
for (auto ritr = level_files.rbegin(); ritr != level_files.rend(); ++ritr) {
FileMetaData* f = *ritr;
assert(f);
if (f->fd.table_reader && f->fd.table_reader->GetTableProperties()) {
TableReader* reader = f->fd.pinned_reader.Get();
if (reader != nullptr && reader->GetTableProperties() != nullptr) {
uint64_t newest_key_time = f->TryGetNewestKeyTime();
uint64_t creation_time =
f->fd.table_reader->GetTableProperties()->creation_time;
uint64_t creation_time = reader->GetTableProperties()->creation_time;
uint64_t est_newest_key_time = newest_key_time == kUnknownNewestKeyTime
? creation_time
: newest_key_time;
@@ -166,8 +166,9 @@ Compaction* FIFOCompactionPicker::PickTTLCompaction(
assert(f);
uint64_t newest_key_time = f->TryGetNewestKeyTime();
uint64_t creation_time = 0;
if (f->fd.table_reader && f->fd.table_reader->GetTableProperties()) {
creation_time = f->fd.table_reader->GetTableProperties()->creation_time;
TableReader* reader = f->fd.pinned_reader.Get();
if (reader != nullptr && reader->GetTableProperties() != nullptr) {
creation_time = reader->GetTableProperties()->creation_time;
}
uint64_t est_newest_key_time = newest_key_time == kUnknownNewestKeyTime
? creation_time
+9 -1
View File
@@ -404,7 +404,15 @@ void LevelCompactionBuilder::SetupOtherFilesWithRoundRobinExpansion() {
tmp_start_level_inputs = start_level_inputs_;
// TODO (zichen): Future parallel round-robin may also need to update this
// Constraint 1b (only expand till the end)
for (size_t i = start_index + 1; i < level_files.size(); i++) {
size_t next_index = start_index + 1;
const FileMetaData* last_file = start_level_inputs_.files.back();
for (size_t j = next_index; j < level_files.size(); j++) {
if (level_files[j] == last_file) {
next_index = j + 1;
break;
}
}
for (size_t i = next_index; i < level_files.size(); i++) {
auto* f = level_files[i];
if (f->being_compacted) {
// Constraint 1a
+5 -3
View File
@@ -178,7 +178,8 @@ class CompactionPickerTestBase : public testing::Test {
}
TableProperties tp;
tp.newest_key_time = newest_key_time;
f->fd.table_reader = new mock::MockTableReader(mock::KVVector{}, tp);
f->fd.pinned_reader.TEST_SetReader(
new mock::MockTableReader(mock::KVVector{}, tp));
vstorage->AddFile(level, f);
files_.emplace_back(f);
@@ -272,8 +273,9 @@ class CompactionPickerTestBase : public testing::Test {
void ClearFiles() {
for (auto& file : files_) {
if (file->fd.table_reader != nullptr) {
delete file->fd.table_reader;
TableReader* t = file->fd.pinned_reader.Get();
if (t != nullptr) {
delete t;
}
}
files_.clear();
+35 -1
View File
@@ -409,6 +409,34 @@ TEST_F(CompactionServiceTest, BasicCompactions) {
SyncPoint::GetInstance()->DisableProcessing();
}
TEST_F(CompactionServiceTest, SkipWALRecoveryInOpenAndCompact) {
// Verify that OpenAndCompact skips WAL recovery when opening the secondary
// instance. WAL replay is unnecessary for remote compaction since it only
// needs the LSM state from MANIFEST.
Options options = CurrentOptions();
ReopenWithCompactionService(&options);
// Track whether FindAndRecoverLogFiles is called during compaction
std::atomic_bool wal_recovery_called{false};
SyncPoint::GetInstance()->SetCallBack(
"DBImplSecondary::FindAndRecoverLogFiles:Begin",
[&](void* /* arg */) { wal_recovery_called.store(true); });
SyncPoint::GetInstance()->EnableProcessing();
// Generate data and trigger compaction (which uses OpenAndCompact)
GenerateTestData();
ASSERT_OK(dbfull()->TEST_WaitForCompact());
// WAL recovery should NOT have been called during OpenAndCompact
ASSERT_FALSE(wal_recovery_called.load());
// Data should still be correct (compaction worked without WAL recovery)
VerifyTestData();
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
}
TEST_F(CompactionServiceTest, ManualCompaction) {
Options options = CurrentOptions();
options.disable_auto_compactions = true;
@@ -1113,6 +1141,8 @@ TEST_F(CompactionServiceTest, CorruptedOutputVerifyOutputFlags) {
VerifyOutputFlags::kVerifyBlockChecksum,
VerifyOutputFlags::kEnableForRemoteCompaction |
VerifyOutputFlags::kVerifyIteration,
VerifyOutputFlags::kEnableForRemoteCompaction |
VerifyOutputFlags::kVerifyFileChecksum,
VerifyOutputFlags::kVerifyAll}) {
SCOPED_TRACE(
"verify_output_flags=" +
@@ -1124,6 +1154,7 @@ TEST_F(CompactionServiceTest, CorruptedOutputVerifyOutputFlags) {
options.disable_auto_compactions = true;
options.paranoid_file_checks = false;
options.verify_output_flags = verify_output_flags;
options.file_checksum_gen_factory = GetFileChecksumGenCrc32cFactory();
ReopenWithCompactionService(&options);
GenerateTestData();
@@ -1159,10 +1190,13 @@ TEST_F(CompactionServiceTest, CorruptedOutputVerifyOutputFlags) {
!!(verify_output_flags & VerifyOutputFlags::kVerifyBlockChecksum);
const bool should_verify_iteration =
!!(verify_output_flags & VerifyOutputFlags::kVerifyIteration);
const bool should_verify_file_checksum =
!!(verify_output_flags & VerifyOutputFlags::kVerifyFileChecksum);
Status s = db_->CompactRange(CompactRangeOptions(), &start, &end);
if (is_enabled_for_remote_compaction &&
(should_verify_block_checksum || should_verify_iteration)) {
(should_verify_block_checksum || should_verify_iteration ||
should_verify_file_checksum)) {
ASSERT_NOK(s);
ASSERT_TRUE(s.IsCorruption());
} else {
+2 -2
View File
@@ -38,11 +38,11 @@ OutputIterator SubcompactionState::GetOutputs() const {
compaction_outputs_.outputs_);
}
void SubcompactionState::Cleanup(Cache* cache) {
void SubcompactionState::Cleanup(Cache* cache, const Status& overall_status) {
proximal_level_outputs_.Cleanup();
compaction_outputs_.Cleanup();
if (!status.ok()) {
if (!status.ok() || !overall_status.ok()) {
for (const auto& out : GetOutputs()) {
// If this file was inserted into the table cache then remove it here
// because this compaction was not committed. This is not strictly
+1 -1
View File
@@ -170,7 +170,7 @@ class SubcompactionState {
}
}
void Cleanup(Cache* cache);
void Cleanup(Cache* cache, const Status& overall_status);
void AggregateCompactionOutputStats(
InternalStats::CompactionStatsFull& internal_stats) const;
+313
View File
@@ -2535,6 +2535,319 @@ TEST_P(DBMultiGetTestWithParam, MultiGetBatchedValueSizeMultiLevelMerge) {
}
}
TEST_P(DBMultiGetTestWithParam, MultiGetMemtableBatchLookup) {
#ifndef USE_COROUTINES
if (std::get<1>(GetParam())) {
ROCKSDB_GTEST_SKIP("This test requires coroutine support");
return;
}
#endif // USE_COROUTINES
// Skip for unbatched MultiGet
if (!std::get<0>(GetParam())) {
ROCKSDB_GTEST_BYPASS("This test is only for batched MultiGet");
return;
}
Options options = CurrentOptions();
options.memtable_batch_lookup_optimization = true;
CreateAndReopenWithCF({"pikachu"}, options);
// Insert sorted keys into memtable
for (int i = 0; i < 100; i++) {
ASSERT_OK(Put(1, Key(i), "val" + std::to_string(i)));
}
// Delete some keys
ASSERT_OK(Delete(1, Key(25)));
ASSERT_OK(Delete(1, Key(75)));
// MultiGet a batch of keys - mix of existing, deleted, and missing
// Store key strings to keep Slice data alive
std::vector<std::string> key_strs = {Key(0), Key(10), Key(25), Key(50),
Key(75), Key(99), Key(200)};
std::vector<Slice> keys(key_strs.begin(), key_strs.end());
std::vector<PinnableSlice> values(keys.size());
std::vector<Status> statuses(keys.size());
ReadOptions ro;
ro.async_io = std::get<1>(GetParam());
db_->MultiGet(ro, handles_[1], keys.size(), keys.data(), values.data(),
statuses.data(), true);
ASSERT_OK(statuses[0]);
ASSERT_EQ(values[0].ToString(), "val0");
ASSERT_OK(statuses[1]);
ASSERT_EQ(values[1].ToString(), "val10");
ASSERT_TRUE(statuses[2].IsNotFound()); // deleted
ASSERT_OK(statuses[3]);
ASSERT_EQ(values[3].ToString(), "val50");
ASSERT_TRUE(statuses[4].IsNotFound()); // deleted
ASSERT_OK(statuses[5]);
ASSERT_EQ(values[5].ToString(), "val99");
ASSERT_TRUE(statuses[6].IsNotFound()); // never inserted
}
TEST_P(DBMultiGetTestWithParam, MultiGetBatchLookupOverwrite) {
#ifndef USE_COROUTINES
if (std::get<1>(GetParam())) {
ROCKSDB_GTEST_SKIP("This test requires coroutine support");
return;
}
#endif // USE_COROUTINES
if (!std::get<0>(GetParam())) {
ROCKSDB_GTEST_BYPASS("This test is only for batched MultiGet");
return;
}
Options options = CurrentOptions();
options.memtable_batch_lookup_optimization = true;
CreateAndReopenWithCF({"pikachu"}, options);
// Insert, then overwrite some keys
for (int i = 0; i < 50; i++) {
ASSERT_OK(Put(1, Key(i), "old" + std::to_string(i)));
}
for (int i = 0; i < 50; i += 5) {
ASSERT_OK(Put(1, Key(i), "new" + std::to_string(i)));
}
std::vector<std::string> key_strs;
for (int i = 0; i < 50; i += 5) {
key_strs.push_back(Key(i));
}
std::vector<Slice> keys(key_strs.begin(), key_strs.end());
std::vector<PinnableSlice> values(keys.size());
std::vector<Status> statuses(keys.size());
ReadOptions ro;
ro.async_io = std::get<1>(GetParam());
db_->MultiGet(ro, handles_[1], keys.size(), keys.data(), values.data(),
statuses.data(), true);
for (size_t i = 0; i < keys.size(); i++) {
ASSERT_OK(statuses[i]);
ASSERT_EQ(values[i].ToString(), "new" + std::to_string(i * 5));
}
}
TEST_P(DBMultiGetTestWithParam, MultiGetBatchLookupWithFlush) {
#ifndef USE_COROUTINES
if (std::get<1>(GetParam())) {
ROCKSDB_GTEST_SKIP("This test requires coroutine support");
return;
}
#endif // USE_COROUTINES
if (!std::get<0>(GetParam())) {
ROCKSDB_GTEST_BYPASS("This test is only for batched MultiGet");
return;
}
Options options = CurrentOptions();
options.memtable_batch_lookup_optimization = true;
CreateAndReopenWithCF({"pikachu"}, options);
// Put data into SST
for (int i = 0; i < 50; i++) {
ASSERT_OK(Put(1, Key(i), "sst" + std::to_string(i)));
}
ASSERT_OK(Flush(1));
// Put different data into memtable (overlapping some keys)
for (int i = 25; i < 75; i++) {
ASSERT_OK(Put(1, Key(i), "mem" + std::to_string(i)));
}
// MultiGet keys spanning both SST and memtable
std::vector<std::string> key_strs = {Key(10), Key(30), Key(60), Key(80)};
std::vector<Slice> keys(key_strs.begin(), key_strs.end());
std::vector<PinnableSlice> values(keys.size());
std::vector<Status> statuses(keys.size());
ReadOptions ro;
ro.async_io = std::get<1>(GetParam());
db_->MultiGet(ro, handles_[1], keys.size(), keys.data(), values.data(),
statuses.data(), true);
ASSERT_OK(statuses[0]);
ASSERT_EQ(values[0].ToString(), "sst10");
ASSERT_OK(statuses[1]);
ASSERT_EQ(values[1].ToString(), "mem30");
ASSERT_OK(statuses[2]);
ASSERT_EQ(values[2].ToString(), "mem60");
ASSERT_TRUE(statuses[3].IsNotFound());
}
TEST_P(DBMultiGetTestWithParam, MultiGetBatchLookupWithMerge) {
#ifndef USE_COROUTINES
if (std::get<1>(GetParam())) {
ROCKSDB_GTEST_SKIP("This test requires coroutine support");
return;
}
#endif // USE_COROUTINES
if (!std::get<0>(GetParam())) {
ROCKSDB_GTEST_BYPASS("This test is only for batched MultiGet");
return;
}
Options options = CurrentOptions();
options.memtable_batch_lookup_optimization = true;
options.merge_operator = MergeOperators::CreateStringAppendOperator();
CreateAndReopenWithCF({"pikachu"}, options);
// Put base values
ASSERT_OK(Put(1, Key(1), "a"));
ASSERT_OK(Put(1, Key(2), "x"));
// Merge on top
ASSERT_OK(Merge(1, Key(1), "b"));
ASSERT_OK(Merge(1, Key(1), "c"));
ASSERT_OK(Merge(1, Key(2), "y"));
std::vector<std::string> key_strs = {Key(1), Key(2), Key(3)};
std::vector<Slice> keys(key_strs.begin(), key_strs.end());
std::vector<PinnableSlice> values(keys.size());
std::vector<Status> statuses(keys.size());
ReadOptions ro;
ro.async_io = std::get<1>(GetParam());
db_->MultiGet(ro, handles_[1], keys.size(), keys.data(), values.data(),
statuses.data(), true);
ASSERT_OK(statuses[0]);
ASSERT_EQ(values[0].ToString(), "a,b,c");
ASSERT_OK(statuses[1]);
ASSERT_EQ(values[1].ToString(), "x,y");
ASSERT_TRUE(statuses[2].IsNotFound());
}
TEST_F(DBBasicTest, MultiGetBatchLookupDisabledByDefault) {
// Verify that finger search is off by default and MultiGet still works
Options options = CurrentOptions();
ASSERT_FALSE(options.memtable_batch_lookup_optimization);
CreateAndReopenWithCF({"pikachu"}, options);
ASSERT_OK(Put(1, "k1", "v1"));
ASSERT_OK(Put(1, "k2", "v2"));
std::vector<Slice> keys = {"k1", "k2", "k3"};
std::vector<PinnableSlice> values(3);
std::vector<Status> statuses(3);
db_->MultiGet(ReadOptions(), handles_[1], 3, keys.data(), values.data(),
statuses.data(), true);
ASSERT_OK(statuses[0]);
ASSERT_EQ(values[0].ToString(), "v1");
ASSERT_OK(statuses[1]);
ASSERT_EQ(values[1].ToString(), "v2");
ASSERT_TRUE(statuses[2].IsNotFound());
}
TEST_P(DBMultiGetTestWithParam, MultiGetBatchLookupWithParanoid) {
#ifndef USE_COROUTINES
if (std::get<1>(GetParam())) {
ROCKSDB_GTEST_SKIP("This test requires coroutine support");
return;
}
#endif // USE_COROUTINES
if (!std::get<0>(GetParam())) {
ROCKSDB_GTEST_BYPASS("This test is only for batched MultiGet");
return;
}
Options options = CurrentOptions();
options.memtable_batch_lookup_optimization = true;
options.paranoid_memory_checks = true;
CreateAndReopenWithCF({"pikachu"}, options);
// Insert sorted keys into memtable
for (int i = 0; i < 100; i++) {
ASSERT_OK(Put(1, Key(i), "val" + std::to_string(i)));
}
ASSERT_OK(Delete(1, Key(25)));
// MultiGet with both batch optimization and paranoid checks enabled
std::vector<std::string> key_strs = {Key(0), Key(10), Key(25),
Key(50), Key(99), Key(200)};
std::vector<Slice> keys(key_strs.begin(), key_strs.end());
std::vector<PinnableSlice> values(keys.size());
std::vector<Status> statuses(keys.size());
ReadOptions ro;
ro.async_io = std::get<1>(GetParam());
db_->MultiGet(ro, handles_[1], keys.size(), keys.data(), values.data(),
statuses.data(), true);
ASSERT_OK(statuses[0]);
ASSERT_EQ(values[0].ToString(), "val0");
ASSERT_OK(statuses[1]);
ASSERT_EQ(values[1].ToString(), "val10");
ASSERT_TRUE(statuses[2].IsNotFound()); // deleted
ASSERT_OK(statuses[3]);
ASSERT_EQ(values[3].ToString(), "val50");
ASSERT_OK(statuses[4]);
ASSERT_EQ(values[4].ToString(), "val99");
ASSERT_TRUE(statuses[5].IsNotFound()); // never inserted
}
TEST_P(DBMultiGetTestWithParam, MultiGetBatchLookupSnapshot) {
#ifndef USE_COROUTINES
if (std::get<1>(GetParam())) {
ROCKSDB_GTEST_SKIP("This test requires coroutine support");
return;
}
#endif // USE_COROUTINES
if (!std::get<0>(GetParam())) {
ROCKSDB_GTEST_BYPASS("This test is only for batched MultiGet");
return;
}
Options options = CurrentOptions();
options.memtable_batch_lookup_optimization = true;
CreateAndReopenWithCF({"pikachu"}, options);
ASSERT_OK(Put(1, Key(1), "v1_old"));
ASSERT_OK(Put(1, Key(2), "v2_old"));
const Snapshot* snap = db_->GetSnapshot();
// Write new values after snapshot
ASSERT_OK(Put(1, Key(1), "v1_new"));
ASSERT_OK(Put(1, Key(2), "v2_new"));
ASSERT_OK(Put(1, Key(3), "v3_new"));
// MultiGet with snapshot should see old values
std::vector<std::string> key_strs = {Key(1), Key(2), Key(3)};
std::vector<Slice> keys(key_strs.begin(), key_strs.end());
std::vector<PinnableSlice> values(keys.size());
std::vector<Status> statuses(keys.size());
ReadOptions ro;
ro.snapshot = snap;
ro.async_io = std::get<1>(GetParam());
db_->MultiGet(ro, handles_[1], keys.size(), keys.data(), values.data(),
statuses.data(), true);
ASSERT_OK(statuses[0]);
ASSERT_EQ(values[0].ToString(), "v1_old");
ASSERT_OK(statuses[1]);
ASSERT_EQ(values[1].ToString(), "v2_old");
ASSERT_TRUE(statuses[2].IsNotFound()); // didn't exist at snapshot
db_->ReleaseSnapshot(snap);
// MultiGet without snapshot should see new values
for (auto& v : values) {
v.Reset();
}
db_->MultiGet(ReadOptions(), handles_[1], keys.size(), keys.data(),
values.data(), statuses.data(), true);
ASSERT_OK(statuses[0]);
ASSERT_EQ(values[0].ToString(), "v1_new");
ASSERT_OK(statuses[1]);
ASSERT_EQ(values[1].ToString(), "v2_new");
ASSERT_OK(statuses[2]);
ASSERT_EQ(values[2].ToString(), "v3_new");
}
INSTANTIATE_TEST_CASE_P(DBMultiGetTestWithParam, DBMultiGetTestWithParam,
testing::Combine(testing::Bool(), testing::Bool()));
+114
View File
@@ -466,6 +466,90 @@ TEST_F(DBBlockCacheTest, WarmCacheWithDataBlocksDuringFlush) {
options.statistics->getTickerCount(BLOCK_CACHE_DATA_ADD));
}
// Cache wrapper that tracks the priority of each Insert call.
namespace {
class PriorityTrackingCache : public CacheWrapper {
public:
explicit PriorityTrackingCache(std::shared_ptr<Cache> target)
: CacheWrapper(std::move(target)) {}
const char* Name() const override { return "PriorityTrackingCache"; }
Status Insert(const Slice& key, ObjectPtr value,
const CacheItemHelper* helper, size_t charge,
Handle** handle = nullptr, Priority priority = Priority::LOW,
const Slice& compressed_value = Slice(),
CompressionType type = kNoCompression) override {
insert_priorities_.push_back(priority);
return CacheWrapper::Insert(key, value, helper, charge, handle, priority,
compressed_value, type);
}
void ResetPriorities() { insert_priorities_.clear(); }
bool HasPriority(Priority p) const {
return std::find(insert_priorities_.begin(), insert_priorities_.end(), p) !=
insert_priorities_.end();
}
private:
std::vector<Priority> insert_priorities_;
};
} // namespace
TEST_F(DBBlockCacheTest, WarmCacheWithDataBlocksDuringCompaction) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.statistics = ROCKSDB_NAMESPACE::CreateDBStatistics();
options.disable_auto_compactions = true;
auto tracking_cache =
std::make_shared<PriorityTrackingCache>(NewLRUCache(1 << 25, 0, false));
BlockBasedTableOptions table_options;
table_options.block_cache = tracking_cache;
table_options.cache_index_and_filter_blocks = false;
table_options.prepopulate_block_cache =
BlockBasedTableOptions::PrepopulateBlockCache::kFlushAndCompaction;
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
DestroyAndReopen(options);
std::string value(kValueSize, 'a');
// Flush warming: inserts should use LOW priority.
tracking_cache->ResetPriorities();
ASSERT_OK(Put("key", value));
ASSERT_OK(Flush());
EXPECT_TRUE(tracking_cache->HasPriority(Cache::Priority::LOW));
EXPECT_FALSE(tracking_cache->HasPriority(Cache::Priority::BOTTOM));
// Write overlapping key to force a real merge compaction (not trivial move).
ASSERT_OK(Put("key", value + "2"));
ASSERT_OK(Flush());
auto data_add_before =
options.statistics->getTickerCount(BLOCK_CACHE_DATA_ADD);
// Compaction warming: data block inserts should use BOTTOM priority.
// Internal cache bookkeeping (e.g., cache entry stats) may insert at HIGH,
// so we check for BOTTOM presence and LOW absence.
tracking_cache->ResetPriorities();
CompactRangeOptions cro;
cro.bottommost_level_compaction = BottommostLevelCompaction::kForceOptimized;
ASSERT_OK(db_->CompactRange(cro, /*begin=*/nullptr, /*end=*/nullptr));
EXPECT_GT(options.statistics->getTickerCount(BLOCK_CACHE_DATA_ADD),
data_add_before);
EXPECT_TRUE(tracking_cache->HasPriority(Cache::Priority::BOTTOM));
EXPECT_FALSE(tracking_cache->HasPriority(Cache::Priority::LOW));
// Compaction output is in cache — reads should have zero misses.
auto data_miss_before =
options.statistics->getTickerCount(BLOCK_CACHE_DATA_MISS);
ASSERT_EQ(value + "2", Get("key"));
EXPECT_EQ(data_miss_before,
options.statistics->getTickerCount(BLOCK_CACHE_DATA_MISS));
}
// This test cache data, index and filter blocks during flush.
class DBBlockCacheTest1 : public DBTestBase,
public ::testing::WithParamInterface<uint32_t> {
@@ -620,6 +704,36 @@ TEST_F(DBBlockCacheTest, DynamicOptions) {
ASSERT_EQ(0, st->getAndResetTickerCount(BLOCK_CACHE_DATA_MISS));
ASSERT_EQ(1, st->getAndResetTickerCount(BLOCK_CACHE_DATA_HIT));
// Switch to kFlushAndCompaction
++i;
ASSERT_OK(dbfull()->SetOptions(
{{"block_based_table_factory",
"{prepopulate_block_cache=kFlushAndCompaction;}"}}));
ASSERT_OK(Put(std::to_string(i), value));
ASSERT_OK(Flush());
// Flush warming still works
ASSERT_EQ(1, st->getAndResetTickerCount(BLOCK_CACHE_DATA_ADD));
ASSERT_EQ(value, Get(std::to_string(i)));
ASSERT_EQ(0, st->getAndResetTickerCount(BLOCK_CACHE_DATA_ADD));
ASSERT_EQ(0, st->getAndResetTickerCount(BLOCK_CACHE_DATA_MISS));
ASSERT_EQ(1, st->getAndResetTickerCount(BLOCK_CACHE_DATA_HIT));
// Switch back to kDisable
++i;
ASSERT_OK(dbfull()->SetOptions(
{{"block_based_table_factory", "{prepopulate_block_cache=kDisable;}"}}));
ASSERT_OK(Put(std::to_string(i), value));
ASSERT_OK(Flush());
ASSERT_EQ(0, st->getAndResetTickerCount(BLOCK_CACHE_DATA_ADD));
ASSERT_EQ(value, Get(std::to_string(i)));
ASSERT_EQ(1, st->getAndResetTickerCount(BLOCK_CACHE_DATA_ADD));
ASSERT_EQ(1, st->getAndResetTickerCount(BLOCK_CACHE_DATA_MISS));
ASSERT_EQ(0, st->getAndResetTickerCount(BLOCK_CACHE_DATA_HIT));
++i;
// NOT YET SUPPORTED
// FIXME: find a way to make this fail again (until well supported)
+305 -10
View File
@@ -14,11 +14,13 @@
#include "db/db_test_util.h"
#include "db/dbformat.h"
#include "env/mock_env.h"
#include "file/filename.h"
#include "port/port.h"
#include "port/stack_trace.h"
#include "rocksdb/advanced_options.h"
#include "rocksdb/concurrent_task_limiter.h"
#include "rocksdb/experimental.h"
#include "rocksdb/file_checksum.h"
#include "rocksdb/iostats_context.h"
#include "rocksdb/sst_file_writer.h"
#include "test_util/mock_time_env.h"
@@ -797,6 +799,13 @@ TEST_F(DBCompactionTest, CompactRangeBottomPri) {
// and one compact to L2 in bottom pri pool.
int low_pri_count = 0;
int bottom_pri_count = 0;
bool bottom_running_seen = false;
SyncPoint::GetInstance()->SetCallBack(
"BackgroundCallCompaction:1", [&](void*) {
if (dbfull()->TEST_NumRunningBottomCompactions() > 0) {
bottom_running_seen = true;
}
});
SyncPoint::GetInstance()->SetCallBack(
"ThreadPoolImpl::Impl::BGThread:BeforeRun", [&](void* arg) {
Env::Priority* pri = static_cast<Env::Priority*>(arg);
@@ -815,6 +824,7 @@ TEST_F(DBCompactionTest, CompactRangeBottomPri) {
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), nullptr, nullptr));
ASSERT_EQ(1, low_pri_count);
ASSERT_EQ(1, bottom_pri_count);
ASSERT_TRUE(bottom_running_seen);
ASSERT_EQ("0,0,2", FilesPerLevel(0));
// Recompact bottom most level uses bottom pool
@@ -11488,20 +11498,24 @@ TEST_F(DBCompactionTest, RecordNewestKeyTimeForTtlCompaction) {
// Check that we are populating newest_key_time on flush
std::vector<FileMetaData*> file_metadatas = GetLevelFileMetadatas(0);
ASSERT_EQ(file_metadatas.size(), 4);
uint64_t first_newest_key_time =
file_metadatas[0]->fd.table_reader->GetTableProperties()->newest_key_time;
uint64_t first_newest_key_time = file_metadatas[0]
->fd.pinned_reader.Get()
->GetTableProperties()
->newest_key_time;
ASSERT_NE(first_newest_key_time, kUnknownNewestKeyTime);
// Check that the newest_key_times are in expected ordering
uint64_t prev_newest_key_time = first_newest_key_time;
for (size_t idx = 1; idx < file_metadatas.size(); idx++) {
uint64_t newest_key_time = file_metadatas[idx]
->fd.table_reader->GetTableProperties()
->fd.pinned_reader.Get()
->GetTableProperties()
->newest_key_time;
ASSERT_LT(newest_key_time, prev_newest_key_time);
prev_newest_key_time = newest_key_time;
ASSERT_EQ(newest_key_time, file_metadatas[idx]
->fd.table_reader->GetTableProperties()
->fd.pinned_reader.Get()
->GetTableProperties()
->creation_time);
}
// The delta between the first and last newest_key_times is 15s
@@ -11516,14 +11530,18 @@ TEST_F(DBCompactionTest, RecordNewestKeyTimeForTtlCompaction) {
ASSERT_EQ(NumTableFilesAtLevel(0), 1);
file_metadatas = GetLevelFileMetadatas(0);
ASSERT_EQ(file_metadatas.size(), 1);
ASSERT_EQ(
file_metadatas[0]->fd.table_reader->GetTableProperties()->newest_key_time,
first_newest_key_time);
ASSERT_EQ(file_metadatas[0]
->fd.pinned_reader.Get()
->GetTableProperties()
->newest_key_time,
first_newest_key_time);
// Contrast newest_key_time with creation_time, which records the oldest
// ancestor time (15s older than newest_key_time)
ASSERT_EQ(
file_metadatas[0]->fd.table_reader->GetTableProperties()->creation_time,
last_newest_key_time);
ASSERT_EQ(file_metadatas[0]
->fd.pinned_reader.Get()
->GetTableProperties()
->creation_time,
last_newest_key_time);
ASSERT_EQ(file_metadatas[0]->oldest_ancester_time, last_newest_key_time);
// Make sure TTL of 5s causes compaction
@@ -11663,6 +11681,283 @@ TEST_F(DBCompactionTest, PeriodicTask) {
ASSERT_EQ(listener->num_periodic_compactions, 1);
Close();
}
// Regression test for a bug in SetupOtherFilesWithRoundRobinExpansion where
// duplicate files are added to the compaction input, corrupting
// ExpandInputsToCleanCut and violating the clean-cut invariant. The bug
// requires: (1) kRoundRobin compaction priority, (2) files at a non-L0 level
// with shared user key boundaries (adjacent files whose boundary keys share
// the same user key), and (3) ExpandInputsToCleanCut expanding the initially
// picked file to include multiple adjacent files in PickFileToCompact.
TEST_F(DBCompactionTest, RoundRobinCleanCutWithSharedBoundary) {
Options options = CurrentOptions();
options.compaction_style = kCompactionStyleLevel;
options.compaction_pri = kRoundRobin;
options.level_compaction_dynamic_level_bytes = false;
options.max_bytes_for_level_base = 100;
options.disable_auto_compactions = true;
DestroyAndReopen(options);
std::vector<const Snapshot*> snapshots;
for (int v = 0; v < 5; v++) {
for (int k = 0; k < 3; k++) {
ASSERT_OK(Put("key" + std::to_string(k), "v" + std::to_string(v)));
}
snapshots.push_back(db_->GetSnapshot());
ASSERT_OK(Flush());
}
// Force L0->L1 compaction output to split every 3 keys. With 3 keys x 5
// versions (15 KVs) sorted by (user_key asc, seq desc), splitting every 3
// creates 5 files where adjacent files share boundaries across different
// user keys (e.g., File0 ends with key0, File1 starts with key0 and ends
// with key1, etc.). This chain of 4+ shared boundaries across 3 different
// user keys is needed so that ExpandInputsToCleanCut expands the picked
// file to multiple files, and the duplicate in the round-robin loop causes
// GetRange to return a truncated range that drops files from the set.
std::atomic<int> key_count{0};
SyncPoint::GetInstance()->SetCallBack(
"CompactionOutputs::ShouldStopBefore::manual_decision", [&](void* arg) {
auto* p = static_cast<std::pair<bool*, const Slice>*>(arg);
int n = key_count.fetch_add(1);
if (n > 0 && n % 3 == 0) {
*(p->first) = true;
}
});
SyncPoint::GetInstance()->EnableProcessing();
ASSERT_OK(dbfull()->TEST_CompactRange(0, nullptr, nullptr));
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
ColumnFamilyMetaData cf_meta;
db_->GetColumnFamilyMetaData(&cf_meta);
ASSERT_EQ(cf_meta.levels[1].files.size(), 5U);
for (auto s : snapshots) {
db_->ReleaseSnapshot(s);
}
ASSERT_OK(dbfull()->SetOptions({{"disable_auto_compactions", "false"}}));
ASSERT_OK(dbfull()->TEST_WaitForCompact());
for (int k = 0; k < 3; k++) {
ASSERT_EQ(Get("key" + std::to_string(k)), "v4");
}
}
// Regression test:
// 1. Compaction succeeds at subcompaction level, VerifyOutputFiles adds cache
// entries for output files via table_cache()->NewIterator().
// 2. A post-verification step fails (injected here via sync point), setting
// compact_->status to error while each subcompaction's status stays OK.
// 3. SubcompactionState::Cleanup checks individual status (OK) and skips
// ReleaseObsolete — the cache entries leak.
// 4. FaultInjectionTestFS injects metadata read errors, causing GetChildren
// to fail in FindObsoleteFiles.
// 5. Close()'s FindObsoleteFiles also fails to find the orphan for the same
// reason. TEST_VerifyNoObsoleteFilesCached finds the leaked entry.
TEST_F(DBCompactionTest, LeakedTableCacheEntryOnCompactionFailure) {
auto fault_fs = std::make_shared<FaultInjectionTestFS>(env_->GetFileSystem());
std::unique_ptr<Env> fault_env(NewCompositeEnv(fault_fs));
Options options = CurrentOptions();
options.env = fault_env.get();
options.paranoid_file_checks = true;
options.level0_file_num_compaction_trigger = 2;
options.disable_auto_compactions = true;
options.num_levels = 3;
DestroyAndReopen(options);
// Write overlapping data to force a real (non-trivial) compaction.
ASSERT_OK(Put("a", std::string(1024, 'x')));
ASSERT_OK(Put("z", std::string(1024, 'x')));
ASSERT_OK(Flush());
ASSERT_OK(Put("a", std::string(1024, 'y')));
ASSERT_OK(Put("z", std::string(1024, 'y')));
ASSERT_OK(Flush());
ASSERT_EQ(NumTableFilesAtLevel(0), 2);
// After VerifyOutputFiles succeeds (cache entries created), inject error
// and deactivate the filesystem. The error makes the overall compaction
// fail while individual subcompaction statuses stay OK (so Cleanup skips
// ReleaseObsolete). The filesystem deactivation makes GetChildren fail
// in FindObsoleteFiles, preventing the backstop from evicting the leaked
// cache entries — matching the crash test's metadata read fault injection.
std::atomic<bool> inject_error{true};
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"CompactionJob::Run():AfterVerifyOutputFiles", [&](void* arg) {
if (inject_error.exchange(false)) {
*static_cast<Status*>(arg) = Status::Corruption("injected");
}
});
// Enable metadata read fault injection on the bg compaction thread after
// the compaction job finishes but before FindObsoleteFiles runs. This
// makes GetChildren fail (metadata read), matching crash test's
// --open_metadata_read_fault_one_in=8. Only metadata reads fail —
// logging and other IO operations continue normally.
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"BackgroundCallCompaction:1", [&](void*) {
fault_fs->SetThreadLocalErrorContext(
FaultInjectionIOType::kMetadataRead, /*seed=*/0, /*one_in=*/1,
/*retryable=*/false, /*has_data_loss=*/false);
fault_fs->EnableThreadLocalErrorInjection(
FaultInjectionIOType::kMetadataRead);
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
// Trigger compaction — fails after VerifyOutputFiles.
Status s = dbfull()->TEST_CompactRange(0, nullptr, nullptr);
ASSERT_NOK(s);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
// Enable metadata read fault injection on the main thread too, so
// Close()'s FindObsoleteFiles also fails to find the orphan file.
fault_fs->SetThreadLocalErrorContext(
FaultInjectionIOType::kMetadataRead, /*seed=*/0, /*one_in=*/1,
/*retryable=*/false, /*has_data_loss=*/false);
fault_fs->EnableThreadLocalErrorInjection(
FaultInjectionIOType::kMetadataRead);
// TEST_VerifyNoObsoleteFilesCached asserted within Close on ASAN builds
s = db_->Close();
ASSERT_OK(s);
// Release DB before fault_env goes out of scope to avoid use-after-free.
db_ = nullptr;
}
TEST_F(DBCompactionTest, VerifyFileChecksumOnCompactionOutput) {
Options options = CurrentOptions();
options.disable_auto_compactions = true;
options.file_checksum_gen_factory = GetFileChecksumGenCrc32cFactory();
options.verify_output_flags = VerifyOutputFlags::kVerifyFileChecksum |
VerifyOutputFlags::kEnableForLocalCompaction;
DestroyAndReopen(options);
// Create 2 L0 files to trigger compaction
for (int i = 0; i < 10; i++) {
ASSERT_OK(Put(Key(i), "value" + std::to_string(i)));
}
ASSERT_OK(Flush());
for (int i = 5; i < 15; i++) {
ASSERT_OK(Put(Key(i), "value2_" + std::to_string(i)));
}
ASSERT_OK(Flush());
// Corrupt output files right before verification
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"CompactionJob::Run:BeforeVerify", [&](void* /*arg*/) {
// Find and corrupt the newest SST file (compaction output)
std::vector<std::string> filenames;
ASSERT_OK(env_->GetChildren(dbname_, &filenames));
uint64_t max_number = 0;
std::string target_fname;
for (const auto& f : filenames) {
uint64_t number;
FileType type;
if (ParseFileName(f, &number, &type) && type == kTableFile &&
number > max_number) {
max_number = number;
target_fname = dbname_ + "/" + f;
}
}
ASSERT_FALSE(target_fname.empty());
ASSERT_OK(test::CorruptFile(env_, target_fname, 0, 1,
false /* verifyChecksum */));
});
SyncPoint::GetInstance()->EnableProcessing();
Status s = db_->CompactRange(CompactRangeOptions(), nullptr, nullptr);
ASSERT_TRUE(s.IsCorruption()) << s.ToString();
ASSERT_TRUE(
std::strstr(s.getState(), "File checksum mismatch for compaction output"))
<< s.ToString();
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
}
// Regression test: verify_output_flags with kVerifyIteration should work
// correctly even when paranoid_file_checks is false. Before the fix, the
// OutputValidator hash was only computed during writing when
// paranoid_file_checks was true, but the verification always computed the
// hash, leading to a false positive "Key-value checksum of compaction output
// doesn't match" error.
TEST_F(DBCompactionTest, VerifyIterationWithoutParanoidFileChecks) {
Options options = CurrentOptions();
options.disable_auto_compactions = true;
options.paranoid_file_checks = false;
options.verify_output_flags = VerifyOutputFlags::kVerifyIteration |
VerifyOutputFlags::kEnableForLocalCompaction;
DestroyAndReopen(options);
// Create 2 L0 files to trigger compaction
for (int i = 0; i < 10; i++) {
ASSERT_OK(Put(Key(i), "value" + std::to_string(i)));
}
ASSERT_OK(Flush());
for (int i = 5; i < 15; i++) {
ASSERT_OK(Put(Key(i), "value2_" + std::to_string(i)));
}
ASSERT_OK(Flush());
// Compaction should succeed without false corruption errors
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
// Verify data is intact
for (int i = 0; i < 15; i++) {
std::string expected;
if (i >= 5) {
expected = "value2_" + std::to_string(i);
} else {
expected = "value" + std::to_string(i);
}
ASSERT_EQ(Get(Key(i)), expected);
}
}
// Also test all verification types combined without paranoid_file_checks
TEST_F(DBCompactionTest, VerifyAllOutputFlagsWithoutParanoidFileChecks) {
Options options = CurrentOptions();
options.disable_auto_compactions = true;
options.paranoid_file_checks = false;
options.file_checksum_gen_factory = GetFileChecksumGenCrc32cFactory();
options.verify_output_flags = VerifyOutputFlags::kVerifyBlockChecksum |
VerifyOutputFlags::kVerifyIteration |
VerifyOutputFlags::kVerifyFileChecksum |
VerifyOutputFlags::kEnableForLocalCompaction;
DestroyAndReopen(options);
for (int i = 0; i < 10; i++) {
ASSERT_OK(Put(Key(i), "value" + std::to_string(i)));
}
ASSERT_OK(Flush());
for (int i = 5; i < 15; i++) {
ASSERT_OK(Put(Key(i), "value2_" + std::to_string(i)));
}
ASSERT_OK(Flush());
// Compaction should succeed with all verification types enabled
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
for (int i = 0; i < 15; i++) {
std::string expected;
if (i >= 5) {
expected = "value2_" + std::to_string(i);
} else {
expected = "value" + std::to_string(i);
}
ASSERT_EQ(Get(Key(i)), expected);
}
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
+143
View File
@@ -17,6 +17,7 @@
#include "port/port.h"
#include "port/stack_trace.h"
#include "rocksdb/utilities/transaction_db.h"
#include "table/block_based/block_based_table_builder.h"
#include "test_util/sync_point.h"
#include "test_util/testutil.h"
#include "util/cast_util.h"
@@ -33,6 +34,17 @@ static std::string NEW_VALUE = "NewValue";
class DBFlushTest : public DBTestBase {
public:
DBFlushTest() : DBTestBase("db_flush_test", /*env_do_fsync=*/true) {}
// Wait for all background flush callbacks to complete before Close().
// Close() calls db_.reset() which nulls the unique_ptr before the destructor
// runs, so a concurrent OnFlushCompleted reading test_->db_ would race.
// WaitForCompact() waits for bg_flush_scheduled_==0 (which is decremented
// after listener callbacks return) without setting shutting_down_ (so
// callbacks are not skipped) and without requiring NumNotFlushed()==0 (which
// would hang when mempurge leaves a memtable in imm()).
Status WaitForFlushCallbacks() {
return db_->WaitForCompact(WaitForCompactOptions());
}
};
class DBFlushDirectIOTest : public DBFlushTest,
@@ -1055,6 +1067,7 @@ TEST_F(DBFlushTest, MemPurgeBasic) {
ASSERT_EQ(Get(RNDKEY2), p_rv2);
ASSERT_EQ(Get(RNDKEY3), p_rv3);
ASSERT_OK(WaitForFlushCallbacks());
Close();
}
@@ -1169,6 +1182,7 @@ TEST_F(DBFlushTest, MemPurgeBasicToggle) {
// We expect no mempurge at all.
EXPECT_EQ(mempurge_count.exchange(0), ZERO);
ASSERT_OK(WaitForFlushCallbacks());
Close();
}
// End of MemPurgeBasicToggle, which is not
@@ -1425,6 +1439,7 @@ TEST_F(DBFlushTest, MemPurgeDeleteAndDeleteRange) {
delete iter;
}
ASSERT_OK(WaitForFlushCallbacks());
Close();
}
@@ -1879,6 +1894,85 @@ TEST_F(DBFlushTest, MemPurgeCorrectLogNumberAndSSTFileCreation) {
Close();
}
// Reproduction for MemPurge memtable ID ordering bug.
// When MemPurge runs, it releases db_mutex_. During that window, new
// memtables can be switched to the immutable list with higher IDs. When
// MemPurge re-acquires the mutex and adds its output memtable using the
// stale ID from mems_.back(), the ordering assertion fires.
TEST_F(DBFlushTest, MemPurgeIdOrdering) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.compression = kNoCompression;
options.inplace_update_support = false;
options.allow_concurrent_memtable_write = true;
options.write_buffer_size = 1 << 20; // 1MB
// Allow enough immutable memtables so writes don't stall while the
// flush thread is paused in the sync point.
options.max_write_buffer_number = 8;
// Always attempt MemPurge on flush.
options.experimental_mempurge_threshold = 1.0;
ASSERT_OK(TryReopen(options));
// Coordinate via LoadDependency:
// 1. Flush thread hits BeforeReacquireMutex -> foreground unblocks
// 2. Foreground writes data, triggers memtable switch
// 3. Foreground hits SwitchDone -> flush thread unblocks at
// AfterWaitForTest
// 4. Flush thread re-acquires mutex, tries AddMemTable with stale ID
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency(
{{"FlushJob::MemPurge:BeforeReacquireMutex",
"DBFlushTest::MemPurgeIdOrdering:StartWriting"},
{"DBFlushTest::MemPurgeIdOrdering:SwitchDone",
"FlushJob::MemPurge:AfterWaitForTest"}});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
const int kValueSize = 10240; // 10KB values like MemPurgeBasic
const int kNumKeys = 30;
Random rnd(301);
// Fill the memtable with overwrites of the same keys so MemPurge
// can compact them down (high garbage ratio). Each round is ~300KB,
// need ~3 rounds to approach the 1MB write_buffer_size.
for (int round = 0; round < 3; round++) {
for (int i = 0; i < kNumKeys; i++) {
ASSERT_OK(Put("key" + std::to_string(i), rnd.RandomString(kValueSize)));
}
}
// One more round should trigger a flush with MemPurge in the background.
for (int i = 0; i < kNumKeys; i++) {
ASSERT_OK(Put("key" + std::to_string(i), rnd.RandomString(kValueSize)));
}
// Block until MemPurge reaches the sync point (db_mutex_ released).
TEST_SYNC_POINT("DBFlushTest::MemPurgeIdOrdering:StartWriting");
// Now MemPurge is paused with db_mutex_ released. Write enough data
// to fill another memtable and trigger a switch. This creates an
// immutable memtable with a higher ID than the one being mempurged.
for (int i = 0; i < kNumKeys * 4; i++) {
ASSERT_OK(Put("newkey" + std::to_string(i), rnd.RandomString(kValueSize)));
}
// Let MemPurge continue -- it will re-acquire the mutex and try to
// add the output memtable with the stale ID.
TEST_SYNC_POINT("DBFlushTest::MemPurgeIdOrdering:SwitchDone");
// Allow background work to finish.
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
// If the bug is present, the assertion in AddMemTable fires before
// we reach here.
for (int i = 0; i < kNumKeys; i++) {
ASSERT_NE(Get("key" + std::to_string(i)), "NOT_FOUND");
}
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
Close();
}
TEST_P(DBFlushDirectIOTest, DirectIO) {
Options options;
options.create_if_missing = true;
@@ -3706,6 +3800,55 @@ INSTANTIATE_TEST_CASE_P(
// larger than the max allowed padding size
testing::Values(4, kLowSpaceOverheadRatio)));
// Test that when the table builder's io_status becomes bad during flush
// (simulating write fault injection), BuildTable properly propagates the
// builder's IO error instead of producing a misleading Corruption from the
// num_entries mismatch check.
TEST_F(DBFlushTest, BuilderWriteFaultPropagationDuringFlush) {
Options options = CurrentOptions();
options.flush_verify_memtable_count = true;
options.table_factory.reset(
NewBlockBasedTableFactory(BlockBasedTableOptions()));
DestroyAndReopen(options);
for (int i = 0; i < 100; i++) {
ASSERT_OK(Put("key" + std::to_string(i),
std::string(100, 'v') + std::to_string(i)));
}
// Skip all Add() calls to simulate entries not being committed (builder
// stays empty), as happens when fault injection causes early returns.
SyncPoint::GetInstance()->SetCallBack(
"BlockBasedTableBuilder::Add::skip",
[&](void* skip) { *(bool*)skip = true; });
// Inject an IOError into the builder's status before the empty check.
// This simulates the scenario where write fault injection puts the builder
// in an error state, causing all Add() calls to return early (ok() is false)
// and leaving the builder empty.
SyncPoint::GetInstance()->SetCallBack(
"BuildTable:BeforeCheckEmpty", [&](void* arg) {
auto* builder = static_cast<BlockBasedTableBuilder*>(
static_cast<TableBuilder*>(arg));
builder->TEST_InjectIOError();
});
SyncPoint::GetInstance()->EnableProcessing();
Status s = Flush();
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
// With the fix, the builder's IOError is propagated instead of the
// misleading Corruption from the key count mismatch check.
ASSERT_NOK(s);
ASSERT_TRUE(s.IsIOError())
<< "Expected IOError from builder error propagation, got: "
<< s.ToString();
Close();
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
+54 -31
View File
@@ -91,8 +91,21 @@ Status CompactedDBImpl::Get(const ReadOptions& _read_options,
/*b_has_ts=*/false) < 0) {
return Status::NotFound();
}
Status s = f.fd.table_reader->Get(read_options, lkey.internal_key(),
&get_context, nullptr);
TableReader* t = nullptr;
TableCache::TypedHandle* handle = nullptr;
Status s = cfd_->table_cache()->FindTable(
read_options, cfd_->table_cache()->file_options(),
cfd_->internal_comparator(), *f.file_metadata, &handle,
version_->GetMutableCFOptions(), &t, false /* no_io */,
nullptr /* file_read_hist */, false /* skip_filters */, files_level_,
true /* prefetch_index_and_filter_in_cache */,
0 /* max_file_size_for_l0_meta_pin */, f.file_metadata->temperature,
true /* pin_table_handle */);
if (s.ok()) {
assert(handle == nullptr);
// Use TableReader directly to avoid extra table_cache->Get() overheads
s = t->Get(read_options, lkey.internal_key(), &get_context, nullptr);
}
if (!s.ok() && !s.IsNotFound()) {
return s;
}
@@ -152,7 +165,6 @@ void CompactedDBImpl::MultiGet(const ReadOptions& _read_options,
}
GetWithTimestampReadCallback read_cb(kMaxSequenceNumber);
autovector<TableReader*, 16> reader_list;
for (size_t i = 0; i < num_keys; ++i) {
const Slice& key = keys[i];
LookupKey lkey(key, kMaxSequenceNumber, read_options.timestamp);
@@ -162,38 +174,40 @@ void CompactedDBImpl::MultiGet(const ReadOptions& _read_options,
ExtractUserKeyAndStripTimestamp(f.smallest_key,
user_comparator_->timestamp_size()),
/*b_has_ts=*/false) < 0) {
reader_list.push_back(nullptr);
} else {
f.fd.table_reader->Prepare(lkey.internal_key());
reader_list.push_back(f.fd.table_reader);
statuses[i] = Status::NotFound();
continue;
}
}
for (size_t i = 0; i < num_keys; ++i) {
statuses[i] = Status::NotFound();
}
int idx = 0;
for (auto* r : reader_list) {
if (r != nullptr) {
PinnableSlice& pinnable_val = values[idx];
LookupKey lkey(keys[idx], kMaxSequenceNumber, read_options.timestamp);
std::string* timestamp = timestamps ? &timestamps[idx] : nullptr;
GetContext get_context(
user_comparator_, nullptr, nullptr, nullptr, GetContext::kNotFound,
lkey.user_key(), &pinnable_val, /*columns=*/nullptr,
user_comparator_->timestamp_size() > 0 ? timestamp : nullptr, nullptr,
nullptr, true, nullptr, nullptr, nullptr, nullptr, &read_cb);
Status status =
r->Get(read_options, lkey.internal_key(), &get_context, nullptr);
assert(static_cast<size_t>(idx) < num_keys);
if (!status.ok() && !status.IsNotFound()) {
statuses[idx] = status;
PinnableSlice& pinnable_val = values[i];
std::string* timestamp = timestamps ? &timestamps[i] : nullptr;
GetContext get_context(
user_comparator_, nullptr, nullptr, nullptr, GetContext::kNotFound,
lkey.user_key(), &pinnable_val, /*columns=*/nullptr,
user_comparator_->timestamp_size() > 0 ? timestamp : nullptr, nullptr,
nullptr, true, nullptr, nullptr, nullptr, nullptr, &read_cb);
TableReader* t = nullptr;
TableCache::TypedHandle* handle = nullptr;
Status status = cfd_->table_cache()->FindTable(
read_options, cfd_->table_cache()->file_options(),
cfd_->internal_comparator(), *f.file_metadata, &handle,
version_->GetMutableCFOptions(), &t, false /* no_io */,
nullptr /* file_read_hist */, false /* skip_filters */, files_level_,
true /* prefetch_index_and_filter_in_cache */,
0 /* max_file_size_for_l0_meta_pin */, f.file_metadata->temperature,
true /* pin_table_handle */);
if (status.ok()) {
assert(handle == nullptr);
// Use TableReader directly to avoid extra table_cache->Get() overheads
status = t->Get(read_options, lkey.internal_key(), &get_context, nullptr);
}
if (!status.ok() && !status.IsNotFound()) {
statuses[i] = status;
} else {
if (get_context.State() == GetContext::kFound) {
statuses[i] = Status::OK();
} else {
if (get_context.State() == GetContext::kFound) {
statuses[idx] = Status::OK();
}
statuses[i] = Status::NotFound();
}
}
++idx;
}
}
@@ -230,6 +244,7 @@ Status CompactedDBImpl::Init(const Options& options) {
return Status::NotSupported("Both L0 and other level contain files");
}
files_ = l0;
files_level_ = 0;
return Status::OK();
}
@@ -242,6 +257,7 @@ Status CompactedDBImpl::Init(const Options& options) {
int level = vstorage->num_non_empty_levels() - 1;
if (vstorage->LevelFilesBrief(level).num_files > 0) {
files_ = vstorage->LevelFilesBrief(level);
files_level_ = level;
return Status::OK();
}
return Status::NotSupported("no file exists");
@@ -261,6 +277,13 @@ Status CompactedDBImpl::Open(const Options& options, const std::string& dbname,
std::unique_ptr<CompactedDBImpl> db(new CompactedDBImpl(db_options, dbname));
Status s = db->Init(options);
if (s.ok()) {
{
InstrumentedMutexLock l(&db->mutex_);
db->opened_successfully_ = true;
if (db->immutable_db_options_.open_files_async) {
db->ScheduleAsyncFileOpening();
}
}
s = db->StartPeriodicTaskScheduler();
}
if (s.ok()) {
+1
View File
@@ -143,5 +143,6 @@ class CompactedDBImpl : public DBImpl {
Version* version_;
const Comparator* user_comparator_;
LevelFilesBrief files_;
int files_level_{0};
};
} // namespace ROCKSDB_NAMESPACE
+108 -14
View File
@@ -169,6 +169,7 @@ DBImpl::DBImpl(const DBOptions& options, const std::string& dbname,
const bool seq_per_batch, const bool batch_per_txn,
bool read_only)
: dbname_(dbname),
read_only_(read_only),
own_info_log_(options.info_log == nullptr),
initial_db_options_(SanitizeOptions(dbname, options, read_only,
&init_logger_creation_s_)),
@@ -472,7 +473,7 @@ Status DBImpl::ResumeImpl(DBRecoverContext context) {
void DBImpl::WaitForBackgroundWork() {
// Wait for background work to finish
while (bg_bottom_compaction_scheduled_ || bg_compaction_scheduled_ ||
bg_flush_scheduled_) {
bg_flush_scheduled_ || bg_pressure_callback_in_progress_) {
bg_cv_.Wait();
}
}
@@ -561,11 +562,18 @@ Status DBImpl::CloseHelper() {
// Wait for background work to finish
while (bg_bottom_compaction_scheduled_ || bg_compaction_scheduled_ ||
bg_flush_scheduled_ || bg_purge_scheduled_ ||
bg_pressure_callback_in_progress_ ||
bg_async_file_open_state_ == AsyncFileOpenState::kScheduled ||
pending_purge_obsolete_files_ ||
error_handler_.IsRecoveryInProgress()) {
TEST_SYNC_POINT("DBImpl::~DBImpl:WaitJob");
bg_cv_.Wait();
}
// Ensure subclasses don't forget to schedule async file opening
assert(!immutable_db_options_.open_files_async || !opened_successfully_ ||
bg_async_file_open_state_ != AsyncFileOpenState::kNotScheduled);
TEST_SYNC_POINT_CALLBACK("DBImpl::CloseHelper:PendingPurgeFinished",
&files_grabbed_for_purge_);
EraseThreadStatusDbInfo();
@@ -607,7 +615,7 @@ Status DBImpl::CloseHelper() {
// manifest file), it is not able to identify live files correctly. As a
// result, all "live" files can get deleted by accident. However, corrupted
// manifest is recoverable by RepairDB().
if (opened_successfully_) {
if (opened_successfully_ && !read_only_) {
JobContext job_context(next_job_id_.fetch_add(1));
FindObsoleteFiles(&job_context, true);
@@ -784,7 +792,76 @@ void DBImpl::PrintStatistics() {
}
}
// Computes the minimum time-based compaction interval for a CF based on
// various options.
// Returns 0 if all time-based compaction options are disabled.
static uint64_t GetMinTimeBasedCompactionInterval(
const ColumnFamilyOptions& cf_opts) {
uint64_t min_interval = UINT64_MAX;
if (cf_opts.periodic_compaction_seconds > 0) {
min_interval = std::min(min_interval, cf_opts.periodic_compaction_seconds);
}
if (cf_opts.ttl > 0) {
min_interval = std::min(min_interval, cf_opts.ttl);
}
const auto& fifo_thresholds =
cf_opts.compaction_options_fifo.file_temperature_age_thresholds;
if (!fifo_thresholds.empty() && fifo_thresholds[0].age > 0) {
// Thresholds are in increasing order by age, so first is smallest
min_interval = std::min(min_interval, fifo_thresholds[0].age);
}
if (cf_opts.bottommost_file_compaction_delay > 0) {
// NOTE: 0 does not exactly mean "disabled" in this case but it does mean
// there's no time component to the relevant compaction picking.
min_interval = std::min(min_interval,
uint64_t{cf_opts.bottommost_file_compaction_delay});
}
// Note: Assume sentinel values like UINT64_MAX - 1 (used by ttl and
// periodic_compaction_seconds) are like disabling, if they reach here
// unsanitized.
return min_interval > UINT64_MAX / 2 ? 0 : min_interval;
}
uint64_t DBImpl::ComputeTriggerCompactionPeriod() {
// Start with a maximum period of every 12 hours.
uint64_t period_sec = 12 * 60 * 60;
// Consider DB-level options that have the DB waking up periodically anyway.
// Waking up to check for compactions at the same interval should be no
// problem, as it should be less overhead than these.
if (mutable_db_options_.stats_dump_period_sec > 0) {
period_sec = std::min(period_sec,
(uint64_t)mutable_db_options_.stats_dump_period_sec);
}
if (mutable_db_options_.stats_persist_period_sec > 0) {
period_sec = std::min(
period_sec, (uint64_t)mutable_db_options_.stats_persist_period_sec);
}
// Consider per-CF settings that can trigger compaction based on time.
uint64_t compaction_trigger_sec = UINT64_MAX;
for (auto cfd : *versions_->GetColumnFamilySet()) {
if (cfd->IsDropped()) {
continue;
}
uint64_t cf_min =
GetMinTimeBasedCompactionInterval(cfd->GetLatestCFOptions());
if (cf_min > 0) {
compaction_trigger_sec = std::min(compaction_trigger_sec, cf_min);
}
}
// We might not align with those timings perfectly, so we tolerate only some
// proportional delay, up to 1/kTriggerDivisor ~= 20%.
constexpr uint64_t kTriggerDivisor = 5;
period_sec = std::min(period_sec, compaction_trigger_sec / kTriggerDivisor);
// But must be > 0
period_sec = std::max(period_sec, uint64_t{1});
return period_sec;
}
Status DBImpl::StartPeriodicTaskScheduler() {
Status s;
#ifndef NDEBUG
// It only used by test to disable scheduler
bool disable_scheduler = false;
@@ -792,7 +869,7 @@ Status DBImpl::StartPeriodicTaskScheduler() {
"DBImpl::StartPeriodicTaskScheduler:DisableScheduler",
&disable_scheduler);
if (disable_scheduler) {
return Status::OK();
return s;
}
{
@@ -803,7 +880,7 @@ Status DBImpl::StartPeriodicTaskScheduler() {
#endif // !NDEBUG
if (mutable_db_options_.stats_dump_period_sec > 0) {
Status s = periodic_task_scheduler_.Register(
s = periodic_task_scheduler_.Register(
PeriodicTaskType::kDumpStats,
periodic_task_functions_.at(PeriodicTaskType::kDumpStats),
mutable_db_options_.stats_dump_period_sec,
@@ -813,7 +890,7 @@ Status DBImpl::StartPeriodicTaskScheduler() {
}
}
if (mutable_db_options_.stats_persist_period_sec > 0) {
Status s = periodic_task_scheduler_.Register(
s = periodic_task_scheduler_.Register(
PeriodicTaskType::kPersistStats,
periodic_task_functions_.at(PeriodicTaskType::kPersistStats),
mutable_db_options_.stats_persist_period_sec,
@@ -823,18 +900,19 @@ Status DBImpl::StartPeriodicTaskScheduler() {
}
}
Status s = periodic_task_scheduler_.Register(
s = periodic_task_scheduler_.Register(
PeriodicTaskType::kFlushInfoLog,
periodic_task_functions_.at(PeriodicTaskType::kFlushInfoLog),
/*run_immediately=*/true);
if (s.ok()) {
s = periodic_task_scheduler_.Register(
PeriodicTaskType::kTriggerCompaction,
periodic_task_functions_.at(PeriodicTaskType::kTriggerCompaction),
/*run_immediately=*/false);
if (!s.ok()) {
return s;
}
s = periodic_task_scheduler_.Register(
PeriodicTaskType::kTriggerCompaction,
periodic_task_functions_.at(PeriodicTaskType::kTriggerCompaction),
ComputeTriggerCompactionPeriod(), /*run_immediately=*/false);
return s;
}
@@ -1471,6 +1549,13 @@ Status DBImpl::SetDBOptions(
table_cache_.get()->SetCapacity(new_options.max_open_files == -1
? TableCache::kInfiniteCapacity
: new_options.max_open_files - 10);
// Potential table cache capacity change requires updating if table
// handles should get pinned.
for (auto cfd : *versions_->GetColumnFamilySet()) {
if (!cfd->IsDropped()) {
cfd->table_cache()->UpdateShouldPinTableHandles();
}
}
wal_other_option_changed = mutable_db_options_.wal_bytes_per_sync !=
new_options.wal_bytes_per_sync;
wal_size_option_changed = mutable_db_options_.max_total_wal_size !=
@@ -6916,8 +7001,17 @@ void DBImpl::TriggerPeriodicCompaction() {
if (cfd->IsDropped()) {
continue;
}
if (cfd->GetLatestCFOptions().periodic_compaction_seconds &&
!cfd->queued_for_compaction()) {
if (cfd->queued_for_compaction()) {
continue;
}
// Check if this CF has any time-based compaction trigger configured.
// This includes periodic_compaction_seconds, ttl, or FIFO temperature
// thresholds. Note: periodic_compaction_seconds may be 0 even when
// ttl or temperature thresholds are set, due to option sanitization.
if (GetMinTimeBasedCompactionInterval(cfd->GetLatestCFOptions()) > 0) {
TEST_SYNC_POINT_CALLBACK(
"DBImpl::TriggerPeriodicCompaction:BeforeComputeCompactionScore",
cfd);
cfd->current()->storage_info()->ComputeCompactionScore(
cfd->ioptions(), cfd->GetLatestMutableCFOptions(),
cfd->GetFullHistoryTsLow());
+45 -5
View File
@@ -1248,6 +1248,7 @@ class DBImpl : public DB {
int TEST_BGCompactionsAllowed() const;
int TEST_BGFlushesAllowed() const;
int TEST_NumRunningBottomCompactions() const;
size_t TEST_GetWalPreallocateBlockSize(uint64_t write_buffer_size) const;
void TEST_WaitForPeriodicTaskRun(std::function<void()> callback) const;
SeqnoToTimeMapping TEST_GetSeqnoToTimeMapping() const;
@@ -1366,6 +1367,7 @@ class DBImpl : public DB {
protected:
const std::string dbname_;
const bool read_only_;
// TODO(peterd): unify with VersionSet::db_id_
std::string db_id_;
// db_session_id_ is an identifier that gets reset
@@ -1718,6 +1720,17 @@ class DBImpl : public DB {
// recovery.
Status LogAndApplyForRecovery(const RecoveryContext& recovery_ctx);
// Schedule background work to open and validate SST files asynchronously.
// Called when open_files_async is enabled.
void ScheduleAsyncFileOpening();
// Mark async file opening as not needed. Used by subclasses that load
// table files through a different mechanism (e.g., ReactiveVersionSet).
void MarkAsyncFileOpenNotNeeded();
// Background work function for async file opening.
static void BGWorkAsyncFileOpen(void* arg);
void InvokeWalFilterIfNeededOnColumnFamilyToWalNumberMap();
// Return true to proceed with current WAL record whose content is stored in
@@ -1728,8 +1741,12 @@ class DBImpl : public DB {
Status& status, bool& stop_replay,
WriteBatch& batch);
// Indicate DB was opened successfully
bool opened_successfully_ = false;
private:
friend class DB;
friend class DBImplSecondary;
friend class ErrorHandler;
friend class InternalStats;
friend class PessimisticTransaction;
@@ -2150,7 +2167,7 @@ class DBImpl : public DB {
Status InsertLogRecordToMemtable(WriteBatch* batch_to_use,
uint64_t wal_number,
SequenceNumber* next_sequence,
bool* has_valid_writes);
bool* has_valid_writes, bool read_only);
Status MaybeWriteLevel0TableForRecovery(
bool has_valid_writes, bool read_only, uint64_t wal_number, int job_id,
@@ -2416,6 +2433,9 @@ class DBImpl : public DB {
void MaybeScheduleFlushOrCompaction();
BackgroundJobPressure CaptureBackgroundJobPressure() const;
void NotifyOnBackgroundJobPressureChanged();
struct FlushRequest {
FlushReason flush_reason;
// A map from column family to flush to largest memtable id to persist for
@@ -2506,6 +2526,13 @@ class DBImpl : public DB {
// Schedule background tasks
Status StartPeriodicTaskScheduler();
// Compute the repeat period for the kTriggerCompaction task, which ensures
// compactions not dependent on writes (flushes) are eventually triggered when
// there are no writes (flushes). NOT thread safe; only called during DB open
// (StartPeriodicTaskScheduler). KNOWN LIMITATION: doesn't get updated with
// dynamic option updates. (Probably not worth the extra complexity.)
uint64_t ComputeTriggerCompactionPeriod();
// Cancel scheduled periodic tasks
Status CancelPeriodicTaskScheduler();
@@ -3051,6 +3078,9 @@ class DBImpl : public DB {
// stores the number of compactions are currently running
int num_running_compactions_ = 0;
// stores the number of BOTTOM-priority compactions currently running
int num_running_bottom_compactions_ = 0;
// number of background memtable flush jobs, submitted to the HIGH pool
int bg_flush_scheduled_ = 0;
@@ -3060,6 +3090,19 @@ class DBImpl : public DB {
// number of background obsolete file purge jobs, submitted to the HIGH pool
int bg_purge_scheduled_ = 0;
// number of pressure callbacks currently in progress (for destructor safety)
int bg_pressure_callback_in_progress_ = 0;
enum class AsyncFileOpenState : uint8_t {
kNotScheduled = 0, // Async file opening has not been scheduled.
kScheduled, // Async file opening is in-flight in the HIGH pool.
kComplete, // Async file opening has finished (or was not needed).
};
// Tracks whether background async file opening has been scheduled/completed.
AsyncFileOpenState bg_async_file_open_state_ =
AsyncFileOpenState::kNotScheduled;
std::deque<ManualCompactionState*> manual_compaction_dequeue_;
// shall we disable deletion of obsolete files
@@ -3115,9 +3158,6 @@ class DBImpl : public DB {
// Guard against multiple concurrent refitting
bool refitting_level_ = false;
// Indicate DB was opened successfully
bool opened_successfully_ = false;
// The min threshold to triggere bottommost compaction for removing
// garbages, among all column families.
SequenceNumber bottommost_files_mark_threshold_ = kMaxSequenceNumber;
@@ -3185,7 +3225,7 @@ class DBImpl : public DB {
// installed to MANIFEST first.
InstrumentedCondVar atomic_flush_install_cv_;
bool wal_in_db_path_;
bool wal_in_db_path_ = false;
std::atomic<uint64_t> max_total_wal_size_;
BlobFileCompletionCallback blob_callback_;
+98 -1
View File
@@ -3021,7 +3021,7 @@ void DBImpl::ResumeAllCompactions() {
void DBImpl::MaybeScheduleFlushOrCompaction() {
mutex_.AssertHeld();
TEST_SYNC_POINT("DBImpl::MaybeScheduleFlushOrCompaction:Start");
if (!opened_successfully_) {
if (!opened_successfully_ || read_only_) {
// Compaction may introduce data race to DB open
return;
}
@@ -3145,6 +3145,93 @@ DBImpl::BGJobLimits DBImpl::GetBGJobLimits(int max_background_flushes,
return res;
}
BackgroundJobPressure DBImpl::CaptureBackgroundJobPressure() const {
mutex_.AssertHeld();
BackgroundJobPressure snapshot;
// Compaction: LOW + BOTTOM combined (matches RocksDB's scheduling check)
snapshot.compaction_scheduled =
bg_compaction_scheduled_ + bg_bottom_compaction_scheduled_;
snapshot.compaction_running = num_running_compactions_;
// Per-priority breakdown for pool-specific expansion
assert(num_running_compactions_ >= num_running_bottom_compactions_);
snapshot.compaction_low_scheduled = bg_compaction_scheduled_;
snapshot.compaction_low_running =
std::max(0, num_running_compactions_ - num_running_bottom_compactions_);
snapshot.compaction_bottom_scheduled = bg_bottom_compaction_scheduled_;
snapshot.compaction_bottom_running = num_running_bottom_compactions_;
// Flush
snapshot.flush_scheduled = bg_flush_scheduled_;
snapshot.flush_running = num_running_flushes_;
snapshot.compaction_speedup_active =
write_controller_.NeedSpeedupCompaction();
// Compute write_stall_proximity_pct: max across all CFs.
// Uses the same inputs as RecalculateWriteStallConditions() in
// column_family.cc:
// - l0_delay_trigger_count(): sorted run count used for write stall
// decisions. Equals NumLevelFiles(0) for level compaction; for universal
// compaction, also counts non-empty levels from L1+ as sorted runs.
// - estimated_compaction_needed_bytes(): pending compaction bytes.
// Prefers slowdown/soft triggers (gradual); falls back to stop/hard triggers.
// level0_slowdown_writes_trigger is sanitized to always be > 0;
// soft_pending_compaction_bytes_limit can be 0 (disabled).
int max_proximity = 0;
for (auto cfd : *versions_->GetColumnFamilySet()) {
if (cfd->IsDropped() || !cfd->initialized()) {
continue;
}
const auto* vstorage = cfd->current()->storage_info();
const auto& mutable_cf_options = cfd->GetCurrentMutableCFOptions();
int l0_proximity = 0;
int l0_trigger = mutable_cf_options.level0_slowdown_writes_trigger > 0
? mutable_cf_options.level0_slowdown_writes_trigger
: mutable_cf_options.level0_stop_writes_trigger;
if (l0_trigger > 0) {
l0_proximity = static_cast<int>(
100.0 * vstorage->l0_delay_trigger_count() / l0_trigger);
}
int bytes_proximity = 0;
uint64_t bytes_limit =
mutable_cf_options.soft_pending_compaction_bytes_limit > 0
? mutable_cf_options.soft_pending_compaction_bytes_limit
: mutable_cf_options.hard_pending_compaction_bytes_limit;
if (bytes_limit > 0) {
bytes_proximity = static_cast<int>(
100.0 *
static_cast<double>(vstorage->estimated_compaction_needed_bytes()) /
static_cast<double>(bytes_limit));
}
max_proximity =
std::max(max_proximity, std::max(l0_proximity, bytes_proximity));
}
snapshot.write_stall_proximity_pct = max_proximity;
return snapshot;
}
void DBImpl::NotifyOnBackgroundJobPressureChanged() {
mutex_.AssertHeld();
if (immutable_db_options_.listeners.empty()) {
return;
}
BackgroundJobPressure pressure = CaptureBackgroundJobPressure();
bg_pressure_callback_in_progress_++;
mutex_.Unlock();
for (const auto& listener : immutable_db_options_.listeners) {
listener->OnBackgroundJobPressureChanged(this, pressure);
}
mutex_.Lock();
bg_pressure_callback_in_progress_--;
}
void DBImpl::AddToCompactionQueue(ColumnFamilyData* cfd) {
assert(!cfd->queued_for_compaction());
cfd->Ref();
@@ -3580,6 +3667,9 @@ void DBImpl::BackgroundCallFlush(Env::Priority thread_pri) {
bg_flush_scheduled_--;
// See if there's more work to be done
MaybeScheduleFlushOrCompaction();
NotifyOnBackgroundJobPressureChanged();
atomic_flush_install_cv_.SignalAll();
bg_cv_.SignalAll();
// IMPORTANT: there should be no code after calling SignalAll. This call may
@@ -3604,6 +3694,9 @@ void DBImpl::BackgroundCallCompaction(PrepickedCompaction* prepicked_compaction,
InstrumentedMutexLock l(&mutex_);
num_running_compactions_++;
if (bg_thread_pri == Env::Priority::BOTTOM) {
num_running_bottom_compactions_++;
}
std::unique_ptr<std::list<uint64_t>::iterator>
pending_outputs_inserted_elem(new std::list<uint64_t>::iterator(
@@ -3686,11 +3779,15 @@ void DBImpl::BackgroundCallCompaction(PrepickedCompaction* prepicked_compaction,
} else {
assert(bg_thread_pri == Env::Priority::BOTTOM);
bg_bottom_compaction_scheduled_--;
assert(num_running_bottom_compactions_ > 0);
num_running_bottom_compactions_--;
}
// See if there's more work to be done
MaybeScheduleFlushOrCompaction();
NotifyOnBackgroundJobPressureChanged();
if (prepicked_compaction != nullptr &&
prepicked_compaction->task_token != nullptr) {
// Releasing task tokens affects (and asserts on) the DB state, so
+5
View File
@@ -282,6 +282,11 @@ int DBImpl::TEST_BGFlushesAllowed() const {
return GetBGJobLimits().max_flushes;
}
int DBImpl::TEST_NumRunningBottomCompactions() const {
mutex_.AssertHeld();
return num_running_bottom_compactions_;
}
SequenceNumber DBImpl::TEST_GetLastVisibleSequence() const {
if (last_seq_same_as_publish_seq_) {
return versions_->LastSequence();
+1 -1
View File
@@ -446,7 +446,7 @@ void DBImpl::PurgeObsoleteFiles(JobContext& state, bool schedule_only) {
state.manifest_delete_files.size());
// We may ignore the dbname when generating the file names.
for (auto& file : state.sst_delete_files) {
auto* handle = file.metadata->table_reader_handle;
auto* handle = file.metadata->fd.pinned_reader.GetCacheHandle();
if (file.only_delete_metadata) {
if (handle) {
// Simply release handle of file that is not being deleted
+1
View File
@@ -321,6 +321,7 @@ Status DB::OpenAsFollower(
sv_context.NewSuperVersion();
cfd->InstallSuperVersion(&sv_context, &impl->mutex_);
}
impl->MarkAsyncFileOpenNotNeeded();
}
impl->mutex_.Unlock();
sv_context.Clean();
+158 -3
View File
@@ -12,6 +12,7 @@
#include "db/db_impl/db_impl.h"
#include "db/error_handler.h"
#include "db/periodic_task_scheduler.h"
#include "db/version_util.h"
#include "env/composite_env_wrapper.h"
#include "file/filename.h"
#include "file/read_write_util.h"
@@ -1492,8 +1493,8 @@ Status DBImpl::ProcessLogRecord(
}
assert(process_status.ok());
process_status = InsertLogRecordToMemtable(batch_to_use, wal_number,
next_sequence, &has_valid_writes);
process_status = InsertLogRecordToMemtable(
batch_to_use, wal_number, next_sequence, &has_valid_writes, read_only);
MaybeIgnoreError(&process_status);
// We are treating this as a failure while reading since we read valid
// blocks that do not form coherent data
@@ -1571,7 +1572,8 @@ void DBImpl::MaybeReviseStopReplayForCorruption(
Status DBImpl::InsertLogRecordToMemtable(WriteBatch* batch_to_use,
uint64_t wal_number,
SequenceNumber* next_sequence,
bool* has_valid_writes) {
bool* has_valid_writes,
bool read_only) {
// If column family was not found, it might mean that the WAL write
// batch references to the column family that was dropped after the
// insert. We don't want to fail the whole write batch in that case --
@@ -1584,6 +1586,34 @@ Status DBImpl::InsertLogRecordToMemtable(WriteBatch* batch_to_use,
&trim_history_scheduler_, true, wal_number, this,
false /* concurrent_memtable_writes */, next_sequence, has_valid_writes,
seq_per_batch_, batch_per_txn_);
// Check WriteBufferManager global limit during recovery.
// When multiple RocksDB instances share a WriteBufferManager, a recovering
// instance could exceed the global memory limit. Schedule flushes when needed
// to prevent OOM during WAL recovery.
//
// Skip scheduling in read-only mode since flushes cannot be performed and
// the scheduler would never be drained, causing assertion failures on
// duplicate ScheduleWork() calls.
//
// TODO: Currently we schedule all CFs with non-empty memtables for flush
// (similar to the atomic_flush=false path in the normal write flow). This
// may produce more, smaller L0 files in some CFs. A future improvement
// could flush only the oldest CF or pick CFs more selectively to reduce
// unnecessary L0 file creation.
if (status.ok() && *has_valid_writes && !read_only &&
immutable_db_options_.enforce_write_buffer_manager_during_recovery &&
write_buffer_manager_ != nullptr &&
write_buffer_manager_->ShouldFlush()) {
for (auto cfd : *versions_->GetColumnFamilySet()) {
if (cfd->mem() != nullptr && cfd->mem()->GetFirstSequenceNumber() != 0 &&
!cfd->mem()->HasFlushScheduled()) {
cfd->mem()->MarkFlushScheduled();
flush_scheduler_.ScheduleWork(cfd);
}
}
}
return status;
}
@@ -2657,6 +2687,10 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname,
impl->DeleteObsoleteFiles();
TEST_SYNC_POINT("DBImpl::Open:AfterDeleteFiles");
impl->MaybeScheduleFlushOrCompaction();
if (impl->immutable_db_options_.open_files_async) {
impl->ScheduleAsyncFileOpening();
}
impl->mutex_.Unlock();
}
@@ -2705,4 +2739,125 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname,
}
return s;
}
struct AsyncFileOpenContext {
DBImpl* db = nullptr;
FileOptions file_options;
std::vector<Version*> versions;
AsyncFileOpenContext() = default;
AsyncFileOpenContext(const AsyncFileOpenContext&) = delete;
AsyncFileOpenContext& operator=(const AsyncFileOpenContext&) = delete;
AsyncFileOpenContext(AsyncFileOpenContext&&) = delete;
AsyncFileOpenContext& operator=(AsyncFileOpenContext&&) = delete;
~AsyncFileOpenContext() {
db->mutex()->AssertHeld();
for (auto* v : versions) {
// must unref version before cfd
ColumnFamilyData* cfd = v->cfd();
v->Unref();
cfd->UnrefAndTryDelete();
}
}
};
void DBImpl::ScheduleAsyncFileOpening() {
mutex_.AssertHeld();
auto* ctx = new AsyncFileOpenContext();
ctx->db = this;
ctx->file_options = versions_->file_options();
for (auto cfd : *versions_->GetColumnFamilySet()) {
assert(!cfd->IsDropped());
Version* current = cfd->current();
VersionStorageInfo* vstorage = current->storage_info();
bool has_files = false;
for (int level = 0; level < vstorage->num_levels() && !has_files; level++) {
has_files = !vstorage->LevelFiles(level).empty();
}
if (has_files) {
cfd->Ref();
current->Ref();
ctx->versions.push_back(current);
}
}
bg_async_file_open_state_ = AsyncFileOpenState::kScheduled;
// since this is a one time job, best to schedule it with high priority
env_->Schedule(&DBImpl::BGWorkAsyncFileOpen, ctx, Env::Priority::HIGH,
nullptr);
}
void DBImpl::MarkAsyncFileOpenNotNeeded() {
mutex_.AssertHeld();
assert(bg_async_file_open_state_ == AsyncFileOpenState::kNotScheduled);
bg_async_file_open_state_ = AsyncFileOpenState::kComplete;
}
void DBImpl::BGWorkAsyncFileOpen(void* arg) {
TEST_SYNC_POINT("DBImpl::BGWorkAsyncFileOpen::Start");
AsyncFileOpenContext* raw_ctx = static_cast<AsyncFileOpenContext*>(arg);
DBImpl* db = raw_ctx->db;
auto deleter = [](AsyncFileOpenContext* p) {
auto* dbPtr = p->db;
InstrumentedMutexLock l(&dbPtr->mutex_);
delete p;
dbPtr->bg_async_file_open_state_ = AsyncFileOpenState::kComplete;
dbPtr->bg_cv_.SignalAll();
};
std::unique_ptr<AsyncFileOpenContext, decltype(deleter)> ctx(raw_ctx,
deleter);
ReadOptions ro;
for (size_t i = 0; i < ctx->versions.size(); i++) {
auto* version = ctx->versions[i];
ColumnFamilyData* cfd = version->cfd();
// Skip column families that were dropped after scheduling
if (cfd->IsDropped()) {
continue;
}
VersionStorageInfo* vstorage = version->storage_info();
MutableCFOptions mutable_cf_options;
{
InstrumentedMutexLock l(&db->mutex_);
mutable_cf_options = cfd->GetLatestMutableCFOptions();
}
size_t max_file_size_for_l0_meta_pin =
MaxFileSizeForL0MetaPin(mutable_cf_options);
std::vector<std::pair<FileMetaData*, int>> files_meta;
for (int level = 0; level < vstorage->num_levels(); level++) {
for (FileMetaData* file_meta : vstorage->LevelFiles(level)) {
files_meta.emplace_back(file_meta, level);
}
}
Status s = LoadTableHandlersHelper(
files_meta, cfd->table_cache(), ctx->file_options,
*vstorage->InternalComparator(), cfd->internal_stats(),
db->immutable_db_options_.max_file_opening_threads,
false /* prefetch_index_and_filter_in_cache */, mutable_cf_options,
max_file_size_for_l0_meta_pin, ro, &db->shutting_down_);
if (!s.ok()) {
ROCKS_LOG_ERROR(
db->immutable_db_options_.info_log,
"BGWorkAsyncFileOpen: LoadTableHandlers failed for CF %s: "
"%s",
cfd->GetName().c_str(), s.ToString().c_str());
InstrumentedMutexLock l(&db->mutex_);
db->error_handler_.SetBGError(s, BackgroundErrorReason::kAsyncFileOpen);
break;
}
}
TEST_SYNC_POINT("DBImpl::BGWorkAsyncFileOpen:Done");
}
} // namespace ROCKSDB_NAMESPACE
+6
View File
@@ -357,6 +357,12 @@ Status DBImplReadOnly::OpenForReadOnlyWithoutCheck(
sv_context.NewSuperVersion();
cfd->InstallSuperVersion(&sv_context, &impl->mutex_);
}
impl->opened_successfully_ = true;
if (db_options.open_files_async) {
impl->ScheduleAsyncFileOpening();
}
}
impl->mutex_.Unlock();
sv_context.Clean();
+36 -19
View File
@@ -43,7 +43,6 @@ Status DBImplSecondary::Recover(
RecoveryContext* /*recovery_ctx*/, bool* /*can_retry*/) {
mutex_.AssertHeld();
JobContext job_context(0);
Status s;
s = static_cast<ReactiveVersionSet*>(versions_.get())
->Recover(column_families, &manifest_reader_, &manifest_reporter_,
@@ -61,24 +60,12 @@ Status DBImplSecondary::Recover(
max_total_in_memory_state_ += mutable_cf_options.write_buffer_size *
mutable_cf_options.max_write_buffer_number;
}
if (s.ok()) {
default_cf_handle_ = new ColumnFamilyHandleImpl(
versions_->GetColumnFamilySet()->GetDefault(), this, &mutex_);
default_cf_internal_stats_ = default_cf_handle_->cfd()->internal_stats();
default_cf_handle_ = new ColumnFamilyHandleImpl(
versions_->GetColumnFamilySet()->GetDefault(), this, &mutex_);
default_cf_internal_stats_ = default_cf_handle_->cfd()->internal_stats();
std::unordered_set<ColumnFamilyData*> cfds_changed;
s = FindAndRecoverLogFiles(&cfds_changed, &job_context);
}
if (s.IsPathNotFound()) {
ROCKS_LOG_INFO(immutable_db_options_.info_log,
"Secondary tries to read WAL, but WAL file(s) have already "
"been purged by primary.");
s = Status::OK();
}
// TODO: update options_file_number_ needed?
job_context.Clean();
return s;
}
@@ -88,6 +75,7 @@ Status DBImplSecondary::FindAndRecoverLogFiles(
JobContext* job_context) {
assert(nullptr != cfds_changed);
assert(nullptr != job_context);
TEST_SYNC_POINT("DBImplSecondary::FindAndRecoverLogFiles:Begin");
Status s;
std::vector<uint64_t> logs;
s = FindNewLogNumbers(&logs);
@@ -740,6 +728,17 @@ Status DB::OpenAsSecondary(
const std::string& secondary_path,
const std::vector<ColumnFamilyDescriptor>& column_families,
std::vector<ColumnFamilyHandle*>* handles, std::unique_ptr<DB>* dbptr) {
return DBImplSecondary::OpenAsSecondaryImpl(
db_options, dbname, secondary_path, column_families, handles, dbptr,
/*recover_wal=*/true);
}
Status DBImplSecondary::OpenAsSecondaryImpl(
const DBOptions& db_options, const std::string& dbname,
const std::string& secondary_path,
const std::vector<ColumnFamilyDescriptor>& column_families,
std::vector<ColumnFamilyHandle*>* handles, std::unique_ptr<DB>* dbptr,
bool recover_wal) {
*dbptr = nullptr;
DBOptions tmp_opts(db_options);
@@ -784,7 +783,21 @@ Status DB::OpenAsSecondary(
impl->wal_in_db_path_ = impl->immutable_db_options_.IsWalDirSameAsDBPath();
impl->mutex_.Lock();
JobContext job_context(0);
s = impl->Recover(column_families, true, false, false);
// WAL recovery is optional: DB::OpenAsSecondary() needs it to replay
// memtable data, while DB::OpenAndCompact() skips it since remote
// compaction only needs LSM state from MANIFEST.
if (s.ok() && recover_wal) {
std::unordered_set<ColumnFamilyData*> cfds_changed;
s = impl->FindAndRecoverLogFiles(&cfds_changed, &job_context);
if (s.IsPathNotFound()) {
ROCKS_LOG_INFO(impl->immutable_db_options_.info_log,
"Secondary tries to read WAL, but WAL file(s) have "
"already been purged by primary.");
s = Status::OK();
}
}
if (s.ok()) {
for (const auto& cf : column_families) {
auto cfd =
@@ -802,9 +815,11 @@ Status DB::OpenAsSecondary(
sv_context.NewSuperVersion();
cfd->InstallSuperVersion(&sv_context, &impl->mutex_);
}
impl->MarkAsyncFileOpenNotNeeded();
}
impl->mutex_.Unlock();
sv_context.Clean();
job_context.Clean();
if (s.ok()) {
dbptr->reset(impl);
for (auto h : *handles) {
@@ -1538,11 +1553,13 @@ Status DB::OpenAndCompact(
}
}
// 5. Open db As Secondary
// 5. Open db As Secondary (skip WAL recovery — remote compaction only
// needs LSM state from MANIFEST, not memtable data from WAL replay)
std::unique_ptr<DB> db;
std::vector<ColumnFamilyHandle*> handles;
s = DB::OpenAsSecondary(db_options, name, output_directory, column_families,
&handles, &db);
s = DBImplSecondary::OpenAsSecondaryImpl(db_options, name, output_directory,
column_families, &handles, &db,
/*recover_wal=*/false);
if (!s.ok()) {
return s;
}
+19 -6
View File
@@ -78,8 +78,9 @@ class DBImplSecondary : public DBImpl {
std::string secondary_path);
~DBImplSecondary() override;
// Recover by replaying MANIFEST and WAL. Also initialize manifest_reader_
// and log_readers_ to facilitate future operations.
// Recover by replaying MANIFEST only. Also initialize manifest_reader_
// to facilitate future operations. WAL recovery, if needed, is done
// separately after opening (see DB::OpenAsSecondary).
Status Recover(const std::vector<ColumnFamilyDescriptor>& column_families,
bool read_only, bool error_if_wal_file_exists,
bool error_if_data_exists_in_wals, bool is_retry = false,
@@ -194,10 +195,11 @@ class DBImplSecondary : public DBImpl {
return Status::NotSupported("Not supported operation in secondary mode.");
}
Status GetLiveFiles(std::vector<std::string>&,
uint64_t* /*manifest_file_size*/,
bool /*flush_memtable*/ = true) override {
return Status::NotSupported("Not supported operation in secondary mode.");
Status GetLiveFiles(std::vector<std::string>& ret,
uint64_t* manifest_file_size,
bool /*flush_memtable*/) override {
return DBImpl::GetLiveFiles(ret, manifest_file_size,
false /* flush_memtable */);
}
using DBImpl::Flush;
@@ -384,6 +386,17 @@ class DBImplSecondary : public DBImpl {
uint64_t CalculateResumedCompactionBytes(
const CompactionProgress& compaction_progress) const;
// Internal helper for opening a secondary instance. Recover() replays
// MANIFEST only. When recover_wal is true, WAL files are also replayed
// (needed by DB::OpenAsSecondary). When false, WAL replay is skipped
// (used by DB::OpenAndCompact which only needs LSM state).
static Status OpenAsSecondaryImpl(
const DBOptions& db_options, const std::string& dbname,
const std::string& secondary_path,
const std::vector<ColumnFamilyDescriptor>& column_families,
std::vector<ColumnFamilyHandle*>* handles, std::unique_ptr<DB>* dbptr,
bool recover_wal);
// Cache log readers for each log number, used for continue WAL replay
// after recovery
std::map<uint64_t, std::unique_ptr<LogReaderContainer>> log_readers_;
+54
View File
@@ -1827,6 +1827,60 @@ TEST_F(DBSecondaryTestWithTimestamp, Iterators) {
Close();
}
TEST_F(DBSecondaryTest, GetLiveFilesOnSecondary) {
Options options;
options.env = env_;
options.level0_file_num_compaction_trigger = 4;
Reopen(options);
// Write some data and flush to create SST files on the primary.
for (int i = 0; i < 3; ++i) {
ASSERT_OK(Put("key" + std::to_string(i), "value" + std::to_string(i)));
ASSERT_OK(Flush());
}
// Open secondary and verify GetLiveFiles works.
Options options1;
options1.env = env_;
options1.max_open_files = -1;
OpenSecondary(options1);
std::vector<std::string> live_files;
uint64_t manifest_size = 0;
ASSERT_OK(db_secondary_->GetLiveFiles(live_files, &manifest_size));
ASSERT_GT(live_files.size(), 0);
ASSERT_GT(manifest_size, 0);
// Should contain SST files, CURRENT, MANIFEST, and OPTIONS.
bool has_sst = false;
bool has_current = false;
bool has_manifest = false;
for (const auto& f : live_files) {
if (f.find(".sst") != std::string::npos) {
has_sst = true;
} else if (f.find("CURRENT") != std::string::npos) {
has_current = true;
} else if (f.find("MANIFEST") != std::string::npos) {
has_manifest = true;
}
}
ASSERT_TRUE(has_sst);
ASSERT_TRUE(has_current);
ASSERT_TRUE(has_manifest);
// Write more data on primary, catch up, and verify the file list updates.
ASSERT_OK(Put("key3", "value3"));
ASSERT_OK(Flush());
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
std::vector<std::string> live_files_after;
uint64_t manifest_size_after = 0;
ASSERT_OK(
db_secondary_->GetLiveFiles(live_files_after, &manifest_size_after));
ASSERT_GT(live_files_after.size(), live_files.size());
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
+1 -1
View File
@@ -1535,7 +1535,7 @@ TEST_F(DBSSTTest, OpenDBWithInfiniteMaxOpenFiles) {
for (const auto& level : files) {
for (const auto& file : level) {
ASSERT_TRUE(file.table_reader_handle != nullptr);
ASSERT_TRUE(file.fd.pinned_reader.Get() != nullptr);
}
}
+593
View File
@@ -37,6 +37,7 @@
#include "db/write_batch_internal.h"
#include "env/mock_env.h"
#include "file/filename.h"
#include "monitoring/file_read_sample.h"
#include "monitoring/thread_status_util.h"
#include "port/port.h"
#include "port/stack_trace.h"
@@ -7762,6 +7763,598 @@ TEST_F(DBTest, ShuttingDownNotBlockStalledWrites) {
thd.join();
}
TEST_F(DBTest, FileReadSampledStats) {
SyncPoint::GetInstance()->SetCallBack(
"should_sample_file_read:override",
[](void* arg) { *static_cast<bool*>(arg) = true; });
SyncPoint::GetInstance()->EnableProcessing();
Options options = CurrentOptions();
options.disable_auto_compactions = true;
DestroyAndReopen(options);
// Write a key and flush to L0, then move to L1 so LevelIterator is used.
ASSERT_OK(Put("key1", "val1"));
ASSERT_OK(Flush());
MoveFilesToLevel(1);
auto files = GetLevelFileMetadatas(1);
ASSERT_EQ(files.size(), 1);
auto* meta = files[0];
// Reset counters (may have been incremented during compaction).
meta->stats.num_reads_sampled.store(0, std::memory_order_relaxed);
// A Get that finds the key should increment num_reads_sampled by
// kFileReadSampleRate (one sampled file access).
ASSERT_EQ("val1", Get("key1"));
ASSERT_EQ(meta->stats.num_reads_sampled.load(std::memory_order_relaxed),
kFileReadSampleRate);
meta->stats.num_reads_sampled.store(0, std::memory_order_relaxed);
// An iterator scan should also increment num_reads_sampled by
// kFileReadSampleRate (one file opened by LevelIterator).
{
std::unique_ptr<Iterator> iter(db_->NewIterator(ReadOptions()));
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
}
ASSERT_OK(iter->status());
}
ASSERT_EQ(meta->stats.num_reads_sampled.load(std::memory_order_relaxed),
kFileReadSampleRate);
// -- Test L0 Get path --
ASSERT_OK(Put("key2", "val2"));
ASSERT_OK(Flush());
auto l0_files = GetLevelFileMetadatas(0);
ASSERT_EQ(l0_files.size(), 1);
auto* l0_meta = l0_files[0];
l0_meta->stats.num_reads_sampled.store(0, std::memory_order_relaxed);
// A Get on an L0 file should increment num_reads_sampled.
ASSERT_EQ("val2", Get("key2"));
ASSERT_EQ(l0_meta->stats.num_reads_sampled.load(std::memory_order_relaxed),
kFileReadSampleRate);
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
}
TEST_F(DBTest, FileCollapsibleEntryReadSampledStats) {
SyncPoint::GetInstance()->SetCallBack(
"should_sample_file_read:override",
[](void* arg) { *static_cast<bool*>(arg) = true; });
SyncPoint::GetInstance()->EnableProcessing();
Options options = CurrentOptions();
options.disable_auto_compactions = true;
options.merge_operator = MergeOperators::CreateStringAppendOperator();
DestroyAndReopen(options);
// -- Setup: create files with collapsible entries --
// File in L1 with a value, a delete tombstone, and a merge operand.
ASSERT_OK(Put("a", "val_a"));
ASSERT_OK(Put("b", "val_b"));
ASSERT_OK(Delete("b"));
ASSERT_OK(db_->Merge(WriteOptions(), "c", "merge_c"));
ASSERT_OK(Put("d", "val_d"));
ASSERT_OK(Flush());
MoveFilesToLevel(1);
auto files = GetLevelFileMetadatas(1);
ASSERT_EQ(files.size(), 1);
auto* meta = files[0];
// -- Test Get path --
// Get for a found key should NOT increment collapsible counter.
meta->stats.num_collapsible_entry_reads_sampled.store(
0, std::memory_order_relaxed);
ASSERT_EQ("val_a", Get("a"));
ASSERT_EQ(meta->stats.num_collapsible_entry_reads_sampled.load(
std::memory_order_relaxed),
0);
// Get for a missing key within the file's key range (kNotFound) should
// increment collapsible counter by kFileReadSampleRate.
ASSERT_EQ("NOT_FOUND", Get("aa"));
ASSERT_EQ(meta->stats.num_collapsible_entry_reads_sampled.load(
std::memory_order_relaxed),
kFileReadSampleRate);
// -- Test Iterator path --
// Iterating should count the delete and merge as collapsible entries.
// The file has one delete ("b") and one merge ("c"), so the counter
// should increment by 2 * kFileReadSampleRate.
meta->stats.num_collapsible_entry_reads_sampled.store(
0, std::memory_order_relaxed);
{
std::unique_ptr<Iterator> iter(db_->NewIterator(ReadOptions()));
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
}
ASSERT_OK(iter->status());
}
ASSERT_EQ(meta->stats.num_collapsible_entry_reads_sampled.load(
std::memory_order_relaxed),
2 * kFileReadSampleRate);
// -- Test L0 Get path --
DestroyAndReopen(options);
ASSERT_OK(Put("a", "val_a"));
ASSERT_OK(Delete("b"));
ASSERT_OK(Put("d", "val_d"));
ASSERT_OK(Flush());
auto l0_files = GetLevelFileMetadatas(0);
ASSERT_EQ(l0_files.size(), 1);
auto* l0_meta = l0_files[0];
// Get for a found key in L0 should NOT increment collapsible counter.
l0_meta->stats.num_collapsible_entry_reads_sampled.store(
0, std::memory_order_relaxed);
ASSERT_EQ("val_a", Get("a"));
ASSERT_EQ(l0_meta->stats.num_collapsible_entry_reads_sampled.load(
std::memory_order_relaxed),
0);
// Get for a missing key in L0 (kNotFound) should increment collapsible
// counter.
ASSERT_EQ("NOT_FOUND", Get("aa"));
ASSERT_EQ(l0_meta->stats.num_collapsible_entry_reads_sampled.load(
std::memory_order_relaxed),
kFileReadSampleRate);
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
}
enum class ReadType : uint8_t { kGet, kMultiGet, kIterator, kNumReadTypes };
class OpenFilesAsyncTest
: public DBTest,
public testing::WithParamInterface<std::tuple<uint32_t, int, bool>> {
public:
OpenFilesAsyncTest() {
std::tie(num_flushes_, max_open_files_, read_only_) = GetParam();
}
std::string BeforeFindTableSyncPoint(ReadType rt) {
switch (rt) {
case ReadType::kGet:
return "TableCache::Get::BeforeFindTable";
case ReadType::kMultiGet:
return "TableCache::MultiGet::BeforeFindTable";
case ReadType::kIterator:
return "TableCache::NewIterator::BeforeFindTable";
default:
assert(false);
return "";
}
}
std::string KeyFor(uint32_t f) {
char buf[32];
snprintf(buf, sizeof(buf), "key%04u", f);
return buf;
}
std::string ValueFor(uint32_t f) {
char buf[32];
snprintf(buf, sizeof(buf), "value%04u", f);
return buf;
}
void SetupData(Options& options, const std::vector<std::string>& cfs = {
kDefaultColumnFamilyName}) {
options.create_if_missing = true;
options.max_open_files = max_open_files_;
// disable auto compactions/flushes
options.disable_auto_compactions = true;
options.info_log_level = FATAL_LEVEL; // reduce LOG size
DestroyAndReopen(options);
for (const auto& cf : cfs) {
if (cf != kDefaultColumnFamilyName) {
CreateColumnFamilies({cf}, options);
}
}
ASSERT_OK(TryReopenWithColumnFamilies(cfs, options));
for (uint32_t f = 0; f < num_flushes_; f++) {
for (int cf = 0; cf < static_cast<int>(cfs.size()); cf++) {
ASSERT_OK(Put(cf, KeyFor(f), ValueFor(f)));
ASSERT_OK(Flush(cf));
}
}
Close();
}
void VerifyData(ReadType rt) {
switch (rt) {
case ReadType::kGet:
for (uint32_t f = 0; f < num_flushes_; f++) {
ASSERT_EQ(Get(KeyFor(f)), ValueFor(f));
}
break;
case ReadType::kMultiGet: {
std::vector<std::string> keys;
std::vector<Slice> key_slices;
keys.reserve(num_flushes_);
key_slices.reserve(num_flushes_);
for (uint32_t f = 0; f < num_flushes_; f++) {
keys.push_back(KeyFor(f));
}
for (auto& k : keys) {
key_slices.emplace_back(k);
}
std::vector<std::string> values(num_flushes_);
std::vector<Status> statuses =
db_->MultiGet(ReadOptions(), key_slices, &values);
for (uint32_t f = 0; f < num_flushes_; f++) {
ASSERT_OK(statuses[f]);
ASSERT_EQ(values[f], ValueFor(f));
}
break;
}
case ReadType::kIterator: {
std::unique_ptr<Iterator> iter(db_->NewIterator(ReadOptions()));
iter->SeekToFirst();
for (uint32_t f = 0; f < num_flushes_; f++) {
ASSERT_TRUE(iter->Valid());
ASSERT_EQ(iter->key().ToString(), KeyFor(f));
ASSERT_EQ(iter->value().ToString(), ValueFor(f));
iter->Next();
}
ASSERT_OK(iter->status());
break;
}
default:
assert(false);
}
}
void OpenTestDB(Options& options) {
options.open_files_async = true;
options.skip_stats_update_on_db_open = true;
options.statistics = CreateDBStatistics();
options.info_log_level = FATAL_LEVEL; // reduce LOG size
std::vector<std::string> cfs;
ASSERT_OK(DB::ListColumnFamilies(options, dbname_, &cfs));
if (read_only_) {
ASSERT_OK(TryReopenReadOnlyWithColumnFamilies(cfs, options));
} else {
ASSERT_OK(TryReopenWithColumnFamilies(cfs, options));
}
}
void VerifyReadError(ReadType rt) {
switch (rt) {
case ReadType::kGet: {
PinnableSlice value;
Status read_s = db_->Get(ReadOptions(), db_->DefaultColumnFamily(),
KeyFor(0), &value);
ASSERT_TRUE(read_s.IsCorruption()) << read_s.ToString();
break;
}
case ReadType::kMultiGet: {
std::vector<std::string> keys = {KeyFor(0)};
std::vector<Slice> key_slices = {keys[0]};
std::vector<std::string> values(1);
std::vector<Status> statuses =
db_->MultiGet(ReadOptions(), key_slices, &values);
ASSERT_TRUE(statuses[0].IsCorruption()) << statuses[0].ToString();
break;
}
case ReadType::kIterator: {
std::unique_ptr<Iterator> iter(db_->NewIterator(ReadOptions()));
iter->SeekToFirst();
ASSERT_TRUE(iter->status().IsCorruption()) << iter->status().ToString();
break;
}
default:
assert(false);
}
}
uint32_t num_flushes_{};
int max_open_files_{};
bool read_only_{};
};
INSTANTIATE_TEST_CASE_P(OpenFilesAsync, OpenFilesAsyncTest,
::testing::Combine(::testing::Values(4),
::testing::Values(-1, 10),
::testing::Bool()));
TEST_F(DBTest, ReadOnlyCloseDoesNotDeleteWriterFiles) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.disable_auto_compactions = true;
DestroyAndReopen(options);
ASSERT_OK(Put("before", "value"));
ASSERT_OK(Flush());
std::unique_ptr<DB> read_only_db;
ASSERT_OK(DB::OpenForReadOnly(options, dbname_, &read_only_db));
ASSERT_OK(Put("after", "value"));
ASSERT_OK(Flush());
std::vector<LiveFileMetaData> live_files;
db_->GetLiveFilesMetaData(&live_files);
ASSERT_GE(live_files.size(), 2);
std::string newest_sst_path;
uint64_t newest_file_number = 0;
for (const auto& live_file : live_files) {
if (live_file.file_number > newest_file_number) {
newest_file_number = live_file.file_number;
newest_sst_path = live_file.directory + "/" + live_file.relative_filename;
}
}
ASSERT_FALSE(newest_sst_path.empty());
ASSERT_OK(env_->FileExists(newest_sst_path));
read_only_db.reset();
ASSERT_OK(env_->FileExists(newest_sst_path));
}
// Test mix of races with async file open, reads, compactions
TEST_P(OpenFilesAsyncTest, ConcurrentFileAccess) {
Options options = CurrentOptions();
ASSERT_NO_FATAL_FAILURE(SetupData(options));
for (uint8_t i = 0; i < static_cast<uint8_t>(ReadType::kNumReadTypes); i++) {
auto rt = static_cast<ReadType>(i);
OpenTestDB(options);
port::Thread reader_thread([&]() { VerifyData(rt); });
port::Thread compaction_thread([&]() {
if (!read_only_) {
db_->CompactRange(CompactRangeOptions(), nullptr, nullptr)
.PermitUncheckedError();
}
});
reader_thread.join();
compaction_thread.join();
if (max_open_files_ == -1) {
EXPECT_EQ(TestGetTickerCount(options, NO_FILE_OPENS), num_flushes_);
} else {
EXPECT_GE(TestGetTickerCount(options, NO_FILE_OPENS), num_flushes_);
}
Close();
}
}
TEST_P(OpenFilesAsyncTest, AfterRead) {
Options options = CurrentOptions();
ASSERT_NO_FATAL_FAILURE(SetupData(options));
for (uint8_t i = 0; i < static_cast<uint8_t>(ReadType::kNumReadTypes); i++) {
auto rt = static_cast<ReadType>(i);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency({
{"OpenFilesAsyncTest::AfterRead",
"VersionBuilder::Rep::LoadTableHandlers::BeforeFindTable"},
{"DBImpl::BGWorkAsyncFileOpen:Done",
"OpenFilesAsyncTest::AfterRead::BeforeClose"},
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
BeforeFindTableSyncPoint(rt), [&](void* arg) {
FileDescriptor* fd = static_cast<FileDescriptor*>(arg);
if (max_open_files_ == -1) {
ASSERT_EQ(fd->pinned_reader.Get(), nullptr);
}
});
std::atomic<uint32_t> file_opens = 0;
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"VersionBuilder::Rep::LoadTableHandlers::BeforeFindTable",
[&](void* arg) {
++file_opens;
FileMetaData* file_meta = static_cast<FileMetaData*>(arg);
if (max_open_files_ == -1) {
ASSERT_NE(file_meta->fd.pinned_reader.Get(), nullptr);
}
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
OpenTestDB(options);
VerifyData(rt);
TEST_SYNC_POINT("OpenFilesAsyncTest::AfterRead");
TEST_SYNC_POINT("OpenFilesAsyncTest::AfterRead::BeforeClose");
if (max_open_files_ == -1) {
EXPECT_EQ(file_opens, num_flushes_);
EXPECT_EQ(TestGetTickerCount(options, NO_FILE_OPENS), num_flushes_);
} else {
EXPECT_GE(TestGetTickerCount(options, NO_FILE_OPENS), num_flushes_);
}
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
Close();
}
}
TEST_P(OpenFilesAsyncTest, BeforeRead) {
Options options = CurrentOptions();
ASSERT_NO_FATAL_FAILURE(SetupData(options));
for (uint8_t i = 0; i < static_cast<uint8_t>(ReadType::kNumReadTypes); i++) {
auto rt = static_cast<ReadType>(i);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency({
{"DBImpl::BGWorkAsyncFileOpen:Done", "OpenFilesAsyncTest::BeforeRead"},
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
BeforeFindTableSyncPoint(rt), [&](void* arg) {
FileDescriptor* fd = static_cast<FileDescriptor*>(arg);
if (options.max_open_files == -1) {
ASSERT_NE(fd->pinned_reader.Get(), nullptr);
}
});
std::atomic<uint32_t> file_opens = 0;
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"VersionBuilder::Rep::LoadTableHandlers::BeforeFindTable",
[&](void* arg) {
++file_opens;
FileMetaData* file_meta = static_cast<FileMetaData*>(arg);
ASSERT_EQ(file_meta->fd.pinned_reader.Get(), nullptr);
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
OpenTestDB(options);
TEST_SYNC_POINT("OpenFilesAsyncTest::BeforeRead");
VerifyData(rt);
if (max_open_files_ == -1) {
EXPECT_EQ(file_opens, num_flushes_);
EXPECT_EQ(TestGetTickerCount(options, NO_FILE_OPENS), num_flushes_);
} else {
EXPECT_GE(TestGetTickerCount(options, NO_FILE_OPENS), num_flushes_);
}
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
Close();
}
}
TEST_P(OpenFilesAsyncTest, Shutdown) {
Options options = CurrentOptions();
ASSERT_NO_FATAL_FAILURE(SetupData(options));
std::atomic<uint32_t> file_opens = 0;
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"VersionBuilder::Rep::LoadTableHandlers::BeforeFindTable",
[&](void* /*arg*/) { ++file_opens; });
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency(
{{"DBImpl::~DBImpl:WaitJob", "DBImpl::BGWorkAsyncFileOpen::Start"},
{"DBImpl::BGWorkAsyncFileOpen:Done", "OpenFilesAsyncTest::Shutdown"}});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
OpenTestDB(options);
Close();
TEST_SYNC_POINT("OpenFilesAsyncTest::Shutdown");
ASSERT_EQ(file_opens, 0);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
}
TEST_P(OpenFilesAsyncTest, Error) {
Options options = CurrentOptions();
ASSERT_NO_FATAL_FAILURE(SetupData(options));
// Get SST file paths and corrupt all of them
std::vector<std::string> sst_files;
{
Reopen(options);
std::vector<LiveFileMetaData> metadata;
db_->GetLiveFilesMetaData(&metadata);
ASSERT_GT(metadata.size(), 0);
for (const auto& m : metadata) {
sst_files.push_back(m.directory + "/" + m.name);
}
Close();
}
for (const auto& f : sst_files) {
ASSERT_OK(test::CorruptFile(env_, f, 0, 100));
}
for (uint8_t i = 0; i < static_cast<uint8_t>(ReadType::kNumReadTypes); i++) {
auto rt = static_cast<ReadType>(i);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency({
{"DBImpl::BGWorkAsyncFileOpen:Done", "OpenFilesAsyncTest::Error"},
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
OpenTestDB(options);
TEST_SYNC_POINT("OpenFilesAsyncTest::Error");
ASSERT_TRUE(dbfull()->TEST_GetBGError().IsCorruption());
VerifyReadError(rt);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
Close();
}
}
TEST_P(OpenFilesAsyncTest, DropColumnFamily) {
// Can't drop CFs in read-only mode
if (read_only_) {
return;
}
for (uint8_t i = 0; i < static_cast<uint8_t>(ReadType::kNumReadTypes); i++) {
auto rt = static_cast<ReadType>(i);
Options options = CurrentOptions();
ASSERT_NO_FATAL_FAILURE(
SetupData(options, {kDefaultColumnFamilyName, "cf1"}));
// Delay the async opener until after we drop the CF
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency({
{"OpenFilesAsyncTest::DropColumnFamily::AfterDrop",
"DBImpl::BGWorkAsyncFileOpen::Start"},
{"DBImpl::BGWorkAsyncFileOpen:Done",
"OpenFilesAsyncTest::DropColumnFamily::BeforeClose"},
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
OpenTestDB(options);
// Drop the non-default CF before async opener runs
ASSERT_OK(db_->DropColumnFamily(handles_[1]));
ASSERT_OK(db_->DestroyColumnFamilyHandle(handles_[1]));
handles_[1] = nullptr;
TEST_SYNC_POINT("OpenFilesAsyncTest::DropColumnFamily::AfterDrop");
// Default CF data should still be readable
VerifyData(rt);
TEST_SYNC_POINT("OpenFilesAsyncTest::DropColumnFamily::BeforeClose");
if (max_open_files_ == -1) {
EXPECT_EQ(TestGetTickerCount(options, NO_FILE_OPENS), num_flushes_);
} else {
EXPECT_GE(TestGetTickerCount(options, NO_FILE_OPENS), num_flushes_);
}
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
Close();
}
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
+38
View File
@@ -94,6 +94,44 @@ TEST_F(DBTest2, OpenForReadOnlyWithColumnFamilies) {
ASSERT_NOK(env_->FileExists(dbname));
}
// Regression test: wal_in_db_path_ was not initialized in the read-only DB
// open path, causing UBSan "invalid-bool-load" when CloseHelper calls
// PurgeObsoleteFiles -> DeleteObsoleteFileImpl which reads wal_in_db_path_.
TEST_F(DBTest2, ReadOnlyDBWalInDbPathInitialized) {
// Create a normal DB with some data and WAL files
Options options = CurrentOptions();
options.create_if_missing = true;
DestroyAndReopen(options);
ASSERT_OK(Put("key1", "value1"));
ASSERT_OK(Flush());
ASSERT_OK(Put("key2", "value2"));
Close();
// Reopen as read-only — wal_in_db_path_ must be properly initialized.
// Before the fix, closing this DB would read an uninitialized bool in
// DeleteObsoleteFileImpl, which UBSan catches as undefined behavior.
std::unique_ptr<DB> db_ptr;
ASSERT_OK(DB::OpenForReadOnly(options, dbname_, &db_ptr));
std::string value;
ASSERT_OK(db_ptr->Get(ReadOptions(), "key1", &value));
ASSERT_EQ("value1", value);
// Close the read-only DB — this triggers PurgeObsoleteFiles which reads
// wal_in_db_path_. Under UBSan, an uninitialized bool here would fail.
db_ptr.reset();
// Also test the column-families variant
std::vector<ColumnFamilyDescriptor> column_families;
column_families.emplace_back(kDefaultColumnFamilyName,
ColumnFamilyOptions(options));
std::vector<ColumnFamilyHandle*> handles;
ASSERT_OK(DB::OpenForReadOnly(DBOptions(options), dbname_, column_families,
&handles, &db_ptr));
for (auto* h : handles) {
delete h;
}
db_ptr.reset();
}
class PartitionedIndexTestListener : public EventListener {
public:
void OnFlushCompleted(DB* /*db*/, const FlushJobInfo& info) override {
+22
View File
@@ -675,6 +675,28 @@ Status DBTestBase::TryReopenWithColumnFamilies(
return TryReopenWithColumnFamilies(cfs, v_opts);
}
Status DBTestBase::TryReopenReadOnlyWithColumnFamilies(
const std::vector<std::string>& cfs, const std::vector<Options>& options) {
Close();
EXPECT_EQ(cfs.size(), options.size());
std::vector<ColumnFamilyDescriptor> column_families;
for (size_t i = 0; i < cfs.size(); ++i) {
column_families.emplace_back(cfs[i], options[i]);
}
DBOptions db_opts = DBOptions(options[0]);
last_options_ = options[0];
MaybeInstallTimeElapseOnlySleep(db_opts);
return DB::OpenForReadOnly(db_opts, dbname_, column_families, &handles_,
&db_);
}
Status DBTestBase::TryReopenReadOnlyWithColumnFamilies(
const std::vector<std::string>& cfs, const Options& options) {
Close();
std::vector<Options> v_opts(cfs.size(), options);
return TryReopenReadOnlyWithColumnFamilies(cfs, v_opts);
}
void DBTestBase::Reopen(const Options& options) {
ASSERT_OK(TryReopen(options));
}
+6
View File
@@ -1177,6 +1177,12 @@ class DBTestBase : public testing::Test {
Status TryReopenWithColumnFamilies(const std::vector<std::string>& cfs,
const Options& options);
Status TryReopenReadOnlyWithColumnFamilies(
const std::vector<std::string>& cfs, const std::vector<Options>& options);
Status TryReopenReadOnlyWithColumnFamilies(
const std::vector<std::string>& cfs, const Options& options);
void Reopen(const Options& options);
void Close();
+2 -2
View File
@@ -600,9 +600,9 @@ TEST_F(DBWALTest, RecoverWithTableHandle) {
for (const auto& level : files) {
for (const auto& file : level) {
if (options.max_open_files == kSmallMaxOpenFiles) {
ASSERT_TRUE(file.table_reader_handle == nullptr);
ASSERT_TRUE(file.fd.pinned_reader.Get() == nullptr);
} else {
ASSERT_TRUE(file.table_reader_handle != nullptr);
ASSERT_TRUE(file.fd.pinned_reader.Get() != nullptr);
}
}
}
+264
View File
@@ -913,6 +913,270 @@ TEST_F(DBWriteBufferManagerTest, RuntimeChangeableAllowStall) {
sleeping_task->WakeUp();
}
// Test that enforce_write_buffer_manager_during_recovery option controls
// whether WriteBufferManager limits are respected during WAL recovery.
// When enabled, flushes are triggered to keep memory bounded.
// When disabled (default), memory can grow beyond the configured limit.
TEST_F(DBWriteBufferManagerTest,
WriteBufferManagerLimitDuringWALRecoverySingleDB) {
const size_t kWbmLimit = 1 * 1024 * 1024; // 1 MB
const size_t kWbmLimitForWrites = 100 * 1024 * 1024; // 100 MB (no flush)
Options options = CurrentOptions();
options.arena_block_size = 4096;
options.write_buffer_size = 10 * 1024 * 1024; // 10MB per CF, never hit
options.max_write_buffer_number = 10; // Allow many memtables
options.disable_auto_compactions = true;
// Use avoid_flush_during_recovery = true to prevent any flushes triggered
// during the recovery
options.avoid_flush_during_recovery = true;
const int kNumKeys = 50;
const int kValueSize = 50 * 1024; // 50 KB each, total ~2.5 MB > 1MB limit
// ========== Part 1: Test with enforcement DISABLED (default behavior) =====
// WBM limits are not enforced during recovery
options.enforce_write_buffer_manager_during_recovery = false;
// Use large WBM limit during writes to avoid triggering flushes
options.write_buffer_manager =
std::make_shared<WriteBufferManager>(kWbmLimitForWrites, nullptr, true);
DestroyAndReopen(options);
for (int i = 0; i < kNumKeys; i++) {
ASSERT_OK(Put(Key(i), DummyString(kValueSize)));
}
// Check to make sure there's no L0 file
ASSERT_EQ(0, TotalTableFiles());
Close();
// Use smaller WBM limit for recovery
options.write_buffer_manager =
std::make_shared<WriteBufferManager>(kWbmLimit, nullptr, true);
// Recovery without enforcement - memory should exceed the limit
Reopen(options);
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
size_t memory_without_enforcement =
options.write_buffer_manager->mutable_memtable_memory_usage();
ASSERT_GT(memory_without_enforcement, kWbmLimit)
<< "Without enforcement, memory (" << memory_without_enforcement
<< ") should exceed the WBM limit (" << kWbmLimit << ")";
// Still no L0 file since avoid_flush_during_recovery is true
ASSERT_EQ(0, TotalTableFiles());
// ========== Part 2: Test with enforcement ENABLED ==========================
options.enforce_write_buffer_manager_during_recovery = true;
// Use large WBM limit during writes to avoid triggering flushes
options.write_buffer_manager =
std::make_shared<WriteBufferManager>(kWbmLimitForWrites, nullptr, true);
DestroyAndReopen(options);
for (int i = 0; i < kNumKeys; i++) {
ASSERT_OK(Put(Key(i), DummyString(kValueSize)));
}
// Check to make sure there's no L0 file
ASSERT_EQ(0, TotalTableFiles());
Close();
// Use smaller WBM limit for recovery
options.write_buffer_manager =
std::make_shared<WriteBufferManager>(kWbmLimit, nullptr, true);
// Recovery with enforcement - memory should be bounded
Reopen(options);
// Wait for flush to finish
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
// WBM's ShouldFlush() compares active memtable mem usage against
// mutable_limit_ which is 7/8 of buffer_size.
size_t expected_num_l0_files =
memory_without_enforcement / (kWbmLimit * 7 / 8) + 1;
ASSERT_EQ(expected_num_l0_files, TotalTableFiles());
size_t memory_with_enforcement =
options.write_buffer_manager->mutable_memtable_memory_usage();
ASSERT_LT(memory_with_enforcement, kWbmLimit)
<< "With enforcement, memory (" << memory_with_enforcement
<< ") should be less than the limit " << kWbmLimit << ")";
}
TEST_F(DBWriteBufferManagerTest,
WriteBufferManagerLimitDuringWALRecoveryMultipleDBs) {
// Two DBs with 4MB WBM limit.
// First DB writes 2.5MB and closes, no flush (mem usage goes back to 0)
// Second DB writes 2.5MB then first DB reopens.
const size_t kWbmLimitForTwoDbs = 4 * 1024 * 1024;
Options options = CurrentOptions();
options.arena_block_size = 2048;
options.write_buffer_size = 10 * 1024 * 1024; // 10MB per CF, never hit
options.max_write_buffer_number = 10; // Allow many memtables
options.disable_auto_compactions = true;
// Use avoid_flush_during_recovery = true to prevent any flushes triggered
// during the recovery
options.avoid_flush_during_recovery = true;
const int kNumKeys = 50;
const int kValueSize = 50 * 1024;
// ========== Part 1: Test with enforcement DISABLED (default behavior) =====
// WBM limits are not enforced during recovery
options.enforce_write_buffer_manager_during_recovery = false;
options.write_buffer_manager =
std::make_shared<WriteBufferManager>(kWbmLimitForTwoDbs, nullptr, true);
DestroyAndReopen(options);
// Use of 2.5MB shouldn't trigger flush
for (int i = 0; i < kNumKeys; i++) {
ASSERT_OK(Put(Key(i), DummyString(kValueSize)));
}
ASSERT_EQ(0, TotalTableFiles());
size_t mem_usage_first_db_only =
options.write_buffer_manager->mutable_memtable_memory_usage();
ASSERT_LT(mem_usage_first_db_only, kWbmLimitForTwoDbs)
<< "Memory (" << mem_usage_first_db_only
<< ") should be less than the limit " << kWbmLimitForTwoDbs << ")";
Close();
ASSERT_EQ(0, options.write_buffer_manager->mutable_memtable_memory_usage());
// Open a second DB sharing the same WBM, write data to consume memory
std::string second_dbname = test::PerThreadDBPath("db_shared_wbm_recovery");
std::unique_ptr<DB> second_db;
ASSERT_OK(DestroyDB(second_dbname, options));
ASSERT_OK(DB::Open(options, second_dbname, &second_db));
WriteOptions wo;
for (int i = 0; i < kNumKeys; i++) {
ASSERT_OK(second_db->Put(wo, Key(i), DummyString(kValueSize)));
}
// First DB reopens without enforcement
Reopen(options);
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
// No flush
ASSERT_EQ(0, TotalTableFiles());
size_t memory_usage_for_both =
options.write_buffer_manager->mutable_memtable_memory_usage();
ASSERT_GT(memory_usage_for_both, kWbmLimitForTwoDbs)
<< "Without enforcement + shared WBM, memory (" << memory_usage_for_both
<< ") should be greater than the limit (" << kWbmLimitForTwoDbs << ")";
// Close the first DB
Close();
// ========== Part 2: Test with enforcement ENABLED =====
// WBM limits enforced during recovery
options.enforce_write_buffer_manager_during_recovery = true;
// Reopen the first DB with enforcement option enabled.
Reopen(options);
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
// With enforcement enabled, there were flushes
ASSERT_GT(TotalTableFiles(), 0);
memory_usage_for_both =
options.write_buffer_manager->mutable_memtable_memory_usage();
ASSERT_LT(memory_usage_for_both, kWbmLimitForTwoDbs)
<< "With enforcement + shared WBM, memory (" << memory_usage_for_both
<< ") should be less than the limit (" << kWbmLimitForTwoDbs << ")";
Close();
// Clean up second DB
ASSERT_OK(second_db->Close());
ASSERT_OK(DestroyDB(second_dbname, options));
second_db.reset();
}
// Regression test: a WriteBatch that exceeds both per-CF memtable limit and
// WBM global limit during WAL recovery should not double-schedule a CF on
// flush_scheduler_ (which crashes debug builds via assert).
TEST_F(DBWriteBufferManagerTest, DoubleSchedulingBugDuringWALRecovery) {
Options options = CurrentOptions();
options.arena_block_size = 4096;
// Small per-CF limit so memtable triggers CheckMemtableFull during recovery
options.write_buffer_size = 64 * 1024; // 64KB
options.max_write_buffer_number = 10;
options.disable_auto_compactions = true;
options.avoid_flush_during_recovery = true;
options.enforce_write_buffer_manager_during_recovery = true;
// WBM limit also small so the WBM loop in InsertLogRecordToMemtable fires
options.write_buffer_manager =
std::make_shared<WriteBufferManager>(128 * 1024, nullptr, true);
DestroyAndReopen(options);
// Write enough data to exceed both limits during recovery replay
for (int i = 0; i < 50; i++) {
ASSERT_OK(Put(Key(i), DummyString(4096))); // ~200KB total > both limits
}
Close();
// Reopen triggers WAL recovery. Without the fix, this crashes in debug
// builds with assert(checking_set_.count(cfd) == 0) in ScheduleWork().
ASSERT_OK(TryReopen(options));
}
// Read-only WAL recovery with enforce_write_buffer_manager_during_recovery
// should not crash due to duplicate ScheduleWork() calls on the
// flush_scheduler_. The flush scheduler was not drained in read-only mode,
// causing the same CFD to be scheduled twice on successive WAL records,
// triggering assert(checking_set_.count(cfd) == 0).
TEST_F(DBWriteBufferManagerTest, ReadOnlyRecoveryWithEnforceWBMDoesNotAssert) {
Options options = CurrentOptions();
options.arena_block_size = 4096;
options.write_buffer_size = 10 * 1024 * 1024; // 10MB, never hit
options.max_write_buffer_number = 10;
options.disable_auto_compactions = true;
options.avoid_flush_during_recovery = true;
options.enforce_write_buffer_manager_during_recovery = true;
const size_t kWbmLimitForWrites = 100 * 1024 * 1024; // 100MB (no flush)
options.write_buffer_manager =
std::make_shared<WriteBufferManager>(kWbmLimitForWrites, nullptr, true);
DestroyAndReopen(options);
// Write enough data so that WAL recovery will trigger
// WriteBufferManager::ShouldFlush() multiple times with a small WBM limit.
for (int i = 0; i < 50; i++) {
ASSERT_OK(Put(Key(i), DummyString(50 * 1024))); // ~2.5MB total
}
ASSERT_EQ(0, TotalTableFiles());
Close();
// Reopen read-only with a small WBM limit that will trigger ShouldFlush()
// during WAL recovery. Without the fix, this crashes in debug builds with
// assert(checking_set_.count(cfd) == 0) in FlushScheduler::ScheduleWork().
const size_t kWbmLimit = 1 * 1024 * 1024; // 1MB
options.write_buffer_manager =
std::make_shared<WriteBufferManager>(kWbmLimit, nullptr, true);
ASSERT_OK(ReadOnlyReopen(options));
// Verify data is readable
for (int i = 0; i < 50; i++) {
ASSERT_EQ(DummyString(50 * 1024), Get(Key(i)));
}
}
INSTANTIATE_TEST_CASE_P(DBWriteBufferManagerTest, DBWriteBufferManagerTest,
testing::Bool());
+24
View File
@@ -201,6 +201,25 @@ std::map<std::tuple<BackgroundErrorReason, Status::Code, bool>,
{std::make_tuple(BackgroundErrorReason::kManifestWriteNoWAL,
Status::Code::kIOError, false),
Status::Severity::kFatalError},
// Errors during BG async file open
{std::make_tuple(BackgroundErrorReason::kAsyncFileOpen,
Status::Code::kCorruption, true),
Status::Severity::kUnrecoverableError},
{std::make_tuple(BackgroundErrorReason::kAsyncFileOpen,
Status::Code::kCorruption, false),
Status::Severity::kNoError},
{std::make_tuple(BackgroundErrorReason::kAsyncFileOpen,
Status::Code::kIncomplete, true),
Status::Severity::kFatalError},
{std::make_tuple(BackgroundErrorReason::kAsyncFileOpen,
Status::Code::kIncomplete, false),
Status::Severity::kNoError},
{std::make_tuple(BackgroundErrorReason::kAsyncFileOpen,
Status::Code::kNotSupported, true),
Status::Severity::kFatalError},
{std::make_tuple(BackgroundErrorReason::kAsyncFileOpen,
Status::Code::kNotSupported, false),
Status::Severity::kNoError},
};
std::map<std::tuple<BackgroundErrorReason, bool>, Status::Severity>
@@ -225,6 +244,11 @@ std::map<std::tuple<BackgroundErrorReason, bool>, Status::Severity>
Status::Severity::kFatalError},
{std::make_tuple(BackgroundErrorReason::kMemTable, false),
Status::Severity::kFatalError},
// Errors during BG async file open
{std::make_tuple(BackgroundErrorReason::kAsyncFileOpen, true),
Status::Severity::kSoftError},
{std::make_tuple(BackgroundErrorReason::kAsyncFileOpen, false),
Status::Severity::kNoError},
};
void ErrorHandler::CancelErrorRecoveryForShutDown() {
+9 -2
View File
@@ -616,10 +616,17 @@ Status FlushJob::MemPurge() {
!(new_mem->ShouldFlushNow())) {
// Construct fragmented memtable range tombstones without mutex
new_mem->ConstructFragmentedRangeTombstones();
TEST_SYNC_POINT("FlushJob::MemPurge:BeforeReacquireMutex");
TEST_SYNC_POINT("FlushJob::MemPurge:AfterWaitForTest");
db_mutex_->Lock();
// Take the newest id, so that memtables in MemtableList don't have
// out-of-order memtable ids.
uint64_t new_mem_id = mems_.back()->GetID();
// out-of-order memtable ids. While the db mutex was released during
// MemPurge, new memtables may have been switched to the immutable
// list with higher IDs, so we must use the maximum of the original
// flush batch ID and the current latest immutable memtable ID.
uint64_t new_mem_id = std::max(
mems_.back()->GetID(),
cfd_->imm()->GetLatestMemTableID(false /*for_atomic_flush*/));
new_mem->SetID(new_mem_id);
// Take the latest memtable's next log number.
+123
View File
@@ -1643,6 +1643,129 @@ TEST_F(EventListenerTest, BlobDBFileTest) {
blob_event_listener->CheckCounters();
}
class BackgroundJobPressureTestListener : public EventListener {
public:
void OnBackgroundJobPressureChanged(
DB* /*db*/, const BackgroundJobPressure& snapshot) override {
std::lock_guard<std::mutex> lock(mu_);
snapshots_.push_back(snapshot);
}
std::vector<BackgroundJobPressure> GetSnapshots() {
std::lock_guard<std::mutex> lock(mu_);
return snapshots_;
}
void Reset() {
std::lock_guard<std::mutex> lock(mu_);
snapshots_.clear();
}
private:
std::mutex mu_;
std::vector<BackgroundJobPressure> snapshots_;
};
TEST_F(EventListenerTest, BackgroundJobPressure) {
auto listener = std::make_shared<BackgroundJobPressureTestListener>();
Options options = CurrentOptions();
options.create_if_missing = true;
options.max_background_jobs = 8;
options.disable_auto_compactions = false;
options.level0_slowdown_writes_trigger = 4;
options.level0_stop_writes_trigger = 100;
options.level0_file_num_compaction_trigger = 4;
options.listeners.push_back(listener);
DestroyAndReopen(options);
// Structural invariant checked on every snapshot.
auto CheckInvariants = [](const std::vector<BackgroundJobPressure>& snaps) {
ASSERT_GT(snaps.size(), 0u);
for (const auto& s : snaps) {
ASSERT_EQ(s.compaction_scheduled,
s.compaction_low_scheduled + s.compaction_bottom_scheduled);
ASSERT_EQ(s.compaction_running,
s.compaction_low_running + s.compaction_bottom_running);
}
};
// Block compaction so L0 files accumulate.
env_->SetBackgroundThreads(1, Env::Priority::LOW);
test::SleepingBackgroundTask sleeping_task;
env_->Schedule(&test::SleepingBackgroundTask::DoSleepTask, &sleeping_task,
Env::Priority::LOW);
sleeping_task.WaitUntilSleeping();
// Phase 1: No pressure — 3 SST files (below slowdown trigger=4).
for (int i = 0; i < 3; i++) {
ASSERT_OK(Put("k" + std::to_string(i), std::string(100, 'x')));
ASSERT_OK(Flush());
}
auto snapshots = listener->GetSnapshots();
CheckInvariants(snapshots);
for (const auto& s : snapshots) {
ASSERT_EQ(s.compaction_running, 0);
ASSERT_EQ(s.compaction_scheduled, 0);
ASSERT_FALSE(s.compaction_speedup_active);
ASSERT_LT(s.write_stall_proximity_pct, 100);
}
// Note: we don't assert flush_running/flush_scheduled == 0 here because
// MaybeScheduleFlushOrCompaction() runs before the pressure callback and
// may schedule new flush work, making flush counts non-deterministic.
// Phase 2: Build pressure — flush past slowdown trigger (4 L0 SST files).
// Compaction is blocked, so L0 SST files pile up.
listener->Reset();
{
FlushOptions fo;
fo.allow_write_stall = true;
for (int i = 3; i < 10; i++) {
ASSERT_OK(Put("k" + std::to_string(i), std::string(100, 'x')));
ASSERT_OK(db_->Flush(fo));
}
}
snapshots = listener->GetSnapshots();
CheckInvariants(snapshots);
for (const auto& s : snapshots) {
ASSERT_EQ(s.compaction_running, 0);
}
// Scan history: pressure indicators appear as L0 SST files accumulate
bool found_speedup = false;
bool found_compaction_scheduled = false;
bool found_high_proximity = false;
for (const auto& s : snapshots) {
if (s.compaction_speedup_active) {
found_speedup = true;
}
if (s.compaction_scheduled > 0) {
found_compaction_scheduled = true;
}
if (s.write_stall_proximity_pct >= 100) {
found_high_proximity = true;
}
}
ASSERT_TRUE(found_speedup);
ASSERT_TRUE(found_compaction_scheduled);
ASSERT_TRUE(found_high_proximity);
// Phase 3: Relieve pressure — unblock compaction, wait for completion.
listener->Reset();
sleeping_task.WakeUp();
sleeping_task.WaitUntilDone();
ASSERT_OK(dbfull()->TEST_WaitForCompact());
snapshots = listener->GetSnapshots();
CheckInvariants(snapshots);
// Latest: all compactions finished, healthy
const auto& latest3 = snapshots.back();
ASSERT_EQ(latest3.compaction_running, 0);
ASSERT_EQ(latest3.compaction_scheduled, 0);
ASSERT_FALSE(latest3.compaction_speedup_active);
ASSERT_LT(latest3.write_stall_proximity_pct, 100);
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
+197 -51
View File
@@ -72,7 +72,9 @@ ImmutableMemTableOptions::ImmutableMemTableOptions(
allow_data_in_errors(ioptions.allow_data_in_errors),
paranoid_memory_checks(mutable_cf_options.paranoid_memory_checks),
memtable_veirfy_per_key_checksum_on_seek(
mutable_cf_options.memtable_veirfy_per_key_checksum_on_seek) {}
mutable_cf_options.memtable_veirfy_per_key_checksum_on_seek),
memtable_batch_lookup_optimization(
ioptions.memtable_batch_lookup_optimization) {}
MemTable::MemTable(const InternalKeyComparator& cmp,
const ImmutableOptions& ioptions,
@@ -1575,65 +1577,179 @@ void MemTable::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
}
}
}
for (auto iter = temp_range.begin(); iter != temp_range.end(); ++iter) {
bool found_final_value{false};
bool merge_in_progress = iter->s->IsMergeInProgress();
if (!no_range_del) {
std::unique_ptr<FragmentedRangeTombstoneIterator> range_del_iter(
NewRangeTombstoneIteratorInternal(
read_options, GetInternalKeySeqno(iter->lkey->internal_key()),
immutable_memtable));
SequenceNumber covering_seq =
range_del_iter->MaxCoveringTombstoneSeqnum(iter->lkey->user_key());
if (covering_seq > iter->max_covering_tombstone_seq) {
iter->max_covering_tombstone_seq = covering_seq;
if (iter->timestamp) {
// Will be overwritten in SaveValue() if there is a point key with
// a higher seqno.
iter->timestamp->assign(range_del_iter->timestamp().data(),
range_del_iter->timestamp().size());
// Use batch lookup optimization when enabled
bool use_batch_optimization = moptions_.memtable_batch_lookup_optimization;
bool validate = moptions_.paranoid_memory_checks ||
moptions_.memtable_veirfy_per_key_checksum_on_seek;
if (use_batch_optimization) {
// Phase 1: Handle range tombstones and set up Savers for batched lookup
std::array<Saver, MultiGetContext::MAX_BATCH_SIZE> savers{};
std::array<const char*, MultiGetContext::MAX_BATCH_SIZE> memtable_keys{};
std::array<void*, MultiGetContext::MAX_BATCH_SIZE> callback_args{};
std::array<bool, MultiGetContext::MAX_BATCH_SIZE> found_final_values{};
std::array<bool, MultiGetContext::MAX_BATCH_SIZE> merge_in_progresses{};
size_t num_keys = 0;
for (auto iter = temp_range.begin(); iter != temp_range.end(); ++iter) {
if (!no_range_del) {
std::unique_ptr<FragmentedRangeTombstoneIterator> range_del_iter(
NewRangeTombstoneIteratorInternal(
read_options, GetInternalKeySeqno(iter->lkey->internal_key()),
immutable_memtable));
SequenceNumber covering_seq =
range_del_iter->MaxCoveringTombstoneSeqnum(iter->lkey->user_key());
if (covering_seq > iter->max_covering_tombstone_seq) {
iter->max_covering_tombstone_seq = covering_seq;
if (iter->timestamp) {
iter->timestamp->assign(range_del_iter->timestamp().data(),
range_del_iter->timestamp().size());
}
}
}
}
SequenceNumber dummy_seq;
GetFromTable(*(iter->lkey), iter->max_covering_tombstone_seq, true,
callback, &iter->is_blob_index,
iter->value ? iter->value->GetSelf() : nullptr, iter->columns,
iter->timestamp, iter->s, &(iter->merge_context), &dummy_seq,
&found_final_value, &merge_in_progress);
if (!found_final_value && merge_in_progress) {
if (iter->s->ok()) {
*(iter->s) = Status::MergeInProgress();
} else {
assert(iter->s->IsMergeInProgress());
found_final_values[num_keys] = false;
merge_in_progresses[num_keys] = iter->s->IsMergeInProgress();
Saver& saver = savers[num_keys];
saver.status = iter->s;
saver.found_final_value = &found_final_values[num_keys];
saver.merge_in_progress = &merge_in_progresses[num_keys];
saver.key = iter->lkey;
saver.value = iter->value ? iter->value->GetSelf() : nullptr;
saver.columns = iter->columns;
saver.timestamp = iter->timestamp;
saver.seq = kMaxSequenceNumber;
saver.mem = this;
saver.merge_context = &(iter->merge_context);
saver.max_covering_tombstone_seq = iter->max_covering_tombstone_seq;
saver.merge_operator = moptions_.merge_operator;
saver.logger = moptions_.info_log;
saver.inplace_update_support = moptions_.inplace_update_support;
saver.statistics = moptions_.statistics;
saver.clock = clock_;
saver.callback_ = callback;
saver.is_blob_index = &iter->is_blob_index;
saver.do_merge = true;
saver.allow_data_in_errors = moptions_.allow_data_in_errors;
saver.protection_bytes_per_key = moptions_.protection_bytes_per_key;
memtable_keys[num_keys] = iter->lkey->memtable_key().data();
callback_args[num_keys] = &savers[num_keys];
num_keys++;
}
// Phase 2: Batched lookup
if (num_keys > 0) {
Status check_s =
table_->MultiGet(num_keys, memtable_keys.data(), callback_args.data(),
SaveValue, moptions_.allow_data_in_errors,
validate ? moptions_.paranoid_memory_checks : false,
validate ? key_validation_callback_ : nullptr);
if (check_s.IsCorruption()) {
// Mark all remaining keys as corruption
for (auto iter = temp_range.begin(); iter != temp_range.end(); ++iter) {
*(iter->s) = check_s;
range->MarkKeyDone(iter);
}
return;
}
}
if (found_final_value ||
(!iter->s->ok() && !iter->s->IsMergeInProgress())) {
// `found_final_value` should be set if an error/corruption occurs.
// The check on iter->s is just there in case GetFromTable() did not
// set `found_final_value` properly.
assert(found_final_value);
if (iter->value) {
iter->value->PinSelf();
range->AddValueSize(iter->value->size());
} else {
assert(iter->columns);
range->AddValueSize(iter->columns->serialized_size());
// Phase 3: Process results
size_t result_idx = 0;
for (auto iter = temp_range.begin(); iter != temp_range.end();
++iter, ++result_idx) {
bool found_final_value = found_final_values[result_idx];
bool merge_in_progress = merge_in_progresses[result_idx];
if (!found_final_value && merge_in_progress) {
if (iter->s->ok()) {
*(iter->s) = Status::MergeInProgress();
} else {
assert(iter->s->IsMergeInProgress());
}
}
range->MarkKeyDone(iter);
RecordTick(moptions_.statistics, MEMTABLE_HIT);
if (range->GetValueSize() > read_options.value_size_soft_limit) {
// Set all remaining keys in range to Abort
for (auto range_iter = range->begin(); range_iter != range->end();
++range_iter) {
range->MarkKeyDone(range_iter);
*(range_iter->s) = Status::Aborted();
if (found_final_value ||
(!iter->s->ok() && !iter->s->IsMergeInProgress())) {
assert(found_final_value);
if (iter->value) {
iter->value->PinSelf();
range->AddValueSize(iter->value->size());
} else {
assert(iter->columns);
range->AddValueSize(iter->columns->serialized_size());
}
range->MarkKeyDone(iter);
RecordTick(moptions_.statistics, MEMTABLE_HIT);
if (range->GetValueSize() > read_options.value_size_soft_limit) {
for (auto range_iter = range->begin(); range_iter != range->end();
++range_iter) {
range->MarkKeyDone(range_iter);
*(range_iter->s) = Status::Aborted();
}
break;
}
}
}
} else {
// Per-key lookup path
for (auto iter = temp_range.begin(); iter != temp_range.end(); ++iter) {
bool found_final_value{false};
bool merge_in_progress = iter->s->IsMergeInProgress();
if (!no_range_del) {
std::unique_ptr<FragmentedRangeTombstoneIterator> range_del_iter(
NewRangeTombstoneIteratorInternal(
read_options, GetInternalKeySeqno(iter->lkey->internal_key()),
immutable_memtable));
SequenceNumber covering_seq =
range_del_iter->MaxCoveringTombstoneSeqnum(iter->lkey->user_key());
if (covering_seq > iter->max_covering_tombstone_seq) {
iter->max_covering_tombstone_seq = covering_seq;
if (iter->timestamp) {
iter->timestamp->assign(range_del_iter->timestamp().data(),
range_del_iter->timestamp().size());
}
}
}
SequenceNumber dummy_seq;
GetFromTable(
*(iter->lkey), iter->max_covering_tombstone_seq, true, callback,
&iter->is_blob_index, iter->value ? iter->value->GetSelf() : nullptr,
iter->columns, iter->timestamp, iter->s, &(iter->merge_context),
&dummy_seq, &found_final_value, &merge_in_progress);
if (!found_final_value && merge_in_progress) {
if (iter->s->ok()) {
*(iter->s) = Status::MergeInProgress();
} else {
assert(iter->s->IsMergeInProgress());
}
}
if (found_final_value ||
(!iter->s->ok() && !iter->s->IsMergeInProgress())) {
assert(found_final_value);
if (iter->value) {
iter->value->PinSelf();
range->AddValueSize(iter->value->size());
} else {
assert(iter->columns);
range->AddValueSize(iter->columns->serialized_size());
}
range->MarkKeyDone(iter);
RecordTick(moptions_.statistics, MEMTABLE_HIT);
if (range->GetValueSize() > read_options.value_size_soft_limit) {
for (auto range_iter = range->begin(); range_iter != range->end();
++range_iter) {
range->MarkKeyDone(range_iter);
*(range_iter->s) = Status::Aborted();
}
break;
}
break;
}
}
}
@@ -1841,6 +1957,36 @@ void MemTableRep::Get(const LookupKey& k, void* callback_args,
}
}
Status MemTableRep::MultiGet(
size_t num_keys, const char* const* keys, void** callback_args,
bool (*callback_func)(void* arg, const char* entry),
bool allow_data_in_errors, bool detect_key_out_of_order,
const std::function<Status(const char*, bool)>& key_validation_callback) {
bool validate = detect_key_out_of_order || key_validation_callback != nullptr;
std::unique_ptr<Iterator> iter(GetIterator());
for (size_t i = 0; i < num_keys; ++i) {
Slice dummy;
if (validate) {
Status s = iter->SeekAndValidate(dummy, keys[i], allow_data_in_errors,
detect_key_out_of_order,
key_validation_callback);
for (; iter->Valid() && s.ok() &&
callback_func(callback_args[i], iter->key());
s = iter->NextAndValidate(allow_data_in_errors)) {
}
if (!s.ok()) {
return s;
}
} else {
iter->Seek(dummy, keys[i]);
for (; iter->Valid() && callback_func(callback_args[i], iter->key());
iter->Next()) {
}
}
}
return Status::OK();
}
void MemTable::RefLogContainingPrepSection(uint64_t log) {
assert(log > 0);
auto cur = min_prep_log_referenced_.load();
+6
View File
@@ -65,6 +65,7 @@ struct ImmutableMemTableOptions {
bool allow_data_in_errors;
bool paranoid_memory_checks;
bool memtable_veirfy_per_key_checksum_on_seek;
bool memtable_batch_lookup_optimization;
};
// Batched counters to updated when inserting keys in one write batch.
@@ -599,6 +600,11 @@ class MemTable final : public ReadOnlyMemTable {
std::memory_order_relaxed);
}
// Returns true if a flush has already been scheduled for this memtable
bool HasFlushScheduled() const {
return flush_state_.load(std::memory_order_relaxed) == FLUSH_SCHEDULED;
}
InternalIterator* NewIterator(
const ReadOptions& read_options,
UnownedPtr<const SeqnoToTimeMapping> seqno_to_time_mapping, Arena* arena,
+10 -1
View File
@@ -7,6 +7,7 @@
#include "db/periodic_task_scheduler.h"
#include "rocksdb/system_clock.h"
#include "test_util/sync_point.h"
namespace ROCKSDB_NAMESPACE {
@@ -26,7 +27,7 @@ static const std::map<PeriodicTaskType, uint64_t> kDefaultPeriodSeconds = {
{PeriodicTaskType::kPersistStats, kInvalidPeriodSec},
{PeriodicTaskType::kFlushInfoLog, 10},
{PeriodicTaskType::kRecordSeqnoTime, kInvalidPeriodSec},
{PeriodicTaskType::kTriggerCompaction, 12 * 60 * 60} // 12 hours
{PeriodicTaskType::kTriggerCompaction, kInvalidPeriodSec},
};
static const std::map<PeriodicTaskType, std::string> kPeriodicTaskTypeNames = {
@@ -85,6 +86,14 @@ Status PeriodicTaskScheduler::Register(PeriodicTaskType task_type,
if (!result.second) {
return Status::Aborted("Failed to add periodic task");
}
#ifndef NDEBUG
{
std::pair<PeriodicTaskType, uint64_t> task_info{task_type,
repeat_period_seconds};
TEST_SYNC_POINT_CALLBACK("PeriodicTaskScheduler::Register:TaskRegistered",
&task_info);
}
#endif // NDEBUG
return Status::OK();
}
+286 -9
View File
@@ -6,6 +6,9 @@
#include "db/periodic_task_scheduler.h"
#include <set>
#include "db/column_family.h"
#include "db/db_test_util.h"
#include "env/composite_env_wrapper.h"
#include "test_util/mock_time_env.h"
@@ -124,15 +127,289 @@ TEST_F(PeriodicTaskSchedulerTest, Basic) {
ASSERT_EQ(3, pst_st_counter);
ASSERT_EQ(5, flush_info_log_counter);
ASSERT_EQ(0, trigger_compaction_counter);
dbfull()->TEST_WaitForPeriodicTaskRun([&] {
mock_clock_->MockSleepForSeconds(static_cast<int>(12 * 60 * 60));
});
ASSERT_EQ(1, trigger_compaction_counter);
dbfull()->TEST_WaitForPeriodicTaskRun([&] {
mock_clock_->MockSleepForSeconds(static_cast<int>(12 * 60 * 60));
});
ASSERT_EQ(2, trigger_compaction_counter);
// With the new ComputeTriggerCompactionPeriod() logic, since
// stats_dump_period_sec = stats_persist_period_sec = kPeriodSec, the trigger
// compaction period is also kPeriodSec. Unlike other periodic tasks,
// kTriggerCompaction uses run_immediately=false, so it doesn't fire on
// initial registration. It fires after each period elapses.
// By now we've had: initial 9s + 10s + 10s + 10s + 10s = 49s elapsed.
// kTriggerCompaction fires at t=10, 20, 30, 40 = 4 times.
ASSERT_EQ(4, trigger_compaction_counter);
// Sleep one more period and verify it increments
dbfull()->TEST_WaitForPeriodicTaskRun(
[&] { mock_clock_->MockSleepForSeconds(static_cast<int>(kPeriodSec)); });
ASSERT_EQ(5, trigger_compaction_counter);
Close();
}
TEST_F(PeriodicTaskSchedulerTest, TriggerCompactionPeriodComputation) {
// Test that ComputeTriggerCompactionPeriod() returns the expected values
// for different configurations.
Close();
// Helper to capture the trigger compaction period on Reopen
uint64_t trigger_compaction_period = 0;
SyncPoint::GetInstance()->SetCallBack(
"PeriodicTaskScheduler::Register:TaskRegistered", [&](void* arg) {
auto* task_info =
static_cast<std::pair<PeriodicTaskType, uint64_t>*>(arg);
if (task_info->first == PeriodicTaskType::kTriggerCompaction) {
trigger_compaction_period = task_info->second;
}
});
SyncPoint::GetInstance()->EnableProcessing();
// Helper to test the trigger compaction period with optional second CF
auto test_period = [&](Options& options, uint64_t expected_period,
const ColumnFamilyOptions* cf2_options = nullptr) {
options.create_if_missing = true;
options.env = mock_env_.get();
trigger_compaction_period = 0;
if (cf2_options == nullptr) {
Reopen(options);
} else {
// Open with two column families
// First destroy DB to start fresh
ASSERT_OK(DestroyDB(dbname_, options));
// Open and create cf2
ASSERT_OK(TryReopen(options));
CreateColumnFamilies({"cf2"}, options);
Close();
// Now reopen with both CFs
std::vector<ColumnFamilyDescriptor> cf_descs;
cf_descs.emplace_back(kDefaultColumnFamilyName, options);
cf_descs.emplace_back("cf2", *cf2_options);
std::vector<ColumnFamilyHandle*> handles;
ASSERT_OK(DB::Open(options, dbname_, cf_descs, &handles, &db_));
for (auto* h : handles) {
delete h;
}
}
ASSERT_EQ(expected_period, trigger_compaction_period);
Close();
// Destroy DB to clean up for next test case
ASSERT_OK(DestroyDB(dbname_, options));
};
// Case 1: 12-hour cap when stats_dump_period is longer
{
Options options;
options.stats_dump_period_sec = 24 * 60 * 60; // 24 hours
options.stats_persist_period_sec = 0;
test_period(options, 12 * 60 * 60); // Expect 12 hours (cap)
}
// Case 2: stats_dump_period_sec sets the period (when smaller than 12 hours)
{
Options options;
options.stats_dump_period_sec = 3600; // 1 hour
options.stats_persist_period_sec = 0;
test_period(options, 3600);
}
// Case 3: stats_persist_period_sec sets the period
{
Options options;
options.stats_dump_period_sec = 0;
options.stats_persist_period_sec = 1800; // 30 minutes
test_period(options, 1800);
}
// Case 4: periodic_compaction_seconds / kTriggerDivisor
{
Options options;
options.stats_dump_period_sec = 24 * 60 * 60;
options.stats_persist_period_sec = 0;
options.periodic_compaction_seconds = 500; // 500 / 5 = 100
test_period(options, 100);
}
// Case 5: ttl / kTriggerDivisor
{
Options options;
options.stats_dump_period_sec = 24 * 60 * 60;
options.stats_persist_period_sec = 0;
options.ttl = 600; // 600 / 5 = 120
test_period(options, 120);
}
// Case 6: bottommost_file_compaction_delay / kTriggerDivisor
{
Options options;
options.stats_dump_period_sec = 24 * 60 * 60;
options.stats_persist_period_sec = 0;
options.bottommost_file_compaction_delay = 250; // 250 / 5 = 50
test_period(options, 50);
}
// Case 7: file_temperature_age_thresholds / kTriggerDivisor (FIFO)
{
Options options;
options.stats_dump_period_sec = 24 * 60 * 60;
options.stats_persist_period_sec = 0;
options.compaction_style = kCompactionStyleFIFO;
options.num_levels = 1; // Required for file_temperature_age_thresholds
options.compaction_options_fifo.file_temperature_age_thresholds = {
{Temperature::kWarm, 1000}}; // 1000 / 5 = 200
// FIFO requires max_table_files_size to be set
options.compaction_options_fifo.max_table_files_size = 1024 * 1024 * 1024;
test_period(options, 200);
}
// Case 8: Minimum of multiple CF options is used
{
Options options;
options.stats_dump_period_sec = 24 * 60 * 60;
options.stats_persist_period_sec = 0;
options.periodic_compaction_seconds = 1000; // 1000 / 5 = 200
ColumnFamilyOptions cf2_opts;
cf2_opts.ttl = 500; // 500 / 5 = 100 (smaller, should be used)
test_period(options, 100, &cf2_opts);
}
// Case 9: Second CF has larger value, first CF's value is used
{
Options options;
options.stats_dump_period_sec = 24 * 60 * 60;
options.stats_persist_period_sec = 0;
options.ttl = 300; // 300 / 5 = 60
ColumnFamilyOptions cf2_opts;
cf2_opts.periodic_compaction_seconds = 1000; // 1000 / 5 = 200 (larger)
test_period(options, 60, &cf2_opts);
}
// Case 10: Minimum is 1 second (never 0)
{
Options options;
options.stats_dump_period_sec = 0;
options.stats_persist_period_sec = 0;
options.ttl = 3; // 3 / 5 = 0, but clamped to 1
test_period(options, 1);
}
}
// Test that TriggerPeriodicCompaction() properly considers CFs for compaction
// based on all time-based compaction options (not just
// periodic_compaction_seconds). This is a regression test for the bug where
// only periodic_compaction_seconds was checked, causing CFs with ttl,
// bottommost_file_compaction_delay, or file_temperature_age_thresholds to not
// be considered.
//
// NOTE: This test uses a separate test class without mock time because the
// PeriodicTaskSchedulerTest fixture's mock clock setup does not integrate
// properly with opening the DB via DB::Open() with multiple column families
// having different options.
class TriggerCompactionTest : public DBTestBase {
public:
TriggerCompactionTest()
: DBTestBase("trigger_compaction_test", /*env_do_fsync=*/true) {}
};
TEST_F(TriggerCompactionTest, QueuesAllTimeBasedOptions) {
Close();
// Track which CFs get their compaction score computed during
// TriggerPeriodicCompaction
std::set<std::string> cfs_with_score_computed;
SyncPoint::GetInstance()->SetCallBack(
"DBImpl::TriggerPeriodicCompaction:BeforeComputeCompactionScore",
[&](void* arg) {
auto* cfd = static_cast<ColumnFamilyData*>(arg);
cfs_with_score_computed.insert(cfd->GetName());
});
SyncPoint::GetInstance()->EnableProcessing();
// Use small period so the test completes quickly with real time
constexpr unsigned int kPeriodSec = 1;
// Default CF: no time-based compaction options
Options options;
options.create_if_missing = true;
options.stats_dump_period_sec = kPeriodSec;
options.stats_persist_period_sec = 0;
options.periodic_compaction_seconds = 0;
options.ttl = 0;
options.bottommost_file_compaction_delay = 0;
// Create the DB with default CF first
DestroyAndReopen(options);
// Create the additional CFs (using same options, will set specific options
// on reopen)
CreateColumnFamilies(
{"cf_periodic", "cf_ttl", "cf_bottommost", "cf_fifo", "cf_none"},
options);
Close();
// Now reopen with specific options for each CF
std::vector<std::string> cf_names = {
kDefaultColumnFamilyName, "cf_periodic", "cf_ttl",
"cf_bottommost", "cf_fifo", "cf_none"};
std::vector<Options> cf_options;
// default: no time-based options
cf_options.push_back(options);
// cf_periodic: periodic_compaction_seconds
Options opt_periodic = options;
opt_periodic.periodic_compaction_seconds = 100;
cf_options.push_back(opt_periodic);
// cf_ttl: ttl
Options opt_ttl = options;
opt_ttl.ttl = 100;
cf_options.push_back(opt_ttl);
// cf_bottommost: bottommost_file_compaction_delay
Options opt_bottommost = options;
opt_bottommost.bottommost_file_compaction_delay = 100;
cf_options.push_back(opt_bottommost);
// cf_fifo: file_temperature_age_thresholds (requires FIFO compaction)
Options opt_fifo = options;
opt_fifo.compaction_style = kCompactionStyleFIFO;
opt_fifo.num_levels = 1;
opt_fifo.compaction_options_fifo.max_table_files_size = 1024 * 1024 * 1024;
opt_fifo.compaction_options_fifo.file_temperature_age_thresholds = {
{Temperature::kWarm, 100}};
cf_options.push_back(opt_fifo);
// cf_none: explicitly no time-based options
Options opt_none = options;
opt_none.periodic_compaction_seconds = 0;
opt_none.ttl = 0;
opt_none.bottommost_file_compaction_delay = 0;
cf_options.push_back(opt_none);
ReopenWithColumnFamilies(cf_names, cf_options);
// Wait for TriggerPeriodicCompaction to run using TEST_WaitForPeriodicTaskRun
// with a real sleep callback to advance real time
dbfull()->TEST_WaitForPeriodicTaskRun(
[&] { Env::Default()->SleepForMicroseconds(kPeriodSec * 1000000); });
// Verify CFs with time-based options had their compaction score computed
EXPECT_GT(cfs_with_score_computed.count("cf_periodic"), 0u)
<< "Expected cf_periodic to have compaction score computed";
EXPECT_GT(cfs_with_score_computed.count("cf_ttl"), 0u)
<< "Expected cf_ttl to have compaction score computed";
EXPECT_GT(cfs_with_score_computed.count("cf_bottommost"), 0u)
<< "Expected cf_bottommost to have compaction score computed";
EXPECT_GT(cfs_with_score_computed.count("cf_fifo"), 0u)
<< "Expected cf_fifo to have compaction score computed";
// CFs without time-based options should NOT have score computed
EXPECT_EQ(cfs_with_score_computed.count("default"), 0u)
<< "Expected default CF to NOT have compaction score computed";
EXPECT_EQ(cfs_with_score_computed.count("cf_none"), 0u)
<< "Expected cf_none to NOT have compaction score computed";
Close();
}
+141 -135
View File
@@ -16,6 +16,7 @@
#include "file/file_util.h"
#include "file/filename.h"
#include "file/random_access_file_reader.h"
#include "monitoring/file_read_sample.h"
#include "monitoring/perf_context_imp.h"
#include "rocksdb/advanced_options.h"
#include "rocksdb/statistics.h"
@@ -72,6 +73,8 @@ TableCache::TableCache(const ImmutableOptions& ioptions,
file_options_(*file_options),
cache_(cache),
immortal_tables_(false),
should_pin_table_handles_(cache_.get()->GetCapacity() >=
kInfiniteCapacity),
block_cache_tracer_(block_cache_tracer),
loader_mutex_(kLoadConcurency),
io_tracer_(io_tracer),
@@ -169,15 +172,29 @@ Cache::Handle* TableCache::Lookup(Cache* cache, uint64_t file_number) {
return cache->Lookup(key);
}
// TODO: consider making handle RAII.
Status TableCache::FindTable(
const ReadOptions& ro, const FileOptions& file_options,
const InternalKeyComparator& internal_comparator,
const FileMetaData& file_meta, TypedHandle** handle,
const MutableCFOptions& mutable_cf_options, const bool no_io,
HistogramImpl* file_read_hist, bool skip_filters, int level,
bool prefetch_index_and_filter_in_cache,
size_t max_file_size_for_l0_meta_pin, Temperature file_temperature) {
const MutableCFOptions& mutable_cf_options, TableReader** out_table_reader,
const bool no_io, HistogramImpl* file_read_hist, bool skip_filters,
int level, bool prefetch_index_and_filter_in_cache,
size_t max_file_size_for_l0_meta_pin, Temperature file_temperature,
bool pin_table_handle) {
assert(out_table_reader != nullptr && *out_table_reader == nullptr);
assert(handle != nullptr && *handle == nullptr);
PERF_TIMER_GUARD_WITH_CLOCK(find_table_nanos, ioptions_.clock);
// Fast path: if table reader is already pinned, return it directly without a
// cache lookup.
auto pinned_reader = file_meta.fd.pinned_reader.Get();
if (pinned_reader != nullptr) {
*handle = nullptr;
*out_table_reader = pinned_reader;
return Status::OK();
}
uint64_t number = file_meta.fd.GetNumber();
// NOTE: sharing same Cache with BlobFileCache
Slice key = GetSliceForFileNumber(&number);
@@ -185,39 +202,71 @@ Status TableCache::FindTable(
TEST_SYNC_POINT_CALLBACK("TableCache::FindTable:0",
const_cast<bool*>(&no_io));
Status s = Status::OK();
if (*handle == nullptr) {
if (no_io) {
return Status::Incomplete("Table not found in table_cache, no_io is set");
s = Status::Incomplete("Table not found in table_cache, no_io is set");
return s;
}
MutexLock load_lock(&loader_mutex_.Get(key));
// We check the cache again under loading mutex
*handle = cache_.Lookup(key);
if (*handle != nullptr) {
return Status::OK();
// Check if another thread has already pinned the table reader
pinned_reader = file_meta.fd.pinned_reader.Get();
if (pinned_reader != nullptr) {
*handle = nullptr;
*out_table_reader = pinned_reader;
return s;
}
std::unique_ptr<TableReader> table_reader;
Status s = GetTableReader(ro, file_options, internal_comparator, file_meta,
false /* sequential mode */, file_read_hist,
&table_reader, mutable_cf_options, skip_filters,
level, prefetch_index_and_filter_in_cache,
max_file_size_for_l0_meta_pin, file_temperature);
if (!s.ok()) {
assert(table_reader == nullptr);
RecordTick(ioptions_.stats, NO_FILE_ERRORS);
// We do not cache error results so that if the error is transient,
// or somebody repairs the file, we recover automatically.
IGNORE_STATUS_IF_ERROR(s);
} else {
s = cache_.Insert(key, table_reader.get(), 1, handle);
if (s.ok()) {
// Release ownership of table reader.
table_reader.release();
// We check the cache again under loading mutex
*handle = cache_.Lookup(key);
if (*handle == nullptr) {
std::unique_ptr<TableReader> table_reader;
s = GetTableReader(ro, file_options, internal_comparator, file_meta,
false /* sequential mode */, file_read_hist,
&table_reader, mutable_cf_options, skip_filters, level,
prefetch_index_and_filter_in_cache,
max_file_size_for_l0_meta_pin, file_temperature);
if (!s.ok()) {
assert(table_reader == nullptr);
RecordTick(ioptions_.stats, NO_FILE_ERRORS);
// We do not cache error results so that if the error is transient,
// or somebody repairs the file, we recover automatically.
IGNORE_STATUS_IF_ERROR(s);
} else {
s = cache_.Insert(key, table_reader.get(), 1, handle);
if (s.ok()) {
// Release ownership of table reader.
(void)table_reader.release();
}
}
}
return s;
if (s.ok()) {
*out_table_reader = cache_.Value(*handle);
if (pin_table_handle) {
file_meta.fd.pinned_reader.Pin(*handle, *out_table_reader);
*handle = nullptr;
}
}
} else {
*out_table_reader = cache_.Value(*handle);
if (pin_table_handle) {
// handle is in cache but not pinned. This should happen fairly rarely,
// and once the reader is pinned, we will no longer need to go through
// these mutexes again.
MutexLock load_lock(&loader_mutex_.Get(key));
if (file_meta.fd.pinned_reader.Get() != nullptr) {
// Another thread has pinned the handle; release our lookup ref.
cache_.Release(*handle);
} else {
file_meta.fd.pinned_reader.Pin(*handle, *out_table_reader);
}
*handle = nullptr;
}
}
return Status::OK();
return s;
}
InternalIterator* TableCache::NewIterator(
@@ -230,7 +279,8 @@ InternalIterator* TableCache::NewIterator(
const InternalKey* smallest_compaction_key,
const InternalKey* largest_compaction_key, bool allow_unprepared_value,
const SequenceNumber* read_seqno,
std::unique_ptr<TruncatedRangeDelIterator>* range_del_iter) {
std::unique_ptr<TruncatedRangeDelIterator>* range_del_iter,
bool maybe_pin_table_handle) {
PERF_TIMER_GUARD(new_table_iterator_nanos);
Status s;
@@ -240,19 +290,15 @@ InternalIterator* TableCache::NewIterator(
*table_reader_ptr = nullptr;
}
bool for_compaction = caller == TableReaderCaller::kCompaction;
auto& fd = file_meta.fd;
table_reader = fd.table_reader;
if (table_reader == nullptr) {
s = FindTable(options, file_options, icomparator, file_meta, &handle,
mutable_cf_options,
options.read_tier == kBlockCacheTier /* no_io */,
file_read_hist, skip_filters, level,
true /* prefetch_index_and_filter_in_cache */,
max_file_size_for_l0_meta_pin, file_meta.temperature);
if (s.ok()) {
table_reader = cache_.Value(handle);
}
}
TEST_SYNC_POINT_CALLBACK("TableCache::NewIterator::BeforeFindTable",
const_cast<FileDescriptor*>(&file_meta.fd));
s = FindTable(options, file_options, icomparator, file_meta, &handle,
mutable_cf_options, &table_reader,
options.read_tier == kBlockCacheTier /* no_io */,
file_read_hist, skip_filters, level,
true /* prefetch_index_and_filter_in_cache */,
max_file_size_for_l0_meta_pin, file_meta.temperature,
maybe_pin_table_handle && should_pin_table_handles_);
InternalIterator* result = nullptr;
if (s.ok()) {
if (options.table_filter &&
@@ -293,7 +339,7 @@ InternalIterator* TableCache::NewIterator(
}
}
if (range_del_agg != nullptr) {
if (range_del_agg->AddFile(fd.GetNumber())) {
if (range_del_agg->AddFile(file_meta.fd.GetNumber())) {
std::unique_ptr<FragmentedRangeTombstoneIterator> new_range_del_iter(
static_cast<FragmentedRangeTombstoneIterator*>(
table_reader->NewRangeTombstoneIterator(options)));
@@ -332,17 +378,11 @@ Status TableCache::GetRangeTombstoneIterator(
const FileMetaData& file_meta, const MutableCFOptions& mutable_cf_options,
std::unique_ptr<FragmentedRangeTombstoneIterator>* out_iter) {
assert(out_iter);
const FileDescriptor& fd = file_meta.fd;
Status s;
TableReader* t = fd.table_reader;
TableReader* t = nullptr;
TypedHandle* handle = nullptr;
if (t == nullptr) {
s = FindTable(options, file_options_, internal_comparator, file_meta,
&handle, mutable_cf_options);
if (s.ok()) {
t = cache_.Value(handle);
}
}
s = FindTable(options, file_options_, internal_comparator, file_meta, &handle,
mutable_cf_options, &t);
if (s.ok()) {
// Note: NewRangeTombstoneIterator could return nullptr
out_iter->reset(t->NewRangeTombstoneIterator(options));
@@ -456,20 +496,18 @@ Status TableCache::Get(const ReadOptions& options,
row_cache_entry = &row_cache_entry_buffer;
}
}
TableReader* t = fd.table_reader;
TEST_SYNC_POINT_CALLBACK("TableCache::Get::BeforeFindTable",
const_cast<FileDescriptor*>(&fd));
TableReader* t = nullptr;
TypedHandle* handle = nullptr;
if (s.ok() && !done) {
if (t == nullptr) {
s = FindTable(options, file_options_, internal_comparator, file_meta,
&handle, mutable_cf_options,
options.read_tier == kBlockCacheTier /* no_io */,
file_read_hist, skip_filters, level,
true /* prefetch_index_and_filter_in_cache */,
max_file_size_for_l0_meta_pin, file_meta.temperature);
if (s.ok()) {
t = cache_.Value(handle);
}
}
s = FindTable(options, file_options_, internal_comparator, file_meta,
&handle, mutable_cf_options, &t,
options.read_tier == kBlockCacheTier /* no_io */,
file_read_hist, skip_filters, level,
true /* prefetch_index_and_filter_in_cache */,
max_file_size_for_l0_meta_pin, file_meta.temperature,
should_pin_table_handles_);
SequenceNumber* max_covering_tombstone_seq =
get_context->max_covering_tombstone_seq();
if (s.ok() && max_covering_tombstone_seq != nullptr &&
@@ -546,8 +584,8 @@ Status TableCache::MultiGetFilter(
const InternalKeyComparator& internal_comparator,
const FileMetaData& file_meta, const MutableCFOptions& mutable_cf_options,
HistogramImpl* file_read_hist, int level,
MultiGetContext::Range* mget_range, TypedHandle** table_handle) {
auto& fd = file_meta.fd;
MultiGetContext::Range* mget_range, TypedHandle** handle) {
assert(*handle == nullptr);
IterKey row_cache_key;
std::string row_cache_entry_buffer;
@@ -559,23 +597,17 @@ Status TableCache::MultiGetFilter(
return Status::NotSupported();
}
Status s;
TableReader* t = fd.table_reader;
TypedHandle* handle = nullptr;
TableReader* t = nullptr;
MultiGetContext::Range tombstone_range(*mget_range, mget_range->begin(),
mget_range->end());
if (t == nullptr) {
s = FindTable(options, file_options_, internal_comparator, file_meta,
&handle, mutable_cf_options,
options.read_tier == kBlockCacheTier /* no_io */,
file_read_hist,
/*skip_filters=*/false, level,
true /* prefetch_index_and_filter_in_cache */,
/*max_file_size_for_l0_meta_pin=*/0, file_meta.temperature);
if (s.ok()) {
t = cache_.Value(handle);
}
*table_handle = handle;
}
s = FindTable(options, file_options_, internal_comparator, file_meta, handle,
mutable_cf_options, &t,
options.read_tier == kBlockCacheTier /* no_io */,
file_read_hist,
/*skip_filters=*/false, level,
true /* prefetch_index_and_filter_in_cache */,
/*max_file_size_for_l0_meta_pin=*/0, file_meta.temperature,
should_pin_table_handles_);
if (s.ok()) {
s = t->MultiGetFilter(options, mutable_cf_options.prefix_extractor.get(),
mget_range);
@@ -586,9 +618,9 @@ Status TableCache::MultiGetFilter(
// is, it may be called with fewer keys in the rangedue to filtering.
UpdateRangeTombstoneSeqnums(options, t, tombstone_range);
}
if (mget_range->empty() && handle) {
cache_.Release(handle);
*table_handle = nullptr;
if (mget_range->empty() && *handle) {
cache_.Release(*handle);
*handle = nullptr;
}
return s;
@@ -600,24 +632,18 @@ Status TableCache::GetTableProperties(
const FileMetaData& file_meta,
std::shared_ptr<const TableProperties>* properties,
const MutableCFOptions& mutable_cf_options, bool no_io) {
auto table_reader = file_meta.fd.table_reader;
// table already been pre-loaded?
if (table_reader) {
*properties = table_reader->GetTableProperties();
return Status::OK();
}
TypedHandle* table_handle = nullptr;
Status s = FindTable(read_options, file_options, internal_comparator,
file_meta, &table_handle, mutable_cf_options, no_io);
TableReader* table = nullptr;
Status s =
FindTable(read_options, file_options, internal_comparator, file_meta,
&table_handle, mutable_cf_options, &table, no_io);
if (!s.ok()) {
return s;
}
assert(table_handle);
auto table = cache_.Value(table_handle);
*properties = table->GetTableProperties();
cache_.Release(table_handle);
if (table_handle) {
cache_.Release(table_handle);
}
return s;
}
@@ -627,15 +653,10 @@ Status TableCache::ApproximateKeyAnchors(
std::vector<TableReader::Anchor>& anchors) {
Status s;
TableReader* t = file_meta.fd.table_reader;
TableReader* t = nullptr;
TypedHandle* handle = nullptr;
if (t == nullptr) {
s = FindTable(ro, file_options_, internal_comparator, file_meta, &handle,
mutable_cf_options);
if (s.ok()) {
t = cache_.Value(handle);
}
}
s = FindTable(ro, file_options_, internal_comparator, file_meta, &handle,
mutable_cf_options, &t);
if (s.ok() && t != nullptr) {
s = t->ApproximateKeyAnchors(ro, anchors);
}
@@ -649,23 +670,18 @@ size_t TableCache::GetMemoryUsageByTableReader(
const FileOptions& file_options, const ReadOptions& read_options,
const InternalKeyComparator& internal_comparator,
const FileMetaData& file_meta, const MutableCFOptions& mutable_cf_options) {
auto table_reader = file_meta.fd.table_reader;
// table already been pre-loaded?
if (table_reader) {
return table_reader->ApproximateMemoryUsage();
}
TypedHandle* table_handle = nullptr;
TableReader* table = nullptr;
Status s =
FindTable(read_options, file_options, internal_comparator, file_meta,
&table_handle, mutable_cf_options, true /* no_io */);
&table_handle, mutable_cf_options, &table, true /* no_io */);
if (!s.ok()) {
return 0;
}
assert(table_handle);
auto table = cache_.Value(table_handle);
auto ret = table->ApproximateMemoryUsage();
cache_.Release(table_handle);
if (table_handle) {
cache_.Release(table_handle);
}
return ret;
}
@@ -679,18 +695,13 @@ uint64_t TableCache::ApproximateOffsetOf(
const InternalKeyComparator& internal_comparator,
const MutableCFOptions& mutable_cf_options) {
uint64_t result = 0;
TableReader* table_reader = file_meta.fd.table_reader;
TableReader* table_reader = nullptr;
TypedHandle* table_handle = nullptr;
if (table_reader == nullptr) {
Status s =
FindTable(read_options, file_options_, internal_comparator, file_meta,
&table_handle, mutable_cf_options, false /* no_io */);
if (s.ok()) {
table_reader = cache_.Value(table_handle);
}
}
Status s =
FindTable(read_options, file_options_, internal_comparator, file_meta,
&table_handle, mutable_cf_options, &table_reader);
if (table_reader != nullptr) {
if (s.ok() && table_reader != nullptr) {
result = table_reader->ApproximateOffsetOf(read_options, key, caller);
}
if (table_handle != nullptr) {
@@ -706,18 +717,13 @@ uint64_t TableCache::ApproximateSize(
const InternalKeyComparator& internal_comparator,
const MutableCFOptions& mutable_cf_options) {
uint64_t result = 0;
TableReader* table_reader = file_meta.fd.table_reader;
TableReader* table_reader = nullptr;
TypedHandle* table_handle = nullptr;
if (table_reader == nullptr) {
Status s =
FindTable(read_options, file_options_, internal_comparator, file_meta,
&table_handle, mutable_cf_options, false /* no_io */);
if (s.ok()) {
table_reader = cache_.Value(table_handle);
}
}
Status s =
FindTable(read_options, file_options_, internal_comparator, file_meta,
&table_handle, mutable_cf_options, &table_reader);
if (table_reader != nullptr) {
if (s.ok() && table_reader != nullptr) {
result = table_reader->ApproximateSize(read_options, start, end, caller);
}
if (table_handle != nullptr) {
+30 -6
View File
@@ -99,7 +99,8 @@ class TableCache {
const InternalKey* smallest_compaction_key,
const InternalKey* largest_compaction_key, bool allow_unprepared_value,
const SequenceNumber* range_del_read_seqno = nullptr,
std::unique_ptr<TruncatedRangeDelIterator>* range_del_iter = nullptr);
std::unique_ptr<TruncatedRangeDelIterator>* range_del_iter = nullptr,
bool maybe_pin_table_handle = false);
// If a seek to internal key "k" in specified file finds an entry,
// call get_context->SaveValue() repeatedly until
@@ -172,19 +173,31 @@ class TableCache {
// Return handle to an existing cache entry if there is one
static Cache::Handle* Lookup(Cache* cache, uint64_t file_number);
// Find table reader
// @param skip_filters Disables loading/accessing the filter block
// @param level == -1 means not specified
// Look up the TableReader for the given file in the cache, or open the file
// and create a new TableReader if not cached. On success, sets *table_reader
// to point to the TableReader (owned by the cache) and *handle to the cache
// handle (caller must release via cache_.Release() unless pin_table_handle is
// true). If the table reader is already pinned on file_meta, returns it
// directly without a cache lookup.
//
// @param no_io If true, returns Status::Incomplete() when the table is not
// already in cache rather than reading from disk.
// @param skip_filters Disables loading/accessing the filter block.
// @param level The LSM level of this table, -1 if not specified.
// @param pin_table_handle If true, pins the table reader on file_meta so
// future lookups bypass the cache. *handle is set to nullptr
// on return in this case.
Status FindTable(const ReadOptions& ro, const FileOptions& toptions,
const InternalKeyComparator& internal_comparator,
const FileMetaData& file_meta, TypedHandle**,
const MutableCFOptions& mutable_cf_options,
const bool no_io = false,
TableReader** table_reader, const bool no_io = false,
HistogramImpl* file_read_hist = nullptr,
bool skip_filters = false, int level = -1,
bool prefetch_index_and_filter_in_cache = true,
size_t max_file_size_for_l0_meta_pin = 0,
Temperature file_temperature = Temperature::kUnknown);
Temperature file_temperature = Temperature::kUnknown,
bool pin_table_handle = false);
// Get the table properties of a given table.
// @no_io: indicates if we should load table to the cache if it is not present
@@ -231,6 +244,8 @@ class TableCache {
CacheInterface& get_cache() { return cache_; }
const FileOptions& file_options() const { return file_options_; }
// Capacity of the backing Cache that indicates infinite TableCache capacity.
// For example when max_open_files is -1 we set the backing Cache to this.
static const int kInfiniteCapacity = 0x400000;
@@ -243,6 +258,14 @@ class TableCache {
}
}
// Re-evaluates should_pin_table_handles_ from the current cache capacity.
// Must be called after the underlying cache capacity changes (e.g. via
// SetDBOptions changing max_open_files).
void UpdateShouldPinTableHandles() {
should_pin_table_handles_ =
cache_.get()->GetCapacity() >= kInfiniteCapacity;
}
private:
// Build a table reader
Status GetTableReader(const ReadOptions& ro, const FileOptions& file_options,
@@ -283,6 +306,7 @@ class TableCache {
CacheInterface cache_;
std::string row_cache_id_;
bool immortal_tables_;
bool should_pin_table_handles_;
BlockCacheTracer* const block_cache_tracer_;
Striped<CacheAlignedWrapper<port::Mutex>> loader_mutex_;
std::shared_ptr<IOTracer> io_tracer_;
+7 -7
View File
@@ -21,7 +21,9 @@ DEFINE_SYNC_AND_ASYNC(Status, TableCache::MultiGet)
bool skip_filters, bool skip_range_deletions, int level, TypedHandle* handle) {
auto& fd = file_meta.fd;
Status s;
TableReader* t = fd.table_reader;
TEST_SYNC_POINT_CALLBACK("TableCache::MultiGet::BeforeFindTable",
const_cast<FileDescriptor*>(&fd));
TableReader* t = fd.pinned_reader.Get();
MultiGetRange table_range(*mget_range, mget_range->begin(),
mget_range->end());
if (handle != nullptr && t == nullptr) {
@@ -70,16 +72,14 @@ DEFINE_SYNC_AND_ASYNC(Status, TableCache::MultiGet)
if (t == nullptr) {
assert(handle == nullptr);
s = FindTable(options, file_options_, internal_comparator, file_meta,
&handle, mutable_cf_options,
&handle, mutable_cf_options, &t,
options.read_tier == kBlockCacheTier /* no_io */,
file_read_hist, skip_filters, level,
true /* prefetch_index_and_filter_in_cache */,
0 /*max_file_size_for_l0_meta_pin*/, file_meta.temperature);
0 /*max_file_size_for_l0_meta_pin*/, file_meta.temperature,
should_pin_table_handles_);
TEST_SYNC_POINT_CALLBACK("TableCache::MultiGet:FindTable", &s);
if (s.ok()) {
t = cache_.Value(handle);
assert(t);
}
assert(!s.ok() || t);
}
if (s.ok() && !options.ignore_range_deletions && !skip_range_deletions) {
UpdateRangeTombstoneSeqnums(options, t, table_range);
+11 -50
View File
@@ -32,8 +32,10 @@
#include "db/version_edit.h"
#include "db/version_edit_handler.h"
#include "db/version_set.h"
#include "db/version_util.h"
#include "port/port.h"
#include "table/table_reader.h"
#include "test_util/sync_point.h"
#include "util/string_util.h"
namespace ROCKSDB_NAMESPACE {
@@ -430,12 +432,11 @@ class VersionBuilder::Rep {
void UnrefFile(FileMetaData* f) {
f->refs--;
if (f->refs <= 0) {
if (f->table_reader_handle) {
if (f->fd.pinned_reader.Get() != nullptr) {
assert(table_cache_ != nullptr);
// NOTE: have to release in raw cache interface to avoid using a
// TypedHandle for FileMetaData::table_reader_handle
table_cache_->get_cache().get()->Release(f->table_reader_handle);
f->table_reader_handle = nullptr;
// TypedHandle for PinnedTableReader's internal handle
f->fd.pinned_reader.Release(table_cache_->get_cache().get());
}
if (file_metadata_cache_res_mgr_) {
@@ -1680,7 +1681,6 @@ class VersionBuilder::Rep {
// <file metadata, level>
std::vector<std::pair<FileMetaData*, int>> files_meta;
std::vector<Status> statuses;
for (int level = 0; level < num_levels_; level++) {
for (auto& file_meta_pair : levels_[level].added_files) {
auto* file_meta = file_meta_pair.second;
@@ -1690,9 +1690,8 @@ class VersionBuilder::Rep {
continue;
}
// If the file has been opened before, just skip it.
if (!file_meta->table_reader_handle) {
if (file_meta->fd.pinned_reader.Get() == nullptr) {
files_meta.emplace_back(file_meta, level);
statuses.emplace_back(Status::OK());
}
if (files_meta.size() >= max_load) {
break;
@@ -1703,49 +1702,11 @@ class VersionBuilder::Rep {
}
}
std::atomic<size_t> next_file_meta_idx(0);
std::function<void()> load_handlers_func([&]() {
while (true) {
size_t file_idx = next_file_meta_idx.fetch_add(1);
if (file_idx >= files_meta.size()) {
break;
}
auto* file_meta = files_meta[file_idx].first;
int level = files_meta[file_idx].second;
TableCache::TypedHandle* handle = nullptr;
statuses[file_idx] = table_cache_->FindTable(
read_options, file_options_,
*(base_vstorage_->InternalComparator()), *file_meta, &handle,
mutable_cf_options, false /*no_io */,
internal_stats->GetFileReadHist(level), false, level,
prefetch_index_and_filter_in_cache, max_file_size_for_l0_meta_pin,
file_meta->temperature);
if (handle != nullptr) {
file_meta->table_reader_handle = handle;
// Load table_reader
file_meta->fd.table_reader = table_cache_->get_cache().Value(handle);
}
}
});
std::vector<port::Thread> threads;
for (int i = 1; i < max_threads; i++) {
threads.emplace_back(load_handlers_func);
}
load_handlers_func();
for (auto& t : threads) {
t.join();
}
Status ret;
for (const auto& s : statuses) {
if (!s.ok()) {
if (ret.ok()) {
ret = s;
}
}
}
return ret;
return LoadTableHandlersHelper(
files_meta, table_cache_, file_options_,
*base_vstorage_->InternalComparator(), internal_stats, max_threads,
prefetch_index_and_filter_in_cache, mutable_cf_options,
max_file_size_for_l0_meta_pin, read_options);
}
};
+2 -1
View File
@@ -53,7 +53,8 @@ class VersionBuilder {
// Save the current Version to the provided `vstorage`.
Status SaveTo(VersionStorageInfo* vstorage) const;
// Load all the table handlers for the current Version in the builder.
// Load table handlers for newly added files in the builder. This does not
// load any files in the base storage.
Status LoadTableHandlers(InternalStats* internal_stats, int max_threads,
bool prefetch_index_and_filter_in_cache,
bool is_initial_load,
+34
View File
@@ -22,6 +22,40 @@ namespace ROCKSDB_NAMESPACE {
namespace {} // anonymous namespace
// After copying, we rely on the caller to ensure no double releases. Fragile,
// but keeps copying cheap.
PinnedTableReader& PinnedTableReader::operator=(
const PinnedTableReader& other) {
TableReader* r = other.reader_.load(std::memory_order_acquire);
// Only read handle_ when reader_ is non-null. Pin() writes handle_ before
// reader_ (with release), so a non-null reader_ guarantees handle_ is stable.
// If reader_ is null, Pin() may be in progress — avoid reading handle_.
handle_ = (r != nullptr) ? other.handle_ : nullptr;
reader_.store(r, std::memory_order_release);
return *this;
}
Cache::Handle* PinnedTableReader::GetCacheHandle() const {
(void)reader_.load(std::memory_order_acquire);
return handle_;
}
void PinnedTableReader::Pin(Cache::Handle* handle, TableReader* reader) {
assert(handle != nullptr);
assert(reader != nullptr);
handle_ = handle;
reader_.store(reader, std::memory_order_release);
}
void PinnedTableReader::Release(Cache* cache) {
(void)reader_.load(std::memory_order_acquire);
if (handle_ != nullptr) {
cache->Release(handle_);
handle_ = nullptr;
reader_.store(nullptr, std::memory_order_relaxed);
}
}
uint64_t PackFileNumberAndPathId(uint64_t number, uint64_t path_id) {
assert(number <= kFileNumberMask);
return number | (path_id * (kFileNumberMask + 1));
+64 -18
View File
@@ -134,14 +134,57 @@ constexpr uint64_t kReservedEpochNumberForFileIngestedBehind = 1;
uint64_t PackFileNumberAndPathId(uint64_t number, uint64_t path_id);
// PinnedTableReader is used to safely access a table reader in a multi-threaded
// context. It holds both a pointer to the table reader and a cache handle.
class PinnedTableReader {
public:
PinnedTableReader() : reader_(nullptr), handle_(nullptr) {}
~PinnedTableReader() = default;
PinnedTableReader(const PinnedTableReader& other)
: reader_(nullptr), handle_(nullptr) {
*this = other;
}
PinnedTableReader& operator=(const PinnedTableReader& other);
PinnedTableReader(PinnedTableReader&&) = delete;
PinnedTableReader& operator=(PinnedTableReader&&) = delete;
// Returns the pinned TableReader, or nullptr if not pinned.
TableReader* Get() const { return reader_.load(std::memory_order_acquire); }
// Returns the cache handle that keeps TableReader alive, or nullptr if not
// pinned.
Cache::Handle* GetCacheHandle() const;
// Pin a table reader with its cache handle.
void Pin(Cache::Handle* handle, TableReader* reader);
// Release the pinned handle via the given cache and reset state.
void Release(Cache* cache);
// Test-only: set a reader without a cache handle.
void TEST_SetReader(TableReader* reader) {
reader_.store(reader, std::memory_order_release);
}
private:
// Internally, we need to ensure reads and writes to reader_ and handle_ are
// properly ordered. handle_ must be written to before reader_ is written to
// with release semantics. handle_ must be read after reader_ is read with
// acquire semantics.
std::atomic<TableReader*> reader_;
Cache::Handle* handle_;
};
// A copyable structure contains information needed to read data from an SST
// file. It can contain a pointer to a table reader opened for the file, or
// file number and size, which can be used to create a new table reader for it.
// The behavior is undefined when a copied of the structure is used when the
// file is not in any live version any more.
struct FileDescriptor {
// Table reader in table_reader_handle
TableReader* table_reader;
// Fast access to table reader without cache lookup. Marked mutable because
// reads can pin the table reader, but can also be done safely in a
// multi-threaded context.
mutable PinnedTableReader pinned_reader;
uint64_t packed_number_and_path_id;
uint64_t file_size; // File size in bytes
SequenceNumber smallest_seqno; // The smallest seqno in this file
@@ -154,7 +197,7 @@ struct FileDescriptor {
FileDescriptor(uint64_t number, uint32_t path_id, uint64_t _file_size,
SequenceNumber _smallest_seqno, SequenceNumber _largest_seqno)
: table_reader(nullptr),
: pinned_reader(),
packed_number_and_path_id(PackFileNumberAndPathId(number, path_id)),
file_size(_file_size),
smallest_seqno(_smallest_seqno),
@@ -163,7 +206,7 @@ struct FileDescriptor {
FileDescriptor(const FileDescriptor& fd) { *this = fd; }
FileDescriptor& operator=(const FileDescriptor& fd) {
table_reader = fd.table_reader;
pinned_reader = fd.pinned_reader;
packed_number_and_path_id = fd.packed_number_and_path_id;
file_size = fd.file_size;
smallest_seqno = fd.smallest_seqno;
@@ -182,15 +225,20 @@ struct FileDescriptor {
};
struct FileSampledStats {
FileSampledStats() : num_reads_sampled(0) {}
FileSampledStats()
: num_reads_sampled(0), num_collapsible_entry_reads_sampled(0) {}
FileSampledStats(const FileSampledStats& other) { *this = other; }
FileSampledStats& operator=(const FileSampledStats& other) {
num_reads_sampled = other.num_reads_sampled.load();
num_collapsible_entry_reads_sampled =
other.num_collapsible_entry_reads_sampled.load();
return *this;
}
// number of user reads to this file.
mutable std::atomic<uint64_t> num_reads_sampled;
// number of reads of type kNotFound, kMerge, kTypeSingleDeletion
mutable std::atomic<uint64_t> num_collapsible_entry_reads_sampled;
};
struct FileMetaData {
@@ -198,9 +246,6 @@ struct FileMetaData {
InternalKey smallest; // Smallest internal key served by table
InternalKey largest; // Largest internal key served by table
// Needs to be disposed when refs becomes 0.
Cache::Handle* table_reader_handle = nullptr;
FileSampledStats stats;
// Stats for compensating deletion entries during compaction
@@ -347,9 +392,10 @@ struct FileMetaData {
uint64_t TryGetOldestAncesterTime() {
if (oldest_ancester_time != kUnknownOldestAncesterTime) {
return oldest_ancester_time;
} else if (fd.table_reader != nullptr &&
fd.table_reader->GetTableProperties() != nullptr) {
return fd.table_reader->GetTableProperties()->creation_time;
}
TableReader* reader = fd.pinned_reader.Get();
if (reader != nullptr && reader->GetTableProperties() != nullptr) {
return reader->GetTableProperties()->creation_time;
}
return kUnknownOldestAncesterTime;
}
@@ -357,9 +403,10 @@ struct FileMetaData {
uint64_t TryGetFileCreationTime() {
if (file_creation_time != kUnknownFileCreationTime) {
return file_creation_time;
} else if (fd.table_reader != nullptr &&
fd.table_reader->GetTableProperties() != nullptr) {
return fd.table_reader->GetTableProperties()->file_creation_time;
}
TableReader* reader = fd.pinned_reader.Get();
if (reader != nullptr && reader->GetTableProperties() != nullptr) {
return reader->GetTableProperties()->file_creation_time;
}
return kUnknownFileCreationTime;
}
@@ -367,10 +414,9 @@ struct FileMetaData {
// Tries to get the newest key time from the current file
// Falls back on oldest ancestor time of previous (newer) file
uint64_t TryGetNewestKeyTime(FileMetaData* prev_file = nullptr) {
if (fd.table_reader != nullptr &&
fd.table_reader->GetTableProperties() != nullptr) {
uint64_t newest_key_time =
fd.table_reader->GetTableProperties()->newest_key_time;
TableReader* reader = fd.pinned_reader.Get();
if (reader != nullptr && reader->GetTableProperties() != nullptr) {
uint64_t newest_key_time = reader->GetTableProperties()->newest_key_time;
if (newest_key_time != kUnknownNewestKeyTime) {
return newest_key_time;
}
+7 -7
View File
@@ -146,13 +146,13 @@ class VersionEditHandler : public VersionEditHandlerBase {
const std::shared_ptr<IOTracer>& io_tracer,
const ReadOptions& read_options, bool allow_incomplete_valid_version,
EpochNumberRequirement epoch_number_requirement =
EpochNumberRequirement::kMustPresent)
: VersionEditHandler(read_only, column_families, version_set,
track_found_and_missing_files,
no_error_if_files_missing, io_tracer, read_options,
/*skip_load_table_files=*/false,
allow_incomplete_valid_version,
epoch_number_requirement) {}
EpochNumberRequirement::kMustPresent,
bool skip_load_table_files = false)
: VersionEditHandler(
read_only, column_families, version_set,
track_found_and_missing_files, no_error_if_files_missing, io_tracer,
read_options, skip_load_table_files, allow_incomplete_valid_version,
epoch_number_requirement) {}
~VersionEditHandler() override {}
+152 -31
View File
@@ -170,7 +170,7 @@ class FilePicker {
if (!search_ended_) {
// Prefetch Level 0 table data to avoid cache miss if possible.
for (unsigned int i = 0; i < (*level_files_brief_)[0].num_files; ++i) {
auto* r = (*level_files_brief_)[0].files[i].fd.table_reader;
auto* r = (*level_files_brief_)[0].files[i].fd.pinned_reader.Get();
if (r) {
r->Prepare(ikey);
}
@@ -395,7 +395,7 @@ class FilePickerMultiGet {
// prefetching. This may not be necessary anymore once we implement
// batching in those table readers
for (unsigned int i = 0; i < (*level_files_brief_)[0].num_files; ++i) {
auto* r = (*level_files_brief_)[0].files[i].fd.table_reader;
auto* r = (*level_files_brief_)[0].files[i].fd.pinned_reader.Get();
if (r) {
for (auto iter = range_.begin(); iter != range_.end(); ++iter) {
r->Prepare(iter->ikey);
@@ -974,9 +974,8 @@ class LevelIterator final : public InternalIterator {
TableCache* table_cache, const ReadOptions& read_options,
const FileOptions& file_options, const InternalKeyComparator& icomparator,
const LevelFilesBrief* flevel, const MutableCFOptions& mutable_cf_options,
bool should_sample, HistogramImpl* file_read_hist,
TableReaderCaller caller, bool skip_filters, int level,
RangeDelAggregator* range_del_agg,
HistogramImpl* file_read_hist, TableReaderCaller caller,
bool skip_filters, int level, RangeDelAggregator* range_del_agg,
const std::vector<AtomicCompactionUnitBoundary>* compaction_boundaries =
nullptr,
bool allow_unprepared_value = false,
@@ -1002,7 +1001,6 @@ class LevelIterator final : public InternalIterator {
? read_options.snapshot->GetSequenceNumber()
: kMaxSequenceNumber),
level_(level),
should_sample_(should_sample),
skip_filters_(skip_filters),
allow_unprepared_value_(allow_unprepared_value),
is_next_read_sequential_(false),
@@ -1267,6 +1265,26 @@ class LevelIterator final : public InternalIterator {
*read_options_.iterate_upper_bound, /*b_has_ts=*/false) >= 0;
}
template <bool IsSeek>
void SampleRead() {
bool sampled =
IsSeek ? should_sample_file_read() : should_sample_file_read_next();
if (!sampled) {
return;
}
if (file_index_ >= flevel_->num_files || !file_iter_.Valid()) {
return;
}
const FileMetaData* meta = flevel_->files[file_index_].file_metadata;
sample_file_read_inc(meta);
ValueType type = ExtractValueType(file_iter_.key());
if (type == kTypeDeletion || type == kTypeSingleDeletion ||
type == kTypeDeletionWithTimestamp || type == kTypeMerge) {
sample_collapsible_entry_file_read_inc(meta);
}
}
void ClearRangeTombstoneIter() {
if (range_tombstone_iter_) {
range_tombstone_iter_->reset();
@@ -1279,9 +1297,6 @@ class LevelIterator final : public InternalIterator {
InternalIterator* NewFileIterator() {
assert(file_index_ < flevel_->num_files);
auto file_meta = flevel_->files[file_index_];
if (should_sample_) {
sample_file_read_inc(file_meta.file_metadata);
}
const InternalKey* smallest_compaction_key = nullptr;
const InternalKey* largest_compaction_key = nullptr;
@@ -1298,7 +1313,8 @@ class LevelIterator final : public InternalIterator {
/*arena=*/nullptr, skip_filters_, level_,
/*max_file_size_for_l0_meta_pin=*/0, smallest_compaction_key,
largest_compaction_key, allow_unprepared_value_, &read_seq_,
range_tombstone_iter_);
range_tombstone_iter_,
/*maybe_pin_table_handle=*/true);
}
// Check if current file being fully within iterate_lower_bound.
@@ -1361,7 +1377,6 @@ class LevelIterator final : public InternalIterator {
SequenceNumber read_seq_;
int level_;
bool should_sample_;
bool skip_filters_;
bool allow_unprepared_value_;
bool may_be_out_of_lower_bound_ = true;
@@ -1498,6 +1513,7 @@ void LevelIterator::Seek(const Slice& target) {
}
SkipEmptyFileForward();
CheckMayBeOutOfLowerBound();
SampleRead<true>();
}
void LevelIterator::SeekForPrev(const Slice& target) {
@@ -1533,6 +1549,7 @@ void LevelIterator::SeekForPrev(const Slice& target) {
SkipEmptyFileBackward();
}
CheckMayBeOutOfLowerBound();
SampleRead<true>();
}
void LevelIterator::SeekToFirst() {
@@ -1549,6 +1566,7 @@ void LevelIterator::SeekToFirst() {
}
SkipEmptyFileForward();
CheckMayBeOutOfLowerBound();
SampleRead<true>();
}
void LevelIterator::SeekToLast() {
@@ -1563,6 +1581,7 @@ void LevelIterator::SeekToLast() {
}
SkipEmptyFileBackward();
CheckMayBeOutOfLowerBound();
SampleRead<true>();
}
void LevelIterator::Next() {
@@ -1577,6 +1596,7 @@ void LevelIterator::Next() {
}
}
SkipEmptyFileForward();
SampleRead<false>();
}
bool LevelIterator::NextAndGetResult(IterateResult* result) {
@@ -1610,6 +1630,7 @@ bool LevelIterator::NextAndGetResult(IterateResult* result) {
}
}
}
SampleRead<false>();
return is_valid;
}
@@ -1624,6 +1645,7 @@ void LevelIterator::Prev() {
}
}
SkipEmptyFileBackward();
SampleRead<false>();
}
bool LevelIterator::SkipEmptyFileForward() {
@@ -2221,7 +2243,7 @@ void Version::GetCreationTimeOfOldestFile(uint64_t* creation_time) {
uint64_t oldest_time = std::numeric_limits<uint64_t>::max();
for (int level = 0; level < storage_info_.num_non_empty_levels_; level++) {
for (FileMetaData* meta : storage_info_.LevelFiles(level)) {
assert(meta->fd.table_reader != nullptr);
assert(meta->fd.pinned_reader.Get() != nullptr);
uint64_t file_creation_time = meta->TryGetFileCreationTime();
if (file_creation_time == kUnknownFileCreationTime) {
*creation_time = 0;
@@ -2244,8 +2266,7 @@ InternalIterator* Version::TEST_GetLevelIterator(
auto level_iter = new (mem) LevelIterator(
cfd_->table_cache(), read_options, file_options_,
cfd_->internal_comparator(), &storage_info_.LevelFilesBrief(level),
mutable_cf_options_, should_sample_file_read(),
cfd_->internal_stats()->GetFileReadHist(level),
mutable_cf_options_, cfd_->internal_stats()->GetFileReadHist(level),
TableReaderCaller::kUserIterator, IsFilterSkipped(level), level,
nullptr /* range_del_agg */, nullptr /* compaction_boundaries */,
allow_unprepared_value, &tombstone_iter_ptr, db_statistics_, clock_);
@@ -2341,8 +2362,6 @@ void Version::AddIteratorsForLevel(const ReadOptions& read_options,
return;
}
bool should_sample = should_sample_file_read();
auto* arena = merge_iter_builder->GetArena();
if (level == 0) {
// Merge all level zero files together since they may overlap
@@ -2357,7 +2376,8 @@ void Version::AddIteratorsForLevel(const ReadOptions& read_options,
/*skip_filters=*/false, /*level=*/0, max_file_size_for_l0_meta_pin_,
/*smallest_compaction_key=*/nullptr,
/*largest_compaction_key=*/nullptr, allow_unprepared_value,
/*range_del_read_seqno=*/nullptr, &tombstone_iter);
/*range_del_read_seqno=*/nullptr, &tombstone_iter,
/*maybe_pin_table_handle=*/true);
if (read_options.ignore_range_deletions) {
merge_iter_builder->AddIterator(table_iter);
} else {
@@ -2365,11 +2385,10 @@ void Version::AddIteratorsForLevel(const ReadOptions& read_options,
table_iter, std::move(tombstone_iter));
}
}
if (should_sample) {
if (should_sample_file_read()) {
// Count ones for every L0 files. This is done per iterator creation
// rather than Seek(), while files in other levels are recored per seek.
// If users execute one range query per iterator, there may be some
// discrepancy here.
// rather than Seek(), while files in other levels are sampled on
// seek/next/prev.
for (FileMetaData* meta : storage_info_.LevelFiles(0)) {
sample_file_read_inc(meta);
}
@@ -2383,8 +2402,7 @@ void Version::AddIteratorsForLevel(const ReadOptions& read_options,
auto level_iter = new (mem) LevelIterator(
cfd_->table_cache(), read_options, soptions,
cfd_->internal_comparator(), &storage_info_.LevelFilesBrief(level),
mutable_cf_options_, should_sample_file_read(),
cfd_->internal_stats()->GetFileReadHist(level),
mutable_cf_options_, cfd_->internal_stats()->GetFileReadHist(level),
TableReaderCaller::kUserIterator, IsFilterSkipped(level), level,
/*range_del_agg=*/nullptr,
/*compaction_boundaries=*/nullptr, allow_unprepared_value,
@@ -2442,8 +2460,7 @@ Status Version::OverlapWithLevelIterator(const ReadOptions& read_options,
ScopedArenaPtr<InternalIterator> iter(new (mem) LevelIterator(
cfd_->table_cache(), read_options, file_options,
cfd_->internal_comparator(), &storage_info_.LevelFilesBrief(level),
mutable_cf_options_, should_sample_file_read(),
cfd_->internal_stats()->GetFileReadHist(level),
mutable_cf_options_, cfd_->internal_stats()->GetFileReadHist(level),
TableReaderCaller::kUserIterator, IsFilterSkipped(level), level,
&range_del_agg, nullptr, false, nullptr, db_statistics_, clock_));
status = OverlapWithIterator(ucmp, smallest_user_key, largest_user_key,
@@ -2784,9 +2801,15 @@ void Version::Get(const ReadOptions& read_options, const LookupKey& k,
switch (get_context.State()) {
case GetContext::kNotFound:
// Keep searching in other files
if (get_context.sample()) {
sample_collapsible_entry_file_read_inc(f->file_metadata);
}
break;
case GetContext::kMerge:
// TODO: update per-level perfcontext user_key_return_count for kMerge
if (get_context.sample()) {
sample_collapsible_entry_file_read_inc(f->file_metadata);
}
break;
case GetContext::kFound:
if (fp.GetHitFileLevel() == 0) {
@@ -2835,6 +2858,9 @@ void Version::Get(const ReadOptions& read_options, const LookupKey& k,
case GetContext::kDeleted:
// Use empty error message for speed
*status = Status::NotFound();
if (get_context.sample()) {
sample_collapsible_entry_file_read_inc(f->file_metadata);
}
return;
case GetContext::kCorrupt:
*status = Status::Corruption("corrupted key for ", user_key);
@@ -5581,6 +5607,96 @@ Status VersionSet::Close(FSDirectory* db_dir, InstrumentedMutex* mu) {
s = LogAndApply(cfd, ReadOptions(), WriteOptions(), &edit, mu, db_dir);
}
// Content validation: read back the manifest and verify CRC + decode.
// Loop up to 2 checks with 1 rewrite attempt in between, so we also verify
// the rewritten manifest is healthy.
if (s.ok() && verify_manifest_content_on_close_) {
TEST_SYNC_POINT("VersionSet::Close:BeforeContentValidation");
constexpr int kMaxContentChecks = 2;
for (int content_check = 0; s.ok() && content_check < kMaxContentChecks;
++content_check) {
// Re-read the manifest file name in case it was rotated by a rewrite
std::string content_manifest_name =
DescriptorFileName(dbname_, manifest_file_number_);
std::unique_ptr<FSSequentialFile> manifest_file;
IOStatus content_io_s = fs_->NewSequentialFile(
content_manifest_name, fs_->OptimizeForManifestRead(file_options_),
&manifest_file, nullptr);
if (!content_io_s.ok()) {
// Surface I/O errors to the caller — users who call DB::Close() and
// check the status should know about filesystem problems.
s = content_io_s;
ROCKS_LOG_ERROR(db_options_->info_log,
"MANIFEST content verification on Close: "
"could not open %s for reading: %s\n",
content_manifest_name.c_str(),
content_io_s.ToString().c_str());
break;
}
std::unique_ptr<SequentialFileReader> manifest_file_reader(
new SequentialFileReader(std::move(manifest_file),
content_manifest_name,
db_options_->log_readahead_size, io_tracer_,
db_options_->listeners));
LogReporter reporter;
Status log_read_status;
reporter.status = &log_read_status;
log::Reader reader(nullptr, std::move(manifest_file_reader), &reporter,
/*checksum=*/true, /*log_num=*/0);
Slice record;
std::string scratch;
bool content_corrupt = false;
while (reader.ReadRecord(&record, &scratch,
WALRecoveryMode::kAbsoluteConsistency)) {
VersionEdit edit;
Status decode_s = edit.DecodeFrom(record);
if (!decode_s.ok()) {
content_corrupt = true;
break;
}
}
if (!content_corrupt && !log_read_status.ok()) {
content_corrupt = true;
}
if (!content_corrupt) {
// Manifest is healthy, no need to check again
break;
}
IOStatus corrupt_io_s =
IOStatus::Corruption("MANIFEST content validation failed");
IOErrorInfo io_error_info(corrupt_io_s, FileOperationType::kVerify,
content_manifest_name, /*length=*/0,
/*offset=*/0);
for (auto& listener : db_options_->listeners) {
listener->OnIOError(io_error_info);
}
corrupt_io_s.PermitUncheckedError();
io_error_info.io_status.PermitUncheckedError();
if (content_check == 0) {
// First check failed — rewrite and verify again
ROCKS_LOG_ERROR(db_options_->info_log,
"MANIFEST content verification on Close failed, "
"filename %s, rewriting manifest\n",
content_manifest_name.c_str());
ColumnFamilyData* cfd = GetColumnFamilySet()->GetDefault();
VersionEdit recovery_edit;
assert(cfd);
s = LogAndApply(cfd, ReadOptions(), WriteOptions(), &recovery_edit, mu,
db_dir);
} else {
// Rewritten manifest is also corrupt — likely a recurring filesystem
// issue. Surface it so DB::Close() callers can detect the problem.
ROCKS_LOG_ERROR(db_options_->info_log,
"MANIFEST content verification on Close failed again "
"after rewrite, filename %s\n",
content_manifest_name.c_str());
s = Status::Corruption(
"MANIFEST content verification failed after rewrite: " +
content_manifest_name);
}
}
}
closed_ = true;
return s;
}
@@ -5597,9 +5713,10 @@ VersionSet::~VersionSet() {
// Using uncache_aggressiveness=0 overrides any previous marking to
// attempt to uncache the file's blocks (which after cleaning up
// column families could cause use-after-free)
TableCache::ReleaseObsolete(table_cache_, file.metadata->fd.GetNumber(),
file.metadata->table_reader_handle,
/*uncache_aggressiveness=*/0);
TableCache::ReleaseObsolete(
table_cache_, file.metadata->fd.GetNumber(),
file.metadata->fd.pinned_reader.GetCacheHandle(),
/*uncache_aggressiveness=*/0);
file.DeleteMetadata();
}
obsolete_files_.clear();
@@ -5663,6 +5780,8 @@ void VersionSet::UpdatedMutableDbOptions(
max_manifest_space_amp_pct_ = static_cast<unsigned>(
std::max(updated_options.max_manifest_space_amp_pct, 0));
manifest_preallocation_size_ = updated_options.manifest_preallocation_size;
verify_manifest_content_on_close_ =
updated_options.verify_manifest_content_on_close;
TuneMaxManifestFileSize();
}
@@ -6537,7 +6656,8 @@ Status VersionSet::Recover(
read_only, column_families, const_cast<VersionSet*>(this),
/*track_found_and_missing_files=*/false, no_error_if_files_missing,
io_tracer_, read_options, /*allow_incomplete_valid_version=*/false,
EpochNumberRequirement::kMightMissing);
EpochNumberRequirement::kMightMissing,
/*skip_load_table_files=*/db_options_->open_files_async);
handler.Iterate(reader, &log_read_status);
s = handler.status();
if (s.ok()) {
@@ -7590,7 +7710,6 @@ InternalIterator* VersionSet::MakeInputIterator(
list[num++] = new LevelIterator(
cfd->table_cache(), read_options, file_options_compactions,
cfd->internal_comparator(), flevel, c->mutable_cf_options(),
/*should_sample=*/false,
/*no per level latency histogram=*/nullptr,
TableReaderCaller::kCompaction, /*skip_filters=*/false,
/*level=*/static_cast<int>(c->level(which)), range_del_agg,
@@ -7856,10 +7975,12 @@ Status VersionSet::VerifyFileMetadata(const ReadOptions& read_options,
InternalStats* internal_stats = cfd->internal_stats();
TableCache::TypedHandle* handle = nullptr;
TableReader* table_reader = nullptr;
FileMetaData meta_copy = meta;
status = table_cache->FindTable(
read_options, file_opts, *icmp, meta_copy, &handle, cf_opts,
/*no_io=*/false, internal_stats->GetFileReadHist(level), false, level,
&table_reader, /*no_io=*/false, internal_stats->GetFileReadHist(level),
false, level,
/*prefetch_index_and_filter_in_cache*/ false, max_sz_for_l0_meta_pin,
meta_copy.temperature);
if (handle) {
+2
View File
@@ -1779,6 +1779,8 @@ class VersionSet {
unsigned max_manifest_space_amp_pct_;
// Saved copy from (Mutable)DBOptions
size_t manifest_preallocation_size_;
// Saved copy from (Mutable)DBOptions
bool verify_manifest_content_on_close_;
// Obsolete files, or during DB shutdown any files not referenced by what's
// left of the in-memory LSM state.
+5
View File
@@ -58,6 +58,11 @@ DEFINE_SYNC_AND_ASYNC(Status, Version::MultiGetFromSST)
if (get_context.sample()) {
sample_file_read_inc(f->file_metadata);
if (get_context.State() == GetContext::kNotFound ||
get_context.State() == GetContext::kMerge ||
get_context.State() == GetContext::kDeleted) {
sample_collapsible_entry_file_read_inc(f->file_metadata);
}
}
batch_size++;
num_index_read += get_context.get_context_stats_.num_index_read;
+276 -12
View File
@@ -1228,11 +1228,12 @@ class VersionSetTestBase {
SetIdentityFile(WriteOptions(), env_, dbname_, Temperature::kUnknown));
VersionEdit new_db;
if (imm_db_options_.write_dbid_to_manifest) {
DBOptions tmp_db_options;
tmp_db_options.env = env_;
std::unique_ptr<DBImpl> impl(new DBImpl(tmp_db_options, dbname_));
std::string db_id;
ASSERT_OK(impl->GetDbIdentityFromIdentityFile(IOOptions(), &db_id));
ASSERT_OK(ReadFileToString(env_, IdentityFileName(dbname_), &db_id));
// Strip trailing newline if present (old GenerateUniqueId format)
if (!db_id.empty() && db_id.back() == '\n') {
db_id.pop_back();
}
new_db.SetDBId(db_id);
}
new_db.SetLogNumber(0);
@@ -2407,6 +2408,267 @@ TEST_F(VersionSetTest, ManifestTruncateAfterClose) {
ReopenDB();
}
TEST_F(VersionSetTest, ManifestContentValidationOnCloseClean) {
// Enable content validation, perform normal operations, close.
// Verify no manifest rotation (file number unchanged).
NewDB();
mutable_db_options_.verify_manifest_content_on_close = true;
mutex_.Lock();
versions_->UpdatedMutableDbOptions(mutable_db_options_, &mutex_);
mutex_.Unlock();
VersionEdit edit;
ASSERT_OK(LogAndApplyToDefaultCF(edit));
std::string manifest_path_before;
GetManifestPath(&manifest_path_before);
bool content_validation_ran = false;
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
SyncPoint::GetInstance()->SetCallBack(
"VersionSet::Close:BeforeContentValidation",
[&](void*) { content_validation_ran = true; });
SyncPoint::GetInstance()->EnableProcessing();
CloseDB();
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
// Verify content validation actually ran
ASSERT_TRUE(content_validation_ran);
// Manifest path should be unchanged (no rotation)
std::string manifest_path_after;
uint64_t manifest_file_number = 0;
ASSERT_OK(GetCurrentManifestPath(dbname_, fs_.get(), /*is_retry=*/false,
&manifest_path_after,
&manifest_file_number));
ASSERT_EQ(manifest_path_before, manifest_path_after);
ReopenDB();
}
TEST_F(VersionSetTest, ManifestContentValidationOnCloseCorruptRecord) {
// Enable content validation, corrupt the manifest after closing the writer,
// verify manifest rotation occurs and DB reopens cleanly.
NewDB();
mutable_db_options_.verify_manifest_content_on_close = true;
mutex_.Lock();
versions_->UpdatedMutableDbOptions(mutable_db_options_, &mutex_);
mutex_.Unlock();
VersionEdit edit;
ASSERT_OK(LogAndApplyToDefaultCF(edit));
std::string manifest_path_before;
GetManifestPath(&manifest_path_before);
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
SyncPoint::GetInstance()->SetCallBack(
"VersionSet::Close:BeforeContentValidation", [&](void*) {
// Corrupt bytes in the middle of the manifest
std::string manifest_path;
GetManifestPath(&manifest_path);
std::string content;
Status s = ReadFileToString(env_, manifest_path, &content);
EXPECT_OK(s);
if (!s.ok()) {
return;
}
ASSERT_GT(content.size(), 20u);
// Corrupt several bytes in the middle to break CRC
for (size_t i = content.size() / 2; i < content.size() / 2 + 8; i++) {
content[i] ^= 0xFF;
}
s = WriteStringToFile(env_, content, manifest_path);
EXPECT_OK(s);
});
SyncPoint::GetInstance()->EnableProcessing();
CloseDB();
SyncPoint::GetInstance()->DisableProcessing();
// Manifest should have been rotated (new file number)
std::string manifest_path_after;
uint64_t manifest_file_number = 0;
ASSERT_OK(GetCurrentManifestPath(dbname_, fs_.get(), /*is_retry=*/false,
&manifest_path_after,
&manifest_file_number));
ASSERT_NE(manifest_path_before, manifest_path_after);
// DB should reopen cleanly with the fresh manifest
ReopenDB();
}
TEST_F(VersionSetTest, ManifestContentValidationOnCloseDisabled) {
// Default (option disabled), corrupt manifest after writer close,
// verify no rotation occurred — corrupt manifest persists.
NewDB();
// verify_manifest_content_on_close defaults to false
VersionEdit edit;
ASSERT_OK(LogAndApplyToDefaultCF(edit));
std::string manifest_path_before;
GetManifestPath(&manifest_path_before);
bool content_validation_ran = false;
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
SyncPoint::GetInstance()->SetCallBack(
"VersionSet::Close:BeforeContentValidation",
[&](void*) { content_validation_ran = true; });
SyncPoint::GetInstance()->EnableProcessing();
CloseDB();
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
ASSERT_FALSE(content_validation_ran);
// Manifest path should be unchanged (no rotation since validation is off)
std::string manifest_path_after;
uint64_t manifest_file_number = 0;
ASSERT_OK(GetCurrentManifestPath(dbname_, fs_.get(), /*is_retry=*/false,
&manifest_path_after,
&manifest_file_number));
ASSERT_EQ(manifest_path_before, manifest_path_after);
}
TEST_F(VersionSetTest, ManifestContentValidationOnCloseSizeCheckFails) {
// Truncate manifest so size check fails first.
// Verify recovery happens via size-check path. Content validation still
// runs afterward on the freshly rewritten manifest.
NewDB();
mutable_db_options_.verify_manifest_content_on_close = true;
mutex_.Lock();
versions_->UpdatedMutableDbOptions(mutable_db_options_, &mutex_);
mutex_.Unlock();
VersionEdit edit;
ASSERT_OK(LogAndApplyToDefaultCF(edit));
std::string manifest_path_before;
GetManifestPath(&manifest_path_before);
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
SyncPoint::GetInstance()->SetCallBack(
"VersionSet::Close:AfterClose", [&](void*) {
std::string manifest_path;
GetManifestPath(&manifest_path);
std::unique_ptr<WritableFile> manifest_file;
ASSERT_OK(env_->ReopenWritableFile(manifest_path, &manifest_file,
EnvOptions()));
ASSERT_OK(manifest_file->Truncate(0));
ASSERT_OK(manifest_file->Close());
});
SyncPoint::GetInstance()->EnableProcessing();
ASSERT_NO_FATAL_FAILURE(CloseDB());
SyncPoint::GetInstance()->DisableProcessing();
// Size check should have triggered rotation
std::string manifest_path_after;
uint64_t manifest_file_number = 0;
ASSERT_OK(GetCurrentManifestPath(dbname_, fs_.get(), /*is_retry=*/false,
&manifest_path_after,
&manifest_file_number));
ASSERT_NE(manifest_path_before, manifest_path_after);
// DB should reopen cleanly
ReopenDB();
}
TEST_F(VersionSetTest, ManifestContentValidationOnCloseCorruptAfterRewrite) {
// Corrupt the manifest before content validation AND after the rewrite.
// The loop should detect corruption twice: once triggering a rewrite, and
// once reporting that the rewritten manifest is also corrupt.
NewDB();
mutable_db_options_.verify_manifest_content_on_close = true;
mutex_.Lock();
versions_->UpdatedMutableDbOptions(mutable_db_options_, &mutex_);
mutex_.Unlock();
VersionEdit edit;
ASSERT_OK(LogAndApplyToDefaultCF(edit));
int io_error_count = 0;
class IOErrorCountListener : public EventListener {
public:
int* count;
explicit IOErrorCountListener(int* c) : count(c) {}
void OnIOError(const IOErrorInfo& /*info*/) override { ++(*count); }
};
imm_db_options_.listeners.push_back(
std::make_shared<IOErrorCountListener>(&io_error_count));
auto corrupt_current_manifest = [&]() {
std::string manifest_path;
GetManifestPath(&manifest_path);
std::string content;
ASSERT_OK(ReadFileToString(env_, manifest_path, &content));
ASSERT_GT(content.size(), 20u);
for (size_t i = content.size() / 2; i < content.size() / 2 + 8; i++) {
content[i] ^= 0xFF;
}
ASSERT_OK(WriteStringToFile(env_, content, manifest_path));
};
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
// Corrupt before the first content check
SyncPoint::GetInstance()->SetCallBack(
"VersionSet::Close:BeforeContentValidation",
[&](void*) { corrupt_current_manifest(); });
// Corrupt again after the rewrite completes
SyncPoint::GetInstance()->SetCallBack(
"VersionSet::LogAndApply:WriteManifestDone",
[&](void*) { corrupt_current_manifest(); });
SyncPoint::GetInstance()->EnableProcessing();
mutex_.Lock();
Status close_s = versions_->Close(nullptr, &mutex_);
versions_.reset();
mutex_.Unlock();
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
// Close should report the persistent corruption
ASSERT_TRUE(close_s.IsCorruption()) << close_s.ToString();
// OnIOError should have fired twice (once per corrupt detection)
ASSERT_EQ(io_error_count, 2);
}
TEST_F(VersionSetTest, ManifestContentValidationOnCloseOpenFails) {
// Delete the manifest before content validation so it can't be opened.
// Close() should surface the I/O error to the caller.
NewDB();
mutable_db_options_.verify_manifest_content_on_close = true;
mutex_.Lock();
versions_->UpdatedMutableDbOptions(mutable_db_options_, &mutex_);
mutex_.Unlock();
VersionEdit edit;
ASSERT_OK(LogAndApplyToDefaultCF(edit));
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
SyncPoint::GetInstance()->SetCallBack(
"VersionSet::Close:BeforeContentValidation", [&](void*) {
std::string manifest_path;
GetManifestPath(&manifest_path);
ASSERT_OK(env_->DeleteFile(manifest_path));
});
SyncPoint::GetInstance()->EnableProcessing();
mutex_.Lock();
Status close_s = versions_->Close(nullptr, &mutex_);
versions_.reset();
mutex_.Unlock();
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
ASSERT_TRUE(close_s.IsIOError()) << close_s.ToString();
}
TEST_F(VersionStorageInfoTest, AddRangeDeletionCompensatedFileSize) {
// Tests that compensated range deletion size is added to compensated file
// size.
@@ -3512,11 +3774,12 @@ class VersionSetTestEmptyDb
if (imm_db_options_.write_dbid_to_manifest) {
ASSERT_OK(SetIdentityFile(WriteOptions(), env_, dbname_,
Temperature::kUnknown));
DBOptions tmp_db_options;
tmp_db_options.env = env_;
std::unique_ptr<DBImpl> impl(new DBImpl(tmp_db_options, dbname_));
std::string db_id;
ASSERT_OK(impl->GetDbIdentityFromIdentityFile(IOOptions(), &db_id));
ASSERT_OK(ReadFileToString(env_, IdentityFileName(dbname_), &db_id));
// Strip trailing newline if present (old GenerateUniqueId format)
if (!db_id.empty() && db_id.back() == '\n') {
db_id.pop_back();
}
new_db.SetDBId(db_id);
}
const std::string manifest_path = DescriptorFileName(dbname_, 1);
@@ -3838,11 +4101,12 @@ class VersionSetTestMissingFiles : public VersionSetTestBase,
log_writer->reset(new log::Writer(std::move(file_writer), 0, false));
VersionEdit new_db;
if (imm_db_options_.write_dbid_to_manifest) {
DBOptions tmp_db_options;
tmp_db_options.env = env_;
std::unique_ptr<DBImpl> impl(new DBImpl(tmp_db_options, dbname_));
std::string db_id;
ASSERT_OK(impl->GetDbIdentityFromIdentityFile(IOOptions(), &db_id));
ASSERT_OK(ReadFileToString(env_, IdentityFileName(dbname_), &db_id));
// Strip trailing newline if present (old GenerateUniqueId format)
if (!db_id.empty() && db_id.back() == '\n') {
db_id.pop_back();
}
new_db.SetDBId(db_id);
}
{
+96
View File
@@ -0,0 +1,96 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#include "db/version_util.h"
#include <atomic>
#include <functional>
#include <thread>
#include <utility>
#include <vector>
#include "db/internal_stats.h"
#include "db/table_cache.h"
#include "port/port.h"
#include "test_util/sync_point.h"
namespace ROCKSDB_NAMESPACE {
Status LoadTableHandlersHelper(
const std::vector<std::pair<FileMetaData*, int>>& files_meta,
TableCache* table_cache, const FileOptions& file_options,
const InternalKeyComparator& internal_comparator,
InternalStats* internal_stats, int max_threads,
bool prefetch_index_and_filter_in_cache,
const MutableCFOptions& mutable_cf_options,
size_t max_file_size_for_l0_meta_pin, const ReadOptions& read_options,
std::atomic<bool>* stop) {
assert(table_cache != nullptr);
std::atomic<size_t> next_file_meta_idx(0);
std::atomic<bool> has_error(false);
Status ret;
std::function<void()> load_handlers_func([&]() {
while (true) {
size_t file_idx = next_file_meta_idx.fetch_add(1);
if (has_error.load(std::memory_order_relaxed)) {
break;
}
if (file_idx >= files_meta.size()) {
break;
}
if (stop != nullptr && stop->load()) {
break;
}
auto* cache = table_cache->get_cache().get();
if (cache->GetCapacity() < TableCache::kInfiniteCapacity &&
cache->GetUsage() >= cache->GetCapacity()) {
break;
}
auto* file_meta = files_meta[file_idx].first;
int level = files_meta[file_idx].second;
TEST_SYNC_POINT_CALLBACK(
"VersionBuilder::Rep::LoadTableHandlers::BeforeFindTable", file_meta);
TableCache::TypedHandle* handle = nullptr;
TableReader* table_reader = nullptr;
auto status = table_cache->FindTable(
read_options, file_options, internal_comparator, *file_meta, &handle,
mutable_cf_options, &table_reader, false /* no_io */,
internal_stats->GetFileReadHist(level), false /* skip_filters */,
level, prefetch_index_and_filter_in_cache,
max_file_size_for_l0_meta_pin, file_meta->temperature,
true /* pin_table_handle */);
TEST_SYNC_POINT_CALLBACK(
"VersionBuilder::Rep::LoadTableHandlers::AfterFindTable", &status);
if (!status.ok()) {
bool expected = false;
if (has_error.compare_exchange_strong(expected, true)) {
ret = status;
}
}
}
});
std::vector<port::Thread> threads;
for (int i = 1; i < max_threads; i++) {
threads.emplace_back(load_handlers_func);
}
load_handlers_func();
for (auto& t : threads) {
t.join();
}
return ret;
}
} // namespace ROCKSDB_NAMESPACE
+24
View File
@@ -5,10 +5,34 @@
#pragma once
#include <atomic>
#include <utility>
#include <vector>
#include "db/version_set.h"
#include "rocksdb/file_system.h"
#include "rocksdb/options.h"
#include "rocksdb/status.h"
namespace ROCKSDB_NAMESPACE {
class TableCache;
struct FileMetaData;
class InternalKeyComparator;
class InternalStats;
// Load table handlers (i.e., open SST files and populate the table cache) for
// the given set of files. Used during DB open to eagerly warm the table cache.
Status LoadTableHandlersHelper(
const std::vector<std::pair<FileMetaData*, int>>& files_meta,
TableCache* table_cache, const FileOptions& file_options,
const InternalKeyComparator& internal_comparator,
InternalStats* internal_stats, int max_threads,
bool prefetch_index_and_filter_in_cache,
const MutableCFOptions& mutable_cf_options,
size_t max_file_size_for_l0_meta_pin, const ReadOptions& read_options,
std::atomic<bool>* stop = nullptr);
// Instead of opening a `DB` to perform certain manifest updates, this
// uses the underlying `VersionSet` API to read and modify the MANIFEST. This
// allows us to use the user's real options, while not having to worry about
+9
View File
@@ -113,6 +113,7 @@ DECLARE_bool(use_write_buffer_manager);
DECLARE_double(memtable_prefix_bloom_size_ratio);
DECLARE_bool(memtable_whole_key_filtering);
DECLARE_int32(open_files);
DECLARE_bool(open_files_async);
DECLARE_uint64(compressed_secondary_cache_size);
DECLARE_int32(compressed_secondary_cache_numshardbits);
DECLARE_int32(secondary_cache_update_interval);
@@ -179,6 +180,9 @@ DECLARE_bool(use_sqfc_for_range_queries);
DECLARE_int32(index_type);
DECLARE_int32(data_block_index_type);
DECLARE_int32(index_block_search_type);
DECLARE_double(uniform_cv_threshold);
DECLARE_bool(use_trie_index);
DECLARE_bool(test_backward_scan);
DECLARE_string(db);
DECLARE_string(secondaries_base);
DECLARE_bool(test_secondary);
@@ -256,6 +260,7 @@ DECLARE_uint64(ops_per_thread);
DECLARE_uint64(log2_keys_per_lock);
DECLARE_uint64(max_manifest_file_size);
DECLARE_int32(max_manifest_space_amp_pct);
DECLARE_bool(verify_manifest_content_on_close);
DECLARE_bool(in_place_update);
DECLARE_string(memtablerep);
DECLARE_int32(prefix_size);
@@ -269,6 +274,7 @@ DECLARE_bool(avoid_unnecessary_blocking_io);
DECLARE_bool(write_dbid_to_manifest);
DECLARE_bool(write_identity_file);
DECLARE_bool(avoid_flush_during_recovery);
DECLARE_bool(enforce_write_buffer_manager_during_recovery);
DECLARE_uint64(max_write_batch_group_size_bytes);
DECLARE_bool(level_compaction_dynamic_level_bytes);
DECLARE_int32(verify_checksum_one_in);
@@ -282,8 +288,10 @@ DECLARE_bool(verification_only);
DECLARE_string(last_level_temperature);
DECLARE_string(default_write_temperature);
DECLARE_string(default_temperature);
DECLARE_uint32(verify_output_flags);
DECLARE_bool(paranoid_memory_checks);
DECLARE_bool(memtable_veirfy_per_key_checksum_on_seek);
DECLARE_bool(memtable_batch_lookup_optimization);
// Options for transaction dbs.
// Use TransactionDB (a.k.a. Pessimistic Transaction DB)
@@ -441,6 +449,7 @@ DECLARE_uint32(ingest_wbwi_one_in);
DECLARE_bool(universal_reduce_file_locking);
DECLARE_bool(use_multiscan);
DECLARE_bool(multiscan_use_async_io);
DECLARE_uint64(multiscan_max_prefetch_memory_bytes);
// Compaction deletion trigger declarations for stress testing
DECLARE_bool(enable_compaction_on_deletion_trigger);
+39
View File
@@ -203,6 +203,9 @@ DEFINE_int32(open_files, ROCKSDB_NAMESPACE::Options().max_open_files,
"Maximum number of files to keep open at the same time "
"(use default if == 0)");
DEFINE_bool(open_files_async, ROCKSDB_NAMESPACE::Options().open_files_async,
"Options.open_files_async");
DEFINE_uint64(compressed_secondary_cache_size, 0,
"Number of bytes to use as a cache of compressed data."
" 0 means use default settings.");
@@ -619,6 +622,20 @@ DEFINE_int32(index_block_search_type,
"Search algorithm for index blocks (see `enum BlockSearchType` in "
"table.h)");
DEFINE_double(uniform_cv_threshold,
ROCKSDB_NAMESPACE::BlockBasedTableOptions().uniform_cv_threshold,
"CV threshold for marking index blocks as uniform. Set to -1 to "
"disable. (see `uniform_cv_threshold` in table.h)");
DEFINE_bool(use_trie_index, false,
"Use trie-based user defined index (UDI) for SST files. "
"Compatible with all operation types (Put, Delete, Merge, etc.). "
"Backward scan is disabled when this is enabled.");
DEFINE_bool(test_backward_scan, true,
"Test backward iteration (Prev, SeekForPrev) in stress tests. "
"Automatically set to false when use_trie_index is enabled.");
DEFINE_string(db, "", "Use the db with the following name.");
DEFINE_string(secondaries_base, "",
@@ -1003,6 +1020,9 @@ DEFINE_uint64(max_manifest_file_size, 16384,
DEFINE_int32(max_manifest_space_amp_pct, 500,
"Max manifest space amp percentage for auto-tuning");
DEFINE_bool(verify_manifest_content_on_close, false,
"If true, verify MANIFEST content (CRC + decode) on DB close");
DEFINE_bool(in_place_update, false, "On true, does inplace update in memtable");
DEFINE_string(memtablerep, "skip_list", "");
@@ -1060,6 +1080,11 @@ DEFINE_bool(avoid_flush_during_recovery,
ROCKSDB_NAMESPACE::Options().avoid_flush_during_recovery,
"Avoid flush during recovery");
DEFINE_bool(
enforce_write_buffer_manager_during_recovery,
ROCKSDB_NAMESPACE::Options().enforce_write_buffer_manager_during_recovery,
"Enforce write buffer manager memory limit during WAL recovery");
DEFINE_uint64(max_write_batch_group_size_bytes,
ROCKSDB_NAMESPACE::Options().max_write_batch_group_size_bytes,
"Max write batch group size");
@@ -1513,6 +1538,10 @@ DEFINE_uint32(uncache_aggressiveness,
"obsolete. 0 = disabled, 1 = minimum, 100 = moderate, 10000 = "
"normal max");
DEFINE_uint32(verify_output_flags, 0,
"Sets CF option verify_output_flags as a uint32_t bitmask. "
"See VerifyOutputFlags enum for bit definitions.");
DEFINE_bool(paranoid_memory_checks,
ROCKSDB_NAMESPACE::Options().paranoid_memory_checks,
"Sets CF option paranoid_memory_checks.");
@@ -1522,6 +1551,10 @@ DEFINE_bool(
ROCKSDB_NAMESPACE::Options().memtable_veirfy_per_key_checksum_on_seek,
"Sets CF option memtable_veirfy_per_key_checksum_on_seek.");
DEFINE_bool(memtable_batch_lookup_optimization,
ROCKSDB_NAMESPACE::Options().memtable_batch_lookup_optimization,
"Sets CF option memtable_batch_lookup_optimization.");
DEFINE_uint32(commit_bypass_memtable_one_in, 0,
"If greater than zero, transaction option will set "
"commit_bypass_memtable to per every N transactions on average.");
@@ -1576,4 +1609,10 @@ DEFINE_bool(use_multiscan, false,
DEFINE_bool(multiscan_use_async_io, false,
"If set, enable async_io for MultiScan operations.");
DEFINE_uint64(multiscan_max_prefetch_memory_bytes, 0,
"If non-zero, sets the max_prefetch_memory_bytes on the "
"IODispatcher used for MultiScan. This limits the total memory "
"used for prefetching data blocks across all concurrent "
"MultiScan ReadSets.");
#endif // GFLAGS
+9
View File
@@ -226,6 +226,13 @@ class DbStressListener : public EventListener {
RandomSleep();
}
void OnBackgroundJobPressureChanged(
DB* /*db*/, const BackgroundJobPressure& pressure) override {
RandomSleep();
std::lock_guard<std::mutex> lk(bg_pressure_mu_);
last_bg_pressure_ = pressure;
}
void OnFileReadFinish(const FileOperationInfo& info) override {
// Even empty callback is valuable because sometimes some locks are
// released in order to make the callback.
@@ -366,6 +373,8 @@ class DbStressListener : public EventListener {
std::atomic<int> num_pending_file_creations_;
UniqueIdVerifier unique_ids_;
SharedState* shared_;
mutable std::mutex bg_pressure_mu_;
BackgroundJobPressure last_bg_pressure_;
};
} // namespace ROCKSDB_NAMESPACE
#endif // GFLAGS
+86 -17
View File
@@ -29,6 +29,7 @@
#include "options/options_parser.h"
#include "rocksdb/convenience.h"
#include "rocksdb/filter_policy.h"
#include "rocksdb/io_dispatcher.h"
#include "rocksdb/secondary_cache.h"
#include "rocksdb/sst_file_manager.h"
#include "rocksdb/table_properties.h"
@@ -41,6 +42,7 @@
#include "utilities/backup/backup_engine_impl.h"
#include "utilities/fault_injection_fs.h"
#include "utilities/fault_injection_secondary_cache.h"
#include "utilities/trie_index/trie_index_factory.h"
namespace ROCKSDB_NAMESPACE {
@@ -929,6 +931,22 @@ Status StressTest::CommitTxn(Transaction& txn, ThreadState* thread) {
return s;
}
bool StressTest::IsExpectedTxnError(const Status& s) {
if ((s.IsDeadlock() || s.IsTimedOut()) &&
(FLAGS_use_multiget || FLAGS_use_multi_get_entity)) {
return true;
}
// Optimistic transaction may return TryAgain when memtable history is
// insufficient for conflict detection (controlled by
// max_write_buffer_size_to_maintain). ExecuteTransaction retries up to 10
// times, and if all retries fail, it returns TryAgain. This is an expected
// condition and should not crash the stress test.
if (s.IsTryAgain() && FLAGS_use_optimistic_txn) {
return true;
}
return false;
}
Status StressTest::ExecuteTransaction(WriteOptions& write_opts,
ThreadState* thread,
std::function<Status(Transaction&)>&& ops,
@@ -985,6 +1003,9 @@ void StressTest::OperateDb(ThreadState* thread) {
read_opts.allow_unprepared_value = FLAGS_allow_unprepared_value;
read_opts.auto_refresh_iterator_with_snapshot =
FLAGS_auto_refresh_iterator_with_snapshot;
if (FLAGS_use_trie_index && udi_factory_) {
read_opts.table_index_factory = udi_factory_.get();
}
WriteOptions write_opts;
if (FLAGS_rate_limit_auto_wal_flush) {
@@ -1034,6 +1055,9 @@ void StressTest::OperateDb(ThreadState* thread) {
}
// Commenting this out as we don't want to reset stats on each open.
// thread->stats.Start();
if (FLAGS_use_trie_index && udi_factory_) {
read_opts.table_index_factory = udi_factory_.get();
}
}
#ifndef NDEBUG
@@ -1692,6 +1716,14 @@ Status StressTest::TestMultiScan(ThreadState* thread,
FLAGS_multiscan_use_async_io &&
CheckFSFeatureSupport(options_.env->GetFileSystem().get(),
FSSupportedOps::kAsyncIO);
std::shared_ptr<IODispatcher> io_dispatcher;
if (FLAGS_multiscan_max_prefetch_memory_bytes > 0) {
IODispatcherOptions io_opts;
io_opts.max_prefetch_memory_bytes =
FLAGS_multiscan_max_prefetch_memory_bytes;
io_dispatcher.reset(NewIODispatcher(io_opts));
scan_opts.io_dispatcher = io_dispatcher;
}
start_key_strs.reserve(num_scans);
end_key_strs.reserve(num_scans);
@@ -1976,7 +2008,19 @@ Status StressTest::TestIterateImpl(ThreadState* thread,
Slice key(key_str);
const bool support_seek_first_or_last = expect_total_order;
// UserDefinedIndexIterator supports Seek(target), Next(), and
// SeekToFirst(). However, SeekToLast, SeekForPrev, and Prev are not
// supported. Check if UDI is being used either via ReadOptions or
// CF-level configuration.
const bool using_udi =
(ro.table_index_factory != nullptr) || (udi_factory_ != nullptr);
// SeekToFirst is supported by UDI, so only total_order is required.
const bool support_seek_to_first =
expect_total_order && (FLAGS_test_backward_scan || using_udi);
// SeekToLast requires backward scan support which UDI does not provide.
const bool support_seek_to_last =
expect_total_order && FLAGS_test_backward_scan && !using_udi;
const bool support_seek_for_prev = FLAGS_test_backward_scan && !using_udi;
// Write-prepared and Write-unprepared and multi-cf-iterator do not support
// Refresh() yet.
@@ -1991,17 +2035,17 @@ Status StressTest::TestIterateImpl(ThreadState* thread,
}
LastIterateOp last_op;
if (support_seek_first_or_last && thread->rand.OneIn(100)) {
if (support_seek_to_first && thread->rand.OneIn(100)) {
iter->SeekToFirst();
cmp_iter->SeekToFirst();
last_op = kLastOpSeekToFirst;
op_logs += "STF ";
} else if (support_seek_first_or_last && thread->rand.OneIn(100)) {
} else if (support_seek_to_last && thread->rand.OneIn(100)) {
iter->SeekToLast();
cmp_iter->SeekToLast();
last_op = kLastOpSeekToLast;
op_logs += "STL ";
} else if (thread->rand.OneIn(8)) {
} else if (support_seek_for_prev && thread->rand.OneIn(8)) {
iter->SeekForPrev(key);
cmp_iter->SeekForPrev(key);
last_op = kLastOpSeekForPrev;
@@ -2033,7 +2077,8 @@ Status StressTest::TestIterateImpl(ThreadState* thread,
key, op_logs, verify_func, &diverged);
const bool no_reverse =
(FLAGS_memtablerep == "prefix_hash" && !expect_total_order);
(FLAGS_memtablerep == "prefix_hash" && !expect_total_order) ||
!FLAGS_test_backward_scan;
for (uint64_t i = 0; i < FLAGS_num_iterations && iter->Valid(); ++i) {
if (no_reverse || thread->rand.OneIn(2)) {
iter->Next();
@@ -3630,8 +3675,11 @@ void StressTest::Open(SharedState* shared, bool reopen) {
assert(db_ == nullptr);
assert(txn_db_ == nullptr);
assert(optimistic_txn_db_ == nullptr);
if (FLAGS_use_trie_index) {
udi_factory_ = std::make_shared<trie_index::TrieIndexFactory>();
}
if (!InitializeOptionsFromFile(options_)) {
InitializeOptionsFromFlags(cache_, filter_policy_, options_);
InitializeOptionsFromFlags(cache_, filter_policy_, udi_factory_, options_);
}
InitializeOptionsGeneral(cache_, filter_policy_, sqfc_factory_, options_);
DbStressCustomCompressionManager::Register();
@@ -3669,13 +3717,6 @@ void StressTest::Open(SharedState* shared, bool reopen) {
// Remote Compaction
if (FLAGS_remote_compaction_worker_threads > 0) {
// TODO(jaykorean) Remove this after fix - remote worker shouldn't recover
// from WAL
if (!FLAGS_disable_wal) {
fprintf(stderr,
"WAL is not compatible with Remote Compaction in Stress Test\n");
exit(1);
}
if ((options_.enable_blob_files ||
options_.enable_blob_garbage_collection ||
FLAGS_allow_setting_blob_options_dynamically)) {
@@ -4271,6 +4312,7 @@ bool InitializeOptionsFromFile(Options& options) {
void InitializeOptionsFromFlags(
const std::shared_ptr<Cache>& cache,
const std::shared_ptr<const FilterPolicy>& filter_policy,
const std::shared_ptr<UserDefinedIndexFactory>& udi_factory,
Options& options) {
BlockBasedTableOptions block_based_options;
block_based_options.decouple_partitioned_filters =
@@ -4331,6 +4373,7 @@ void InitializeOptionsFromFlags(
block_based_options.index_block_search_type =
static_cast<BlockBasedTableOptions::BlockSearchType>(
FLAGS_index_block_search_type);
block_based_options.uniform_cv_threshold = FLAGS_uniform_cv_threshold;
block_based_options.prepopulate_block_cache =
static_cast<BlockBasedTableOptions::PrepopulateBlockCache>(
FLAGS_prepopulate_block_cache);
@@ -4353,6 +4396,9 @@ void InitializeOptionsFromFlags(
fLU64::FLAGS_super_block_alignment_size;
block_based_options.super_block_alignment_space_overhead_ratio =
fLU64::FLAGS_super_block_alignment_space_overhead_ratio;
if (udi_factory) {
block_based_options.user_defined_index_factory = udi_factory;
}
options.table_factory.reset(NewBlockBasedTableFactory(block_based_options));
options.db_write_buffer_size = FLAGS_db_write_buffer_size;
options.write_buffer_size = FLAGS_write_buffer_size;
@@ -4399,6 +4445,13 @@ void InitializeOptionsFromFlags(
}
}
options.max_open_files = FLAGS_open_files;
options.open_files_async = FLAGS_open_files_async;
if (FLAGS_open_files_async && !FLAGS_skip_stats_update_on_db_open) {
FLAGS_skip_stats_update_on_db_open = true;
fprintf(stderr,
"open_files_async requires skip_stats_update_on_db_open, "
"enabling it automatically\n");
}
if (FLAGS_statistics) {
options.statistics = ROCKSDB_NAMESPACE::CreateDBStatistics();
}
@@ -4444,6 +4497,8 @@ void InitializeOptionsFromFlags(
}
options.max_manifest_file_size = FLAGS_max_manifest_file_size;
options.max_manifest_space_amp_pct = FLAGS_max_manifest_space_amp_pct;
options.verify_manifest_content_on_close =
FLAGS_verify_manifest_content_on_close;
options.max_subcompactions = static_cast<uint32_t>(FLAGS_subcompactions);
options.allow_concurrent_memtable_write =
FLAGS_allow_concurrent_memtable_write;
@@ -4472,6 +4527,8 @@ void InitializeOptionsFromFlags(
options.write_dbid_to_manifest = FLAGS_write_dbid_to_manifest;
options.write_identity_file = FLAGS_write_identity_file;
options.avoid_flush_during_recovery = FLAGS_avoid_flush_during_recovery;
options.enforce_write_buffer_manager_during_recovery =
FLAGS_enforce_write_buffer_manager_during_recovery;
options.max_write_batch_group_size_bytes =
FLAGS_max_write_batch_group_size_bytes;
options.level_compaction_dynamic_level_bytes =
@@ -4483,9 +4540,13 @@ void InitializeOptionsFromFlags(
options.memtable_protection_bytes_per_key =
FLAGS_memtable_protection_bytes_per_key;
options.block_protection_bytes_per_key = FLAGS_block_protection_bytes_per_key;
options.verify_output_flags =
static_cast<VerifyOutputFlags>(FLAGS_verify_output_flags);
options.paranoid_memory_checks = FLAGS_paranoid_memory_checks;
options.memtable_veirfy_per_key_checksum_on_seek =
FLAGS_memtable_veirfy_per_key_checksum_on_seek;
options.memtable_batch_lookup_optimization =
FLAGS_memtable_batch_lookup_optimization;
// Integrated BlobDB
options.enable_blob_files = FLAGS_enable_blob_files;
@@ -4714,10 +4775,18 @@ void InitializeOptionsGeneral(
"`preserve_unverified_changes` to keep all files\n");
options.avoid_flush_during_recovery = true;
}
// Together with `avoid_flush_during_recovery == true`, this will prevent
// live files from becoming obsolete and deleted between `DB::Open()` and
// `DisableFileDeletions()` due to flush or compaction. We do not need to
// warn the user since we will reenable compaction soon.
if (options.enforce_write_buffer_manager_during_recovery) {
fprintf(
stderr,
"WARNING: flipping `enforce_write_buffer_manager_during_recovery` "
"to false for `preserve_unverified_changes` to keep all files\n");
options.enforce_write_buffer_manager_during_recovery = false;
}
// Together with `avoid_flush_during_recovery == true` and
// `enforce_write_buffer_manager_during_recovery == false`, this will
// prevent live files from becoming obsolete and deleted between
// `DB::Open()` and `DisableFileDeletions()` due to flush or compaction.
// We do not need to warn the user since we will reenable compaction soon.
options.disable_auto_compactions = true;
}
+11 -1
View File
@@ -14,6 +14,7 @@
#include "db_stress_tool/db_stress_common.h"
#include "db_stress_tool/db_stress_shared_state.h"
#include "rocksdb/experimental.h"
#include "rocksdb/user_defined_index.h"
#include "utilities/fault_injection_fs.h"
namespace ROCKSDB_NAMESPACE {
@@ -33,6 +34,12 @@ class StressTest {
!status_to_io_status(Status(error_s)).GetDataLoss();
}
// Returns true if the status is an expected transactional error, including
// lock conflicts (deadlock or timeout) from MaybeAddKeyToTxnForRYW writing
// to the same key space without the stress-test-level mutex, and TryAgain
// from optimistic transactions when conflict detection retries are exhausted.
static bool IsExpectedTxnError(const Status& s);
StressTest();
virtual ~StressTest() {}
@@ -422,6 +429,7 @@ class StressTest {
std::vector<std::string> options_index_;
std::atomic<bool> db_preload_finished_;
std::shared_ptr<SstQueryFilterConfigsManager::Factory> sqfc_factory_;
std::shared_ptr<UserDefinedIndexFactory> udi_factory_;
std::unique_ptr<DB> secondary_db_;
std::vector<ColumnFamilyHandle*> secondary_cfhs_;
@@ -437,7 +445,9 @@ bool InitializeOptionsFromFile(Options& options);
// input arguments.
void InitializeOptionsFromFlags(
const std::shared_ptr<Cache>& cache,
const std::shared_ptr<const FilterPolicy>& filter_policy, Options& options);
const std::shared_ptr<const FilterPolicy>& filter_policy,
const std::shared_ptr<UserDefinedIndexFactory>& udi_factory,
Options& options);
// Initialize `options` on which `InitializeOptionsFromFile()` and
// `InitializeOptionsFromFlags()` have both been called already.
+45
View File
@@ -24,6 +24,7 @@
#include "db_stress_tool/db_stress_common.h"
#include "db_stress_tool/db_stress_driver.h"
#include "db_stress_tool/db_stress_shared_state.h"
#include "port/stack_trace.h"
#include "rocksdb/convenience.h"
#include "utilities/fault_injection_fs.h"
@@ -92,6 +93,15 @@ int db_stress_tool(int argc, char** argv) {
fault_env_guard =
std::make_shared<CompositeEnvWrapper>(raw_env, fault_fs_guard);
raw_env = fault_env_guard.get();
// Register a crash callback so that recently injected errors are
// printed to stderr when the process crashes (SIGABRT, SIGSEGV, etc.).
// This helps diagnose stress test failures caused by fault injection.
port::RegisterCrashCallback([]() {
if (fault_fs_guard) {
fault_fs_guard->PrintRecentInjectedErrors();
}
});
}
auto db_stress_fs =
@@ -219,6 +229,22 @@ int db_stress_tool(int argc, char** argv) {
FLAGS_atomic_flush = true;
}
// Trie UDI supports Seek, Next, and SeekToFirst, but not SeekToLast,
// SeekForPrev, or Prev. Disable backward scan testing.
if (FLAGS_use_trie_index) {
FLAGS_test_backward_scan = false;
}
// Trie UDI uses zero-copy pointers into block data, which is
// incompatible with mmap_read.
if (FLAGS_use_trie_index && FLAGS_mmap_read) {
fprintf(stderr,
"Error: use_trie_index is incompatible with mmap_read. "
"The trie index uses zero-copy pointers into block data "
"which is unsafe with mmap'd reads.\n");
exit(1);
}
if (FLAGS_read_only) {
if (FLAGS_writepercent != 0 || FLAGS_delpercent != 0 ||
FLAGS_delrangepercent != 0) {
@@ -240,6 +266,25 @@ int db_stress_tool(int argc, char** argv) {
FLAGS_db = default_db_path;
}
// Now that FLAGS_db is resolved, set the fault injection log file path
// so that PrintAll() writes to a file instead of stderr (signal-safe).
// Store the log in TEST_TMPDIR (outside the DB directory) so it survives
// DB reopen (which cleans untracked files) and gets included in the
// sandcastle db.tar.gz artifact for post-failure analysis.
if (fault_fs_guard) {
std::string log_dir;
const char* test_tmpdir = getenv("TEST_TMPDIR");
if (test_tmpdir && test_tmpdir[0] != '\0') {
log_dir = test_tmpdir;
} else {
log_dir = "/tmp";
}
std::string log_path = log_dir + "/fault_injection_" +
std::to_string(getpid()) + "_" +
std::to_string(time(nullptr)) + ".log";
fault_fs_guard->SetInjectedErrorLogPath(log_path);
}
if ((FLAGS_test_secondary || FLAGS_continuous_verification_interval > 0) &&
FLAGS_secondaries_base.empty()) {
std::string default_secondaries_path;
+87 -79
View File
@@ -543,7 +543,10 @@ class NonBatchedOpsStressTest : public StressTest {
}
std::unique_ptr<Iterator> iter(
secondary_db_->NewIterator(read_opts, handle));
uint32_t rnd = (thread->rand.Next()) % 4;
// Skip SeekToFirst, SeekToLast, SeekForPrev, and Prev when backward
// scan is disabled.
uint32_t rnd =
(!FLAGS_test_backward_scan) ? 2 : (thread->rand.Next()) % 4;
if (0 == rnd) {
// SeekToFirst() + Next()*5
read_opts.total_order_seek = true;
@@ -1905,13 +1908,7 @@ class NonBatchedOpsStressTest : public StressTest {
} while (!s.ok() && IsErrorInjectedAndRetryable(s) &&
initial_wal_write_may_succeed);
if ((s.IsDeadlock() || s.IsTimedOut()) &&
(FLAGS_use_multiget || FLAGS_use_multi_get_entity)) {
// Deadlock or timeout is ok, when multi get is tested. Because multi get
// tests execute MaybeAddKeyToTxnForRYW function which writes to the
// same key space but does not acquire stress test level mutex. So it is
// possible RocksDB returns deadlock or timeout. Return OK() for these
// cases
if (IsExpectedTxnError(s)) {
pending_expected_value.Rollback();
return Status::OK();
}
@@ -2016,6 +2013,11 @@ class NonBatchedOpsStressTest : public StressTest {
} while (!s.ok() && IsErrorInjectedAndRetryable(s) &&
initial_wal_write_may_succeed);
if (IsExpectedTxnError(s)) {
pending_expected_value.Rollback();
return Status::OK();
}
if (!s.ok()) {
pending_expected_value.Rollback();
if (IsErrorInjectedAndRetryable(s)) {
@@ -2083,6 +2085,11 @@ class NonBatchedOpsStressTest : public StressTest {
} while (!s.ok() && IsErrorInjectedAndRetryable(s) &&
initial_wal_write_may_succeed);
if (IsExpectedTxnError(s)) {
pending_expected_value.Rollback();
return Status::OK();
}
if (!s.ok()) {
pending_expected_value.Rollback();
if (IsErrorInjectedAndRetryable(s)) {
@@ -2656,76 +2663,78 @@ class NonBatchedOpsStressTest : public StressTest {
op_logs += "N";
}
// backward scan
key_str = Key(ub - 1);
iter->SeekForPrev(key_str);
// backward scan — skip when backward iteration is not supported
if (FLAGS_test_backward_scan) {
key_str = Key(ub - 1);
iter->SeekForPrev(key_str);
op_logs += " SFP " + Slice(key_str).ToString(true) + " ";
op_logs += " SFP " + Slice(key_str).ToString(true) + " ";
last_key = ub;
while (true) {
assert(lb < last_key);
last_key = ub;
while (true) {
assert(lb < last_key);
if (iter->Valid() && ro.allow_unprepared_value) {
op_logs += "*";
if (iter->Valid() && ro.allow_unprepared_value) {
op_logs += "*";
if (!iter->PrepareValue()) {
assert(!iter->Valid());
assert(!iter->status().ok());
}
}
if (!iter->Valid()) {
if (!iter->status().ok()) {
if (IsErrorInjectedAndRetryable(iter->status())) {
return iter->status();
} else {
thread->shared->SetVerificationFailure();
fprintf(stderr, "TestIterate against expected state error: %s\n",
iter->status().ToString().c_str());
fprintf(stderr, "Column family: %s, op_logs: %s\n",
cfh->GetName().c_str(), op_logs.c_str());
thread->stats.AddErrors(1);
return iter->status();
if (!iter->PrepareValue()) {
assert(!iter->Valid());
assert(!iter->status().ok());
}
}
if (!check_no_key_in_range(lb, last_key)) {
if (!iter->Valid()) {
if (!iter->status().ok()) {
if (IsErrorInjectedAndRetryable(iter->status())) {
return iter->status();
} else {
thread->shared->SetVerificationFailure();
fprintf(stderr, "TestIterate against expected state error: %s\n",
iter->status().ToString().c_str());
fprintf(stderr, "Column family: %s, op_logs: %s\n",
cfh->GetName().c_str(), op_logs.c_str());
thread->stats.AddErrors(1);
return iter->status();
}
}
if (!check_no_key_in_range(lb, last_key)) {
return Status::OK();
}
break;
}
if (!check_columns()) {
return Status::OK();
}
break;
}
if (!check_columns()) {
return Status::OK();
}
// the range (current key, last key) was skipped
GetIntVal(iter->key().ToString(), &curr);
if (last_key <= static_cast<int64_t>(curr)) {
thread->shared->SetVerificationFailure();
fprintf(stderr,
"TestIterateAgainstExpected failed: found unexpectedly large "
"key\n");
fprintf(stderr, "Column family: %s, op_logs: %s\n",
cfh->GetName().c_str(), op_logs.c_str());
fprintf(stderr, "Last op found key: %s, expected at most: %s\n",
Slice(Key(curr)).ToString(true).c_str(),
Slice(Key(last_key - 1)).ToString(true).c_str());
thread->stats.AddErrors(1);
return Status::OK();
}
if (!check_no_key_in_range(static_cast<int64_t>(curr + 1), last_key)) {
return Status::OK();
}
// the range (current key, last key) was skipped
GetIntVal(iter->key().ToString(), &curr);
if (last_key <= static_cast<int64_t>(curr)) {
thread->shared->SetVerificationFailure();
fprintf(stderr,
"TestIterateAgainstExpected failed: found unexpectedly large "
"key\n");
fprintf(stderr, "Column family: %s, op_logs: %s\n",
cfh->GetName().c_str(), op_logs.c_str());
fprintf(stderr, "Last op found key: %s, expected at most: %s\n",
Slice(Key(curr)).ToString(true).c_str(),
Slice(Key(last_key - 1)).ToString(true).c_str());
thread->stats.AddErrors(1);
return Status::OK();
}
if (!check_no_key_in_range(static_cast<int64_t>(curr + 1), last_key)) {
return Status::OK();
}
last_key = static_cast<int64_t>(curr);
if (last_key <= lb) {
break;
}
last_key = static_cast<int64_t>(curr);
if (last_key <= lb) {
break;
iter->Prev();
op_logs += "P";
}
iter->Prev();
op_logs += "P";
}
// Write-prepared/write-unprepared transactions and multi-CF iterator do not
@@ -2767,7 +2776,8 @@ class NonBatchedOpsStressTest : public StressTest {
key_str = Key(mid);
const Slice key(key_str);
if (thread->rand.OneIn(2)) {
// Skip SeekForPrev and Prev when backward scan is not supported.
if (!FLAGS_test_backward_scan || thread->rand.OneIn(2)) {
iter->Seek(key);
op_logs += " S " + key.ToString(true) + " ";
if (!iter->Valid() && iter->status().ok()) {
@@ -2836,8 +2846,14 @@ class NonBatchedOpsStressTest : public StressTest {
iter->Next();
op_logs += "N";
} else if (static_cast<int64_t>(curr) >= ub) {
iter->Prev();
op_logs += "P";
// Use Next when backward scan is not supported.
if (!FLAGS_test_backward_scan) {
iter->Next();
op_logs += "N";
} else {
iter->Prev();
op_logs += "P";
}
} else {
const uint32_t value_base_from_db = GetValueBase(iter->value());
std::size_t index = static_cast<std::size_t>(curr - lb);
@@ -2864,7 +2880,7 @@ class NonBatchedOpsStressTest : public StressTest {
break;
}
if (thread->rand.OneIn(2)) {
if (!FLAGS_test_backward_scan || thread->rand.OneIn(2)) {
iter->Next();
op_logs += "N";
if (!iter->Valid()) {
@@ -3209,15 +3225,7 @@ class NonBatchedOpsStressTest : public StressTest {
assert(false);
}
// It is possible that multiple thread concurrently try to write to the
// same key, which could cause lock timeout or deadlock in the
// transactiondb layer, before transaction is rolled back.
// E.g.
// Timestamp 1: Transaction A: lock key M for write
// Timestamp 2: Transaction B: lock key N for write
// Timestamp 3: Transaction B: try to lock key M for write -> wait
// Timestamp 4: Transaction A: try to lock key N for write -> deadlock
if (s.IsTimedOut() || s.IsDeadlock()) {
if (IsExpectedTxnError(s)) {
return;
}
-9
View File
@@ -1,15 +1,6 @@
<!DOCTYPE html>
<html>
{% include head.html %}
<div class="socialBanner">
<div>
Support Ukraine 🇺🇦
<a href="https://opensource.facebook.com/support-ukraine">
Help Provide Humanitarian Aid to Ukraine
</a>
.
</div>
</div>
<body>
{% include nav.html alwayson=true %}
<div class="navPusher">
+5 -2
View File
@@ -1940,7 +1940,10 @@ IOStatus PosixDirectory::FsyncWithDirOptions(
assert(fd_ >= 0); // Check use after close
IOStatus s = IOStatus::OK();
#ifndef OS_AIX
if (is_btrfs_) {
bool test_is_btrfs = is_btrfs_;
TEST_SYNC_POINT_CALLBACK("PosixDirectory::FsyncWithDirOptions:ForceBtrfs",
&test_is_btrfs);
if (test_is_btrfs) {
// skip dir fsync for new file creation, which is not needed for btrfs
if (dir_fsync_options.reason == DirFsyncOptions::kNewFileSynced) {
return s;
@@ -1959,7 +1962,7 @@ IOStatus PosixDirectory::FsyncWithDirOptions(
} else if (fsync(fd) < 0) {
s = IOError("While fsync renaming file", new_name, errno);
}
if (close(fd) < 0) {
if (fd >= 0 && close(fd) < 0) {
s = IOError("While closing file after fsync", new_name, errno);
}
return s;
+43
View File
@@ -3,11 +3,13 @@
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#include "test_util/sync_point.h"
#include "test_util/testharness.h"
#include "util/random.h"
#ifdef ROCKSDB_LIB_IO_POSIX
#include "env/io_posix.h"
#include "rocksdb/file_system.h"
namespace ROCKSDB_NAMESPACE {
@@ -174,6 +176,47 @@ TEST_F(PosixWritableFileTest, SeekAfterExtend) {
ASSERT_OK(fs->DeleteFile(path, IOOptions(), nullptr));
}
#ifdef OS_LINUX
class PosixDirectoryTest : public testing::Test {};
TEST_F(PosixDirectoryTest, BtrfsFsyncFailedOpenDoesNotCloseInvalidFd) {
// When FsyncWithDirOptions is called with kFileRenamed on a btrfs filesystem
// and open() fails, close(-1) should not be called. Without the fix,
// close(-1) is called which is POSIX undefined behavior and overwrites the
// meaningful open error with a misleading "While closing file after fsync".
std::shared_ptr<FileSystem> fs = FileSystem::Default();
std::string dir_path =
test::PerThreadDBPath("PosixDirectoryTest_BtrfsFsyncFailedOpen");
ASSERT_OK(fs->CreateDirIfMissing(dir_path, IOOptions(), nullptr));
std::unique_ptr<FSDirectory> dir;
ASSERT_OK(fs->NewDirectory(dir_path, IOOptions(), &dir, nullptr));
// Force the btrfs code path via sync point
SyncPoint::GetInstance()->SetCallBack(
"PosixDirectory::FsyncWithDirOptions:ForceBtrfs",
[](void* arg) { *static_cast<bool*>(arg) = true; });
SyncPoint::GetInstance()->EnableProcessing();
// Call FsyncWithDirOptions with a non-existent file for rename sync.
// open() will fail since the file doesn't exist.
DirFsyncOptions opts(std::string(dir_path + "/nonexistent_file"));
IOStatus s = dir->FsyncWithDirOptions(IOOptions(), nullptr, opts);
// Should get an error about open failing, NOT about closing
ASSERT_TRUE(s.IsIOError());
// The error message should mention "open", not "closing"
ASSERT_TRUE(s.ToString().find("open") != std::string::npos);
ASSERT_TRUE(s.ToString().find("closing") == std::string::npos);
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
ASSERT_OK(dir->Close(IOOptions(), nullptr));
ASSERT_OK(fs->DeleteDir(dir_path, IOOptions(), nullptr));
}
#endif
} // namespace ROCKSDB_NAMESPACE
#endif
+1 -1
View File
@@ -351,7 +351,7 @@ void DeleteScheduler::BackgroundEmptyTrash() {
auto iter = pending_files_in_buckets_.find(bucket.value());
assert(iter != pending_files_in_buckets_.end());
if (iter != pending_files_in_buckets_.end()) {
pending_files_in_bucket = iter->second--;
pending_files_in_bucket = --iter->second;
}
}
}
+108
View File
@@ -7,6 +7,7 @@
#include <atomic>
#include <cinttypes>
#include <future>
#include <thread>
#include <vector>
@@ -856,6 +857,113 @@ TEST_F(DeleteSchedulerTest,
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
}
TEST_F(DeleteSchedulerTest, BucketSignalOnSingleFileCompletion) {
// Test that WaitForEmptyTrashBucket wakes up correctly when a single file
// in a bucket is deleted. This requires the pre-decrement fix: with
// post-decrement, the pending count is checked before it reaches zero,
// so the signal is never fired for the bucket, causing a hang.
//
// Strategy:
// 1. Block the background thread inside DeleteTrashFile for file 0 (mu_ is
// NOT held at this point, since it's unlocked before calling
// DeleteTrashFile).
// 2. Start WaitForEmptyTrashBucket(bucket0) on another thread. Since mu_ is
// free, it acquires mu_, sees bucket0's pending count == 1, enters
// cv_.Wait() (which releases mu_).
// 3. Unblock file 0's deletion. The background thread completes
// DeleteTrashFile, re-acquires mu_, decrements the bucket counter. With
// the post-decrement bug, pending_files_in_bucket gets 1 (old value),
// so cv_.SignalAll() is NOT called. The wait thread stays stuck.
// 4. Also block file 1 inside DeleteTrashFile to keep pending_files_ > 0,
// preventing the global pending_files_ == 0 check from triggering a
// signal.
// 5. Assert the wait thread times out (bug) or completes (fix).
rate_bytes_per_sec_ = 1024 * 1024; // 1 MB / s
NewDeleteScheduler();
std::atomic<int> delete_calls{0};
std::atomic<bool> unblock_file0{false};
std::atomic<bool> unblock_file1{false};
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"DeleteScheduler::DeleteTrashFile:DeleteFile", [&](void* /*arg*/) {
int n = delete_calls.fetch_add(1) + 1;
if (n == 1) {
// Block file 0's deletion until we've set up the wait thread
while (!unblock_file0.load()) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
} else if (n == 2) {
// Block file 1's deletion to keep pending_files_ > 0
while (!unblock_file1.load()) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
}
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
// Bucket 0: single file
std::optional<int32_t> bucket0 = delete_scheduler_->NewTrashBucket();
ASSERT_TRUE(bucket0.has_value());
std::string file0 =
NewDummyFile("bucket_signal_file0.data", 1024, 0, /*track=*/false);
ASSERT_OK(delete_scheduler_->DeleteUnaccountedFile(file0, "",
/*force_bg=*/false,
bucket0));
// Bucket 1: single file (keeps pending_files_ > 0 when bucket 0 empties)
std::optional<int32_t> bucket1 = delete_scheduler_->NewTrashBucket();
ASSERT_TRUE(bucket1.has_value());
std::string file1 =
NewDummyFile("bucket_signal_file1.data", 1024, 0, /*track=*/false);
ASSERT_OK(delete_scheduler_->DeleteUnaccountedFile(file1, "",
/*force_bg=*/false,
bucket1));
// Wait for the background thread to reach the DeleteFile syncpoint for
// file 0. At this point mu_ is NOT held by the background thread.
while (delete_calls.load() < 1) {
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
// Start waiting for bucket 0 on another thread. Since the background thread
// doesn't hold mu_, this thread acquires mu_, sees iter->second == 1,
// and enters cv_.Wait() (releasing mu_).
std::atomic<bool> wait_done{false};
auto wait_future = std::async(std::launch::async, [&]() {
delete_scheduler_->WaitForEmptyTrashBucket(bucket0.value());
wait_done.store(true);
});
// Give the wait thread time to enter cv_.Wait(). We need it to be blocked
// in cv_.Wait() before unblocking the background thread.
std::this_thread::sleep_for(std::chrono::milliseconds(100));
// Unblock file 0's deletion. The background thread will:
// - Complete DeleteTrashFile, re-acquire mu_
// - Decrement pending_files_ (2 -> 1) and bucket0 counter (1 -> 0)
// - With bug (post-decrement): pending_files_in_bucket = 1, no signal fired
// - With fix (pre-decrement): pending_files_in_bucket = 0, signal fired
// Then it moves to file 1 and blocks at the second DeleteFile syncpoint.
unblock_file0.store(true);
// Check if WaitForEmptyTrashBucket returns within 5 seconds.
// With the bug: no signal fired, wait thread stays stuck -> timeout
// With the fix: signal fired, wait thread wakes up -> completes quickly
auto status = wait_future.wait_for(std::chrono::seconds(5));
ASSERT_EQ(status, std::future_status::ready)
<< "WaitForEmptyTrashBucket(bucket0) timed out - bucket signal not fired";
// Unblock file 1 and wait for bucket 1
unblock_file1.store(true);
delete_scheduler_->WaitForEmptyTrashBucket(bucket1.value());
ASSERT_EQ(0, delete_scheduler_->GetTotalTrashSize());
ASSERT_GE(delete_calls.load(), 2);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
+10 -2
View File
@@ -3221,7 +3221,11 @@ class FilePrefetchBufferTest : public testing::Test {
stats_ = CreateDBStatistics();
}
void TearDown() override { EXPECT_OK(DestroyDir(env_, test_dir_)); }
void TearDown() override {
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
EXPECT_OK(DestroyDir(env_, test_dir_));
}
void Write(const std::string& fname, const std::string& content) {
std::unique_ptr<FSWritableFile> f;
@@ -3747,7 +3751,11 @@ class FSBufferPrefetchTest
stats_ = CreateDBStatistics();
}
void TearDown() override { EXPECT_OK(DestroyDir(env_, test_dir_)); }
void TearDown() override {
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
EXPECT_OK(DestroyDir(env_, test_dir_));
}
void Write(const std::string& fname, const std::string& content) {
std::unique_ptr<FSWritableFile> f;
+1 -1
View File
@@ -98,7 +98,7 @@ endif # FMT_SOURCE_PATH
PLATFORM_LDFLAGS += -lglog
endif
FOLLY_COMMIT_HASH = 1e8ce1e5d35acff7b78fedbca3e7311b39f43529
FOLLY_COMMIT_HASH = 3b0eed0a128c484a2b077546c3e08c824a311357
# For public CI runs, checkout folly in a way that can build with RocksDB.
# This is mostly intended as a test-only simulation of Meta-internal folly
+13 -2
View File
@@ -213,8 +213,7 @@ enum class VerifyOutputFlags : uint32_t {
// by comparing the one inserted into a
// file, and what is read back.
// TODO - Implement
// kVerifyFileChecksum = 1 << 2, // Verify file-level checksum
kVerifyFileChecksum = 1 << 2, // Verify file-level checksum
// Second set of bits: when to enable verification
kEnableForLocalCompaction = 1 << 10, // Enable for local compaction
@@ -1306,6 +1305,18 @@ struct AdvancedColumnFamilyOptions {
// Immutable.
bool cf_allow_ingest_behind = false;
// If true, use batch lookup optimization for memtable MultiGet. For skip
// list memtables, after looking up each key, the search path is cached and
// reused for the next key, reducing per-key cost from O(log N) to O(log d)
// where d is the distance between consecutive keys.
//
// This optimization exploits the fact that MultiGet keys are sorted.
// Non-skip-list memtable implementations fall back to per-key lookups.
//
// Default: false
// Immutable.
bool memtable_batch_lookup_optimization = false;
// Create ColumnFamilyOptions with default values for all fields
AdvancedColumnFamilyOptions();
// Create ColumnFamilyOptions from Options
+10 -1
View File
@@ -1479,6 +1479,10 @@ extern ROCKSDB_LIBRARY_API void rocksdb_options_set_paranoid_checks(
rocksdb_options_t*, unsigned char);
extern ROCKSDB_LIBRARY_API unsigned char rocksdb_options_get_paranoid_checks(
rocksdb_options_t*);
extern ROCKSDB_LIBRARY_API void rocksdb_options_set_open_files_async(
rocksdb_options_t*, unsigned char);
extern ROCKSDB_LIBRARY_API unsigned char rocksdb_options_get_open_files_async(
rocksdb_options_t*);
extern ROCKSDB_LIBRARY_API void rocksdb_options_set_db_paths(
rocksdb_options_t*, const rocksdb_dbpath_t** path_values, size_t num_paths);
extern ROCKSDB_LIBRARY_API void rocksdb_options_set_cf_paths(
@@ -2177,7 +2181,12 @@ enum {
rocksdb_internal_range_del_reseek_count,
rocksdb_block_read_cpu_time,
rocksdb_internal_merge_point_lookup_count,
rocksdb_total_metric_count = 80
rocksdb_data_block_read_byte,
rocksdb_index_block_read_byte,
rocksdb_filter_block_read_byte,
rocksdb_compression_dict_block_read_byte,
rocksdb_metadata_block_read_byte,
rocksdb_total_metric_count = 85
};
extern ROCKSDB_LIBRARY_API void rocksdb_set_perf_level(int);
+2 -2
View File
@@ -115,14 +115,14 @@ class ExternalTableReader {
// Point lookup the given key and return its value
virtual Status Get(const ReadOptions& read_options, const Slice& key,
const SliceTransform* prefix_extractor,
std::string* value) = 0;
PinnableSlice* value) = 0;
// Point lookup the given vector of keys and return the values, as well
// as status of each individual lookup in statuses.
virtual void MultiGet(const ReadOptions& read_options,
const std::vector<Slice>& keys,
const SliceTransform* prefix_extractor,
std::vector<std::string>* values,
std::vector<PinnableSlice>* values,
std::vector<Status>* statuses) = 0;
// Allocate and return the contents of the properties block. If the builder
+40
View File
@@ -201,6 +201,7 @@ enum class BackgroundErrorReason {
kManifestWrite,
kFlushNoWAL,
kManifestWriteNoWAL,
kAsyncFileOpen,
};
struct WriteStallInfo {
@@ -553,6 +554,34 @@ struct IOErrorInfo {
uint64_t offset;
};
// EXPERIMENTAL — under active development, fields may change.
// Point-in-time snapshot of background job pressure for one DB: how busy
// compaction and flush are, and how close the DB is to write-stalling.
struct BackgroundJobPressure {
// Compaction scheduling (LOW + BOTTOM priority combined)
int compaction_scheduled = 0;
int compaction_running = 0;
// Per-priority compaction breakdown
int compaction_low_scheduled = 0;
int compaction_low_running = 0;
int compaction_bottom_scheduled = 0;
int compaction_bottom_running = 0;
// Flush scheduling
int flush_scheduled = 0;
int flush_running = 0;
// How close the DB is to a write stall, as a percentage (0 = healthy,
// 100 = at stall threshold). Can exceed 100 when already stalling.
// Max across all column families based on write-stall triggers.
int write_stall_proximity_pct = 0;
// Whether RocksDB has activated compaction speedup due to write pressure
bool compaction_speedup_active = false;
bool operator==(const BackgroundJobPressure&) const = default;
};
// EventListener class contains a set of callback functions that will
// be called when specific RocksDB event happens such as flush. It can
// be used as a building block for developing custom features such as
@@ -869,6 +898,17 @@ class EventListener : public Customizable {
// happens. ShouldBeNotifiedOnFileIO should be set to true to get a callback.
virtual void OnIOError(const IOErrorInfo& /*info*/) {}
// EXPERIMENTAL
// Called after a flush or compaction background job completes, providing a
// snapshot of current background job scheduling pressure and write-stall
// proximity. Fires on the background thread that completed the job, without
// holding db_mutex_. This callback fires on every completion, even if
// pressure values have not changed from the previous call.
// Implementations should not run for an extended period of time before
// returning, as this blocks RocksDB background work.
virtual void OnBackgroundJobPressureChanged(
DB* /*db*/, const BackgroundJobPressure& /*pressure*/) {}
~EventListener() override {}
};
+20
View File
@@ -211,6 +211,26 @@ class MemTableRep {
return Status::NotSupported("GetAndValidate() not implemented.");
}
// Batch lookup of multiple sorted keys. For each key, finds the first
// matching entry and calls callback_func with callback_args[i] and the
// entry. Continues calling for subsequent entries until callback_func
// returns false.
//
// Keys must be memtable-encoded and in non-decreasing order. Implementations
// may exploit the sorted key order for more efficient lookups.
//
// When detect_key_out_of_order is true, validates key ordering during
// traversal and returns Corruption if out-of-order keys are found.
// When key_validation_callback is non-null, calls it on each visited node.
//
// Default implementation calls Get() per key via an iterator.
virtual Status MultiGet(
size_t num_keys, const char* const* keys, void** callback_args,
bool (*callback_func)(void* arg, const char* entry),
bool allow_data_in_errors = false, bool detect_key_out_of_order = false,
const std::function<Status(const char*, bool)>& key_validation_callback =
nullptr);
virtual uint64_t ApproximateNumEntries(const Slice& /*start_ikey*/,
const Slice& /*end_key*/) {
return 0;
+51 -3
View File
@@ -783,6 +783,25 @@ struct DBOptions {
// Default: 16
int max_file_opening_threads = 16;
// If true, SST files are opened and validated asynchronously in the
// background after DB::Open returns. This reduces DB open time for
// databases with many SST files and high latency file systems. Mostly useful
// when max_open_files = -1, as max_open_files != -1 usually has fast open
// times. See also `max_file_opening_threads` and
// `skip_stats_update_on_db_open` to improve file open latency.
//
// Note: This option is currently not compatible with FIFO compaction and
// requires skip_stats_update_on_db_open=true.
//
// Errors will no longer show up in DB::Open, but instead can show up as
// either background errors and/or operations that access the file (e.g.
// reads, compactions).
//
// When false (default), SST files are opened and validated during DB::Open.
//
// Default: false
bool open_files_async = false;
// Once write-ahead logs exceed this size, we will start forcing the flush of
// column families whose memtables are backed by the oldest live WAL file
// (i.e. the ones that are causing all the space amplification). If set to 0
@@ -975,6 +994,13 @@ struct DBOptions {
// manifest write (e.g. completed DB compaction or flush).
uint64_t max_manifest_file_size = 1024 * 1024 * 1024;
// If true, on DB close, read back the entire MANIFEST file and validate
// CRC checksums and logical record content. If corruption is detected,
// a fresh MANIFEST is written from in-memory state before closing.
//
// This option is mutable with SetDBOptions().
bool verify_manifest_content_on_close = false;
// This option mostly replaces max_manifest_file_size to control an auto-tuned
// balance of manifest write amplification and space amplification. A new
// manifest file is created with the "compacted" contents of the old one when
@@ -1392,9 +1418,30 @@ struct DBOptions {
// WAL logs will be kept, so that if crash happened before flush, we still
// have logs to recover from.
//
// Note: when `enforce_write_buffer_manager_during_recovery` is also enabled,
// flushes may still occur during recovery to respect the
// WriteBufferManager's global memory limit, even if this option is true.
// Once any such WBM-triggered flush happens, all remaining memtables will
// also be flushed at the end of recovery (similar to the behavior when this
// option is false).
//
// DEFAULT: false
bool avoid_flush_during_recovery = false;
// If true and a WriteBufferManager is configured, RocksDB will check
// WriteBufferManager::ShouldFlush() during WAL recovery and schedule
// flushes when needed. This prevents OOM when multiple RocksDB instances
// share a WriteBufferManager and one instance is recovering from WAL.
//
// When triggered, all column families with non-empty memtables are scheduled
// for flush, which may produce smaller L0 files in some column families.
// This also overrides `avoid_flush_during_recovery`: once a WBM-triggered
// flush occurs mid-recovery, all remaining non-empty memtables will be
// flushed at the end of recovery as well.
//
// DEFAULT: true
bool enforce_write_buffer_manager_during_recovery = true;
// By default RocksDB will flush all memtables on DB close if there are
// unpersisted data (i.e. with WAL disabled) The flush can be skip to speedup
// DB close. Unpersisted data WILL BE LOST.
@@ -2235,9 +2282,10 @@ struct ReadOptions {
// block based table index. The table_factory used for the column family
// must support building/reading this index.
//
// Currently, only forward scans are supported. For forward scans, only Seek()
// is supported. SeekToFirst() is not supported. If the caller wishes to scan
// from start to end, the native index must be used.
// Forward scans (SeekToFirst, Seek, Next) and point lookups (Get) are
// supported. Reverse operations (SeekToLast, SeekForPrev, Prev) are not
// yet supported and will return NotSupported when this is set. Leave this
// null to use the native index for reverse operations.
const UserDefinedIndexFactory* table_index_factory = nullptr;
// *** END options only relevant to iterators or scans ***
+10 -1
View File
@@ -106,7 +106,8 @@ struct PerfContextBase {
uint64_t compressed_sec_cache_compressed_bytes;
uint64_t block_checksum_time; // total nanos spent on block checksum
uint64_t block_decompress_time; // total nanos spent on block decompression
uint64_t block_decompress_time; // total nanos spent on block decompression
uint64_t block_decompress_count; // total number of block decompressions
uint64_t get_read_bytes; // bytes for vals returned by Get
uint64_t multiget_read_bytes; // bytes for vals returned by MultiGet
@@ -290,6 +291,14 @@ struct PerfContextBase {
uint64_t file_ingestion_nanos;
// Time IngestExternalFile blocked live writes.
uint64_t file_ingestion_blocking_live_writes_nanos;
// Bytes read from storage for each block category. These add up to
// block_read_byte.
uint64_t data_block_read_byte;
uint64_t index_block_read_byte;
uint64_t filter_block_read_byte;
uint64_t compression_dict_block_read_byte;
uint64_t metadata_block_read_byte;
};
struct PerfContext : public PerfContextBase {
+13
View File
@@ -35,6 +35,19 @@ class SstFileReader {
const std::vector<Slice>& keys,
std::vector<std::string>* values);
// MultiGet variant that returns PinnableSlice values, enabling zero-copy
// when the underlying TableReader supports pinning.
std::vector<Status> MultiGet(const ReadOptions& options,
const std::vector<Slice>& keys,
std::vector<PinnableSlice>* values);
// Point lookup a single key from the SST.
Status Get(const ReadOptions& options, const Slice& key, std::string* value);
// Point lookup variant that returns a PinnableSlice.
Status Get(const ReadOptions& options, const Slice& key,
PinnableSlice* value);
// Returns a new iterator over the table contents as a raw table iterator,
// a.k.a a `TableIterator`that iterates all point data entries in the table
// including logically invisible entries like delete entries.
+5
View File
@@ -730,6 +730,11 @@ enum Histograms : uint32_t {
// Distribution of blocks prefetched per MultiScan Prepare()
MULTISCAN_BLOCKS_PER_PREPARE,
// Coefficient of variation of key gaps in blocks, scaled by 10000
// (e.g., CV of 0.4532 is recorded as 4532). Currently only used by index
// blocks for uniform key distribution tracking.
BLOCK_KEY_DISTRIBUTION_CV,
HISTOGRAM_ENUM_MAX
};
+49 -3
View File
@@ -264,6 +264,9 @@ struct BlockBasedTableOptions {
IndexType index_type = kBinarySearch;
// The search algorithm used when seeking to entries in the index block.
//
// Note: This option is only used at read time and is compatible with any type
// of block.
enum BlockSearchType : char {
// Standard binary search
kBinary = 0x00,
@@ -274,6 +277,16 @@ struct BlockBasedTableOptions {
// succesor can skew the end key and make interpolation search significantly
// less performant.
kInterpolation = 0x01,
// See `uniform_cv_threshold`. On the write path if `uniform_cv_threshold`
// >= 0, then it is possible for a block to be marked as "is_uniform=true"
// in the block footer via bit flag. On files from older versions or
// produced via `uniform_cv_threshold` < 0, blocks are always marked as
// "is_uniform=false".
//
// When kAuto is used, the search algorithm will use interpolation search if
// "is_uniform" flag is set in the block footer, otherwise it will use
// binary search.
kAuto = 0x02,
};
BlockSearchType index_block_search_type = kBinary;
@@ -620,6 +633,22 @@ struct BlockBasedTableOptions {
// Default: false
bool separate_key_value_in_data_block = false;
// Coefficient of variation (CV) threshold used to determine if keys in an
// index block are uniformly distributed. Lower CV means more "uniform", and
// the more likely interpolation search will outperform binary search.
//
// On the write path, if the CV of key gaps in an index
// block is less than this threshold, the "is_uniform" hint is set in that
// block's footer. To disable (i.e. always have "is_uniform=false"), set value
// to -1.
//
// On the read path, if `BlockSearchType::kAuto` is set, then it will use the
// is_uniform hint to select an appropriate search algorithm for the block.
//
// NOTE: Currently only supports index blocks. May update to include data
// blocks in the future.
double uniform_cv_threshold = -1;
// Store index blocks on disk in compressed format. Changing this option to
// false will avoid the overhead of decompression if index blocks are evicted
// and read back
@@ -710,17 +739,34 @@ struct BlockBasedTableOptions {
// If enabled, prepopulate warm/hot blocks (data, uncompressed dict, index and
// filter blocks) which are already in memory into block cache at the time of
// flush. On a flush, the block that is in memory (in memtables) get flushed
// to the device. If using Direct IO, additional IO is incurred to read this
// data back into memory again, which is avoided by enabling this option. This
// flush or compaction.
//
// On a flush, the data block that is in memory (in memtables) gets flushed to
// the device. If using Direct IO, additional IO is incurred to read this data
// back into memory again, which is avoided by enabling this option. This
// further helps if the workload exhibits high temporal locality, where most
// of the reads go to recently written data. This also helps in case of
// Distributed FileSystem.
//
// On a compaction, output SST files are written to disk but not placed in the
// block cache by default. With tiered or remote storage (e.g., HDFS, S3),
// reading recently compacted data back incurs high latency.
// Enabling compaction warming avoids these cold reads. However, unlike flush
// output, it is hard to distinguish hot from cold blocks in compaction
// output, so warming all of it risks polluting the cache. To mitigate this,
// compaction-warmed blocks are inserted at BOTTOM priority (vs LOW for flush)
// so they are evicted first under cache pressure. Even so,
// kFlushAndCompaction is recommended only when most or all of the database is
// expected to reside in cache. For workloads where only a fraction of the
// data is hot, kFlushOnly is the safer choice.
enum class PrepopulateBlockCache : char {
// Disable prepopulate block cache.
kDisable,
// Prepopulate blocks during flush only.
kFlushOnly,
// Prepopulate blocks during flush and compaction. Flush-warmed blocks are
// inserted at LOW priority, compaction-warmed blocks at BOTTOM priority.
kFlushAndCompaction,
};
PrepopulateBlockCache prepopulate_block_cache =
+106 -46
View File
@@ -16,30 +16,34 @@
#include "rocksdb/options.h"
#include "rocksdb/slice.h"
#include "rocksdb/status.h"
#include "rocksdb/types.h"
namespace ROCKSDB_NAMESPACE {
// Prefix for user-defined index block names
inline const std::string kUserDefinedIndexPrefix =
inline constexpr const char* kUserDefinedIndexPrefix =
"rocksdb.user_defined_index.";
// This is a public API for user-defined index builders.
// It allows users to define their own index format and build custom
// indexes during table building. Currently, only a monolithic index
// block is supported (no partitioned index).
//
// This is currently supported only for a restricted set of use cases. The
// CF must be ingest only, and only files containing Puts generated by
// SstFileWriter are supported.
// The interface for building user-defined index.
class UserDefinedIndexBuilder {
public:
// Right now, we only support Puts. In the future, we may support merges,
// deletions etc.
enum ValueType {
kValue,
kTypeMax,
// Indicates the type of key-value entry being added via OnKeyAdded().
// UDI builders that only use AddIndexEntry() (e.g., trie-based indexes)
// can safely ignore this.
enum ValueType : uint8_t {
kValue = 0, // Put: the value is the full user value.
kDelete = 1, // Deletion (Delete, SingleDelete, or DeleteWithTimestamp):
// the value is typically empty.
kMerge = 2, // Merge operand: the value is a partial update.
kOther = 3, // Other types (e.g., blob reference, wide-column entity).
// The value format is type-specific and may not be the
// actual user data.
kTypeMax, // Sentinel — must be last. Value may change across releases.
};
// File offset and size of the data block
@@ -48,41 +52,68 @@ class UserDefinedIndexBuilder {
uint64_t size;
};
// Optional context for AddIndexEntry providing sequence numbers at block
// boundaries. Passed as a struct for forward-compatible extensibility
// (new fields can be added without breaking existing implementations).
struct IndexEntryContext {
// Sequence number of last_key_in_current_block.
SequenceNumber last_key_seq = 0;
// Sequence number of first_key_in_next_block (valid only when
// first_key_in_next_block != nullptr).
SequenceNumber first_key_seq = 0;
};
virtual ~UserDefinedIndexBuilder() = default;
// Add a new index entry to index block. The key for the new index entry
// should be >= last_key_in_current_block and < first_key_in_next_block.
// The previous index entry key and the new index entry key cover
// all the keys in the data block associated with the new index entry.
// Add a new index entry for a data block boundary.
//
// The last_key_in_current_block and first_key_in_next_block will be user
// keys, i.e the user key string, and optionally the user timestamp if one
// is configured, without a sequence number suffix.
// The keys are user keys (without the 8-byte internal key trailer).
//
// The UDI is free to compute a separator between the two user keys and
// store it along with the block handle. The separator must satisfy:
// last_key_in_current_block <= separator < first_key_in_next_block
// in user-key order (ignoring sequence numbers).
//
// Called before the OnKeyAdded() call for first_key_in_next_block.
// @last_key_in_current_block: The last key in the current data block
// @first_key_in_next_block: it will be nullptr if the entry being added is
// the last one in the table
// @block_handle: offset/size of the data block referenced by this index
// entry. This should be stored along with the index entry
// key
// @separator_scratch: a scratch buffer to back a computed separator between
// those, as needed. May be modified on each call.
// @return: the key or separator stored in the index, which could be
// last_key_in_current_block or a computed separator backed by
// separator_scratch.
// @last_key_in_current_block: The last user key in the current data block
// @first_key_in_next_block: First user key in the next data block, or
// nullptr if this is the last block
// @block_handle: offset/size of the data block
// @separator_scratch: scratch buffer for a computed separator
// @context: sequence number context for block boundaries. The sequence
// numbers are needed when the same user key spans a data block boundary
// (e.g., when snapshots keep multiple versions of a key). Without
// sequence numbers, the UDI cannot produce a separator that distinguishes
// the two blocks. This mirrors the internal index's behavior of switching
// to full internal-key separators (see
// ShortenedIndexBuilder::must_use_separator_with_seq_).
// Implementations that don't need sequence numbers can ignore the context.
// @return: the separator stored in the index
virtual Slice AddIndexEntry(const Slice& last_key_in_current_block,
const Slice* first_key_in_next_block,
const BlockHandle& block_handle,
std::string* separator_scratch) = 0;
std::string* separator_scratch,
const IndexEntryContext& context) = 0;
// This method will be called whenever a key is added. The subclasses may
// override OnKeyAdded() if they need to collect additional information.
// The type argument indicates whether the value is a full value or partial.
// At the moment, only full values are supported.
// Called for every key-value pair added to the SST file. UDI builders may
// override this to collect per-key information (e.g., for secondary
// indexes). Builders that only use separator keys from AddIndexEntry()
// (e.g., trie-based indexes) can leave this as a no-op.
//
// The key will be a user key. RocksDB guarantees that there will only be
// one entry for each key in the file/index.
// @key: The user key (without sequence number or type suffix).
// @type: The entry type — kValue (Put), kDelete, kMerge, or kOther.
// For kDelete entries, the value may be empty. For kOther, the
// value format is type-specific and may not be actual user data.
// @value: The associated value (may be empty for deletions).
//
// NOTE: In SST files produced by flush or compaction, there may be multiple
// entries for the same user key with different sequence numbers (e.g., when
// snapshots are active). UDI builders that use OnKeyAdded() should be
// prepared for this.
//
// Thread safety: For a given builder instance, OnKeyAdded() and
// AddIndexEntry() are always called from a single thread. Builders do
// not need internal synchronization.
virtual void OnKeyAdded(const Slice& /*key*/, ValueType /*type*/,
const Slice& /*value*/) {}
@@ -104,13 +135,36 @@ class UserDefinedIndexIterator {
// this as an opportunity to do any prefetching and buffering of results.
virtual void Prepare(const ScanOptions scan_opts[], size_t num_opts) = 0;
// Optional context for SeekAndGetResult providing the target sequence
// number. Passed as a struct for forward-compatible extensibility.
struct SeekContext {
// Sequence number of the target key. Used by UDI implementations that
// encode sequence numbers (when the same user key spans multiple data
// blocks) to locate the correct block.
SequenceNumber target_seq = 0;
};
// Position the index iterator at the very first index entry. The result
// must be populated the same way as SeekAndGetResult.
//
// The default implementation calls SeekAndGetResult with an empty key,
// which works for BytewiseComparator (empty string is the smallest key).
// Implementations should override this if they can reach the first entry
// more efficiently or if they use a comparator where empty is not smallest.
virtual Status SeekToFirstAndGetResult(IterateResult* result) {
return SeekAndGetResult(Slice(), result, SeekContext{});
}
// Given the target key, position the index iterator at the index entry
// with the smallest key >= target. The result must be updated with the
// index key, and the bound_check_result. The bound_check_result should
// be set to kOutOfBound if no block satisfies the target key and
// termination criteria, kInbound if the data block is definitely fully
// within bounds, or kUnknown if the data block could be partially
// within bounds.
// for the data block that may contain the target.
//
// The target is a user key.
//
// The result must be updated with the index key and bound_check_result.
// bound_check_result should be kOutOfBound if no block satisfies the
// target, kInbound if the data block is definitely within bounds, or
// kUnknown if partially within bounds.
//
// The UDI implementation needs to be careful about returning kOutOfBound.
// If a limit key is specified in ScanOptions, an implementation that
// does not store the first key in the block for the corresponding index
@@ -119,8 +173,14 @@ class UserDefinedIndexIterator {
// is out of bounds w.r.t the limit. Other termination criteria (specified
// in property_bag) may cause the scan to terminate earlier, in which case
// kOutOfBound can be returned earlier.
virtual Status SeekAndGetResult(const Slice& target,
IterateResult* result) = 0;
//
// @context: sequence number context for the seek. The sequence number is
// needed when the same user key spans multiple data blocks with different
// sequence numbers. Without it, the UDI cannot distinguish which block to
// return for a given (user_key, seqno) target. Implementations that don't
// need sequence numbers can ignore the context.
virtual Status SeekAndGetResult(const Slice& target, IterateResult* result,
const SeekContext& context) = 0;
// Advance to the next index entry. The result must be populated similar
// to SeekAndGetResult.
@@ -152,7 +212,7 @@ struct UserDefinedIndexOption {
// Factory for creating user-defined index builders.
class UserDefinedIndexFactory : public Customizable {
public:
virtual ~UserDefinedIndexFactory() = default;
~UserDefinedIndexFactory() override = default;
static const char* Type() { return "UserDefinedIndexFactory"; }
@@ -174,14 +234,14 @@ class UserDefinedIndexFactory : public Customizable {
std::unique_ptr<UserDefinedIndexBuilder>& builder) const {
builder.reset(NewBuilder());
return Status::OK();
};
}
virtual Status NewReader(
const UserDefinedIndexOption& /*option*/, Slice& index_block,
std::unique_ptr<UserDefinedIndexReader>& reader) const {
reader = NewReader(index_block);
return Status::OK();
};
}
};
} // namespace ROCKSDB_NAMESPACE
Loaded 100 of 181 files, more files were not shown because too many files have changed in this diff. Show more