Compare commits

...
172 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
Josh Kang e704352e5d Sanitize crashtest invalid arg for interpolation search (#14374)
Summary:
Sanitize crash test arg for interpolation search when UDTs are used. Currently its not supported and returns invalid arg on DB open.

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

Test Plan: CI

Reviewed By: archang19

Differential Revision: D94123220

Pulled By: joshkang97

fbshipit-source-id: 89b63f14a721bc692465620298161af23eec2446
2026-02-23 14:06:32 -08:00
Josh Kang 901c88e37b Separate keys and values in data blocks (#14287)
Summary:
Introduce new table option with separated key-value storage in data blocks.

This PR implements a new SST block format where keys and values are stored in separate sections within data blocks, rather than interleaved. Keys are stored first, followed by all values in a contiguous section. The motivation is better cpu cache hit rate during seeks and potentially better compression.

The additional storage cost is a varint per restart point, and 4 bytes additional block footer. For a data block with a restart interval of 16, it is approximately 1 bit of overhead per entry. But compression actually performs better, resulting in ~3% storage savings from benchmark.

For now I've opted to not separate kvs in non-data blocks since restart interval for those blocks is typically 1, and values are typically small and probably better inlined.

### New block layout

```
+------------------+
| Keys Section     |  <- Key entries with delta encoding
+------------------+
| Values Section   |  <- (new) Values stored contiguously
+------------------+
| Restart Array    |  <- Fixed32 offsets to restart points
+------------------+
| Values Offset    |  <- (new) 4 bytes: offset to values section
| Footer           |  <- 4 bytes: packed index_type + num_restarts
+------------------+
```

### Entry Format

**At restart points**
```
+--------------+------------------+----------------+-----------------+-----------+
| shared (v32) | non_shared (v32) | value_sz (v32) | value_off (v32) | key_delta |
+--------------+------------------+----------------+-----------------+-----------+
```

**At non-restart points**
```
+--------------+------------------+----------------+-----------+
| shared (v32) | non_shared (v32) | value_sz (v32) | key_delta |
+--------------+------------------+----------------+-----------+
```

- `value_offset` is only stored at restart points to save space
- For non-restart entries, value offset is computed as: `prev_value_offset + prev_value_size`

### Forward Compatibility
- We make use of reserved block footer bits to mark if a block has separated kv format. Should an older version read this, it will assume a very large block restart interval and result in a corruption error.

### Key Changes
- **BlockBuilder**: Accumulates values in a separate buffer; value offsets are stored only at restart points (other entries derive offset from previous value's position). There is an additional memcpy cost to place the value data after the key data.
- **Block iteration**: Iteration now needs to know if we are at a restart point. This will rely on `cur_entry_idx_`, which was previously only used for per-kv checksum purposes. In this new format, we also need to know the block_restart_interval, which was previously also only calculated for per-kv checksums.
- **Table properties**: Store `data_block_restart_interval`, `index_block_restart_interval`, and `separate_key_value_in_data_block` in table properties

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

Test Plan:
- Extended block_test, table_test, compaction_test to contain new separated_kv param
- Added new parameter to crash test

 ---

## Benchmark

### Varying Value Size

**Write:** `db_bench --num=1000000 --separate_key_value_in_data_block=<bool> --value_size=<X> --benchmarks=fillrandom,compact`
**Read:** `db_bench --db=$DB --use_existing_db --benchmarks=readrandom,readseq`

| Value Size | FillRandom (ops/s) | | Compact (s) | | ReadRandom (ops/s) | | ReadSeq (ops/s) | | SST Size (MB) | |
|----------:|-------------------:|-----:|----------:|-----:|-------------------:|-----:|----------------:|-----:|-------------:|-----:|
| | baseline | sep_kv | baseline | sep_kv | baseline | sep_kv | baseline | sep_kv | baseline | sep_kv |
| 1 | 253,280 | 264,977 | 0.516 | 0.497 (**-3.7%**) | 596,328 | 586,625 (**-1.6%**) | 5,904,681 | 6,069,581 (**+2.8%**) | 5.1 | 4.2 (**-18.6%**) |
| 16 | 235,757 | 242,367 | 0.572 | 0.533 (**-6.8%**) | 570,751 | 572,815 (**+0.4%**) | 5,371,138 | 5,604,908 (**+4.4%**) | 14.7 | 13.4 (**-8.5%**) |
| 100 | 264,299 | 265,427 | 0.461 | 0.454 (**-1.5%**) | 323,696 | 332,790 (**+2.8%**) | 4,239,725 | 4,232,416 (**-0.2%**) | 38.8 | 37.6 (**-3.2%**) |
| 1,000 | 238,992 | 242,764 | 2.349 | 2.329 (**-0.9%**) | 244,608 | 261,403 (**+6.9%**) | 1,285,394 | 1,265,868 (**-1.5%**) | 342.1 | 342.0 (**-0.0%**) |

### Varying Block Restart Interval

**Write:** `db_bench --num=1000000 --separate_key_value_in_data_block=<bool> --block_restart_interval=<X> --benchmarks=fillrandom,compact`
**Read:** `db_bench --db=$DB --use_existing_db --benchmarks=readrandom,readseq`

| BRI | FillRandom (ops/s) | | Compact (s) | | ReadRandom (ops/s) | | ReadSeq (ops/s) | | SST Size (MB) | |
|----:|-------------------:|-----:|----------:|-----:|-------------------:|-----:|----------------:|-----:|-------------:|-----:|
| | baseline | sep_kv | baseline | sep_kv | baseline | sep_kv | baseline | sep_kv | baseline | sep_kv |
| 1 | 251,654 | 263,707 | 0.453 | 0.485 (**+7.1%**) | 334,653 | 328,708 (**-1.8%**) | 4,194,342 | 3,954,291 (**-5.7%**) | 40.6 | 42.4 (**+4.5%**) |
| 4 | 253,797 | 252,394 | 0.476 | 0.476 (**+0.0%**) | 332,719 | 341,676 (**+2.7%**) | 4,135,691 | 4,051,151 (**-2.0%**) | 39.2 | 39.3 (**+0.3%**) |
| 8 | 260,143 | 262,273 | 0.496 | 0.460 (**-7.3%**) | 330,859 | 337,567 (**+2.0%**) | 4,144,081 | 4,187,389 (**+1.0%**) | 38.9 | 38.1 (**-2.1%**) |
| 16 | 252,875 | 263,176 | 0.464 | 0.455 (**-1.9%**) | 323,783 | 335,418 (**+3.6%**) | 4,127,310 | 4,217,028 (**+2.2%**) | 38.8 | 37.6 (**-3.2%**) |
| 32 | 260,224 | 269,422 | 0.464 | 0.451 (**-2.8%**) | 304,001 | 314,989 (**+3.6%**) | 4,310,162 | 4,247,248 (**-1.5%**) | 38.8 | 37.3 (**-3.8%**) |

### Varying Compression

**Write:** `db_bench --num=1000000 --separate_key_value_in_data_block=<bool> --compression_type=<X> [--compression_level=<N>] --benchmarks=fillrandom,compact`
**Read:** `db_bench --db=$DB --use_existing_db --benchmarks=readrandom,readseq`

| Compression | FillRandom (ops/s) | | Compact (s) | | ReadRandom (ops/s) | | ReadSeq (ops/s) | | SST Size (MB) | |
|------------|-------------------:|-----:|----------:|-----:|-------------------:|-----:|----------------:|-----:|-------------:|-----:|
| | baseline | sep_kv | baseline | sep_kv | baseline | sep_kv | baseline | sep_kv | baseline | sep_kv |
| None | 252,494 | 260,552 | 0.413 | 0.419 (**+1.5%**) | 356,290 | 371,535 (**+4.3%**) | 4,479,261 | 4,507,133 (**+0.6%**) | 73.5 | 73.6 (**+0.2%**) |
| LZ4 | 246,010 | 256,360 | 0.477 | 0.455 (**-4.6%**) | 342,497 | 345,882 (**+1.0%**) | 4,400,570 | 4,268,102 (**-3.0%**) | 38.3 | 37.6 (**-2.0%**) |
| ZSTD (L3) | 254,748 | 258,556 | 1.067 | 1.055 (**-1.1%**) | 176,724 | 177,566 (**+0.5%**) | 2,736,841 | 2,717,739 (**-0.7%**) | 32.9 | 31.3 (**-4.7%**) |
| ZSTD (L6) | 256,459 | 259,388 | 1.556 | 1.462 (**-6.0%**) | 177,390 | 176,691 (**-0.4%**) | 2,754,336 | 2,688,682 (**-2.4%**) | 32.8 | 31.1 (**-5.1%**) |

### Varying Block Size

**Write:** `db_bench --num=1000000 --separate_key_value_in_data_block=<bool> --block_size=<X> --benchmarks=fillrandom,compact`
**Read:** `db_bench --db=$DB --use_existing_db --benchmarks=readrandom,readseq`

| Block Size | FillRandom (ops/s) | | Compact (s) | | ReadRandom (ops/s) | | ReadSeq (ops/s) | | SST Size (MB) | |
|-----------:|-------------------:|-----:|----------:|-----:|-------------------:|-----:|----------------:|-----:|-------------:|-----:|
| | baseline | sep_kv | baseline | sep_kv | baseline | sep_kv | baseline | sep_kv | baseline | sep_kv |
| 4 KB | 263,203 | 260,362 | 0.469 | 0.461 (**-1.7%**) | 324,178 | 332,249 (**+2.5%**) | 4,231,537 | 4,217,763 (**-0.3%**) | 38.8 | 37.6 (**-3.1%**) |
| 16 KB | 252,742 | 263,161 | 0.426 | 0.428 (**+0.5%**) | 227,805 | 222,873 (**-2.2%**) | 5,146,997 | 5,081,080 (**-1.3%**) | 38.1 | 36.7 (**-3.6%**) |
| 64 KB | 257,490 | 260,225 | 0.423 | 0.414 (**-2.1%**) | 86,807 | 91,586 (**+5.5%**) | 5,380,403 | 5,372,372 (**-0.1%**) | 36.3 | 35.0 (**-3.5%**) |

### Varying Key Size

**Write:** `db_bench --num=1000000 --separate_key_value_in_data_block=<bool> --min_key_size=10 --max_key_size=100 --benchmarks=fillrandom,compact`
**Read:** `db_bench --db=$DB --use_existing_db --benchmarks=readrandom,readseq`

| Key Size | FillRandom (ops/s) | | Compact (s) | | ReadRandom (ops/s) | | ReadSeq (ops/s) | | SST Size (MB) | |
|---------:|-------------------:|-----:|----------:|-----:|-------------------:|-----:|----------------:|-----:|-------------:|-----:|
| | baseline | sep_kv | baseline | sep_kv | baseline | sep_kv | baseline | sep_kv | baseline | sep_kv |
| 10–100 | 243,740 | 255,183 | 0.618 | 0.622 (**+0.6%**) | 284,304 | 307,569 (**+8.2%**) | 3,738,921 | 3,686,676 (**-1.4%**) | 41.5 | 41.2 (**-0.8%**) |

### CPU Profile Notes
- No compression: DataBlock::SeekForGet uses less cpu (13.2% vs 13.9%)
  - https://fburl.com/strobelight/6mwwebft with separated KV
  - https://fburl.com/strobelight/m9m798ka without
- ZSTD compression: rocksdb::DecompressSerializedBlock uses more CPU (45.8% vs 44.9%), while DataBlock::SeekForGet uses less cpu (5.09% vs 6.52%)
  - https://fburl.com/strobelight/3x5nw1k4 with separated KV
  - https://fburl.com/strobelight/e7809046 without

 ---

Reviewed By: xingbowang, pdillinger

Differential Revision: D92103024

Pulled By: joshkang97

fbshipit-source-id: 47cfeb656ff3c20d34975f0b6c4c0462935a83dc
2026-02-23 12:42:05 -08:00
anand76 eb5d12e744 Fix race condition causing flaky hang in WritePreparedTransactionSeqnoTest (#14361)
Summary:
Summary

  - Fix a race condition in three WritePreparedTransactionSeqnoTest tests (SeqnoGoesBackwardsDuringErrorRecovery, SeqnoDiscrepancyDuringErrorRecovery, ConcurrentWritesDuringErrorRecovery) that could cause permanent hangs.
  - The tests inject a filesystem error during flush via a WriteManifest sync point callback, then wait for background error recovery to complete. The bug was in the ordering of operations after recovery starts: SetFilesystemActive(true) was called before ClearCallBack, allowing a window where recovery's ResumeImpl could trigger the callback and re-disable the filesystem. This left the filesystem permanently disabled, causing all recovery retries to fail and exit without firing the RecoverSuccess sync point, leaving the test thread blocked forever.
  - The fix swaps the order so ClearCallBack is called before SetFilesystemActive(true), ensuring the filesystem cannot be re-disabled by a late callback firing.

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

Test Plan:
- Stress tested with gtest_parallel (500 iterations, 32 workers, 60s timeout) with no hangs observed.
  - Previously reproduced the hang at ~7% rate under stress with 15s timeout before the fix.

Reviewed By: pdillinger

Differential Revision: D93929251

Pulled By: anand1976

fbshipit-source-id: 9cb6844ed20146c754091575156b08d5551b3034
2026-02-23 12:01:28 -08:00
Xingbo Wang 4e11dd79e1 V2 serialization format for wide columns with blob references (#14314)
Summary:
Introduce a new V2 serialization format for wide column entities that supports storing individual column values in blob files. The V2 format adds a column type section that marks each column as either inline or blob-index, enabling per-column blob storage for large values.

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

Reviewed By: pdillinger

Differential Revision: D92832066

Pulled By: xingbowang

fbshipit-source-id: 13c24347e1f481a059d67eef987d2d2b184b4a51
2026-02-21 06:19:45 -08:00
xingbowang aa7571c3ad Run clang-tidy in github CI (#14347)
Summary:
RocksDB has been using clang-tidy for a long time inside Meta. However, it is not efficient for external contributor, as the result from clang-tidy has to be ferried back through internal contributor. This PR added support to run clang-tidy on external github CI. It added .clang-tidy file based on internal version. It run clang-tidy in a separate pr job and a workflow step would post the pr job result to the PR itself. See example below.

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

Test Plan: Github CI

Reviewed By: archang19

Differential Revision: D93862467

Pulled By: xingbowang

fbshipit-source-id: bb4330241036894deb619470efd73a7041a8b62f
2026-02-20 14:58:21 -08:00
Hui Xiao 29819f37e1 Remove deprecated ReadOptions::managed, `ColumnFamilyOptions::snap_refresh_nanos (#14350)
Summary:
**Context/Summary:**
Remove deprecated, unused APIs and options:
- ReadOptions::managed: This option was not used anymore. The functionality it controlled has been removed long ago.
- ColumnFamilyOptions::snap_refresh_nanos: Deprecated and unused option.

Corresponding C API (rocksdb_readoptions_set_managed) and Java API (ReadOptions.managed/setManaged) are also removed. All related checks an db_impl and db_impl_secondary iterators are cleaned up.

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

Test Plan: make check

Reviewed By: pdillinger

Differential Revision: D93812438

Pulled By: hx235

fbshipit-source-id: e4a9d21c65f83294b6d0878286ba14024f049bac
2026-02-20 14:00:41 -08:00
Andrew Chang 6d6f7d825b Check io_uring probe result in SupportedOps (#14355)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14355

SupportedOps advertised kAsyncIO based only on the IsIOUringEnabled() weak symbol check, without verifying that the constructor's io_uring probe actually succeeded. Add a thread_local_async_read_io_urings_ null check so kAsyncIO is only reported when the probe passed. Also update the constructor to probe with the same IORING_SETUP_SINGLE_ISSUER | IORING_SETUP_DEFER_TASKRUN flags that ReadAsync and MultiRead use at runtime.

Reviewed By: anand1976

Differential Revision: D93780065

fbshipit-source-id: 6f51f544b267cb39d09b49949a9485f55eeae12e
2026-02-20 10:43:30 -08:00
Hui Xiao 4c89ff1102 Remove deprecated SstFileWriter::Add() and skip_filters parameter (#14352)
Summary:
**Context/Summary:**
Remove `SstFileWriter::Add()` (deprecated in favor of `Put()`) and the `skip_filters` parameter from `SstFileWriter` constructors (deprecated in favor of setting `BlockBasedTableOptions::filter_policy` to `nullptr`).

Both APIs have zero active callers. The `skip_filters` field is also removed from `TableBuilderOptions` (write-side only; the read-side `TableReaderOptions::skip_filters` is unchanged).

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

Test Plan: make check

Reviewed By: xingbowang

Differential Revision: D93812389

Pulled By: hx235

fbshipit-source-id: 236b36a6e664758ab5ad90e606bc195d0a6de70f
2026-02-19 22:10:47 -08:00
Peter Dillinger f1a6759b1f Fix flaky DBTestXactLogIterator.TransactionLogIteratorCheckWhenArchive (#14349)
Summary:
a couple recent failures in this test. Waiting for purge and disabling sync points before Close should resolve the issues.

Also fixing EventListenerTest.BlobDBOnFlushCompleted because it showed up as flaky in CI for this PR

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

Test Plan: watch CI

Reviewed By: mszeszko-meta

Differential Revision: D93619322

Pulled By: pdillinger

fbshipit-source-id: bb9fc7d3c0ecaaeaffe4305e1ad403cbcd597484
2026-02-19 20:26:38 -08:00
Peter Dillinger 520c3ecbf1 Prepare for 11.0 major release (#14357)
Summary:
In my last version bump, I forgot that the next release would be a major release. We can fix that now ahead of release cut.

I'm also updating folly now because I have experience resolving folly issues. Folly commit e04860553 changed libevent to build as static-only so required a change in our build.

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

Test Plan: CI

Reviewed By: xingbowang

Differential Revision: D93797666

Pulled By: pdillinger

fbshipit-source-id: 22179da900f9dc6c5544163071079a4701c7c663
2026-02-19 18:51:34 -08:00
Hui Xiao 407f02da19 Remove deprecated SliceTransform::InRange() virtual method (#14353)
Summary:
**Summary/Context:**

Remove the `InRange()` virtual method from `SliceTransform` and all its overrides. This method was marked DEPRECATED, never called by RocksDB, and existed only for backward compatibility.

Also removes the `in_range` callback parameter from `rocksdb_slicetransform_create()` in the C API, which is a breaking change appropriate for a major version release.

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

Test Plan: Make check

Reviewed By: xingbowang

Differential Revision: D93795070

Pulled By: hx235

fbshipit-source-id: 5eba23f1d038b19c494997a55e5d8ca379fbedcb
2026-02-19 16:45:51 -08:00
Josh Kang 98002215d0 Fix interpolation search target key less than shared prefix length. (#14343)
Summary:
There was an edge case missed in the implementation of interpolation search for target keys that had a length smaller than the shared prefix.

E.g. first_key = "aaaaaa", last_key = "aaaaaz", target_key = "aaz". In the existing setup, we will seek to position 0, but in reality is should be seeked to the end.

#### The fix
The solution here was to also do a bounds check on the first search iteration. We utilize memcmp on the target key with the shared_prefix to determine if the target key is outside the bounds. An edge case here is if the target key itself a prefix of the shared prefix (e.g. target = "aaaa"), in this case memcmp return return 0, but the target key is actually smaller.

### Minor optimizations
- cache left,right values so we don't need to re-compute it when left/right boundaries don't change
- In ReadBe64FromKey, utilize memcpy + swap for fast path
- since we have already computed a shared_prefix, every other comparison only needs to compare the non-shared suffixes

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

Test Plan:
Added new unit tests to test this case

### Benchmarks

No significant regressions due to additional memcmp.

#### Configuration
- **CPU:** 192 * AMD EPYC-Genoa Processor
- **RocksDB Version:** 10.12.0
- **Compression:** Snappy
- **Entries:** 1,000,000
- **Value Size:** 100 bytes
- **Index Search Type:** interpolation_search
- **Index Shortening Mode:** 1

#### Results

| Benchmark | Params | ops/s (main) | ops/s (feature) | % change |
|-----------|--------|-------------|-----------------|----------|
| readrandom | 16B keys, no prefix | 367,264 | 369,163 | +0.52% |
| readrandom | 100B keys, prefix_size=50 | 376,066 | 371,193 | -1.29% |

Reviewed By: pdillinger

Differential Revision: D93535267

Pulled By: joshkang97

fbshipit-source-id: beda182efce1e914ff587e697b927347cfa42656
2026-02-19 15:22:58 -08:00
xingbowang cfc2a523a3 Add clang-tidy-comment workflow (#14348)
Summary:
Add clang-tidy-comment workflow. This workflow allows pr clang tidy pr job to post the clang-tidy finding directly on the PR page.

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

Test Plan: Will be tested with next clang-tidy PR

Reviewed By: joshkang97

Differential Revision: D93670150

Pulled By: xingbowang

fbshipit-source-id: 8245f9d5bde8cf800d88034c4339de9f387c5692
2026-02-19 14:59:37 -08:00
xingbowang 3556c22059 Remove deprecated option skip_checking_sst_file_sizes_on_db_open (#14346)
Summary:
Remove deprecated option skip_checking_sst_file_sizes_on_db_open

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

Test Plan: Unit test

Reviewed By: hx235

Differential Revision: D93602683

Pulled By: xingbowang

fbshipit-source-id: f576825cb107bb0aeb14f4ff29fef0df269b8728
2026-02-19 14:12:38 -08:00
Peter Dillinger 61b4edd15e Test compaction forward compatibility (#14344)
Summary:
Extending https://github.com/facebook/rocksdb/issues/14323 by testing scenarios for compaction after downgrade. Detail: we shouldn't need to test loading options with compaction, as options file inclusion is mostly a sanity check for "can you open the DB with options file?"

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

Test Plan: manual run of SHORT_TEST=1 J=140 tools/check

Reviewed By: xingbowang

Differential Revision: D93553897

Pulled By: pdillinger

fbshipit-source-id: ec08ae2a3d49971e24a215e38df9506fe1133096
2026-02-18 10:52:04 -08:00
Peter Dillinger d3817f058d Remove deprecated DB::Open raw pointer variants (and more) (#14335)
Summary:
and remove deprecated DB::MaxMemCompactionLevel(). In the process of pushing through a relatively clean refactoring of uses of the old functions, some other minor public APIs are also migrated from raw DB pointers to unique_ptr.

Claude did pretty much all the work, but requiring dozens of prompts to actually push through relatively clean phase out of raw DB pointers from what needed to be touched, and leaving that code in better shape. (Hundreds of `DB*` still remain all over the place even outside C and Java bindings.)

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

Test Plan: existing tests; no functional changes intended

Reviewed By: xingbowang, mszeszko-meta

Differential Revision: D93523820

Pulled By: pdillinger

fbshipit-source-id: e4ca22ad81cd2cfe91122d7507d7ca34fe03d043
2026-02-17 23:33:39 -08:00
Peter Dillinger 1f9d8ee302 Remove obsolete code for block-based format_version < 2 (after #14315) (#14327)
Summary:
After PR https://github.com/facebook/rocksdb/issues/14315 dropped support for block-based table format_version < 2, several code paths became obsolete. This change removes them.

Investigation findings:

1. Table properties are now a hard requirement for block-based SST files:
   - format_version >= 2 guarantees a properties block exists
   - Removed defensive conditionals like `if (rep_->table_properties)`
   - Missing properties block now returns Status::Corruption instead of just logging an error. This is important because some properties affect the semantic interpretation of the file.

2. Index type property (kIndexType) is now required:
   - kIndexType was introduced in Feb 2014 (commit 74939a9e1), ~11 months BEFORE format_version was introduced in Jan 2015
   - BlockBasedTablePropertiesCollector::Finish() has always written kIndexType unconditionally for all block-based tables
   - Therefore all format_version >= 2 files have this property
   - Now returns Status::Corruption if missing instead of silently defaulting to kBinarySearch

3. Removed SetOldTableOptions() from sst_file_dumper:
   - This fallback handled files without a properties block
   - Dead code since format_version >= 2 guarantees properties exist

4. Removed kPropertiesBlockOldName ("rocksdb.stats") fallback:
   - The properties block was renamed from "rocksdb.stats" to "rocksdb.properties" in RocksDB 2.7 (April 2014)
   - format_version 2 was introduced in RocksDB 3.10 (Oct 2015)
   - All table formats (block-based, plain, cuckoo) were created after the rename, so they all use "rocksdb.properties"
   - The backward compatibility fallback in FindOptionalMetaBlock() was dead code for all supported table formats

5. Removed obsolete assertion about format_version 0 checksum in BlockBasedTableBuilder::WriteFooter()

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

Test Plan: some tests updated for updated requirements. Mostly, CI including format compatible test

Reviewed By: mszeszko-meta

Differential Revision: D93124820

Pulled By: pdillinger

fbshipit-source-id: eb12cbdca0e69f34a08051d5160c282384128a4a
2026-02-17 23:31:30 -08:00
Peter Dillinger 641f4703ac Refactor data block footer to reserve metadata bits for future features (#14332)
Summary:
I'm implementing this intending it to be used for https://github.com/facebook/rocksdb/issues/14287

Refactor the data block footer encoding/decoding to use a struct-based Encode/Decode API (DataBlockFooter), reserving the top 4 bits of the footer for metadata:
- Bit 31: Hash index present (kDataBlockBinaryAndHash) - existing use
- Bits 28-30: Reserved for future features

Comments have some detail for why it is safe to assume no practical existing SST files would use these newly reserved bits. And for forward compatibility, existing versions detect (non-zero) use of these new bits as impossibly large num_restarts and report "bad block contents". Not perfect, but not bad.

Key changes:
- Replace PackIndexTypeAndNumRestarts/UnPackIndexTypeAndNumRestarts with DataBlockFooter::EncodeTo/DecodeFrom methods
- DecodeFrom returns a detailed error when reserved bits are set, enabling graceful failure on newer format versions
- Reduce kMaxNumRestarts from 2^31-1 to 2^28-1 (268M), which is adequate for the maximum possible restarts in a 4GiB block
- Add GetCorruptionStatus() to Block for detailed error messages (Note that we are sensitive to the size of Block objects, so have to avoid adding unnecessary new members.)
- Remove obsolete kMaxBlockSizeSupportedByHashIndex size checks

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

Test Plan:
- Existing unit tests and format compatibility test
- Add test for reserved bit detection (ReservedBitInDataBlockFooter)

Reviewed By: joshkang97

Differential Revision: D93293152

Pulled By: pdillinger

fbshipit-source-id: b65a83e96bb09a98fb9b8b2dd9f754653ca7ed4d
2026-02-17 17:28:54 -08:00
Peter Dillinger d693d5ae26 Blog post on CPU bug (#14078)
Summary:
see draft post

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

Test Plan: markdown preview (simple post)

Reviewed By: hx235

Differential Revision: D85475518

Pulled By: pdillinger

fbshipit-source-id: d7f7b0d68de3880fcffbdbbef27cd2c14fe51f96
2026-02-17 15:30:00 -08:00
anand76 653fd9c65b Bug fix for bg error recovery in TransactionDB (#14313)
Summary:
This PR fixes a bug in the interaction between WritePrepared/WriteUnprepared TransactionDB (with two_write_queues=true) and background error recovery. This bug caused crash tests to fail with a "sequence number going backwards" error during DB open.

Root Cause
------------
When two_write_queues=true, sequence numbers are allocated via FetchAddLastAllocatedSequence() before a write completes, but are only published via SetLastSequence() after the write succeeds. If a background error occurs (e.g., a MANIFEST write failure during flush), the error recovery path in DBImpl::ResumeImpl creates new memtables and WAL files. The new WAL's starting sequence number is based on LastSequence() (the published value), which can be lower than already-allocated sequence numbers that were written to the old WAL. On subsequent recovery, RocksDB detects that sequence numbers in the new WAL are lower than those in the old WAL and reports a "sequence number going backwards" corruption error, causing the DB to fail to open.

Fix
 ---
The fix adds a call to a new VersionSet::SyncLastSequenceWithAllocated() method at the beginning of DBImpl::ResumeImpl, before any new memtables or WALs are created. This method advances last_sequence_ to match last_allocated_sequence_ if the latter is higher, ensuring the new WAL starts with a sequence number that is at least as high as any previously allocated one.

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

Test Plan:
---------
Add new unit tests in write_prepared_transaction_test_seqno

Reviewed By: pdillinger

Differential Revision: D92746944

Pulled By: anand1976

fbshipit-source-id: 34385fc13fd74435dd1c3283637eb118f45d887e
2026-02-17 14:52:00 -08:00
Peter Dillinger f065e1c95d Fix up authors.yml for blog entries (#14342)
Summary:
Fixes blog author display issues on rocksdb.org/blog by:

* Adding missing authors to authors.yml: pdillinger, alanpaxton, akankshamahajan15, anand1976, poojam23
* Standardizing on GitHub usernames: renamed sdong → siying
* Fixing typo in 2016-02-25-rocksdb-ama.markdown: yhchiang → yhciang
* A short note in CLAUDE.md

Authors were not showing on the blog because they were referenced in post frontmatter but not defined in the _data/authors.yml lookup file.

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

Test Plan: push & see ;)

Reviewed By: mszeszko-meta

Differential Revision: D93523972

Pulled By: pdillinger

fbshipit-source-id: 757c33e80f3c1d99ff4134a37321f40634d6e294
2026-02-17 14:30:03 -08:00
Hui Xiao 49695ef868 Add debug assertion for clean cut invariant in expanding compaction inputs (#14333)
Summary:
**Context/Summary:**

Stress test recently encountered a one-off failure where input file selection for trivial move did not select all the files it should and left behind one adjacent file to the input file. This violated the clean cut invariant enforced through `ExpandInputsToCleanCut()` and caused `Get()` to return stale data.

While I had no luck reproducing it nor in code inspection to find the root cause, this debug assertion should help in two ways: 1. Fail fast if the invariant is violated, showing us the file boundary in memory 2. If the assertion doesn't trigger yet the same failure occurs, it points to metadata corruption bypassing this check and ExpandInputsToCleanCut() enforcement

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

Test Plan:
- Existing unit tests
- Manually trace through and run a stress test command that frequently exercises this check for 10 minutes
```
./db_stress --level0_file_num_compaction_trigger=2 --acquire_snapshot_one_in=10000 --adaptive_readahead=1 --allow_concurrent_memtable_write=0 --allow_data_in_errors=True --allow_setting_blob_options_dynamically=1 --async_io=1 --auto_readahead_size=0 --avoid_flush_during_recovery=0 --avoid_unnecessary_blocking_io=1 --backup_max_size=104857600 --backup_one_in=100000 --batch_protection_bytes_per_key=8 --blob_cache_size=1048576 --blob_compaction_readahead_size=4194304 --blob_compression_type=lz4 --blob_file_size=1048576 --blob_file_starting_level=1 --blob_garbage_collection_age_cutoff=1.0 --blob_garbage_collection_force_threshold=0.75 --block_protection_bytes_per_key=2 --block_size=16384 --bloom_before_level=0 --bloom_bits=16 --bottommost_compression_type=zstd --bottommost_file_compaction_delay=0 --bytes_per_sync=0 --cache_index_and_filter_blocks=0 --cache_size=8388608 --cache_type=auto_hyper_clock_cache --charge_compression_dictionary_building_buffer=1 --charge_file_metadata=1 --charge_filter_construction=0 --charge_table_reader=0 --checkpoint_one_in=1000000 --checksum_type=kXXH3 --clear_column_family_one_in=0 --column_families=1 --compact_files_one_in=1000000 --compact_range_one_in=1000000 --compaction_pri=3 --compaction_readahead_size=0 --compaction_ttl=2 --compression_checksum=1 --compression_max_dict_buffer_bytes=0 --compression_max_dict_bytes=0 --compression_parallel_threads=1 --compression_type=xpress --compression_use_zstd_dict_trainer=1 --compression_zstd_max_train_bytes=0 --continuous_verification_interval=0 --data_block_index_type=0 --db=/dev/shm/rocksdb_test/rocksdb_crashtest_whitebox --db_write_buffer_size=0 --delpercent=4 --delrangepercent=1 --destroy_db_initially=0 --detect_filter_construct_corruption=1 --disable_wal=0 --enable_blob_files=0 --enable_blob_garbage_collection=0 --enable_compaction_filter=0 --enable_pipelined_write=1 --enable_thread_tracking=1 --expected_values_dir=/dev/shm/rocksdb_test/rocksdb_crashtest_expected --fail_if_options_file_error=0 --fifo_allow_compaction=0 --file_checksum_impl=big --flush_one_in=1000000 --format_version=3 --get_current_wal_file_one_in=0 --get_live_files_one_in=1000000 --get_property_one_in=1000000 --get_sorted_wal_files_one_in=0 --index_block_restart_interval=2 --index_type=3 --ingest_external_file_one_in=0 --initial_auto_readahead_size=524288 --iterpercent=10 --key_len_percent_dist=1,30,69 --level_compaction_dynamic_level_bytes=1 --lock_wal_one_in=1000000 --long_running_snapshots=1 --manual_wal_flush_one_in=1000 --mark_for_compaction_one_file_in=0 --max_auto_readahead_size=0 --max_background_compactions=1 --max_bytes_for_level_base=1000 --max_key=25000000 --max_key_len=3 --max_manifest_file_size=16384 --max_write_batch_group_size_bytes=64 --max_write_buffer_number=3 --max_write_buffer_size_to_maintain=1000 --memtable_max_range_deletions=100 --memtable_prefix_bloom_size_ratio=0.01 --memtable_protection_bytes_per_key=4 --memtable_whole_key_filtering=0 --memtablerep=skip_list --min_blob_size=0 --min_write_buffer_number_to_merge=1 --mmap_read=1 --mock_direct_io=False --nooverwritepercent=1 --num_file_reads_for_auto_readahead=2 --open_files=-1 --open_metadata_write_fault_one_in=0 --open_read_fault_one_in=0 --open_write_fault_one_in=0 --ops_per_thread=100000000 --optimize_filters_for_memory=1 --paranoid_file_checks=0 --partition_filters=0 --partition_pinning=1 --pause_background_one_in=1000000 --periodic_compaction_seconds=1 --prefix_size=8 --prefixpercent=5 --prepopulate_blob_cache=0 --prepopulate_block_cache=0 --preserve_internal_time_seconds=0 --progress_reports=0 --read_fault_one_in=32 --readahead_size=0 --readpercent=45 --recycle_log_file_num=0 --reopen=0 --secondary_cache_fault_one_in=32 --secondary_cache_uri=compressed_secondary_cache://capacity=8388608 --set_options_one_in=0 --snapshot_hold_ops=100000 --sst_file_manager_bytes_per_sec=0 --sst_file_manager_bytes_per_truncate=0 --stats_dump_period_sec=10 --subcompactions=1 --sync=0 --sync_fault_injection=0 --target_file_size_base=1000 --target_file_size_multiplier=1 --test_batches_snapshots=0 --top_level_index_pinning=0 --unpartitioned_pinning=3 --use_blob_cache=0 --use_direct_io_for_flush_and_compaction=0 --use_direct_reads=0 --use_full_merge_v1=0 --use_get_entity=0 --use_merge=0 --use_multi_get_entity=0 --use_multiget=0 --use_put_entity_one_in=5 --use_shared_block_and_blob_cache=0 --user_timestamp_size=0 --value_size_mult=32 --verification_only=0 --verify_checksum=1 --verify_checksum_one_in=1000000 --verify_db_one_in=100000 --verify_file_checksums_one_in=1000000 --verify_iterator_with_expected_state_one_in=5 --verify_sst_unique_id_in_manifest=1 --wal_bytes_per_sync=524288 --wal_compression=none --write_buffer_size=1000 --write_dbid_to_manifest=1 --write_fault_one_in=0 --writepercent=35
```

Reviewed By: mszeszko-meta

Differential Revision: D93300664

Pulled By: hx235

fbshipit-source-id: b56f01c08a7348ba383110dd8f89b5b1b7961c55
2026-02-17 14:12:08 -08:00
Andrew Chang 09bda51c50 Propagate file_checksum through FileOptions on NewRandomAccessFile (#14321)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14321

Add file_checksum and file_checksum_func_name fields to FileOptions so that downstream FileSystem implementations can access per-file checksum metadata when SST files are opened. The fields are populated from FileMetaData at all call sites where SST files are opened via NewRandomAccessFile: TableCache::GetTableReader, Version::GetTableProperties, and CompactionJob::ReadTablePropertiesDirectly. Also fixes the fallback path in TableCache::GetTableReader to use the local fopts (with temperature and checksum) instead of the original file_options.

Added a kNoFileChecksumFuncName which is distinct from  kUnknownFileChecksumFuncName:

 - kUnknownFileChecksumFuncName ("Unknown"): We have FileMetaData for this file, and the metadata says no checksum was computed (no factory was configured when the file was written). This is a property of the file itself.
- kNoFileChecksumFuncName ("Unavailable"): We don't even have FileMetaData — we're opening this file in a context where there's no checksum metadata to propagate at all (e.g., SstFileDumper, SstFileReader, checksum generation). It's a property of the call site, not the file.

So the assertion file_checksum.empty() is correct for both, but for different reasons — one says "the file has no checksum," the other says "we have no idea about this file's checksum."

Reviewed By: pdillinger

Differential Revision: D92728944

fbshipit-source-id: 8fd34ea22ca87090b26d0a55c921f354f97f1ffc
2026-02-17 13:05:44 -08:00
Maciej Szeszko 5f692d747c Fall back to sync read when async IO is unavailable (#14337)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14337

## Context:

D91624185 changed `FilePrefetchBuffer::PollIfNeeded` from `void` to returning `Status`, correctly propagating `Poll` errors instead of silently swallowing them. A side effect is that when io_uring fails to initialize at runtime (e.g., sandcastle seccomp restrictions), `ReadAsync` returns `NotSupported` which now propagates through `PrefetchRemBuffers` and `HandleOverlappingAsyncData`, causing `PrefetchInternal` to return early before executing the synchronous `Read` that the caller actually depends on. This leaves iterators invalid with no recovery path — both in `db_stress` crash tests and in production. The filesystem advertises async IO support (`CheckFSFeatureSupport` passes), so the failure only surfaces at runtime when io_uring initialization fails. The prior behavior silently degraded to sync reads because `PollIfNeeded` swallowed the error.

## Changes

Add a sync fallback in `FilePrefetchBuffer::ReadAsync` — the single chokepoint for all async reads. When `reader->ReadAsync()` returns `NotSupported`, fall back to `reader->Read()` synchronously, populate the buffer inline, and return OK. Since `async_read_in_progress_` stays false, `PollIfNeeded` becomes a no-op (nothing to poll, data is already there). All callers — `PrefetchRemBuffers`, `HandleOverlappingAsyncData`, `PrefetchAsync` — work transparently without any per-site changes.

Reviewed By: archang19

Differential Revision: D93432284

fbshipit-source-id: daef185fc3535e347d182e75dd443ae921eeb495
2026-02-17 12:51:36 -08:00
Maciej Szeszko ebd1000008 Fix UB in ReadBe64FromKey shift by 64 (#14340)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14340

`ReadBe64FromKey` pads its result with `val <<= (8 - len) * 8` to right-align partial reads. When the seek target's user key is shorter than shared_prefix_len, len is 0 and this becomes a shift by 64, which is undefined behavior for uint64_t. On x86 this happens to produce 0 (the correct result), but UBSan rightfully flags it. Guard the shift with `len > 0 && len < 8`.

Reviewed By: joshkang97

Differential Revision: D93435715

fbshipit-source-id: bab128e9a65ea18d401670268cbac77d45e11340
2026-02-17 12:28:31 -08:00
Maciej Szeszko 821bd37d09 Cap max table files size in db stress FIFO compactions (#14341)
Summary:
FIFO crash tests fail on DB open when `fifo_compaction_max_data_files_size_mb` is randomly set to 100 or 500 MB, because `max_table_files_size` defaults to 1GB and the validation requires `max_data_files_size` >= `max_table_files_size` when non-zero. Cap `max_table_files_size` to `max_data_files_size` in db_stress when the latter is set. `max_table_files_size` is ignored at runtime when `max_data_files_size` is non-zero, so this only satisfies the validation constraint.

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

Reviewed By: pdillinger

Differential Revision: D93503113

Pulled By: mszeszko-meta

fbshipit-source-id: 5c3e7c9b568661244c71c548cb0fe5e55472c0ca
2026-02-17 11:49:20 -08:00
Maciej Szeszko 88ff4f6b12 Disable Interpolation Search in DB Stress (#14339)
Summary:
Temporarily disable interpolation search.

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

Reviewed By: pdillinger

Differential Revision: D93497658

Pulled By: mszeszko-meta

fbshipit-source-id: 6ac826dd3fc354e18af0d928f87ed71e2cef3f14
2026-02-17 11:13:17 -08:00
Xingbo Wang b040ab83e1 Add a new picking algorithm in fifo compaction (#14326)
Summary:
Add a new kv ratio based compaction picking algorithm in fifo compaction

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

Test Plan: Unit test

Reviewed By: pdillinger

Differential Revision: D93257941

Pulled By: xingbowang

fbshipit-source-id: fd2d0e1356c7b54682a1197475a1bd26cb45c9d4
2026-02-15 10:04:58 -08:00
Josh Kang 9f47518676 Add interpolation search as an alternative to binary search (#14247)
Summary:
Interpolation search is an alternative algorithm to binary search, which performs better on uniformly distributed keys. Instead of binary search always computing the mid point of the left and right boundaries, interpolation search "interpolates" the mid point based on the distance to the target. Fortunately, we can re-use existing block format to support interpolation search.

For a given block, we compute the shared_prefix length of the first and last key. Interpolation search is usually done with numerical target values, so for a variable binary length key, we calculate the "value" as the first 8 non-shared bytes. This also means interpolation search would only really be effective for bytewise comparator (guarded via options validations).

#### Fallback to binary search
- if the the val(left_key) == val(right_key) then we fallback to classic binary search (to avoid divide by 0)
- interpolation search is significantly more computationally expensive than binary search, so when the search distance is small, we also fallback to binary search.
- if interpolation search does not make significant progress (i.e. reduces search space by more than half each iteration), we can assume data is non-uniform and fallback.

Interpolation search also performs best when there is minimal shortening, especially shortening of the last block, as it can heavily skew the distribution of the actual keys.

Note that each search algorithm is guaranteed to make progress because at each iteration the search space is guaranteed to be reduce by at least 1.

For now this change only applies to index block seeks, as data block seeks and other blocks do not have as many entries and would not require significant number of search rounds, but it could be easily extended to include that support.

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

Test Plan:
Updated unit tests and crash test with new search option

### Benchmark
The default benchmark sets up keys in generally uniform distribution, so it was a good way to test performance improvements.

Setup: `./db_bench -benchmarks=fillseq,compact -index_shortening_mode=1`

#### Before this change
```
./db_bench -use_existing_db=true -benchmarks=readrandom -seed=1

readrandom   :       2.899 micros/op 344973 ops/sec 2.899 seconds 1000000 operations;   38.2 MB/s (1000000 of 1000000 found)
```

#### After this change

Notice how key comparison counts are the same between the two.
```
./db_bench -use_existing_db=true -benchmarks=readrandom -seed=1 -index_search_type=binary_search

readrandom   :       2.881 micros/op 347128 ops/sec 2.881 seconds 1000000 operations;   38.4 MB/s (1000000 of 1000000 found)
```

```
./db_bench -use_existing_db=true -benchmarks=readrandom -seed=1 -index_search_type=interpolation_search

readrandom   :       2.609 micros/op 383209 ops/sec 2.610 seconds 1000000 operations;   42.4 MB/s (1000000 of 1000000 found)
```

With a non-uniform distribution, `i.e. index_shortening_mode=2`

```
./db_bench -use_existing_db=true -benchmarks=readrandom -seed=1 -index_search_type=binary_search

readrandom   :       2.958 micros/op 338075 ops/sec 2.958 seconds 1000000 operations;   37.4 MB/s (1000000 of 1000000 found)
```

```
./db_bench -use_existing_db=true -benchmarks=readrandom -seed=1 -index_search_type=interpolation_search

readrandom   :       5.502 micros/op 181750 ops/sec 5.502 seconds 1000000 operations;   20.1 MB/s (1000000 of 1000000 found)
```

Reviewed By: pdillinger

Differential Revision: D91063163

Pulled By: joshkang97

fbshipit-source-id: 151d6aa76f8713740b714de6e406aff40d28ccbc
2026-02-13 17:15:10 -08:00
Peter Dillinger 871f79d6ef Reformat source files (#14331)
Summary:
probably something changed, maybe https://github.com/facebook/rocksdb/issues/14311

Full command:
```
git ls-files '*.cc' '*.h' | grep -v '^third-party/' | grep -v 'range_tree' | xargs clang-format -i
```

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

Test Plan: CI

Reviewed By: mszeszko-meta

Differential Revision: D93246992

Pulled By: pdillinger

fbshipit-source-id: 6bc5b97978fef8aee52823dadb6daa4bea57343d
2026-02-13 11:56:22 -08:00
Peter Dillinger 672389fd8c Remove obsolete compression code and some .h->.cc movement (#14325)
Summary:
In follow-up to https://github.com/facebook/rocksdb/issues/14315

Remove obsolete code replaced by new Compressor/Decompressor interface:
* OLD_CompressData and OLD_UncompressData
* Individual compression/decompression functions (Snappy_*, Zlib_*, BZip2_*, LZ4_*, LZ4HC_*, XPRESS_*, ZSTD_Compress, ZSTD_Uncompress)
* CompressionInfo and UncompressionInfo classes
* UncompressionDict class
* compression::PutDecompressedSizeInfo and GetDecompressedSizeInfo

The only small refactoring in this change that is not pure code removal or movement is in blob_file_builder_test.cc.

Move some function implementations etc. from compression.h to compression.cc:
* CompressionTypeToString, CompressionTypeFromString, CompressionOptionsToString
* ZSTD_TrainDictionary (both overloads), ZSTD_FinalizeDictionary
* DecompressorDict::Populate
* Most compression library includes

Also cleaned up other includes of compression.h, which caused some other files to need new includes.

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

Test Plan: existing tests

Reviewed By: hx235

Differential Revision: D93120580

Pulled By: pdillinger

fbshipit-source-id: ab5c50db7379c0387a8c0e379642c9ea2799eae5
2026-02-13 11:18:05 -08:00
Hui Xiao c33a4989ad Default CompactionOptionsUniversal::reduce_file_locking to be true (#14329)
Summary:
**Context/Summary:**

Internal adoption has demonstrated stability and measurable improvements of this feature without much cost so we can turn it on by default. Eventually we'd like to remove this configuration and make this an expected behavior.

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

Test Plan: Existing unit test

Reviewed By: mszeszko-meta

Differential Revision: D93210059

Pulled By: hx235

fbshipit-source-id: 04f77954e6624c8e60a2db030eb19eb341dd0fcf
2026-02-13 10:37:34 -08:00
Peter Dillinger 7ecc12110c Fix format compatibility issues, extend test (#14323)
Summary:
See https://github.com/facebook/rocksdb/issues/14240 which brought this to my attention. Here I've added range deletions and compactions to the format compatible test, and fixed or worked-around compatibility issues (likely longstanding).

The first fix was in Version::MaybeInitializeFileMetaData for an assertion failure simply from adding range deletions from some 5.x version.

The second fix is a broader work-around for older SST files with unreliable num_entries/num_range_deletions/num_deletions statistics in their table properties. We depend on them only for some paranoid checks for compaction, so in my assessment the best way to deal with those files is to exclude the paranoid checks when dealing with the files with unrelaible data. (Details in code comments.) The important part is that compacting old files is exceptionally rare, so we aren't really interefering with the paranoid checks doing thier job on an ongoing basis.

This depends on https://github.com/facebook/rocksdb/issues/14315 (just landed) because there is a remaining undiagnosed problem with some very early releases, but I'm not fixing that because its support is being dropped.

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

Test Plan: test extended (ran locally excluding some releases)

Reviewed By: xingbowang

Differential Revision: D93032653

Pulled By: pdillinger

fbshipit-source-id: f90b32f30ba4764692e68d23705f42c778e0dc1d
2026-02-13 09:18:40 -08:00
Hui Xiao d72a471749 Replace resumable compaction job unit test with compaction service unit test (#14191)
Summary:
**Context/Summary**:
compaction_job_test does low level assertions on what keys were saved and to resume in https://github.com/facebook/rocksdb/commit/1e5fa69c99ac8765783f5ce8a3a065b08f5b08a7 before the integration of the feature is done in a separate PR https://github.com/facebook/rocksdb/commit/f7e4009de1d16421a254dd7e799dd91c522d832c. Such low-level test makes it difficult to assert data correctness, is hard to understand by being tied to implementation details.

Therefore they are now replaced with db-level tests  with data correctness check, which is what we ultimately care out of those details. I also expand the test to cover wide column and TimedPut() which associates a key with write time.

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

Test Plan:
- Only test change; I also manually traced every test to ensure correct resumption point; also removing
```
if (c_iter->IsCurrentKeyAlreadyScanned()) {
    return false;
  }
```
correctly leads to the expected error with resumption at merge, single delete and deletion at bottom

Reviewed By: jaykorean

Differential Revision: D89492846

Pulled By: hx235

fbshipit-source-id: 6c6ab3cbd643ca1b15d049a062da2c76165ef9db
2026-02-12 22:30:55 -08:00
Hui Xiao c3184220b8 Fix an internal comment about resumable compaction (#14215)
Summary:
**Context/Summary:** as titled

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

Test Plan: no code change

Reviewed By: jaykorean

Differential Revision: D90037003

Pulled By: hx235

fbshipit-source-id: 8621a8dedef474b02bb16531e0de4ea399659d21
2026-02-12 21:29:10 -08:00
Peter Dillinger d8b1893c9d DROP support for block-based SST format_version < 2 (#14315)
Summary:
... and remove some old code and tech debt in the process.

This is arguably a great milestone and precendent in RocksDB history as for the first time we are explicitly dropping support for the ability to read source-of-truth data in old formats. (We previously dropped support for reading some old bloom filters, but those are performance optimizers not source-of-truth. https://github.com/facebook/rocksdb/issues/10184) However, DBs written with default settings since release 4.6.0, which is very nearly 10 years ago, can still be read. And by using compaction with intermediate versions, there's an upgrade path going back to (AFAIK) early releases of LevelDB (from which RocksDB was forked).

Some detail:
* The magic number for LevelDB SST files (0xdb4775248b80fb57, most recently called kLegacyBlockBasedTableMagicNumber) now only exists in the code to provide a good error message and to test that good error message.
* There is some notable refactoring and renaming around format_version handling. This is a bit of a messy area of code because the footer code being shared between different table formats (block-based, plain, cuckoo) means format_version in the footer is in ways tied to all of them, but in other ways is just tied to block-based table where we have been making updates. Hopefully code comments keep this clear.
* Now that there are old format_versions we can't read (and can't write authoritatively in tests), I've needed to split out kMinSupportedFormatVersion into a constant for reads and for writes, currently the same at format_version=2. Comments describe how to update these in the future.
* The idea of versioning the compression format is basically going away, though we're keeping BuiltinV2 in places just because it's already there. There's lots of room in the BuiltinV2 schema to expand to new built-in compression types, or new ways of handling existing compression algorithms. CompressionManager with CompatibilityName gives users the power to customize compression without the need for versions tied to format_version.

Immediate follow-up:
* Clean up compression loose ends like OLD_Compress, OLD_Uncompress

Suggested follow-up:
* Update plain table builder to migrate to new footer version so that we can drop support for legacy footer. We have to be careful that the (likely untested) forward compatibility path I put in place a while back works (or fix it and wait a while) before dropping support for plain table with legacy footer.

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

Test Plan:
* Some tests updated / added
* A couple tests are obsolete: removed
* Also updated format compatible test, which now doesn't need to dig as far back into history building RocksDB.

Reviewed By: hx235

Differential Revision: D92577766

Pulled By: pdillinger

fbshipit-source-id: a23be846189d901ce087af4ca9a99cef18445cb7
2026-02-11 14:43:41 -08:00
Richard Barnes 3148c6cad4 Fix string-conversion issue in internal_repo_rocksdb/repo/table/block_based/index_builder.cc +1
Summary:
This could is triggering `-Wstring-conversion`, which presents as:
```
warning: implicit conversion turns string literal into bool: A to B
```
This is often a bug and what was intended. The most frequent cause is the code was:
```
void foo(bool) { ... }
void foo(std::string) { ... }
foo("this gets interpreted as a bool");
```

It is also possible the issue is innocuous as part of an assert:
```
assert(false && "this string is true, so the assertion is false");
EXPECT_FALSE("this string is true, so the expect fails");
```
in these cases the use is to "cute", so we modify the code to make it more obvious.
```
assert(false && "the compiler recognizes and doesn't complain about this pattern");
FAIL() << "much more obvious";
```

Differential Revision: D92886041

fbshipit-source-id: 6adfaa102f12e293491cc579ec92b48834d1d0a8
2026-02-11 14:09:12 -08:00
Anand Ananthabhotla 56cb88ec79 Fix racy assertion in AbortIOPartialHandlesBug test (#14319)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14319

The test asserted h1->is_finished and h2->is_finished immediately after AbortIO({H0}), before calling Poll. This is invalid because AbortIO only guarantees that handles in its abort set are finalized. Non-aborted handles' CQEs may or may not be consumed during AbortIO depending on io_uring completion ordering. If H0's two CQEs (original read + cancel) arrive before H1/H2's CQEs, AbortIO breaks out of its wait loop without processing them. Move the H1/H2 is_finished assertions to after Poll, which correctly handles either case. Also remove the racy req_count checks for non-aborted handles since Poll does not increment req_count.

Reviewed By: jaykorean

Differential Revision: D92848827

fbshipit-source-id: 0c09b44ceada99877e8311cff799fa94f1056545
2026-02-10 15:05:12 -08:00
Richard Barnes 51feb25567 Fix string-conversion issue in internal_repo_rocksdb/repo/utilities/persistent_cache/block_cache_tier_file.cc +2 (#14312)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14312

This could is triggering `-Wstring-conversion`, which presents as:
```
warning: implicit conversion turns string literal into bool: A to B
```
This is often a bug and what was intended. The most frequent cause is the code was:
```
void foo(bool) { ... }
void foo(std::string) { ... }
foo("this gets interpreted as a bool");
```

It is also possible the issue is innocuous as part of an assert:
```
assert(!"this string is true, so the assertion is false");
EXPECT_FALSE("this string is true, so the expect fails");
```
in these cases the use is to "cute", so we modify the code to make it more obvious.
```
assert(false && "the compiler recognizes and doesn't complain about this pattern");
FAIL() << "much more obvious";
```

Reviewed By: dmm-fb

Differential Revision: D92528316

fbshipit-source-id: 93fbb624e8731c4cdb559746b44c1aa71d786304
2026-02-09 09:21:12 -08:00
Josh Kang 6284a79847 Upgrade clang format in CI to 21.1.2 (#14311)
Summary:
To make CI consistent with internal meta clang version.

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

Test Plan:
CI shows correct version
```
Successfully installed clang-format-21.1.2
clang-format version 21.1.2
```

Reviewed By: xingbowang

Differential Revision: D92535441

Pulled By: joshkang97

fbshipit-source-id: ea21ea97b13a35b286f0c2ce18b3f01ffbf49afd
2026-02-06 13:41:32 -08:00
Ryan Hancock 8f9cb1a708 Introduce Memory restrictions for IO Dispatcher. (#14300)
Summary:
Introduction of memory limiter for IO Dispatch.

Currently, the user has no way of enacting policy with IO dispatcher. One important policy is the ability to restrict the amount of memory a multiscan or set of multiscans is allowed to pin. This PR introduces the max_prefetch_memory_bytes in the IODispatcherOptions, allowing for users to specify bounds on block cache memory usage.

There seems to be a minor performance increase however, I have found the scans to be a bit noisy. Each benchmark is run with a stride size of 30000 keys. This was done to ensure we maintain parity with trunk.
```
Configuration: 10 concurrent scans, 1024B values, 5242880 byte SST files
Scan sizes: 1024 keys = 1MiB, 2048 keys = 2MiB, 4096 keys = 4MiB per scan

| Keys/Scan | Mode  | Main (ops/sec)   | Main (us/op)     | limiter              (ops/sec) | limiter            (us/op) | Delta ops/sec |
|-----------|-------|------------------|------------------|----------------------|--------------------|---------------|
| 1024      | sync  |   151.6 +/- 8.0   | 6591.14 +/- 343.30 |    170.6 +/- 4.0      | 5855.32 +/- 136.19   | +12.00%       |
| 1024      | async |   156.4 +/- 24.7  | 6589.64 +/- 1345.73 |    173.8 +/- 2.7      | 5744.51 +/- 91.35   | +11.00%       |
| 2048      | sync  |    77.8 +/- 1.6   | 12785.64 +/- 286.49 |     87.6 +/- 3.4      | 11354.01 +/- 441.71   | +12.00%       |
| 2048      | async |    85.6 +/- 4.7   | 11658.11 +/- 618.49 |     91.4 +/- 1.2      | 10873.63 +/- 143.49   | +6.00%        |
| 4096      | sync  |    43.2 +/- 1.5   | 22932.27 +/- 730.66 |     43.8 +/- 0.7      | 22563.90 +/- 320.93   | +1.00%        |
| 4096      | async |    45.4 +/- 0.8   | 21875.64 +/- 357.04 |     46.2 +/- 0.7      | 21416.95 +/- 311.89   | +1.00%        |

```

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

Reviewed By: anand1976

Differential Revision: D92316556

Pulled By: krhancoc

fbshipit-source-id: dc0b7958a33b8ef5fa5af82b1c6d960041837fc1
2026-02-06 11:29:30 -08:00
Anand Ananthabhotla 3695cb6767 Fix AbortIO consuming completions for non-aborted handles (#14301)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14301

When AbortIO was called with a subset of outstanding async read handles,
it would consume io_uring completions for handles NOT in the abort set
but fail to finalize them. This caused subsequent Poll calls on those
handles to hang forever waiting for completions that had already been
consumed.

The fix adds an `is_being_aborted` flag to Posix_IOHandle that is set
when submitting the cancel request. When processing completions in
AbortIO, handles with this flag wait for req_count==2 (original + cancel),
while handles without the flag are finalized immediately at req_count==1.

Also refactored the completion finalization logic into a shared
FinalizeAsyncRead() helper function used by both Poll and AbortIO.

Reviewed By: mszeszko-meta, archang19

Differential Revision: D92230883

fbshipit-source-id: e6d11e009a4930e5608459771990f6cf7d46d827
2026-02-06 10:51:21 -08:00
anand76 a668dcbe8c Add Txn db support to ldb (#14304)
Summary:
This change adds the ability to open and operate on databases as TransactionDB in the ldb command-line tool.

  New Command-Line Options

  - --use_txn - Opens the database as a TransactionDB instead of a regular DB
  - --txn_write_policy=<0|1|2> - Sets the transaction write policy:
    - 0 = WRITE_COMMITTED (default)
    - 1 = WRITE_PREPARED
    - 2 = WRITE_UNPREPARED

  Use Case
                                                                                                                                                                                                                      This is needed to inspect or modify databases that were created with WritePrepared or WriteUnprepared transactions, which require opening via TransactionDB::Open() rather than the regular DB::Open().

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

Test Plan:
Tests (tools/ldb_test.py): Adds testTxnPutGet() covering:
    - Basic put/get/delete with TransactionDB
    - All three write policies
    - Validation that --use_txn and --ttl are mutually exclusive

Reviewed By: pdillinger

Differential Revision: D92323195

Pulled By: anand1976

fbshipit-source-id: 0a62b8ea4e2985feed977fad72595d6fff75db09
2026-02-06 10:47:19 -08:00
Andrew Chang 6ac0da313e Fix crash in GetLiveFilesStorageInfo on read-only DB (#14306)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14306

GetLiveFilesStorageInfo crashes when called on a read-only RocksDB because
it calls FlushWAL(), which accesses logs_.back() on an empty deque.

Root cause: DBImplReadOnly overrides SyncWAL() to return NotSupported, but
does NOT override FlushWAL(). Read-only DBs have an empty logs_ deque
because they don't create WAL writers during recovery - there's nothing to
write, so no WAL infrastructure is initialized.

The reason SyncWAL was originally marked NotSupported is that these WAL
operations (SyncWAL syncs buffer to disk, FlushWAL flushes to OS buffer)
require an active WAL writer at logs_.back().writer. Since read-only DBs:
1. Cannot perform writes
2. Don't create WAL files for writing
3. Have an empty logs_ deque

...there's no WAL writer to sync or flush. The operations are semantically
meaningless, not just "forbidden write operations."

The fix adds a FlushWAL override matching the SyncWAL pattern. The caller
in db_filesnapshot.cc:403-405 already handles IsNotSupported() gracefully:
  if (s.IsNotSupported()) { s = Status::OK(); }

Reviewed By: pdillinger

Differential Revision: D92419557

fbshipit-source-id: 7079071209b3c7be41a2c98c9b691e68bc031595
2026-02-05 15:22:52 -08:00
Richard Barnes 47344a0feb Fix string-conversion issue in internal_repo_rocksdb/repo/utilities/persistent_cache/volatile_tier_impl.cc +5 (#14296)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14296

This could is triggering `-Wstring-conversion`, which presents as:
```
warning: implicit conversion turns string literal into bool: A to B
```
This is often a bug and what was intended. The most frequent cause is the code was:
```
void foo(bool) { ... }
void foo(std::string) { ... }
foo("this gets interpreted as a bool");
```

It is also possible the issue is innocuous as part of an assert:
```
assert(!"this string is true, so the assertion is false");
EXPECT_FALSE("this string is true, so the expect fails");
```
in these cases the use is to "cute", so we modify the code to make it more obvious.
```
assert(false && "the compiler recognizes and doesn't complain about this pattern");
FAIL() << "much more obvious";
```

Reviewed By: anand1976

Differential Revision: D92013593

fbshipit-source-id: 0b4e00339bef3f76fc5b9ad35e2383c5e4f828f9
2026-02-05 11:22:28 -08:00
Peter Dillinger 48ec45d7bb Remove useless option CompressedSecondaryCacheOptions::compress_format_version (#14302)
Summary:
I don't think this option was ever useful. There was no compressed secondary cache compatibility issue that needed to accommodate compression format version 1. It was needlessly imported from legacy SST file formats. Version 1 is simply an inefficient format because it requires guessing the uncompressed size on decompression.

And as far as I know, we don't have any plans to make compressed secondary cache entries persistable across RocksDB versions. I.e. if persisting, we would simply tag the persistence layer with the version (perhaps major and minor) and throw out the cache whenever that changes. Then we don't have to deal with explicit schema versioning in persistenct caches. This is a workable approach because unlike SSTs, caches are not source-of-truth that need to survive version rollback.

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

Test Plan: existing tests

Reviewed By: anand1976

Differential Revision: D92315003

Pulled By: pdillinger

fbshipit-source-id: 0b82cfdbd92bcd2b8fbddd6586824f53c88069c4
2026-02-04 15:11:09 -08:00
Facebook GitHub Bot c2fab4629b Re-sync with internal repository
The internal and external repositories are out of sync. This Pull Request attempts to brings them back in sync by patching the GitHub repository. Please carefully review this patch. You must disable ShipIt for your project in order to merge this pull request. DO NOT IMPORT this pull request. Instead, merge it directly on GitHub using the MERGE BUTTON. Re-enable ShipIt after merging.

fbshipit-source-id: 08b287a3f343f6ac5872c2a059d91d1bed9ff0a8
2026-02-03 14:32:21 -08:00
Anand Ananthabhotla 82ff0678d4 Add a cleanup target to crash_test.mk (#14286)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14286

Add the db_c leanup target which can be used by CI test scripts to delete the db on failure. The db_crashtest.py doesn't automatically delete on error.

Reviewed By: jaykorean

Differential Revision: D91912877

fbshipit-source-id: d36ec0896fba64faaafe055d8673e437e85d0c3a
2026-02-03 12:22:09 -08:00
Xingbo Wang 92378eb3b8 Add CLAUDE.md and optimize tooling for claude code (#14293)
Summary:
* Add CLAUDE.md This CLAUDE.md is generated through the analysis of 9,012 commits from the RocksDB repository since 2016. It aggregate the commits into 8 major components based on the component it is changing. It selected top 100 most complex PR based on line of changes to collect the code review feedbacks. For each PR, we collected:

  • PR title and description to understand the change context
  • Files changed to identify affected components and measure complexity
  • Inline code review comments from reviewers
  • General review summaries and approval/change request feedback

The feedback was then categorized by RocksDB component and analyzed for recurring themes, patterns, and best practices.

This CLAUDE.md file could be used for guiding code generation and code review.

* Optimize tooling for claude code Add make check-progress and format-auto targets for automation

Add machine-parseable progress reporting for `make check` to support automated monitoring tools like Claude Code:

- Add `build_tools/check_progress.sh` script that outputs JSON progress
- Add `make check-progress` target to poll build/test progress
- Detects phases: compiling -> linking -> generating -> testing
- Reports failed tests with exit codes, signals, and log output
- Limits to 10 failures with last 50 lines of output each

Also add non-interactive formatting support:

- Add `-y` flag to format-diff.sh for auto-apply without prompts
- Add `make format-auto` target for CI/automation use

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

Test Plan: Local build

Reviewed By: pdillinger

Differential Revision: D92085763

Pulled By: xingbowang

fbshipit-source-id: ba122a4ff51087aec5c06bab804edfee34e13880
2026-02-03 05:53:50 -08:00
Hemal Shah d1b63738e0 Add WriteBatch::Handler::LogData iteration callback function (#14245)
Summary:
Change adds `log_data_` function callback for when iterating over a `WriteBatch`. Previously only the `Put`, `Delete`, `Merge` operations were called into when iterating over an `WriteBatch` (and their `*_cf` equivalent through a different `WriteBatch::Handler` implementation).

To maintain backwards compatibility, previously exported function definitions remain the same, but new functions are exported for different languages to use the `LogData` callback on an iteration.

### Background

Hi - we use the [`rust-rocksdb`](https://github.com/rust-rocksdb/rust-rocksdb) bindings to work with `rocksdb` at Stripe. We are starting to make small contributions https://github.com/facebook/rocksdb/pull/14183 & https://github.com/facebook/rocksdb/pull/14136 and this adds on top of it. I saw that the `PutLogData` method is already exported for a `WriteBatch`, but there's no way to consume that. This change allows us to consume that information (with a follow up change on the [`rust-rocksdb`](https://github.com/rust-rocksdb/rust-rocksdb) repo.).

Thanks for your time looking into this. Previously we had trouble with meta's internal linters - I am happy to make appropriate change if something like that pops up again.

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

Reviewed By: archang19

Differential Revision: D92069503

Pulled By: jaykorean

fbshipit-source-id: a4a3c885462f641c8df9e3401a0e4c1d38871c6f
2026-02-02 17:36:44 -08:00
Ryan Hancock feffb67303 Replace Prefetch Logic in BlockBasedTableIterator with IODispatcher. (#14255)
Summary:
This diff introduces the IODispatcher into the BlockBasedTableIterator. This replaces much of the prefetch logic with the logic found in IODispatcher.

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

Test Plan:
I ran the following benchmark, %change is within noise tolerance. There shouldn't be any large performance improvement with this change, more so there should also not be any performance degradation.

MultiScan Benchmark: Current Branch vs Main

  Configuration:
  - Threads: 4
  - Ranges per scan: 10
  - Stride: 5000
  - Seek nexts: 100
  - Cache: Cold (dropped before each run)
  - Runs: 3

  Results:
  │ Mode  │ Main (ops/sec) │ Current (ops/sec)  │ Change │
  │ Sync   │ 8,901                 │ 9,032                      │ +1.5%  │
  │ Async │ 11,297                │ 11,947                     │ +5.8%  │

I further run db_stress test
```
make -j32 -f crash_test.mk J=32 blackbox_crash_test
```
Against my local machine for 60 minutes, on local flash, with async-io for multiscans always on.

Reviewed By: anand1976

Differential Revision: D91705195

Pulled By: krhancoc

fbshipit-source-id: acf2f944e8b715e99384c8cee79f8d241eadf5b8
2026-02-02 13:18:02 -08:00
Andrew Chang 27d70ecd74 Propagate Poll errors in FilePrefetchBuffer::PollIfNeeded (#14282)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14282

Previously, `PollIfNeeded` returned `void` and silently ignored errors from
`fs_->Poll()` by calling `PermitUncheckedError()`. This could lead to silent
data corruption or unexpected behavior when Poll operations fail.

This diff changes `PollIfNeeded` to return `Status` and properly propagate
Poll errors to callers. When Poll fails:
1. The IO handle is cleaned up via `DestroyAndClearIOHandle`
2. The error status is returned to the caller
3. Callers (`HandleOverlappingAsyncData` and `PrefetchInternal`) now check
   and propagate this error

Also adds a `TEST_SYNC_POINT_CALLBACK` to allow tests to inject Poll errors.

Reviewed By: anand1976

Differential Revision: D91624185

fbshipit-source-id: 8dd0ee6588ed1ce4bf080bcf857b778c5140ccf5
2026-01-30 12:18:44 -08:00
Xingbo Wang 656b734a5f Support abort background compaction jobs. (#14227)
Summary:
This adds a new public API to allow applications to abort all running compactions and prevent new ones from starting. Unlike DisableManualCompaction() which only pauses manual compactions and waits for them to finish naturally, AbortAllCompactions() actively signals running compactions (both automatic and manual) to terminate early and waits for them to complete before returning.

The abort signal is checked periodically during compaction (every 100 keys), so ongoing compactions abort quickly. Any output files from aborted compactions are automatically cleaned up to prevent partial results from being installed.

This is useful for scenarios where applications need to quickly stop all compaction activity, such as during graceful shutdown or when performing maintenance operations.

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

Test Plan:
- Unit tests in db_compaction_abort_test.cc cover various abort scenarios including: abort before/during compaction, abort with multiple subcompactions, nested abort/resume calls, abort with CompactFiles API, abort across multiple column families, and timing guarantees
- Updated compaction_job_test.cc to include the new parameter

Reviewed By: anand1976

Differential Revision: D91480994

Pulled By: xingbowang

fbshipit-source-id: 36837971d8a540cd34d3ec28a78bc94b582625b0
2026-01-30 05:53:04 -08:00
Adam Retter 21a8b5f77f Fixes the Windows VS 2022 build (#14280)
Summary:
When building a Release on Windows RTTI is not available, so asserts that use dynamic_cast need to be disabled

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

Reviewed By: nmk70

Differential Revision: D91807791

Pulled By: mszeszko-meta

fbshipit-source-id: e29c19c757bcd076a1f09ed40b306bb50ba9e882
2026-01-29 12:44:27 -08:00
Maciej Szeszko e94df3db52 Remove dead code and unused includes (#14278)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14278

Remove dead code from `BlobDBImpl`:
- `debug_level_ `member and associated unreachable debug logging
- `CopyBlobFiles()` - private method that was never called
- `FileDeleteOk_SnapshotCheckLocked()` - declared but never implemented
- `RemoveTimerQ()` - declared but never implemented

Remove unused includes:
- rocksdb/wal_filter.h from blob_db_impl.h
- rocksdb/utilities/transaction.h from blob_db_impl.cc
- table/meta_blocks.h from blob_db_impl.cc
- util/random.h from blob_db_impl.cc

Remove from BlobFile:
- `GetColumnFamilyId()` - declared/implemented but never called

Reviewed By: xingbowang

Differential Revision: D91089144

fbshipit-source-id: d9bce24122b3bb790644fe4e51ce4403c77a1abf
2026-01-29 00:53:52 -08:00
Maciej Szeszko 83d24db3d5 Remove unused public APIs (#14277)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14277

Remove `GetBlobDBOptions()` and `SyncBlobFiles()` from the public `BlobDB` interface. These methods were only used internally or in tests and are not needed by any production code. `GetBlobDBOptions()` is now replaced by storing bdb_options_ as a member in the test class. `SyncBlobFiles()` is moved to private in `BlobDBImpl` since it's only called internally. Also remove unused `kDeleteCheckPeriodMillisecs` constant.

Reviewed By: xingbowang

Differential Revision: D91089111

fbshipit-source-id: 9c92b6d9563cf241c69d8880b418e8bcb7acb6c5
2026-01-29 00:04:09 -08:00
Maciej Szeszko 374c8dd635 Remove bytes_per_sync config option (#14276)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14276

No production user of Legacy overrides the default value of `bytes_per_sync`. Replace the option with a constant `kBytesPerSync` to further reduce legacy blob db customizability / configuration surface.

Reviewed By: xingbowang

Differential Revision: D91089096

fbshipit-source-id: 162df65646a4f3a3fab3586cf6ff223e1917d86e
2026-01-28 23:13:03 -08:00
Maciej Szeszko 053b0d54dc Remove blob_dir config option (#14275)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14275

No one ever sets `blob_dir` to a non-default value. Replace the configurable `blob_dir` option with a constant `kBlobDirName`. This simplifies the code and further reduces the configurability surface.

Reviewed By: xingbowang

Differential Revision: D91089039

fbshipit-source-id: 7d82e86415cc4bc89a7fe1399c29d4cc3058d1de
2026-01-28 22:05:33 -08:00
Maciej Szeszko 56d40be243 Remove path_relative config option (#14273)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14273

The `path_relative` option in `BlobDBOptions` was never used in practice - all
production deployments use the default value of `true` (relative path). The
absolute path mode (`path_relative = false`) was essentially unsupported:
- `GetLiveFiles()` returned `NotSupported` for absolute paths
- `GetLiveFilesMetaData()` had an assertion that would crash for absolute paths

This change removes the option and simplifies the code to always use relative
paths for the blob directory.

Changes:
- Remove `path_relative` field from `BlobDBOptions`
- Simplify `blob_dir_` construction in `BlobDBImpl` constructor
- Simplify path construction in `DestroyBlobDB()`
- Remove `NotSupported` check in `GetLiveFiles()`
- Remove assertion in `GetLiveFilesMetaData()`
- Remove logging of `path_relative` in `Dump()`
- Remove redundant `path_relative = true` in tests

Reviewed By: xingbowang

Differential Revision: D91089016

fbshipit-source-id: 947b129e405a315b94ac73bc48b23103ba12d73b
2026-01-28 18:03:05 -08:00
Maciej Szeszko 148f6c9845 Replace GC cutoff threshold with a constant (#14272)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14272

All production deployments have the hardcoded (non-configurable) default for`garbage_collection_cutoff = 0.25`. This change removes the configurable option and replaces it with a fixed constant `kGarbageCollectionCutoff = 0.25`, simplifying the configuration surface.

Changes:
- Remove `garbage_collection_cutoff` from `BlobDBOptions`
- Add `kGarbageCollectionCutoff` constant (0.25) in blob_db_impl.cc
- Remove `--blob_db_gc_cutoff` flag from db_bench tool and db_stress
- Update tests to work with the fixed cutoff value

Reviewed By: xingbowang

Differential Revision: D91088998

fbshipit-source-id: 820fc7f1ad4c3fe8a15f22a92cd53fb96c56c6e1
2026-01-28 16:58:23 -08:00
Maciej Szeszko 6b5ccbbec6 Remove inline values support (#14270)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14270

Legacy BlobDB's inline values feature (storing small values directly in the LSM tree via `min_blob_size` threshold) is unused in production - all
deployments use `min_blob_size = 0`. This removes the functionality entirely.

Changes:
- Remove `min_blob_size` from `BlobDBOptions`
- Remove `IsInlined()` check from compaction filter (dead code path)
- Remove inline-related statistics (`BLOB_DB_WRITE_INLINED*`)
- Remove `InlineSmallValues` test
- Update stale comments referencing inlined data

Reviewed By: xingbowang

Differential Revision: D91088985

fbshipit-source-id: ec67848ece1a7dc071ca8e8a17faebb435394733
2026-01-28 16:08:10 -08:00
Maciej Szeszko 80f3d86f21 Remove FIFO eviction support (#14268)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14268

FIFO eviction is unused in production, making this dead code that adds complexity.

Core changes:
- Remove `is_fifo` from `BlobDBOptions`
- Remove `CheckSizeAndEvictBlobFiles` and related methods
- Remove `fifo_eviction_seq` and `evict_expiration_up_to` from `BlobCompactionContext`

CLI tool cleanup:
- Remove `--blob_db_is_fifo` flag from db_bench
- Remove stale FIFO eviction comments

Tests:
- Remove FIFO-related tests (`FIFOEviction_*`, `FilterForFIFOEviction`)

Note: TTL-based expiration (`EvictExpiredFiles`) is preserved as it handles blob file cleanup based on TTL, which is separate from FIFO eviction.

Reviewed By: xingbowang

Differential Revision: D91088968

fbshipit-source-id: 123df98d1132095cef15473b76011de030c5df34
2026-01-28 14:02:49 -08:00
Maciej Szeszko 2366f63e4f Remove compression support (#14266)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14266

Compression is unused in production, making this dead code that adds unnecessary complexity.

Core changes:
- Remove `compression` field from `BlobDBOptions`
- Remove compression/decompression methods (`GetCompressedSlice`,
  `DecompressSlice`, `BlobDecompressor`)
- Simplify `ReadBlobFromOldFile` and `GetBlobValue` to handle only
  uncompressed blobs
- Update compaction filter to skip compression/decompression

CLI tool cleanup:
- Remove `--blob_db_compression_type` flag from db_bench (Stacked BlobDB)
- Remove `--show_uncompressed_blob` from blob_dump tool
- Remove `--dump_uncompressed_blobs` from ldb dump/file_dump commands
- blob_dump_tool now fails fast with NotSupported for compressed files

Tests:
- Remove compression-related tests (`Compression`, `DecompressAfterReopen`,
  `EnableDisableCompressionGC`, `ChangeCompressionGC`)

Reviewed By: xingbowang

Differential Revision: D91088957

fbshipit-source-id: 496ee41dcbd0023b794aa8a6d7dcc9c2451b7470
2026-01-28 11:30:37 -08:00
Maciej Szeszko acfea34c91 Work around GCC 12 false positive warning for string::insert (#14265)
Summary:
Work around a warning/linter false positive related to the use of string::insert. The code in question is legal C++, but GCC 12's libstdc++ implementation of string::insert internally uses memcpy, which can trigger undefined behavior warnings when the source and destination overlap.

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

Reviewed By: pdillinger

Differential Revision: D91594561

Pulled By: mszeszko-meta

fbshipit-source-id: faa1487aba11a6581bf9ac8eb89442b6e4120427
2026-01-27 13:06:39 -08:00
Maciej Szeszko de06ce37db Remove PutUntil API (#14257)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14257

Removes the 'unused' `PutUntil` API and updates `Put/PutWithTTL` to inline the previous implementation. Test helpers are updated to use `PutWithTTL` with computed TTL values instead.

Reviewed By: xingbowang

Differential Revision: D90900841

fbshipit-source-id: c6ab89fe32773f426b0bedc706bf5a2683ec31cf
2026-01-27 11:13:48 -08:00
Evan Jones a3fe685cdc math.h BottomNBits: Fix integer underflow (#14231)
Summary:
When running make check on aarch64, hash_test reports an integer underflows:

    util/math.h:44:46: runtime error: signed integer overflow:
    -2147483648 - 1 cannot be represented in type 'int'
    util/math.h:44:46: runtime error: signed integer overflow:
    -9223372036854775808 - 1 cannot be represented in type 'long long'
    util/math.h:44:46: runtime error: signed integer overflow:
    -9223372036854775808 - 1 cannot be represented in type 'long'

The issue is when BottomNBits(int32 value, 31) does not use BMI2, it executes the following:

    return static_cast<T>(v & ((T{1} << nbits) - 1));

For int32_t, (1 << 31) is the minimum value, and -1 is an integer underflow. The fix is to cast T to an unsigned type and use that for the bit manipulation.

I used Compiler Explorer to verify that this still compiles to the BZHI instruction mentioned in the comment with -march=x86-64-v3: https://godbolt.org/z/8bcTE8xbf

To reproduce these errors on x86-64, disable the BMI code path:
```
USE_CLANG=1 PORTABLE=x86-64-v2 LDFLAGS=-fsanitize=undefined CXXFLAGS=-fsanitize=undefined make -j20 hash_test
```

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

Reviewed By: mszeszko-meta

Differential Revision: D91353147

Pulled By: pdillinger

fbshipit-source-id: 64cc191ccb9ecba20c260fab759e8881e30d2352
2026-01-27 01:26:28 -08:00
Peter Dillinger cc691126e5 Start version 10.12 development (#14259)
Summary:
Update HISTORY, version number, format compatible test, and folly version

folly build now depends on libaio

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

Test Plan: CI

Reviewed By: anand1976

Differential Revision: D91356493

Pulled By: pdillinger

fbshipit-source-id: 9d85960c647758d5cb33e3910e714e2f7785fd06
2026-01-26 09:50:59 -08:00
Peter Dillinger ad218cacf6 Temporary disable multiscan_use_async_io in crash test (#14263)
Summary:
Seeing many errors like this

```
Iterator diverged from control iterator which has value ...
iterator is not valid with status: IO error: Req failed: Unknown error -14
VerifyIterator failed. Control CF default
```

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

Test Plan: CI

Reviewed By: archang19

Differential Revision: D91478886

Pulled By: pdillinger

fbshipit-source-id: 94b955b6ecdb7a3cab39dac8e7b0d1047d49a0bb
2026-01-26 09:06:33 -08:00
Peter Dillinger 6a79e02ebd Support pre-defined compression dictionaries (#14253)
Summary:
... in addition to those derived from samples. This could be useful when trade-offs favor an offline trained dictionary that's good for the whole work load, which can involve heavy-weight training, vs. on-the-fly training on samples for each file, which has limitations.

This involves some breaking changes to some deeper parts of the new compression API. I'm not concerned about performance because this doesn't touch the per-block parts of the API, just the per-file parts.

Bonus: change to
CompressionManagerWrapper::FindCompatibleCompressionManager to implement what is likely the preferred behavior.

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

Test Plan: unit test included

Reviewed By: hx235

Differential Revision: D91082208

Pulled By: pdillinger

fbshipit-source-id: 1442db65e15c9435437204c19787c96f7a40a207
2026-01-23 10:01:50 -08:00
Peter Dillinger a9906f0dd0 A better approach to clearing DBs for crash test (#14254)
Summary:
Clearing DB dir for crash test is currently a hodgepodge of
1. Caller of db_crashtest.py maybe tries to clear the dir
2. db_crashtest.py tries to clear the dir in get_dbname() (but ignoring failure)
3. db_crashtest.py passes --destroy_db_initially to some db_stress calls as needed
4. db_crashtest.py tries to clear the dir between some db_stress calls
5. db_crashtest.py tries to clear the dir after everything is done and successful (no artifacts to investigate or save) (but ignoring failure)
6. Try to add more uniqueness to the directory from https://github.com/facebook/rocksdb/issues/14249

This change reverts or replaces 2, 4, 5, and 6 by doubling-down on (expanding) 3 and a small variant of it:

* crash_test.mk passes --destroy_db_initially=1 so that the first run of db_stress clears the db dir.
* After each db_stress invocation, db_crashtest.py resets destroy_db_initially=0 so that the next invocation reuses the same DB, except in cases where there is an incompatibility that requires a fresh DB (from cases 3 and 4 above).
* On success, uses new `db_stress --destroy_db_and_exit` option to clean up the DB dir without needing a custom cleanup_cmd (now ignored)

Note that although case 1 is likely obsolete, it is out of control of an open source PR.

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

Test Plan: some manual runs

Reviewed By: xingbowang

Differential Revision: D91164731

Pulled By: pdillinger

fbshipit-source-id: 0a66c8c0e130c9eeacc55af411a18a09bc9debdf
2026-01-22 11:48:06 -08:00
Anand Ananthabhotla b89d290c20 Add MultiScan statistics (#14248)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14248

### Overview

This diff introduces the addition of multi-scan statistics to RocksDB, enhancing the database's ability to monitor and analyze performance during multi-scan operations.

### Key Changes

#### Implemented Multi-Scan Statistics

The following statistics were implemented to provide deeper insights into multi-scan operations:

- **MULTISCAN_PREPARE_MICROS**: Measures the time (in microseconds) spent preparing for multi-scan operations.
- **MULTISCAN_BLOCKS_PER_PREPARE**: Tracks the number of blocks processed per multi-scan prepare operation.
- **Wasted Prefetch Blocks Count**: Counts the number of prefetched blocks that were not used (i.e., wasted) if the iterator is abandoned before accessing them.
- **MULTISCAN_TOTAL_BLOCKS_SCANNED**: Tracks the total number of blocks scanned during all multi-scan operations.
- **MULTISCAN_TOTAL_KEYS_SCANNED**: Measures the total number of keys scanned across all multi-scan operations.
- **MULTISCAN_TOTAL_MICROS**: Captures the total time (in microseconds) spent in multi-scan operations.
- **MULTISCAN_PREFETCHED_BLOCKS**: Counts the number of blocks that were prefetched during multi-scan operations.
- **MULTISCAN_USED_PREFETCH_BLOCKS**: Tracks the number of prefetched blocks that were actually used during multi-scan operations.

### Impact

This diff provides more fine-grained statistics for multi-scan operations, allowing developers and users to better understand and optimize the performance of their RocksDB instances.

Reviewed By: krhancoc

Differential Revision: D91053297

fbshipit-source-id: 7158741b9f026c0b5ce8ba1264dbd137e7fe985d
2026-01-21 23:23:38 -08:00
Andrew Chang f84351de98 Fix AbortIO hang when aborting multiple io_uring handles (#14252)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14252

Fixed a bug in PosixFileSystem::AbortIO that could cause an infinite hang
when aborting multiple concurrent async IO handles.

The bug occurred in the completion processing loop: when an io_uring
completion arrived for a handle other than the one currently being waited
for (io_handles[i]), the code would increment that handle's req_count but
only mark it as finished if it also matched io_handles[i]. This meant
completions for other handles were consumed but those handles were never
marked as finished.

Later, when iterating to those handles, the code would enter
io_uring_wait_cqe expecting more completions, but they had already been
consumed - causing an infinite hang.

The fix aligns AbortIO's completion handling with what Poll() already does:
mark handles as finished whenever their completions arrive, regardless of
which handle we're currently waiting for in the outer loop. Only the break
statement remains conditional on matching io_handles[i].

Reviewed By: anand1976

Differential Revision: D91070044

fbshipit-source-id: 47faf5f0df3e26a2aa83444bbac623f43f560933
2026-01-21 19:17:05 -08:00
Andrew Chang f312633aff Fix AbortIO documentation to match actual behavior (#14251)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14251

The AbortIO API documentation incorrectly stated that the callback
should NOT be called for aborted io_handles. However, the actual
implementation in fs_posix.cc does invoke the callback with
IOStatus::Aborted() status after cancelling requests:

```
// fs_posix.cc:1252-1260
if (posix_handle->req_count == 2 &&
    static_cast<Posix_IOHandle*>(io_handles[i]) == posix_handle) {
  posix_handle->is_finished = true;
  FSReadRequest req;
  req.status = IOStatus::Aborted();
  posix_handle->cb(req, posix_handle->cb_arg);

  break;
}
```

This change corrects the documentation to match the actual behavior
in RocksDB.

Reviewed By: anand1976

Differential Revision: D91073466

fbshipit-source-id: 47ae14a09e9386cc68049ca272d6b712f5a9bed7
2026-01-21 16:26:06 -08:00
Peter Dillinger a6af317476 Use format_version=7 by default, fix perf bug (#14239)
Summary:
Since it's been > 6 months and we have production uses, migrate to fv=7 by default. One unit test needed an update for the change to table properties with fv=7.

On making this change, PresetCompressionDictTest tests detected extra memory usage by decompressing LZ4 with dictionary compression. This turned out to be a bug in `std::find` usage that led to using the ZSTD-optimized decompressor (with digested dictionary usage) in cases where it is not needed. I've fixed the bug and improved the unit tests that found the bug.

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

Test Plan: existing tests, including format compatible CI job (updated, and run locally with SHORT_TEST=1)

Reviewed By: hx235

Differential Revision: D90728697

Pulled By: pdillinger

fbshipit-source-id: 8f1a0e9ca59a88c18eaa4cdfdea00309175ce30a
2026-01-21 09:28:06 -08:00
Xingbo Wang eb5e1a2d1f Use unique DB directory when TEST_TMPDIR is set (#14249)
Summary:
Some of the stress tests script run tests multiple times with TEST_TMPDIR set. When TEST_TMPDIR is set, the db directory is a fixed string. This caused the same DB directory was reused across db_crashtest.py script run. Typically, the DB folder is cleaned up after db_crashtest.py complete. But sometimes, the clean up command could fail. This caused the DB folder to be reused across different db_crashtest.py runs. Meantime, each db_crashtest.py run would randomize some of the parameters. This caused different parameters to be used with same DB directory, violating some of the assumption such as use_put_entity_one_in parameter to be not changed between runs. This change added a suffix to DB directory, so that each db_crashtest.py script run would generate a unique DB directory, which prevents the clean up failure issue causing test flaky.

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

Test Plan:
Stress test local run

```
TEST_TMPDIR=/tmp/aaa /usr/local/bin/python3 -u tools/db_crashtest.py --stress_cmd=./db_stress --cleanup_cmd='' --simple blackbox  --duration 15 --interval 10

>>> Running db_stress with pid=113810: ./db_stress ... --db=/tmp/aaa/rocksdb_crashtest_blackbox_6967584463401575611 ...
```

Reviewed By: hx235

Differential Revision: D91069655

Pulled By: xingbowang

fbshipit-source-id: 327fc3cd0d8e3ef4b49e182e21bcd91a10647710
2026-01-20 17:31:28 -08:00
Xingbo Wang ea5e649225 Fix an infinite compaction loop bug with udt (#14228)
Summary:
Problem

The TEST_WaitForCompact in
TimestampCompatibleCompactionTest.UdtTombstoneCollapsingTest would sometimes
run forever, indicating an infinite compaction loop.
Issue https://github.com/facebook/rocksdb/issues/14223

Root Cause

In ComputeBottommostFilesMarkedForCompaction(), files were marked for
bottommost compaction based only on the condition largest_seqno <
oldest_snapshot_seqnum. However, for User-Defined Timestamps (UDT) columns,
compaction can only zero sequence numbers when the file's maximum timestamp is
below full_history_ts_low.

When timestamps were above this threshold:
1. File gets marked for compaction (seqno condition met)
2. Compaction runs but cannot zero seqno (timestamp condition not met)
3. Output file immediately gets re-marked for compaction
4. Infinite loop

Solution

Added timestamp range tracking to FileMetaData and updated the marking logic to
check timestamps before marking files.

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

Test Plan: Unit test

Reviewed By: pdillinger

Differential Revision: D90586045

Pulled By: xingbowang

fbshipit-source-id: addfa4f988db8c87fb513a1bf58ee54623a6c210
2026-01-20 14:10:41 -08:00
anand76 2a7a6a6d72 Don't assert on async_read.status in MultiScan code path (#14244)
Summary:
Surface async read errors instead of asserting on them. This makes it easier to debug stress test failures. Async reads can fail for legitimate reasons, such as fs errors.

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

Reviewed By: hx235

Differential Revision: D90878515

Pulled By: anand1976

fbshipit-source-id: 6335d4b06ddf250b26842ce94e3f5263356b2695
2026-01-20 12:18:19 -08:00
Peter Dillinger 88aff40c97 New io stats for unknown file temperature last vs. non-last (#14243)
Summary:
These will be useful for qualifying non-tiered workloads for tiered storage.

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

Test Plan:
unit test included

I'm not concerned about performance because this fits pretty nicely into some existing code and only adds overhead when (expensive) IOs are done.

Reviewed By: jaykorean

Differential Revision: D90870348

Pulled By: pdillinger

fbshipit-source-id: 984411123bcd54c249a949da813ff04fedacc6a4
2026-01-16 11:01:37 -08:00
Peter Dillinger c6d08d3efe Use new compression APIs in db_bench (#14241)
Summary:
To move away from OLD_CompressData / OLD_UncompressData. Also improved some error/warning messages.

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

Test Plan: manual tests showing similar performance, runs with ASAN/UBSAN to check for issues

Reviewed By: hx235

Differential Revision: D90793708

Pulled By: pdillinger

fbshipit-source-id: e0655f7bed8d85e5ea110167dca73c6664f7465b
2026-01-16 10:01:42 -08:00
Peter Dillinger a1af6f9f64 Use new compression APIs internally for sample_for_compression (#14230)
Summary:
Trying to get rid of uses of OLD_CompressData / OLD_UncompressData. Some performance optimizations and corrections for better accounting also.

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

Test Plan:
* exanded unit test to be more complete / rigorous
* manual before-and-after db_bench runs with the option, seeing table properties as expected

Reviewed By: hx235

Differential Revision: D90545476

Pulled By: pdillinger

fbshipit-source-id: 2f7c577574bcc4b2acafa002761ec1cad7fdb093
2026-01-14 09:35:54 -08:00
Peter Dillinger 57036b68d9 Migrate blob handling to new compression APIs (#14234)
Summary:
as part of the effort to get rid of OLD_CompressData and OLD_UncompressData and the old implementations in compression.h.

It's unfortunate the the existing blob file schema doesn't allow storing blobs uncompressed when the compressed version is larger, so we have to work around that.

Note that use of GrowableBuffer in place of std::string is intended to avoid the potential performance overhead of zeroing out memory before overwriting it.

Also includes some cleanup of includes

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

Test Plan:
some unit test updates as needed. Crash test covers integrated blob support.

I'm not too concerned about performance, as until a future schema change, this code is committing the grave performance error of storing compressed data larger than uncompressed.

Reviewed By: mszeszko-meta, hx235

Differential Revision: D90544049

Pulled By: pdillinger

fbshipit-source-id: 2f2ed16de63990b797cc06c8dad36b5869dac302
2026-01-14 09:35:16 -08:00
Xingbo Wang 256838180e Fix stress test deadlock failure in TestPut (#14235)
Summary:
Deadlock or timeout is possible in TestPut, when TestMultiGet was executed at the same time, because it executes MaybeAddKeyToTxnForRYW, which writes to the same key space but does not acquire stress test level mutex. Therefore, RocksDB could return deadlock error.

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

Test Plan: Stress test

Reviewed By: hx235

Differential Revision: D90621772

Pulled By: xingbowang

fbshipit-source-id: eb808193ded06b69a8161320f88d5ba4e20b4901
2026-01-14 06:44:01 -08:00
Xingbo Wang 2893c25ca2 Support printing block checksum in sst_dump (#14222)
Summary:
Support printing block checksum in sst_dump

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

Test Plan:
manual test, sample output

```
Data Block # 1 @ 00B021
   Data block checksum type: 4  checksum value: 0x40614fb3  offset: 0  size: 4272  compression type: 0
```

Reviewed By: jaykorean

Differential Revision: D90286789

Pulled By: xingbowang

fbshipit-source-id: 71324e04549bea070d80b45a81b562ad331a7840
2026-01-10 05:05:18 -08:00
Xingbo Wang a6325e9564 Add block type to corruption error message (#14225)
Summary:
Add block type to corruption error message

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

Test Plan: Unit test

Reviewed By: jaykorean

Differential Revision: D90329899

Pulled By: xingbowang

fbshipit-source-id: 6fa925d1704c7c19c98d6067628b73a7c0904c3e
2026-01-09 14:37:46 -08:00
Xingbo Wang 4bcec5ae89 Fix autoconf download failure in folly build (#14226)
Summary:
Folly download dependencies directly from external source. Sometimes, this could fail due to external website instability. To solve this, we added github cache to cache the dependencies. We also added a python script to try different sources during download to reduce the chance of failure.

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

Test Plan: github CI

Reviewed By: krhancoc, archang19

Differential Revision: D90343051

Pulled By: xingbowang

fbshipit-source-id: 3faad6aaa6c1bfd361b9e405c298856cd64bf457
2026-01-09 10:43:54 -08:00
Josh Kang 4bf2bcdbb3 Allow setting options for multiple column families (#14201)
Summary:
Currently to set options for multiple CFs, the caller must repeatedly call SetOptions() for each CF. This in turn serializes the entire options file each time. This PR exposes a new API that allows SetOptions to be called on multiple CFs at once, thus only paying the OPTIONS file serialization once.

Also added a new unit test for SetOptions.

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

Reviewed By: pdillinger

Differential Revision: D89735181

Pulled By: joshkang97

fbshipit-source-id: 9b7a721b7e8769b653243b1581678ffd05d038e8
2026-01-08 15:15:31 -08:00
Ryan Hancock 2b28885c80 Introducing IO Dispatcher (#14135)
Summary:
This diff introduces the IO Dispatcher, which will be used to simplify the code path for MultiScan, while further providing a centralized place to enact policy on how MultiScan is done (i.e., limit memory usage and pinned buffers for example). Right now this diff only encapsulates the functionality done during the Prepare of MultiScan.

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

Reviewed By: anand1976

Differential Revision: D87837261

Pulled By: krhancoc

fbshipit-source-id: 2698910ade02bc3d182413ae07ce69fe7abb7ec5
2026-01-07 10:34:21 -08:00
zaidoon 429b36c22d Add C API for block_align option in BlockBasedTableOptions (#14153)
Summary: Pull Request resolved: https://github.com/facebook/rocksdb/pull/14153

Reviewed By: archang19

Differential Revision: D90211012

Pulled By: jaykorean

fbshipit-source-id: fd87d3d74664f75fbe47946764b1d25aa731c020
2026-01-06 19:03:18 -08:00
Xingbo Wang 6f03c3dfea Fix a flaky unit test UdtTombstoneCollapsingTest (#14220)
Summary:
As compaction scheduling is not deterministic, the existing check is too strict sometimes, causing test to be flaky.

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

Test Plan: Unit test

Reviewed By: pdillinger

Differential Revision: D90143556

Pulled By: xingbowang

fbshipit-source-id: 6780423c63324a4b20fc8b8ccac2051a094c9f4a
2026-01-06 14:29:02 -08:00
Xingbo Wang a7c1acbe9f Mark RateLimiter::GetMode method as const (#14221)
Summary:
Mark RateLimiter::GetMode method as const

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

Test Plan: existing unit test

Reviewed By: jaykorean

Differential Revision: D90182630

Pulled By: xingbowang

fbshipit-source-id: 119f3cf0082e285a84ecdca224535f03f2afbf12
2026-01-06 10:30:34 -08:00
Peter Dillinger 4e10e0bcac Fix cases of db dir going missing in db_crashtest.py (#14219)
Summary:
My PR https://github.com/facebook/rocksdb/issues/14195 regressed a case in which db_crashtest.py calling db_stress with --destroy_db_initially=1 could lead to dbname directory being nonexistant for subsequent calls to gen_cmd -> finalize_and_sanitize -> is_direct_io_supported which would fail in creating a temporary file. Fix this (and clean up existing related code) using os.makedirs.

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

Test Plan: I don't have a good reproducer for the error but some manual testing indicates this change is at least safe

Reviewed By: virajthakur

Differential Revision: D90138248

Pulled By: pdillinger

fbshipit-source-id: 0ed6524cd50f8632346a8583f26bf1f4941817ce
2026-01-06 10:09:07 -08:00
Peter Dillinger 387cb4aae7 Clarify/rename atomic wrapper stuff + blog post (#14213)
Summary:
* Some existing commentary and motivation around my atomic wrappers in atomic.h was based on a misreading of documentation. seq_cst *is* a safe substitute for acq_rel in all cases. I still like having a distinct type for RelaxedAtomic (as folly does) and a wrapper also for other cases to avoid readability traps like implicit conversion and implicit memory order. This PR is only comment changes and renaming.
* Create a blog post about bit fields API to help with lock-free (and low-lock) programming.

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

Test Plan: esiting tests

Reviewed By: xingbowang

Differential Revision: D89971581

Pulled By: pdillinger

fbshipit-source-id: 9bd1181d692258d668189c2da8bd0e5d98fd6230
2026-01-05 20:47:46 -08:00
Pierre Moulon 8b6f98cdb4 Fixing typos in comments and documentation (#14205)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14205

Fixed various spelling errors throughout RocksDB codebase including:
- assiciated → associated
- disucssion → discussion
- satisifed → satisfied
- supoort → support
- capacit_limit → capacity_limit
- direclty → directly
- diable → disable
- opeartions → operations
- paylaod → payload
- happenning/happended → happening/happened
- intialized/initiallized → initialized
- asynchronosuly → asynchronously
- exisiting → existing
- persitence → persistence
- and several others

These changes are in comments, test code, and documentation only.

Reviewed By: pdillinger

Differential Revision: D89800154

fbshipit-source-id: 1681ec95a687b038c2bad48856f1abb4dbeb42cf
2026-01-02 17:25:57 -08:00
Peter Dillinger 1cc1df8dab Finish migrating HCC to BitFields API (#14154)
Summary:
This change builds on https://github.com/facebook/rocksdb/issues/14027 and https://github.com/facebook/rocksdb/issues/13965 to complete migration
of the HyperClockCache implementation to using the hygienic BitFields API.
No semantic change in the implementation details is intended, just
greatly improving readability and safety of the code while maintaining
the same performance.

In more detail,
* Refactor the main metadata atomic for each slot in an HCC table into
SlotMeta using BitFields.
* Extended BitFields APIs with some additional features, and renamed
  BlahTransform classes to BlahTransformer to resolve potential naming
  conflicts with member functions to create them.

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

Test Plan:
for correctness, mostly existing tests. but also added tests
for new BitFields features. I especially ran local TSAN whitebox crash
test extensively which caught a couple of refactoring errors.

For performance, I verified with release builds of cache_bench, using
default options, that there was no noticeable/consistent difference
after all these HCC migrations vs. backing them out. That test was with
GCC 11 and -O2, which is a reasonable baseline for expected compiler
optimizations.

Reviewed By: xingbowang

Differential Revision: D87960540

Pulled By: pdillinger

fbshipit-source-id: e0257b7fea8a5c7709daef18911959201ce4e0f3
2025-12-29 17:13:50 -08:00
Xingbo Wang 3818cc1aca Fix a bug in seqno zeroing logic with UDT (#14207)
Summary:
This bug caused seqno to be incorrectly zeroed when UDT is enabled. This is one of the contributing factor that caused tombstones to be accumulated at bottommost level, causing high space amp.

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

Test Plan: Unit test

Reviewed By: pdillinger

Differential Revision: D89826564

Pulled By: xingbowang

fbshipit-source-id: 62ab1e37c36ae1ed95f26213c97a591a17e962a6
2025-12-29 10:53:58 -08:00
zaidoon 276721cd10 eliminate per-iterator heap allocation by constructing InternalKeyComparator in-place (#14044)
Summary:
resolve [13951](https://github.com/facebook/rocksdb/issues/13951)

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

Reviewed By: xingbowang

Differential Revision: D86217603

Pulled By: jaykorean

fbshipit-source-id: 8ed62503cfcfdfb26f7af7b0a5641cd47dd9e54c
2025-12-29 10:32:36 -08:00
nsaji-stripe 45690d0f6a CompactionServiceOptionsOverride setters C API (#14183)
Summary:
## Context
1. OpenAndCompact required CompactionServiceOptionsOverride
2. Currently there are no C APIs to create CompactionServiceOptionsOverride

## Changes
1. Create C API for compactionServiceOptionsOverride
2. Create helper function to create compactionServiceOptionsOverride from Options.  This was added in because The C API lacks getter methods for non-serializable options (comparator, table_factory, etc.). Without this, users would need to maintain separate references to all these options just to pass them to the override. If the user need to create a new comparator or table factory then C API for compactionServiceOptionsOverride already as the setters for the same.

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

Reviewed By: hx235

Differential Revision: D89690005

Pulled By: jaykorean

fbshipit-source-id: efe8211feec9d144b32be0f5e66c8cf8bde8dac0
2025-12-29 10:15:38 -08:00
Maciej Szeszko e77ba4bc95 Start 10.11.0 development (#14192)
Summary:
10.10.0 branch has been cut.

### Updated:

HISTORY.md
include/rocksdb/version.h
tools/check_format_compatible.sh

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

Reviewed By: virajthakur

Differential Revision: D89736225

Pulled By: pdillinger

fbshipit-source-id: d7fc592b33a5c60dc2b53aa72ceafaa507730f20
2025-12-23 12:18:44 -08:00
Peter Dillinger 7e9f54d56b Remove remaining pieces of Lua integration (#14200)
Summary:
Going from deprecated and partly removed to fully removed

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

Test Plan: existing tests

Reviewed By: archang19

Differential Revision: D89697543

Pulled By: pdillinger

fbshipit-source-id: bbf6161c04322e9756a9479758488cac6d03473a
2025-12-23 09:10:12 -08:00
Peter Dillinger 41beb1422f Improve db_crashtest.py for remote DB (#14195)
Summary:
Let db_crashtest.py work with TEST_TMPDIR on remote filesystem, by infering whether it's remote from the env_uri argument. Note that some other paths passed to db_stress are local paths and we can't reuse TEST_TMPDIR for those cases when it's remote.

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

Test Plan: public and private CI

Reviewed By: archang19

Differential Revision: D89590246

Pulled By: pdillinger

fbshipit-source-id: db6eb9c16d4e76617183780747353c798cc9bef6
2025-12-22 13:03:00 -08:00
488 changed files with 46561 additions and 10549 deletions

No files matched your search

+86
View File
@@ -0,0 +1,86 @@
# When making changes, verify the output of:
# clang-tidy -list-checks
---
Checks: "-*,\
bugprone-argument-comment,\
bugprone-dangling-handle,\
bugprone-fold-init-type,\
bugprone-forward-declaration-namespace,\
bugprone-forwarding-reference-overload,\
bugprone-shadow,\
bugprone-sizeof-*,\
bugprone-string-constructor,\
bugprone-undefined-memory-manipulation,\
bugprone-unused-return-value,\
bugprone-use-after-move,\
cert-env33-c,\
cert-err58-cpp,\
cert-msc30-c,\
cert-msc50-cpp,\
clang-analyzer-*,\
clang-diagnostic-*,\
-clang-diagnostic-missing-designated-field-initializers,\
concurrency-mt-unsafe,\
cppcoreguidelines-avoid-non-const-global-variables,\
cppcoreguidelines-missing-std-forward,\
cppcoreguidelines-pro-type-member-init,\
cppcoreguidelines-special-member-functions,\
cppcoreguidelines-virtual-class-destructor,\
google-build-using-namespace,\
google-explicit-constructor,\
google-readability-avoid-underscore-in-googletest-name,\
misc-definitions-in-headers,\
misc-redundant-expression,\
modernize-make-shared,\
modernize-use-emplace,\
modernize-use-noexcept,\
modernize-use-override,\
modernize-use-using,\
performance-faster-string-find,\
performance-for-range-copy,\
performance-implicit-conversion-in-loop,\
performance-inefficient-algorithm,\
performance-inefficient-string-concatenation,\
performance-inefficient-vector-operation,\
performance-move-const-arg,\
performance-move-constructor-init,\
performance-no-automatic-move,\
performance-no-int-to-ptr,\
performance-noexcept-move-constructor,\
performance-noexcept-swap,\
performance-trivially-destructible,\
performance-type-promotion-in-math-fn,\
performance-unnecessary-copy-initialization,\
performance-unnecessary-value-param,\
readability-braces-around-statements,\
readability-duplicate-include,\
readability-isolate-declaration,\
readability-operators-representation,\
readability-redundant-string-init"
WarningsAsErrors: "bugprone-use-after-move"
CheckOptions:
- key: bugprone-easily-swappable-parameters.MinimumLength
value: 4
- key: cppcoreguidelines-avoid-non-const-global-variables.AllowThreadLocal
value: true
- key: cppcoreguidelines-special-member-functions.AllowSoleDefaultDtor
value: true
- key: cppcoreguidelines-special-member-functions.AllowImplicitlyDeletedCopyOrMove
value: true
- key: modernize-use-using.IgnoreExternC
value: true
- key: performance-move-const-arg.CheckTriviallyCopyableMove
value: false
- key: performance-unnecessary-value-param.AllowedTypes
value: '[Pp]ointer$;[Pp]tr$;[Rr]ef(erence)?$'
- key: performance-unnecessary-copy-initialization.AllowedTypes
value: '[Pp]ointer$;[Pp]tr$;[Rr]ef(erence)?$'
- key: readability-operators-representation.BinaryOperators
value: '&&;&=;&;|;~;!;!=;||;|=;^;^='
- key: readability-redundant-string-init.StringNames
value: '::std::basic_string'
- key: readability-named-parameter.InsertPlainNamesInForwardDecls
value: true
...
+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' }}
@@ -0,0 +1,21 @@
name: cache-getdeps-downloads
description: Cache getdeps downloads to avoid unreliable mirrors and speed up builds
outputs:
cache-hit:
description: Whether the cache was hit
value: ${{ steps.cache-downloads.outputs.cache-hit }}
runs:
using: composite
steps:
- name: Cache getdeps downloads
id: cache-downloads
uses: actions/cache@v4
with:
# Use a fixed path that we control - folly.mk will sync with getdeps downloads dir
path: /tmp/rocksdb-getdeps-cache
# Use a rolling cache key - the cache accumulates downloads over time
# The key includes a weekly timestamp to ensure periodic refresh
key: getdeps-downloads-${{ runner.os }}-${{ runner.arch }}-week-${{ github.run_id }}
restore-keys: |
getdeps-downloads-${{ runner.os }}-${{ runner.arch }}-week-
getdeps-downloads-${{ runner.os }}-${{ runner.arch }}-
+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
+2 -2
View File
@@ -6,6 +6,6 @@ runs:
run: |
make checkout_folly
shell: bash
- name: Install patchelf
run: apt-get update -y && apt-get install -y patchelf
- name: Install patchelf and libaio
run: apt-get update -y && apt-get install -y patchelf libaio-dev
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');
}
};
+51
View File
@@ -0,0 +1,51 @@
name: Post clang-tidy PR comment
on:
workflow_run:
workflows: ["clang-tidy"]
types: [completed]
permissions:
pull-requests: write
jobs:
comment:
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
with:
name: clang-tidy-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('clang-tidy-comment.md') || !fs.existsSync('pr_number.txt')) {
core.info('No clang-tidy results found; skipping.');
return;
}
const body = fs.readFileSync('clang-tidy-comment.md', 'utf8');
const prNumber = parseInt(fs.readFileSync('pr_number.txt', 'utf8').trim());
const post = require('./.github/scripts/post-pr-comment.js');
await post({
github,
context,
core,
prNumber,
body,
marker: '<!-- clang-tidy-bot -->',
});
+74
View File
@@ -0,0 +1,74 @@
name: clang-tidy
on:
push:
pull_request:
permissions: {}
jobs:
clang-tidy:
if: github.repository_owner == 'facebook'
runs-on:
labels: 4-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
steps:
- uses: actions/checkout@v4.1.0
with:
fetch-depth: 2
- name: Mark workspace as safe for git
run: git config --global --add safe.directory $GITHUB_WORKSPACE
- name: Determine diff base
id: diff-base
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
BASE="${{ github.event.pull_request.base.sha }}"
git fetch --depth=1 origin "$BASE"
else
BASE="${{ github.event.before }}"
if echo "$BASE" | grep -q '^0\{40\}$'; then
echo "skip=true" >> "$GITHUB_OUTPUT"
echo "New branch push; skipping clang-tidy."
exit 0
fi
fi
echo "ref=$BASE" >> "$GITHUB_OUTPUT"
echo "skip=false" >> "$GITHUB_OUTPUT"
- name: Install clang-tidy
if: steps.diff-base.outputs.skip != 'true'
run: apt-get update && apt-get install -y clang-tidy
- name: Generate compile_commands.json
if: steps.diff-base.outputs.skip != 'true'
run: |
mkdir build && cd build
cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON \
-DCMAKE_C_COMPILER=clang-18 \
-DCMAKE_CXX_COMPILER=clang++-18 ..
cd ..
ln -sf build/compile_commands.json compile_commands.json
- name: Run clang-tidy on changed files
id: clang-tidy
if: steps.diff-base.outputs.skip != 'true'
run: |
python3 tools/run_clang_tidy.py \
-j 4 \
--diff-base ${{ steps.diff-base.outputs.ref }} \
--github-annotations \
--github-step-summary \
--comment-output clang-tidy-comment.md
continue-on-error: true
- name: Save PR number
if: github.event_name == 'pull_request' && always()
run: echo "${{ github.event.pull_request.number }}" > pr_number.txt
- name: Upload clang-tidy results
if: always()
uses: actions/upload-artifact@v4.0.0
with:
name: clang-tidy-result
path: |
clang-tidy-comment.md
pr_number.txt
if-no-files-found: ignore
- name: Fail if clang-tidy found issues
if: steps.clang-tidy.outcome == 'failure'
run: exit 1
+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
+4
View File
@@ -54,6 +54,7 @@ jobs:
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/cache-getdeps-downloads"
- uses: "./.github/actions/setup-folly"
- uses: "./.github/actions/build-folly"
- run: LIB_MODE=static USE_CLANG=1 USE_FOLLY=1 COMPILE_WITH_UBSAN=1 COMPILE_WITH_ASAN=1 make -j32 check
@@ -68,6 +69,7 @@ jobs:
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/cache-getdeps-downloads"
- uses: "./.github/actions/setup-folly"
- uses: "./.github/actions/cache-folly"
id: cache-folly
@@ -86,6 +88,7 @@ jobs:
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/cache-getdeps-downloads"
- uses: "./.github/actions/setup-folly"
- run: "DEBUG_LEVEL=0 make -j20 build_folly"
- run: "USE_FOLLY=1 LIB_MODE=static DEBUG_LEVEL=0 V=1 make -j20 release"
@@ -162,6 +165,7 @@ jobs:
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/cache-getdeps-downloads"
- uses: "./.github/actions/setup-folly"
- run: "(mkdir build && cd build && cmake -DUSE_FOLLY_LITE=1 -DWITH_GFLAGS=1 -DCMAKE_CXX_FLAGS=-DGLOG_USE_GLOG_EXPORT .. && make VERBOSE=1 -j20 && ctest -j20)"
- uses: "./.github/actions/post-steps"
+253 -82
View File
@@ -1,7 +1,18 @@
name: facebook/rocksdb/pr-jobs
on: [push, pull_request]
permissions: {}
env:
# Set to a job name to run only that job (on any repo), or leave empty for
# normal behavior (all jobs on facebook repo only).
ONLY_JOB: ''
jobs:
config:
runs-on: ubuntu-latest
outputs:
only_job: ${{ steps.set.outputs.only_job }}
steps:
- id: set
run: echo "only_job=$ONLY_JOB" >> "$GITHUB_OUTPUT"
# NOTE: multiple workflows would be recommended, but the current GHA UI in
# PRs doesn't make it clear when there's an overall error with a workflow,
# making it easy to overlook something broken. Grouping everything into one
@@ -19,6 +30,10 @@ jobs:
# increasing the risk of misconfiguration, especially on forks that might
# want to run with this GHA setup.
#
# SELECTIVE JOB EXECUTION: Set the ONLY_JOB env var at the top of this file
# to a job name (e.g. "build-linux-clang-tidy") to run only that job,
# bypassing the repository owner check. Leave it empty for normal behavior.
#
# DEBUGGING WITH SSH: Temporarily add this as a job step, either before the
# step of interest without the "if:" line or after the failing step with the
# "if:" line. Then use ssh command printed in CI output.
@@ -30,7 +45,8 @@ jobs:
# ======================== Fast Initial Checks ====================== #
check-format-and-targets:
if: ${{ github.repository_owner == 'facebook' }}
if: needs.config.outputs.only_job == 'check-format-and-targets' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4.1.0
@@ -44,6 +60,10 @@ jobs:
run: python -m pip install --upgrade pip
- name: Install argparse
run: pip install argparse
- name: Install clang-format
run: |
pip install https://files.pythonhosted.org/packages/fb/ac/3c04772acc0257f5730e83adb542b2603c1a62d1315010ab593a980af404/clang_format-21.1.2-py2.py3-none-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl
clang-format --version
- name: Download clang-format-diff.py
run: wget https://rocksdb-deps.s3.us-west-2.amazonaws.com/llvm/llvm-project/release/12.x/clang/tools/clang-format/clang-format-diff.py
- name: Check format
@@ -62,7 +82,8 @@ jobs:
SANITY_CHECK=1 LONG_TEST=1 tools/check_format_compatible.sh
# ========================= Linux With Tests ======================== #
build-linux:
if: ${{ github.repository_owner == 'facebook' }}
if: needs.config.outputs.only_job == 'build-linux' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
runs-on:
labels: 16-core-ubuntu
container:
@@ -71,10 +92,18 @@ 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: ${{ github.repository_owner == 'facebook' }}
if: needs.config.outputs.only_job == 'build-linux-cmake-mingw' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
runs-on:
labels: 4-core-ubuntu
container:
@@ -83,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: |-
@@ -91,27 +123,41 @@ 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: ${{ github.repository_owner == 'facebook' }}
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
steps:
- uses: actions/checkout@v4.1.0
- 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: ${{ github.repository_owner == 'facebook' }}
if: needs.config.outputs.only_job == 'build-linux-make-with-folly-lite-no-test' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
runs-on:
labels: 16-core-ubuntu
container:
@@ -120,29 +166,47 @@ jobs:
steps:
- uses: actions/checkout@v4.1.0
- 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: ${{ github.repository_owner == 'facebook' }}
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
steps:
- uses: actions/checkout@v4.1.0
- 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: ${{ github.repository_owner == 'facebook' }}
if: needs.config.outputs.only_job == 'build-linux-cmake-with-benchmark-no-thread-status' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
runs-on:
labels: 16-core-ubuntu
container:
@@ -151,10 +215,18 @@ 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: ${{ github.repository_owner == 'facebook' }}
if: needs.config.outputs.only_job == 'build-linux-encrypted_env-no_compression' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
runs-on:
labels: 16-core-ubuntu
container:
@@ -163,12 +235,20 @@ 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:
if: ${{ github.repository_owner == 'facebook' }}
if: needs.config.outputs.only_job == 'build-linux-release' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
runs-on:
labels: 16-core-ubuntu
container:
@@ -176,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
@@ -192,9 +275,14 @@ 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: ${{ github.repository_owner == 'facebook' }}
if: needs.config.outputs.only_job == 'build-linux-clang-13-no_test_run' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
runs-on:
labels: 8-core-ubuntu
container:
@@ -203,14 +291,22 @@ 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: ${{ github.repository_owner == 'facebook' }}
if: needs.config.outputs.only_job == 'build-linux-clang-18-no_test_run' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
runs-on:
labels: 16-core-ubuntu
container:
@@ -219,12 +315,20 @@ 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: ${{ github.repository_owner == 'facebook' }}
if: needs.config.outputs.only_job == 'build-linux-gcc-14-no_test_run' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
runs-on:
labels: 16-core-ubuntu
container:
@@ -233,31 +337,21 @@ 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: ${{ github.repository_owner == 'facebook' }}
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: ${{ github.repository_owner == 'facebook' }}
if: needs.config.outputs.only_job == 'build-linux-unity-and-headers' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
runs-on:
labels: 4-core-ubuntu
container:
@@ -266,12 +360,20 @@ 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: ${{ github.repository_owner == 'facebook' }}
if: needs.config.outputs.only_job == 'build-linux-mini-crashtest' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
runs-on:
labels: 4-core-ubuntu
container:
@@ -280,11 +382,19 @@ 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:
if: ${{ github.repository_owner == 'facebook' }}
if: needs.config.outputs.only_job == 'build-linux-clang18-asan-ubsan' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
runs-on:
labels: 32-core-ubuntu
container:
@@ -293,10 +403,18 @@ 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: ${{ github.repository_owner == 'facebook' }}
if: needs.config.outputs.only_job == 'build-linux-clang18-mini-tsan' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
runs-on:
labels: 32-core-ubuntu
container:
@@ -305,10 +423,18 @@ 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: ${{ github.repository_owner == 'facebook' }}
if: needs.config.outputs.only_job == 'build-linux-static_lib-alt_namespace-status_checked' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
runs-on:
labels: 16-core-ubuntu
container:
@@ -317,11 +443,19 @@ 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:
if: ${{ github.repository_owner == 'facebook' }}
if: needs.config.outputs.only_job == 'build-macos' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
runs-on: macos-15-xlarge
env:
ROCKSDB_DISABLE_JEMALLOC: 1
@@ -330,15 +464,23 @@ 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:
if: ${{ github.repository_owner == 'facebook' }}
if: needs.config.outputs.only_job == 'build-macos-cmake' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
runs-on: macos-15-xlarge
strategy:
matrix:
@@ -348,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"
@@ -367,11 +512,16 @@ 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
build-windows-vs2022:
if: ${{ github.repository_owner == 'facebook' }}
if: needs.config.outputs.only_job == 'build-windows-vs2022' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
runs-on: windows-8-core
env:
CMAKE_GENERATOR: Visual Studio 17 2022
@@ -381,69 +531,58 @@ jobs:
- uses: "./.github/actions/windows-build-steps"
# ============================ Java Jobs ============================ #
build-linux-java:
if: ${{ github.repository_owner == 'facebook' }}
if: needs.config.outputs.only_job == 'build-linux-java' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
runs-on:
labels: 4-core-ubuntu
container:
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: ${{ github.repository_owner == 'facebook' }}
if: needs.config.outputs.only_job == 'build-linux-java-static' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
runs-on:
labels: 4-core-ubuntu
container:
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: ${{ github.repository_owner == 'facebook' }}
if: needs.config.outputs.only_job == 'build-macos-java' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
runs-on: macos-15-xlarge
env:
JAVA_HOME: "/Library/Java/JavaVirtualMachines/liberica-jdk-8.jdk/Contents/Home"
@@ -453,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"
@@ -464,9 +606,14 @@ 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: ${{ github.repository_owner == 'facebook' }}
if: needs.config.outputs.only_job == 'build-macos-java-static' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
runs-on: macos-15-xlarge
env:
JAVA_HOME: "/Library/Java/JavaVirtualMachines/liberica-jdk-8.jdk/Contents/Home"
@@ -475,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"
@@ -486,9 +636,14 @@ 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: ${{ github.repository_owner == 'facebook' }}
if: needs.config.outputs.only_job == 'build-macos-java-static-universal' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
runs-on: macos-15-xlarge
env:
JAVA_HOME: "/Library/Java/JavaVirtualMachines/liberica-jdk-8.jdk/Contents/Home"
@@ -497,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"
@@ -508,9 +666,14 @@ 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: ${{ github.repository_owner == 'facebook' }}
if: needs.config.outputs.only_job == 'build-linux-java-pmd' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
runs-on:
labels: 4-core-ubuntu
container:
@@ -536,12 +699,20 @@ jobs:
name: maven-site
path: "${{ github.workspace }}/java/target/site"
build-linux-arm:
if: ${{ github.repository_owner == 'facebook' }}
if: needs.config.outputs.only_job == 'build-linux-arm' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
runs-on:
labels: 4-core-ubuntu-arm
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"
+42
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",
@@ -268,6 +270,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
"util/dynamic_bloom.cc",
"util/file_checksum_helper.cc",
"util/hash.cc",
"util/io_dispatcher_imp.cc",
"util/murmurhash.cc",
"util/random.cc",
"util/rate_limiter.cc",
@@ -361,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",
@@ -4822,6 +4828,12 @@ cpp_unittest_wrapper(name="db_clip_test",
extra_compiler_flags=[])
cpp_unittest_wrapper(name="db_compaction_abort_test",
srcs=["db/db_compaction_abort_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="db_compaction_filter_test",
srcs=["db/db_compaction_filter_test.cc"],
deps=[":rocksdb_test_lib"],
@@ -5130,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"],
@@ -5214,6 +5232,12 @@ cpp_unittest_wrapper(name="interval_test",
extra_compiler_flags=[])
cpp_unittest_wrapper(name="io_dispatcher_test",
srcs=["util/io_dispatcher_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="io_posix_test",
srcs=["env/io_posix_test.cc"],
deps=[":rocksdb_test_lib"],
@@ -5610,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"],
@@ -5712,6 +5748,12 @@ cpp_unittest_wrapper(name="write_controller_test",
extra_compiler_flags=[])
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",
srcs=["utilities/transactions/write_prepared_transaction_test.cc"],
deps=[":rocksdb_test_lib"],
+315
View File
@@ -0,0 +1,315 @@
# RocksDB Code Generation and Review Guidance
This document provides guidance for generating and reviewing code in the RocksDB project, derived from analysis of code review feedback across hundreds of complex merged Pull Requests. Use this as a reference when writing code with AI assistants or conducting code reviews.
---
## General Best Practices
### Code Quality and Maintainability
**Clarity and Readability:** Write clear, self-documenting code. Use meaningful variable names, add comments for complex logic, and structure code to minimize cognitive load. Avoid clever tricks that sacrifice readability for marginal performance gains unless absolutely necessary.
**Consistent Style:** Follow existing code style conventions. RocksDB uses `.clang-format` for formatting, specific naming conventions, and structural patterns. Deviations from these patterns are frequently flagged in reviews.
**Error Handling:** Ensure robust error handling throughout the codebase. Use RocksDB's `Status` type consistently, propagate errors appropriately, and avoid silently ignoring failures. Reviewers pay close attention to edge cases and failure modes.
### Testing Philosophy
**Comprehensive Coverage:** Every change should include appropriate test coverage. This includes unit tests for isolated functionality, integration tests for component interactions, and stress tests for concurrency and performance validation. Reviewers will ask for additional tests if coverage is insufficient.
**Edge Cases and Failure Modes:** Tests should explicitly cover edge cases, boundary conditions, and potential failure scenarios. This is especially important for changes affecting core database operations, compaction, or recovery logic.
**Platform-Specific Testing:** RocksDB supports multiple platforms (Linux, Windows, macOS) and compilers (GCC, Clang, MSVC). Changes should be tested across relevant platforms, particularly when touching platform-specific code or using compiler-specific features.
### Performance Considerations
**⚠️ PERFORMANCE IS CRITICAL:** RocksDB is a high-performance storage engine where every CPU cycle and memory access matters. When writing code, always evaluate from a performance perspective. This is not optional—performance-aware coding is a fundamental requirement for all contributions.
**Benchmarking and Profiling:** Performance claims should be backed by empirical evidence. Use RocksDB's benchmarking tools (e.g., `db_bench`) to validate improvements. Reviewers will request benchmark results for changes that could impact performance.
**Memory Allocation:** Minimize dynamic memory allocations, especially in hot paths. Prefer stack allocation over heap allocation. Reuse buffers when possible. Consider using arena allocators or memory pools for frequent small allocations. Every `new`, `malloc`, or container resize has a cost.
**Memory Copy:** Avoid unnecessary memory copies. Use move semantics, `std::string_view`, `Slice`, and pass-by-reference where appropriate. Be aware of implicit copies in STL containers and function returns. Prefer in-place operations over copy-and-modify patterns.
**CPU Cache Efficiency:** Design data structures and access patterns to be cache-friendly. Keep frequently accessed data together (data locality). Prefer sequential memory access over random access. Be mindful of cache line sizes (typically 64 bytes) and avoid false sharing in concurrent code. Consider struct packing and field ordering to improve cache utilization.
**Loop Optimization:** Look for opportunities to collapse nested loops, reduce loop overhead, and minimize branch mispredictions. Hoist invariant computations out of loops. Consider loop unrolling for tight inner loops. Batch operations when possible to amortize per-operation overhead.
**SIMD and Vectorization:** Leverage SIMD instructions (SSE, AVX) for data-parallel operations when appropriate. Structure data to enable auto-vectorization by the compiler. Consider explicit SIMD intrinsics for critical hot paths like checksum computation, encoding/decoding, and bulk data processing.
**Branch Prediction:** Minimize unpredictable branches in hot paths. Use `LIKELY`/`UNLIKELY` macros to hint branch prediction. Consider branchless alternatives for simple conditionals. Order switch cases and if-else chains by frequency.
**Memory and Resource Management:** Be mindful of memory allocations, especially in hot paths. Use RAII patterns, smart pointers, and RocksDB's memory management utilities appropriately.
**Hot Path Analysis:** When deciding how aggressively to optimize code, consider whether it's on a hot path:
- **Hot path** (executed thousands+ times, e.g., data access, iteration, compaction loops): Performance is paramount. Apply all optimization techniques—loop collapsing, SIMD, cache optimization, pre-allocation, etc. The cost of each operation is multiplied by execution frequency.
- **Cold path** (executed rarely, e.g., DB open, configuration parsing, error handling): Maintainability and clarity are more important. Prefer readable code over micro-optimizations. Complex optimizations here add maintenance burden with negligible performance benefit.
- **Warm path** (moderate frequency): Balance both concerns. Use profiling data to guide optimization decisions.
**Avoid Premature Optimization:** While performance is critical, focus on correctness first, then optimize based on profiling data. However, be performance-aware from the start—choosing the right algorithm and data structure upfront is not premature optimization. Use the hot path analysis above to decide how much optimization effort is warranted.
### API Design and Compatibility
**Backwards Compatibility:** RocksDB maintains strong backwards compatibility guarantees. Breaking changes are rare and require extensive justification. When deprecating features, follow the project's deprecation policy (typically spanning multiple releases).
**API Consistency:** New APIs should be consistent with existing patterns. Use similar naming conventions, parameter ordering, and return types. Reviewers will suggest changes to improve consistency with the broader codebase.
**Documentation:** Public APIs must be thoroughly documented. Include usage examples, parameter descriptions, and notes on thread safety, performance characteristics, and compatibility considerations.
---
## Component-Specific Guidance
### Database Core (`db`)
The database core handles write-ahead logging (WAL), memtables, compaction, and recovery. This component receives the most scrutiny in code reviews.
**Concurrency and Thread Safety:** Database operations are highly concurrent. Reviewers carefully examine locking strategies, atomic operations, and memory ordering. Document synchronization assumptions clearly. Use appropriate memory ordering semantics (`acquire`/`release` vs. `seq_cst`).
**Compaction Logic:** Changes to compaction are complex and high-risk. Ensure that compaction logic respects configured parameters, handles edge cases (empty databases, single-file compactions), and maintains correctness under concurrent operations.
**Error Propagation:** Database operations can fail in many ways (I/O errors, corruption, resource exhaustion). Ensure that errors are properly propagated, logged, and handled. Avoid assertions in production code paths.
**Testing:** Database core changes require extensive testing, including unit tests, integration tests, and stress tests. Test with various configurations, compaction styles, and concurrent workloads.
### Public Headers (`include`)
Public headers define RocksDB's API surface. Changes here have the highest compatibility impact.
**API Design:** New APIs should be intuitive, consistent with existing patterns, and well-documented. Consider how the API will be used in practice and avoid adding unnecessary complexity.
**Backwards Compatibility:** Breaking changes to public APIs require extensive justification and a deprecation plan. Maintain ABI compatibility for bug fixes and patch releases.
**Documentation:** Every public API must be thoroughly documented with usage examples, parameter descriptions, and notes on thread safety and performance characteristics.
**Deprecation:** When deprecating APIs, follow the project's policy. Mark deprecated APIs clearly, provide migration guidance, and maintain support for at least one major release.
### Internal Utilities (`util`)
Internal utilities provide common functionality used throughout the codebase.
**Code Reuse:** Utilities should be general-purpose and reusable. Avoid duplicating functionality that already exists elsewhere in the codebase.
**Error Handling:** Utility functions should handle errors robustly and propagate them appropriately. Consider edge cases like overflow, underflow, and invalid inputs.
**Testing:** Utility functions should have comprehensive test coverage, including edge cases and failure modes. Consider adding death tests for assertions.
**Performance:** Utilities are often used in hot paths. Ensure that implementations are efficient and avoid unnecessary allocations or copies.
### Table Management (`table`)
Table management handles SST file format, block-based tables, and table readers/writers.
**Block Format and Checksums:** Changes to block format require extreme care. Ensure that checksums are computed and verified correctly. Test with various compression algorithms and block sizes.
**Iterator Correctness:** Table iterators are used throughout the codebase. Ensure that iterator semantics (Seek, Next, Prev) are correct, especially at boundaries and with deletions.
**Caching and Prefetching:** Table readers interact with the block cache and prefetching logic. Ensure that cache keys are unique and that prefetching respects configured limits.
**Performance:** Table operations are performance-critical. Benchmark changes that could impact read or write performance.
### Utilities (`utilities`)
Utilities include optional features like transactions, backup engine, and checkpoint.
**Feature Isolation:** Utilities should be self-contained and not introduce unnecessary dependencies on core database internals.
**Deprecation and Cleanup:** Legacy features are being phased out. When removing deprecated code, ensure that migration paths are documented and that users have sufficient warning.
**Cross-Platform Compatibility:** Utilities often interact with OS-specific APIs. Ensure that code works on all supported platforms.
### Options and Configuration (`options`)
Options define RocksDB's configuration system.
**Type Safety:** Use appropriate types for options (e.g., `uint32_t` for flags, scoped enums for enumerated values).
**Deprecation Policy:** When deprecating options, follow the project's policy. Document the deprecation, provide migration guidance, and maintain support for at least one major release.
**Dynamic Configuration:** Some options can be changed dynamically. Ensure that dynamic changes are thread-safe and take effect correctly.
**Validation:** Validate option values and provide clear error messages for invalid configurations.
### Cache (`cache`)
Cache management is critical for RocksDB's performance.
**Concurrency:** Cache operations are highly concurrent. Ensure that implementations are thread-safe and use appropriate synchronization primitives.
**Performance:** Cache operations are in the hot path. Optimize for low latency and high throughput. Benchmark changes carefully.
**Memory Management:** Cache implementations must manage memory carefully to avoid leaks and excessive allocations.
**Eviction Policies:** Changes to eviction policies should be well-tested and benchmarked to ensure they improve overall performance.
---
## Code Review Checklist
When reviewing RocksDB code (or preparing code for review), use this checklist:
### Correctness
- [ ] Does the change preserve database semantics (e.g., snapshot isolation, key ordering)?
- [ ] Are all error cases handled appropriately?
- [ ] Is the change thread-safe? Are synchronization primitives used correctly?
- [ ] Are there any potential data races or deadlocks?
### Testing
- [ ] Does the change include appropriate test coverage?
- [ ] Are edge cases and failure modes tested?
- [ ] Have the tests been run on all supported platforms?
- [ ] Are stress tests passing?
### Performance
- [ ] Are there benchmark results for performance-sensitive changes?
- [ ] Does the change avoid unnecessary allocations or copies?
- [ ] Are hot paths optimized appropriately?
### API and Compatibility
- [ ] Is the change backwards compatible?
- [ ] Are new APIs consistent with existing patterns?
- [ ] Is the public API documented?
- [ ] Are deprecated features handled according to policy?
### Code Quality
- [ ] Does the code follow RocksDB's style conventions?
- [ ] Is the code clear and maintainable?
- [ ] Are comments and documentation sufficient?
- [ ] Are there any code smells or anti-patterns?
---
## Common Review Feedback Patterns
The following patterns emerged as frequent sources of review feedback:
1. **Test Coverage:** Reviewers frequently request additional tests for edge cases, platform-specific behavior, and failure modes. Complex changes require comprehensive test coverage including unit tests, integration tests, and stress tests.
2. **Error Handling:** Ensure proper error propagation using RocksDB's `Status` type. Avoid silent failures and provide clear error messages that include context about what failed and why.
3. **API Design:** New APIs should be consistent with existing patterns. Use descriptive names that follow established conventions. Avoid breaking changes without strong justification and a clear deprecation plan.
4. **Documentation:** Public APIs must be documented with usage examples and notes on thread safety, performance characteristics, and compatibility considerations. Complex internal logic should also be well-commented.
5. **Performance:** Performance-sensitive changes require benchmark results to validate improvements. Use `db_bench` and other profiling tools to measure impact. Avoid premature optimization that adds complexity without measurable benefit.
6. **Concurrency:** Thread safety is critical in RocksDB. Document synchronization assumptions clearly. Use appropriate memory ordering semantics. Consider potential race conditions and deadlocks.
7. **Code Style:** Follow existing conventions for naming, formatting, and structure. Use `.clang-format` for consistent formatting. Prefer scoped enums (`enum class`) over unscoped enums.
8. **Backwards Compatibility:** RocksDB maintains strong compatibility guarantees. Breaking changes require extensive justification. When deprecating features, provide migration guidance and maintain support across multiple releases.
9. **Refactoring:** Reviewers appreciate refactoring that improves code readability and maintainability. Look for opportunities to deduplicate code and simplify complex logic.
10. **Platform Compatibility:** Ensure changes work correctly on all supported platforms (Linux, Windows, macOS) and with all supported compilers (GCC, Clang, MSVC).
---
## Important tips
### Build system
* There are 3 build system. Make, CMake, BUCK(meta internal).
* When a new .cc file is added, update Makefile, CMakeLists.txt, src.mk, BUCK.
* Don't manually edit BUCK file, after updating src.mk, run
/usr/local/bin/python3 buckifier/buckify_rocksdb.py to update it
* Use make to build and run the test. CMake and BUCK are not used locally.
* Use `make dbg` command to build all of the unit test in debug mode.
* For -j in make command, use the number of CPU cores to decide it.
### Unit Test
* After all of the unit tests are added, review them and try to extract common
reusable utility functions to reduce code duplication due to copy past between
unit tests. This should be done every time unit test is updated.
* Don't use sleep to wait for certain events to happen. This will cause test to
be flaky. Instead, use sync point to synchronize thread progress.
* Cap unit test execution with 60 seconds timeout.
* When there are multiple unit tests need to be executed, try to use
gtest_parallel.py if available. E.g.
python3 ${GTEST_PARALLEL}/gtest_parallel.py ./table_test
### Unit test dedup guidelines
* Extract helper functions for repeated patterns such as object
construction, round-trip (encode → decode → verify), and common
assertion sequences.
* Use table-driven tests (struct array + loop) when multiple test cases
share the same logic but differ only in input/expected data.
* Prefer randomized tests over exhaustive parameter permutations. Use
`Random` from `util/random.h` (not `std::mt19937`). Use a time-based
seed with `SCOPED_TRACE("seed=" + std::to_string(seed))` so failures
are reproducible.
* Keep deterministic edge-case tests separate from randomized tests
(error paths, boundary conditions, format verification).
* Methods only used in tests should be private with `friend class` +
`TEST_F` fixture wrappers. In wrappers, always fully qualify the
target method to avoid infinite recursion.
### Adding new public API
Refer to claude_md/add_public_api.md
### 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
metrics.
### Stress test
* When adding a new feature, make sure stress test covers the new option.
### DB bench update
* When adding a performance related feature, support it in db_bench
### Adding release note
* Release note should be kept short at high level for external user consumption.
### Blog posts (docs/_posts)
* Blog post authors must be defined in `docs/_data/authors.yml` to be displayed
### Final verification of the change
* Execute make clean to clean all of the changes.
* Execute make check to build all of the changes and execute all of the tests.
Note that executing all of the tests could take multiple minutes.
### Monitoring make check progress
* Use `make check-progress` to get machine-parseable JSON progress while
`make check` is running. This is useful for Claude Code to monitor long
builds without timeout issues.
* Run `make check` in background, then poll progress:
```bash
make check &
# Poll periodically:
make check-progress
```
* The output shows current phase and progress:
```json
{"status":"running","phase":"compiling","completed":300,"total":919,...}
{"status":"running","phase":"testing","completed":1500,"total":29962,"failed":0,"percent":5,...}
{"status":"completed","phase":"testing","completed":29962,"total":29962,"failed":0,"percent":100,...}
```
* Phases: `compiling` -> `linking` -> `generating` -> `testing` -> `completed`
* Key fields: `status`, `phase`, `completed`, `total`, `failed`, `percent`
* When tests fail, `failed_tests` array shows details (up to 10 failures):
```json
{"status":"running",...,"failed":3,"failed_tests":[
{"test":"cache_test-CacheTest.Usage","exit_code":1,"signal":0,"output":"...test log..."},
{"test":"env_test-EnvTest.Open","exit_code":0,"signal":11,"output":"...Segmentation fault..."}
]}
```
* `exit_code`: non-zero means test assertion failed
* `signal`: non-zero means test was killed (e.g., 9=SIGKILL, 6=SIGABRT, 11=SIGSEGV)
* `output`: last 50 lines of test log including error messages and stack traces
### Executing benchmark using db_bench
* Since the goal is to measure performance, we need to build a release binary
using `make clean && DEBUG_LEVEL=0 make db_bench`. If there is an engine
crash due to bug, we need to switch back to debug build. Make sure to run
`make clean` before running `make dbg`.
### Formatting code
* After making change, use `make format-auto` to auto-apply formatting without
interactive prompts (Claude Code friendly).
+15
View File
@@ -219,6 +219,10 @@ if(MSVC)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /Zi /nologo /EHsc /GS /Gd /GR /GF /fp:precise /Zc:wchar_t /Zc:forScope /errorReport:queue")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /FC /d2Zi+ /W4 /wd4127 /wd4996 /wd4100 /wd4324")
endif()
if(CMAKE_BUILD_TYPE STREQUAL "Release")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /DNDEBUG")
message(STATUS "Setting /DNDEBUG as CMAKE_BUILD_TYPE is set to ${CMAKE_BUILD_TYPE}")
endif()
else()
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -W -Wextra -Wall -pthread")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wsign-compare -Wshadow -Wno-unused-parameter -Wno-unused-variable -Woverloaded-virtual -Wnon-virtual-dtor -Wno-missing-field-initializers -Wno-strict-aliasing -Wno-invalid-offsetof")
@@ -762,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
@@ -838,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
@@ -914,6 +920,7 @@ set(SOURCES
util/data_structure.cc
util/dynamic_bloom.cc
util/hash.cc
util/io_dispatcher_imp.cc
util/murmurhash.cc
util/random.cc
util/rate_limiter.cc
@@ -994,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
@@ -1377,6 +1387,7 @@ if(WITH_TESTS)
db/db_bloom_filter_test.cc
db/db_compaction_filter_test.cc
db/db_compaction_test.cc
db/db_compaction_abort_test.cc
db/db_clip_test.cc
db/db_dynamic_level_test.cc
db/db_encryption_test.cc
@@ -1525,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
@@ -1542,9 +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_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
+75
View File
@@ -1,6 +1,81 @@
# Rocksdb Change Log
> NOTE: Entries for next release do not go here. Follow instructions in `unreleased_history/README.txt`
## 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.1.1 (04/10/2026)
### Public API Changes
* 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
### 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
* Added support for storing wide-column entity column values in blob files. When `min_blob_size` is configured, large column values in wide-column entities will be stored in blob files, reducing SST file size and improving read performance.
* Added `CompactionOptionsFIFO::max_data_files_size` to support FIFO compaction trimming based on combined SST and blob file sizes. Added `CompactionOptionsFIFO::use_kv_ratio_compaction` to enable a capacity-derived intra-L0 compaction strategy optimized for BlobDB workloads, producing uniform-sized compacted files for predictable FIFO trimming.
* Include interpolation search as an alternative to binary search, which typically performs better when keys are uniformly distributed. This is exposed as a new table option `index_block_search_type`. The default is `binary_search`.
### Public API Changes
* Added new virtual methods `AbortAllCompactions()` and `ResumeAllCompactions()` to the `DB` class. Added new `Status::SubCode::kCompactionAborted` to indicate a compaction was aborted. Added `Status::IsCompactionAborted()` helper method to check if a status represents an aborted compaction.
* Drop support for reading (and writing) SST files using `BlockBasedTableOptions.format_version` < 2, which hasn't been the default format for about 10 years. An upgrade path is still possible with full compaction using a RocksDB version >= 4.6.0 and < 11.0.0 and then using the newer version.
* Remove deprecated raw `DB*` variants of `DB::Open` and related functions. Some other minor public APIs were updated as a result
* Remove deprecated `DB::MaxMemCompactionLevel()`
* Remove useless option `CompressedSecondaryCacheOptions::compress_format_version`
* Remove deprecated DB option `skip_checking_sst_file_sizes_on_db_open`. The option was deprecated in 10.5.0 and has been a no-op since then. File size validation is now always performed in parallel during DB open.
* Remove deprecated `SliceTransform::InRange()` virtual method and the `in_range` callback parameter from `rocksdb_slicetransform_create()` in the C API. `InRange()` was never called by RocksDB and existed only for backward compatibility.
* Remove deprecated, unused APIs and options: `ReadOptions::managed` and `ColumnFamilyOptions::snap_refresh_nanos`. Corresponding C and Java APIs are also removed.
* Remove deprecated `SstFileWriter::Add()` method (use `Put()` instead) and the deprecated `skip_filters` parameter from `SstFileWriter` constructors (use `BlockBasedTableOptions::filter_policy` set to `nullptr` to skip filter generation instead).
### Behavior Changes
* Change the default value of `CompactionOptionsUniversal::reduce_file_locking` from `false` to `true` to improve write stall and reduce read regression
### Bug Fixes
* Fix longstanding failures that can arise from reading and/or compacting old DB dirs with range deletions (likely from version < 5.19.0) in many newer versions.
* Fix a bug where WritePrepared/WriteUnprepared TransactionDB with two_write_queues=true could experience "sequence number going backwards" corruption during recovery from a background error, due to allocated-but-not-published sequence numbers not being synced before creating new WAL files.
### Performance Improvements
* Add a new table option `separate_key_value_in_data_block`. When set to true keys and values will be stored separately in the data block, which can result in higher cpu cache hit rate and better compression. Works best with data blocks with sufficient restart intervals and large values. Previous versions of RocksDB will reject files written using this option.
## 10.11.0 (01/23/2026)
### Public API Changes
* New SetOptions API that allows setting options for multiple CFs, avoiding the need to reserialize OPTIONS file for each CF
* Remove remaining pieces of Lua integration
### Behavior Changes
* The new default for `BlockBasedTableOptions::format_version` is 7, which has been supported since RocksDB 10.4.0 and is required in order to use CompressionManagers supporting custom compression types.
### Bug Fixes
* Fixed a small performance bug with `format_version=7` when decompressing formats other than Snappy and ZSTD.
* Fixed an infinite compaction loop bug with User-Defined Timestamps (UDT) where bottommost files were repeatedly marked for compaction even though their timestamp could not be collapsed.
* Bugfix for persisted UDT record sequence number zeroing logic.
## 10.10.0 (12/16/2025)
### Bug Fixes
* Fixed a bug in best-efforts recovery that causes use-after-free crashes when accessing SST files that were cached during the recovery.
* Fix resumable compaction incorrectly allowing resumption from a truncated range deletion that is not well handled currently.
* Fixed a bug in `PosixRandomFileAccess` IO uring submission queue ownership & management. Fix eliminates the false positive 'Bad cqe data' IO errors in `PosixRandomFileAccess::MultiRead` when interleaved with `PosixRandomFileAccess::ReadAsync` on the same thread.
## 10.9.0 (11/21/2025)
### New Features
* Added an auto-tuning feature for DB manifest file size that also (by default) improves the safety of existing configurations in case `max_manifest_file_size` is repeatedly exceeded. The new recommendation is to set `max_manifest_file_size` to something small like 1MB and tune `max_manifest_space_amp_pct` as needed to balance write amp and space amp in the manifest. Refer to comments on those options in `DBOptions` for details. Both options are (now) mutable.
+59 -32
View File
@@ -390,8 +390,6 @@ ifdef COMPILE_WITH_TSAN
# Turn off -pg when enabling TSAN testing, because that induces
# a link failure. TODO: find the root cause
PROFILING_FLAGS =
# LUA is not supported under TSAN
LUA_PATH =
# Limit keys for crash test under TSAN to avoid error:
# "ThreadSanitizer: DenseSlabAllocator overflow. Dying."
CRASH_TEST_EXT_ARGS += --max_key=1000000
@@ -508,32 +506,6 @@ ifndef DISABLE_WARNING_AS_ERROR
endif
ifdef LUA_PATH
ifndef LUA_INCLUDE
LUA_INCLUDE=$(LUA_PATH)/include
endif
LUA_INCLUDE_FILE=$(LUA_INCLUDE)/lualib.h
ifeq ("$(wildcard $(LUA_INCLUDE_FILE))", "")
# LUA_INCLUDE_FILE does not exist
$(error Cannot find lualib.h under $(LUA_INCLUDE). Try to specify both LUA_PATH and LUA_INCLUDE manually)
endif
LUA_FLAGS = -I$(LUA_INCLUDE) -DLUA -DLUA_COMPAT_ALL
CFLAGS += $(LUA_FLAGS)
CXXFLAGS += $(LUA_FLAGS)
ifndef LUA_LIB
LUA_LIB = $(LUA_PATH)/lib/liblua.a
endif
ifeq ("$(wildcard $(LUA_LIB))", "") # LUA_LIB does not exist
$(error $(LUA_LIB) does not exist. Try to specify both LUA_PATH and LUA_LIB manually)
endif
EXEC_LDFLAGS += $(LUA_LIB)
endif
ifeq ($(NO_THREEWAY_CRC32C), 1)
CXXFLAGS += -DNO_THREEWAY_CRC32C
endif
@@ -604,8 +576,8 @@ ifneq ($(filter check-headers, $(MAKECMDGOALS)),)
# TODO: add/support JNI headers
DEV_HEADER_DIRS := $(sort include/ $(dir $(ALL_SOURCES)))
# Some headers like in port/ are platform-specific
DEV_HEADERS_TO_CHECK := $(shell $(FIND) $(DEV_HEADER_DIRS) -type f -name '*.h' | grep -E -v 'port/|plugin/|lua/|range_tree/|secondary_index/')
PUBLIC_HEADERS_TO_CHECK := $(shell $(FIND) include/ -type f -name '*.h' | grep -E -v 'lua/')
DEV_HEADERS_TO_CHECK := $(shell $(FIND) $(DEV_HEADER_DIRS) -type f -name '*.h' | grep -E -v 'port/|plugin/|range_tree/|secondary_index/')
PUBLIC_HEADERS_TO_CHECK := $(shell $(FIND) include/ -type f -name '*.h')
else
DEV_HEADERS_TO_CHECK :=
PUBLIC_HEADERS_TO_CHECK :=
@@ -655,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))
@@ -833,7 +816,7 @@ endif # PLATFORM_SHARED_EXT
.PHONY: check clean coverage ldb_tests package dbg gen-pc build_size \
release tags tags0 valgrind_check format static_lib shared_lib all \
rocksdbjavastatic rocksdbjava install install-static install-shared \
uninstall analyze tools tools_lib check-headers checkout_folly
uninstall analyze tools tools_lib check-headers checkout_folly clang-tidy
all: $(LIBRARY) $(BENCHMARKS) tools tools_lib test_libs $(TESTS)
@@ -912,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
@@ -1021,9 +1007,19 @@ watch-log:
dump-log:
bash -c '$(quoted_perl_command)' < LOG
# Machine-parseable progress output for automated monitoring (e.g., Claude Code)
# Outputs JSON: {"status":"running","completed":45,"total":100,"failed":0,"percent":45,"eta_seconds":120}
check-progress:
@build_tools/check_progress.sh
# 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) | \
@@ -1232,6 +1228,10 @@ tags0:
format:
build_tools/format-diff.sh
# Non-interactive format (auto-apply without prompts, for CI/automation/Claude Code)
format-auto:
build_tools/format-diff.sh -y
check-format:
build_tools/format-diff.sh -c
@@ -1241,6 +1241,15 @@ check-buck-targets:
check-sources:
build_tools/check-sources.sh
# Run clang-tidy on locally changed files, filtered to changed lines only.
# Requires compile_commands.json (generate with cmake -DCMAKE_EXPORT_COMPILE_COMMANDS=ON).
# Override CLANG_TIDY_BINARY and CLANG_TIDY_JOBS as needed:
# make clang-tidy CLANG_TIDY_BINARY=/usr/bin/clang-tidy CLANG_TIDY_JOBS=8
CLANG_TIDY_BINARY ?= /opt/homebrew/opt/llvm/bin/clang-tidy
CLANG_TIDY_JOBS ?= $(shell nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)
clang-tidy:
python3 tools/run_clang_tidy.py --clang-tidy-binary $(CLANG_TIDY_BINARY) -j $(CLANG_TIDY_JOBS)
package:
bash build_tools/make_package.sh $(SHARED_MAJOR).$(SHARED_MINOR)
@@ -1470,6 +1479,9 @@ db_compaction_filter_test: $(OBJ_DIR)/db/db_compaction_filter_test.o $(TEST_LIBR
db_compaction_test: $(OBJ_DIR)/db/db_compaction_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
db_compaction_abort_test: $(OBJ_DIR)/db/db_compaction_abort_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
db_clip_test: $(OBJ_DIR)/db/db_clip_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
@@ -1596,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)
@@ -1647,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)
@@ -1845,6 +1866,9 @@ 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_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)
$(AM_LINK)
@@ -1950,6 +1974,9 @@ blob_source_test: $(OBJ_DIR)/db/blob/blob_source_test.o $(TEST_LIBRARY) $(LIBRAR
blob_garbage_meter_test: $(OBJ_DIR)/db/blob/blob_garbage_meter_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
io_dispatcher_test: $(OBJ_DIR)/util/io_dispatcher_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
timer_test: $(OBJ_DIR)/util/timer_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
@@ -2562,7 +2589,7 @@ list_all_tests:
# Remove the rules for which dependencies should not be generated and see if any are left.
#If so, include the dependencies; if not, do not include the dependency files
ROCKS_DEP_RULES=$(filter-out clean format check-format check-buck-targets check-headers check-sources jclean jtest package analyze tags rocksdbjavastatic% unity.% unity_test checkout_folly, $(MAKECMDGOALS))
ROCKS_DEP_RULES=$(filter-out clean format check-format check-buck-targets check-headers check-sources clang-tidy jclean jtest package analyze tags rocksdbjavastatic% unity.% unity_test checkout_folly, $(MAKECMDGOALS))
ifneq ("$(ROCKS_DEP_RULES)", "")
-include $(DEPFILES)
endif
-1
View File
@@ -831,7 +831,6 @@ fi
if test -n "$WITH_JEMALLOC_FLAG"; then
echo "WITH_JEMALLOC_FLAG=$WITH_JEMALLOC_FLAG" >> "$OUTPUT"
fi
echo "LUA_PATH=$LUA_PATH" >> "$OUTPUT"
if test -n "$USE_FOLLY"; then
echo "USE_FOLLY=$USE_FOLLY" >> "$OUTPUT"
fi
+2 -2
View File
@@ -8,7 +8,7 @@ BAD=""
# Look for potential for ODR violations caused by public headers depending on
# build parameters that could vary between RocksDB build and application build.
# * Cases like LUA, ROCKSDB_NAMESPACE, and ROCKSDB_ASSERT_STATUS_CHECKED are
# * Cases like ROCKSDB_NAMESPACE, and ROCKSDB_ASSERT_STATUS_CHECKED are
# intentional, hard to avoid. (We expect definitions to change and the user
# should also.)
# * Cases like _WIN32, OS_WIN, and __cplusplus are essentially ODR-safe.
@@ -19,7 +19,7 @@ BAD=""
# that should not cause ODR violations can be exempted with the ODR-SAFE
# marker recognized here.
grep -nHE '^#if' -- "$@" | grep -vE 'ROCKSDB_NAMESPACE|ROCKSDB_ASSERT_STATUS_CHECKED|LUA|_WIN32|OS_WIN|ODR-SAFE|__cplusplus|ROCKSDB_DLL|ROCKSDB_LIBRARY_EXPORTS'
grep -nHE '^#if' -- "$@" | grep -vE 'ROCKSDB_NAMESPACE|ROCKSDB_ASSERT_STATUS_CHECKED|_WIN32|OS_WIN|ODR-SAFE|__cplusplus|ROCKSDB_DLL|ROCKSDB_LIBRARY_EXPORTS'
if [ "$?" != "1" ]; then
echo "^^^^^ #if in public API could cause an ODR violation."
echo " Add // ODR-SAFE if verified safe."
+231
View File
@@ -0,0 +1,231 @@
#!/bin/bash
# Output test progress in JSON format for machine parsing
# Usage: build_tools/check_progress.sh
LOG_FILE="LOG"
T_DIR="t"
SRC_MK="src.mk"
# Maximum lines of test output to include per failed test
MAX_OUTPUT_LINES=50
# Helper to escape string for JSON (handles newlines, quotes, backslashes, tabs)
json_escape() {
local str="$1"
# Use python for reliable JSON escaping if available, otherwise use sed
if command -v python3 &>/dev/null; then
printf '%s' "$str" | python3 -c 'import json,sys; print(json.dumps(sys.stdin.read())[1:-1], end="")'
else
printf '%s' "$str" | sed 's/\\/\\\\/g; s/"/\\"/g; s/\t/\\t/g; s/\r/\\r/g' | awk '{printf "%s\\n", $0}' | sed 's/\\n$//'
fi
}
# Helper to output JSON and exit
output_json() {
local status="$1"
local completed="${2:-0}"
local total="${3:-0}"
local failed="${4:-0}"
local percent="${5:-0}"
local eta="${6:-0}"
local avg_time="${7:-0}"
local last_item="${8:-}"
local phase="${9:-}"
local failed_tests="${10:-}"
# Build JSON output
local json="{\"status\":\"$status\""
if [[ -n "$phase" ]]; then
json="$json,\"phase\":\"$phase\""
fi
json="$json,\"completed\":$completed,\"total\":$total,\"failed\":$failed,\"percent\":$percent"
json="$json,\"eta_seconds\":$eta,\"avg_time\":\"$avg_time\",\"last_item\":\"$(json_escape "$last_item")\""
if [[ -n "$failed_tests" ]]; then
json="$json,\"failed_tests\":[$failed_tests]"
fi
json="$json}"
echo "$json"
}
# Get failed test info with log output
get_failed_tests_json() {
local log_file="$1"
local t_dir="$2"
local max_failures=10
local count=0
local first=true
# Get failed tests from LOG file
while IFS=$'\t' read -r seq host starttime runtime send recv exitval signal cmd; do
# Skip header line
[[ "$seq" == "Seq" ]] && continue
# Check if failed (exitval != 0 or signal != 0)
if [[ "$exitval" != "0" || "$signal" != "0" ]]; then
# Extract test name from command
test_name=$(echo "$cmd" | sed 's,.*/run-,,;s, .*,,')
# Get log file path
log_path="$t_dir/log-run-$test_name"
# Read test output (last N lines)
if [[ -f "$log_path" ]]; then
output=$(tail -n "$MAX_OUTPUT_LINES" "$log_path" 2>/dev/null)
else
output="(log file not found: $log_path)"
fi
# Escape output for JSON
escaped_output=$(json_escape "$output")
# Build JSON object for this failure
if [[ "$first" == "true" ]]; then
first=false
else
printf ","
fi
printf '{"test":"%s","exit_code":%d,"signal":%d,"output":"%s"}' \
"$test_name" "$exitval" "$signal" "$escaped_output"
((count++))
if [[ $count -ge $max_failures ]]; then
break
fi
fi
done < "$log_file"
}
# Check if tests are running (LOG file exists)
if [[ -f "$LOG_FILE" ]]; then
# Count total tests from t/run-* files
if [[ -d "$T_DIR" ]]; then
total=$(find "$T_DIR" -name 'run-*' -type f 2>/dev/null | wc -l)
else
total=0
fi
# If no parallel tests generated yet
if [[ "$total" -eq 0 ]]; then
output_json "running" 0 0 0 0 0 "0" "" "generating"
exit 0
fi
# Parse LOG file (skip header line)
# LOG format: Seq Host Starttime JobRuntime Send Receive Exitval Signal Command
completed=$(tail -n +2 "$LOG_FILE" 2>/dev/null | wc -l)
# Count failures
failed=$(awk -F'\t' 'NR>1 && ($7 != 0 || $8 != 0) {count++} END {print count+0}' "$LOG_FILE" 2>/dev/null)
# Get failed tests JSON with output (only if there are failures)
if [[ "$failed" -gt 0 ]]; then
failed_tests=$(get_failed_tests_json "$LOG_FILE" "$T_DIR")
else
failed_tests=""
fi
# Calculate percentage
if [[ "$total" -gt 0 ]]; then
percent=$((completed * 100 / total))
else
percent=0
fi
# Get last completed test name (extract from command column)
last_test=$(tail -1 "$LOG_FILE" 2>/dev/null | awk -F'\t' '{print $9}' | sed 's,.*/run-,,;s, .*,,;s,^./,,')
# Calculate ETA based on average time
if [[ "$completed" -gt 0 ]]; then
avg_time=$(awk -F'\t' 'NR>1 {sum+=$4; count++} END {if(count>0) printf "%.1f", sum/count; else print "0"}' "$LOG_FILE")
remaining=$((total - completed))
eta=$(awk "BEGIN {printf \"%.0f\", $avg_time * $remaining}")
else
avg_time="0"
eta="0"
fi
# Determine status
if [[ "$completed" -ge "$total" ]]; then
status="completed"
elif [[ "$completed" -gt 0 ]]; then
status="running"
else
status="starting"
fi
output_json "$status" "$completed" "$total" "$failed" "$percent" "$eta" "$avg_time" "$last_test" "testing" "$failed_tests"
exit 0
fi
# No LOG file - check if we're in compilation/linking phase
# Count expected source files from src.mk
if [[ -f "$SRC_MK" ]]; then
# Count LIB_SOURCES (library object files to compile)
expected_lib_objects=$(grep -E '\.cc\s*\\?$' "$SRC_MK" | grep -v '^#' | wc -l)
# Count TEST_MAIN_SOURCES (test binaries to link)
expected_test_binaries=$(sed -n '/^TEST_MAIN_SOURCES =/,/^[^ ]/p' "$SRC_MK" | grep -cE '\.cc\s*\\?$' 2>/dev/null || echo 0)
else
expected_lib_objects=0
expected_test_binaries=0
fi
# Check for test generation phase (t/ directory being created)
if [[ -d "$T_DIR" ]]; then
total=$(find "$T_DIR" -name 'run-*' -type f 2>/dev/null | wc -l)
if [[ "$total" -gt 0 ]]; then
output_json "running" 0 "$total" 0 0 0 "0" "" "generating"
exit 0
fi
fi
# Count compiled object files (in subdirectories matching source structure)
# Object files are created as dir/file.o (e.g., cache/cache.o, db/db_impl.o)
compiled_objects=0
if [[ "$expected_lib_objects" -gt 0 ]]; then
# Count .o files in source directories
compiled_objects=$(find cache db env file logging memory memtable monitoring options port table test_util trace_replay util utilities -name '*.o' -type f 2>/dev/null | wc -l)
fi
# Count linked test binaries (test binaries are in current directory with _test suffix)
linked_tests=0
if [[ "$expected_test_binaries" -gt 0 ]]; then
linked_tests=$(find . -maxdepth 1 -name '*_test' -type f -executable 2>/dev/null | wc -l)
fi
# Determine phase based on what exists
if [[ "$compiled_objects" -eq 0 && "$linked_tests" -eq 0 ]]; then
# Nothing compiled yet - not started or just beginning
output_json "not_started" 0 0 0 0 0 "0" ""
exit 0
fi
# Calculate total work units: compiling + linking
total_work=$((expected_lib_objects + expected_test_binaries))
completed_work=$((compiled_objects + linked_tests))
if [[ "$total_work" -gt 0 ]]; then
percent=$((completed_work * 100 / total_work))
else
percent=0
fi
# Determine phase
if [[ "$compiled_objects" -lt "$expected_lib_objects" ]]; then
phase="compiling"
# Get most recently modified .o file as last_item
last_item=$(find cache db env file logging memory memtable monitoring options port table test_util trace_replay util utilities -name '*.o' -type f -printf '%T@ %p\n' 2>/dev/null | sort -rn | head -1 | cut -d' ' -f2- | sed 's,^\./,,;s,\.o$,,')
elif [[ "$linked_tests" -lt "$expected_test_binaries" ]]; then
phase="linking"
# Get most recently modified test binary as last_item
last_item=$(find . -maxdepth 1 -name '*_test' -type f -executable -printf '%T@ %p\n' 2>/dev/null | sort -rn | head -1 | cut -d' ' -f2- | sed 's,^\./,,')
else
phase="generating"
last_item=""
fi
output_json "running" "$completed_work" "$total_work" 0 "$percent" 0 "0" "$last_item" "$phase"
-1
View File
@@ -19,4 +19,3 @@ BENCHMARK_BASE=/mnt/gvfs/third-party2/benchmark/780c7a0f9cf0967961e69ad08e61cddd
KERNEL_HEADERS_BASE=/mnt/gvfs/third-party2/kernel-headers/624a2f8f6c93c3c1df8aa4a6255d8202631a6c80/fb/platform010/da39a3e
BINUTILS_BASE=/mnt/gvfs/third-party2/binutils/39579e8603b48b3540f8b0633f43adf29acccb8b/2.37/centos8-native/da39a3e
VALGRIND_BASE=/mnt/gvfs/third-party2/valgrind/cd9cc656d49ecb53797ce4d055e49fde29fd57ff/3.19.0/platform010/76ebdda
LUA_BASE=/mnt/gvfs/third-party2/lua/363787fa5cac2a8aa20638909210443278fa138e/5.3.4/platform010/9079c97
+1 -9
View File
@@ -164,12 +164,4 @@ EXEC_LDFLAGS_SHARED="$SNAPPY_LIBS $ZLIB_LIBS $BZIP_LIBS $LZ4_LIBS $ZSTD_LIBS $GF
VALGRIND_VER="$VALGRIND_BASE/bin/"
LUA_PATH="$LUA_BASE"
if test -z $PIC_BUILD; then
LUA_LIB=" $LUA_PATH/lib/liblua.a"
else
LUA_LIB=" $LUA_PATH/lib/liblua_pic.a"
fi
export CC CXX AR CFLAGS CXXFLAGS EXEC_LDFLAGS EXEC_LDFLAGS_SHARED VALGRIND_VER JEMALLOC_LIB JEMALLOC_INCLUDE CLANG_ANALYZER CLANG_SCAN_BUILD LUA_PATH LUA_LIB
export CC CXX AR CFLAGS CXXFLAGS EXEC_LDFLAGS EXEC_LDFLAGS_SHARED VALGRIND_VER JEMALLOC_LIB JEMALLOC_INCLUDE CLANG_ANALYZER CLANG_SCAN_BUILD
+1 -1
View File
@@ -172,4 +172,4 @@ EXEC_LDFLAGS_SHARED="$SNAPPY_LIBS $ZLIB_LIBS $BZIP_LIBS $LZ4_LIBS $ZSTD_LIBS $GF
VALGRIND_VER="$VALGRIND_BASE/bin/"
export CC CXX AR AS CFLAGS CXXFLAGS EXEC_LDFLAGS EXEC_LDFLAGS_SHARED VALGRIND_VER JEMALLOC_LIB JEMALLOC_INCLUDE CLANG_ANALYZER CLANG_SCAN_BUILD LUA_PATH LUA_LIB
export CC CXX AR AS CFLAGS CXXFLAGS EXEC_LDFLAGS EXEC_LDFLAGS_SHARED VALGRIND_VER JEMALLOC_LIB JEMALLOC_INCLUDE CLANG_ANALYZER CLANG_SCAN_BUILD
+16 -6
View File
@@ -7,14 +7,18 @@ print_usage () {
echo "Usage:"
echo "format-diff.sh [OPTIONS]"
echo "-c: check only."
echo "-y: auto-apply formatting without prompts (non-interactive mode)."
echo "-h: print this message."
}
while getopts ':ch' OPTION; do
while getopts ':cyh' OPTION; do
case "$OPTION" in
c)
CHECK_ONLY=1
;;
y)
AUTO_APPLY=1
;;
h)
print_usage
exit 1
@@ -240,11 +244,16 @@ echo "$diffs" |
sed -e "s/\(^-.*$\)/`echo -e \"$COLOR_RED\1$COLOR_END\"`/" |
sed -e "s/\(^+.*$\)/`echo -e \"$COLOR_GREEN\1$COLOR_END\"`/"
echo -e "Would you like to fix the format automatically (y/n): \c"
# Handle auto-apply mode (non-interactive)
if [ "$AUTO_APPLY" ]; then
to_fix="y"
else
echo -e "Would you like to fix the format automatically (y/n): \c"
# Make sure under any mode, we can read user input.
exec < /dev/tty
read to_fix
# Make sure under any mode, we can read user input.
exec < /dev/tty
read to_fix
fi
if [ "$to_fix" != "y" ]
then
@@ -261,7 +270,8 @@ fi
echo "Files reformatted!"
# Amend to last commit if user do the post-commit format check
if [ -z "$uncommitted_code" ]; then
# Skip amend prompt in auto-apply mode (user can amend manually if desired)
if [ -z "$uncommitted_code" ] && [ -z "$AUTO_APPLY" ]; then
echo -e "Would you like to amend the changes to last commit (`git log HEAD --oneline | head -1`)? (y/n): \c"
read to_amend
+123
View File
@@ -0,0 +1,123 @@
#!/usr/bin/env python3
"""
Pre-download packages with unreliable mirrors using fallback mirrors.
Reads package info from folly's getdeps manifest files.
"""
import sys
import os
import hashlib
import subprocess
import configparser
def sha256_file(path):
"""Calculate SHA256 hash of a file."""
h = hashlib.sha256()
try:
with open(path, 'rb') as f:
for chunk in iter(lambda: f.read(65536), b''):
h.update(chunk)
return h.hexdigest()
except Exception:
return None
def parse_manifest(manifest_path):
"""Parse a getdeps manifest file to extract download info."""
config = configparser.ConfigParser()
try:
config.read(manifest_path)
if 'download' in config:
return {
'url': config['download'].get('url', ''),
'sha256': config['download'].get('sha256', ''),
}
except Exception:
pass
return None
def get_fallback_mirrors(url):
"""Get fallback mirror URLs for a given URL."""
# Fallback mirror patterns for known unreliable hosts
mirror_fallbacks = {
"ftp.gnu.org/gnu/": [
"https://mirrors.kernel.org/gnu/",
"https://ftpmirror.gnu.org/gnu/",
"https://ftp.gnu.org/gnu/",
],
"ftpmirror.gnu.org/gnu/": [
"https://mirrors.kernel.org/gnu/",
"https://ftpmirror.gnu.org/gnu/",
"https://ftp.gnu.org/gnu/",
],
}
for pattern, mirrors in mirror_fallbacks.items():
if pattern in url:
# Extract the path after the pattern
path_start = url.find(pattern) + len(pattern)
path = url[path_start:]
return [mirror + path for mirror in mirrors]
return [url] # No fallback, use original
def main():
if len(sys.argv) != 4:
print(f"Usage: {sys.argv[0]} <download_dir> <cache_dir> <manifests_dir>")
sys.exit(1)
download_dir, cache_dir, manifests_dir = sys.argv[1], sys.argv[2], sys.argv[3]
# Packages known to have unreliable mirrors
packages_to_check = ["autoconf", "automake", "libtool"]
for package in packages_to_check:
manifest_path = os.path.join(manifests_dir, package)
if not os.path.exists(manifest_path):
continue
info = parse_manifest(manifest_path)
if not info or not info['url'] or not info['sha256']:
continue
# Determine filename from URL
url = info['url']
expected_sha256 = info['sha256']
url_filename = os.path.basename(url)
# getdeps uses format: {package}-{filename}
filename = f"{package}-{url_filename}"
filepath = os.path.join(download_dir, filename)
cache_path = os.path.join(cache_dir, filename)
# Check if already valid
if os.path.exists(filepath) and sha256_file(filepath) == expected_sha256:
print(f" {filename}: OK (already downloaded)")
continue
# Check cache
if os.path.exists(cache_path) and sha256_file(cache_path) == expected_sha256:
print(f" {filename}: OK (from cache)")
subprocess.run(['cp', cache_path, filepath], check=True)
continue
# Try fallback mirrors
mirrors = get_fallback_mirrors(url)
downloaded = False
for mirror_url in mirrors:
print(f" {filename}: trying {mirror_url}...")
try:
subprocess.run(['wget', '-q', '-O', filepath, mirror_url], check=True, timeout=120)
if sha256_file(filepath) == expected_sha256:
print(f" {filename}: OK (downloaded)")
subprocess.run(['cp', filepath, cache_path], check=False)
downloaded = True
break
else:
os.remove(filepath)
except Exception:
if os.path.exists(filepath):
os.remove(filepath)
if not downloaded:
print(f" {filename}: WARNING - all mirrors failed")
if __name__ == "__main__":
main()
-1
View File
@@ -101,6 +101,5 @@ get_lib_base benchmark LATEST platform010
get_lib_base kernel-headers fb platform010
get_lib_base binutils LATEST centos8-native
get_lib_base valgrind LATEST platform010
get_lib_base lua 5.3.4 platform010
git diff $OUTPUT
-5
View File
@@ -54,11 +54,6 @@ static std::unordered_map<std::string, OptionTypeInfo>
{offsetof(struct CompressedSecondaryCacheOptions, compression_type),
OptionType::kCompressionType, OptionVerificationType::kNormal,
OptionTypeFlags::kMutable}},
{"compress_format_version",
{offsetof(struct CompressedSecondaryCacheOptions,
compress_format_version),
OptionType::kUInt32T, OptionVerificationType::kNormal,
OptionTypeFlags::kMutable}},
{"enable_custom_split_merge",
{offsetof(struct CompressedSecondaryCacheOptions,
enable_custom_split_merge),
+9 -9
View File
@@ -101,23 +101,23 @@ class CacheEntryStatsCollector {
}
// Gets saved stats, regardless of age
void GetStats(Stats *stats) {
void GetStats(Stats* stats) {
std::lock_guard<std::mutex> lock(saved_mutex_);
*stats = saved_stats_;
}
Cache *GetCache() const { return cache_; }
Cache* GetCache() const { return cache_; }
// Gets or creates a shared instance of CacheEntryStatsCollector in the
// cache itself, and saves into `ptr`. This shared_ptr will hold the
// entry in cache until all refs are destroyed.
static Status GetShared(Cache *raw_cache, SystemClock *clock,
std::shared_ptr<CacheEntryStatsCollector> *ptr) {
static Status GetShared(Cache* raw_cache, SystemClock* clock,
std::shared_ptr<CacheEntryStatsCollector>* ptr) {
assert(raw_cache);
BasicTypedCacheInterface<CacheEntryStatsCollector, CacheEntryRole::kMisc>
cache{raw_cache};
const Slice &cache_key = GetCacheKey();
const Slice& cache_key = GetCacheKey();
auto h = cache.Lookup(cache_key);
if (h == nullptr) {
// Not yet in cache, but Cache doesn't provide a built-in way to
@@ -152,7 +152,7 @@ class CacheEntryStatsCollector {
}
private:
explicit CacheEntryStatsCollector(Cache *cache, SystemClock *clock)
explicit CacheEntryStatsCollector(Cache* cache, SystemClock* clock)
: saved_stats_(),
working_stats_(),
last_start_time_micros_(0),
@@ -160,7 +160,7 @@ class CacheEntryStatsCollector {
cache_(cache),
clock_(clock) {}
static const Slice &GetCacheKey() {
static const Slice& GetCacheKey() {
// For each template instantiation
static CacheKey ckey = CacheKey::CreateUniqueForProcessLifetime();
static Slice ckey_slice = ckey.AsSlice();
@@ -175,8 +175,8 @@ class CacheEntryStatsCollector {
uint64_t last_start_time_micros_;
uint64_t last_end_time_micros_;
Cache *const cache_;
SystemClock *const clock_;
Cache* const cache_;
SystemClock* const clock_;
};
} // namespace ROCKSDB_NAMESPACE
+3 -3
View File
@@ -24,7 +24,7 @@ namespace ROCKSDB_NAMESPACE {
// 0 | >= 1<<63 | CreateUniqueForProcessLifetime
// > 0 | any | OffsetableCacheKey.WithOffset
CacheKey CacheKey::CreateUniqueForCacheLifetime(Cache *cache) {
CacheKey CacheKey::CreateUniqueForCacheLifetime(Cache* cache) {
// +1 so that we can reserve all zeros for "unset" cache key
uint64_t id = cache->NewId() + 1;
// Ensure we don't collide with CreateUniqueForProcessLifetime
@@ -297,8 +297,8 @@ CacheKey CacheKey::CreateUniqueForProcessLifetime() {
//
// TODO: Nevertheless / regardless, an efficient way to detect (and thus
// quantify) block cache corruptions, including collisions, should be added.
OffsetableCacheKey::OffsetableCacheKey(const std::string &db_id,
const std::string &db_session_id,
OffsetableCacheKey::OffsetableCacheKey(const std::string& db_id,
const std::string& db_session_id,
uint64_t file_number) {
UniqueId64x2 internal_id;
Status s = GetSstInternalUniqueId(db_id, db_session_id, file_number,
+5 -5
View File
@@ -44,13 +44,13 @@ class CacheKey {
inline Slice AsSlice() const {
static_assert(sizeof(*this) == 16, "Standardized on 16-byte cache key");
assert(!IsEmpty());
return Slice(reinterpret_cast<const char *>(this), sizeof(*this));
return Slice(reinterpret_cast<const char*>(this), sizeof(*this));
}
// Create a CacheKey that is unique among others associated with this Cache
// instance. Depends on Cache::NewId. This is useful for block cache
// "reservations".
static CacheKey CreateUniqueForCacheLifetime(Cache *cache);
static CacheKey CreateUniqueForCacheLifetime(Cache* cache);
// Create a CacheKey that is unique among others for the lifetime of this
// process. This is useful for saving in a static data member so that
@@ -87,7 +87,7 @@ class OffsetableCacheKey : private CacheKey {
// Constructs an OffsetableCacheKey with the given information about a file.
// This constructor never generates an "empty" base key.
OffsetableCacheKey(const std::string &db_id, const std::string &db_session_id,
OffsetableCacheKey(const std::string& db_id, const std::string& db_session_id,
uint64_t file_number);
// Creates an OffsetableCacheKey from an SST unique ID, so that cache keys
@@ -134,9 +134,9 @@ class OffsetableCacheKey : private CacheKey {
static_assert(sizeof(file_num_etc64_) == kCommonPrefixSize,
"8 byte common prefix expected");
assert(!IsEmpty());
assert(&this->file_num_etc64_ == static_cast<const void *>(this));
assert(&this->file_num_etc64_ == static_cast<const void*>(this));
return Slice(reinterpret_cast<const char *>(this), kCommonPrefixSize);
return Slice(reinterpret_cast<const char*>(this), kCommonPrefixSize);
}
};
+16 -16
View File
@@ -44,8 +44,8 @@ class CacheReservationManager {
bool increase) = 0;
virtual Status MakeCacheReservation(
std::size_t incremental_memory_used,
std::unique_ptr<CacheReservationManager::CacheReservationHandle>
*handle) = 0;
std::unique_ptr<CacheReservationManager::CacheReservationHandle>*
handle) = 0;
virtual std::size_t GetTotalReservedCacheSize() = 0;
virtual std::size_t GetTotalMemoryUsed() = 0;
};
@@ -90,11 +90,11 @@ class CacheReservationManagerImpl
bool delayed_decrease = false);
// no copy constructor, copy assignment, move constructor, move assignment
CacheReservationManagerImpl(const CacheReservationManagerImpl &) = delete;
CacheReservationManagerImpl &operator=(const CacheReservationManagerImpl &) =
CacheReservationManagerImpl(const CacheReservationManagerImpl&) = delete;
CacheReservationManagerImpl& operator=(const CacheReservationManagerImpl&) =
delete;
CacheReservationManagerImpl(CacheReservationManagerImpl &&) = delete;
CacheReservationManagerImpl &operator=(CacheReservationManagerImpl &&) =
CacheReservationManagerImpl(CacheReservationManagerImpl&&) = delete;
CacheReservationManagerImpl& operator=(CacheReservationManagerImpl&&) =
delete;
~CacheReservationManagerImpl() override;
@@ -178,7 +178,7 @@ class CacheReservationManagerImpl
// REQUIRES: handle != nullptr
Status MakeCacheReservation(
std::size_t incremental_memory_used,
std::unique_ptr<CacheReservationManager::CacheReservationHandle> *handle)
std::unique_ptr<CacheReservationManager::CacheReservationHandle>* handle)
override;
// Return the size of the cache (which is a multiple of kSizeDummyEntry)
@@ -200,7 +200,7 @@ class CacheReservationManagerImpl
// For testing only - it is to help ensure the CacheItemHelperForRole<R>
// accessed from CacheReservationManagerImpl and the one accessed from the
// test are from the same translation units
static const Cache::CacheItemHelper *TEST_GetCacheItemHelperForRole();
static const Cache::CacheItemHelper* TEST_GetCacheItemHelperForRole();
private:
static constexpr std::size_t kSizeDummyEntry = 256 * 1024;
@@ -216,7 +216,7 @@ class CacheReservationManagerImpl
bool delayed_decrease_;
std::atomic<std::size_t> cache_allocated_size_;
std::size_t memory_used_;
std::vector<Cache::Handle *> dummy_handles_;
std::vector<Cache::Handle*> dummy_handles_;
CacheKey cache_key_;
};
@@ -251,14 +251,14 @@ class ConcurrentCacheReservationManager
std::shared_ptr<CacheReservationManager> cache_res_mgr) {
cache_res_mgr_ = std::move(cache_res_mgr);
}
ConcurrentCacheReservationManager(const ConcurrentCacheReservationManager &) =
ConcurrentCacheReservationManager(const ConcurrentCacheReservationManager&) =
delete;
ConcurrentCacheReservationManager &operator=(
const ConcurrentCacheReservationManager &) = delete;
ConcurrentCacheReservationManager(ConcurrentCacheReservationManager &&) =
ConcurrentCacheReservationManager& operator=(
const ConcurrentCacheReservationManager&) = delete;
ConcurrentCacheReservationManager(ConcurrentCacheReservationManager&&) =
delete;
ConcurrentCacheReservationManager &operator=(
ConcurrentCacheReservationManager &&) = delete;
ConcurrentCacheReservationManager& operator=(
ConcurrentCacheReservationManager&&) = delete;
~ConcurrentCacheReservationManager() override {}
@@ -286,7 +286,7 @@ class ConcurrentCacheReservationManager
inline Status MakeCacheReservation(
std::size_t incremental_memory_used,
std::unique_ptr<CacheReservationManager::CacheReservationHandle> *handle)
std::unique_ptr<CacheReservationManager::CacheReservationHandle>* handle)
override {
std::unique_ptr<CacheReservationManager::CacheReservationHandle>
wrapped_handle;
+1 -1
View File
@@ -644,7 +644,7 @@ using TypedHandle = SharedCache::TypedHandle;
TEST_P(CacheTest, SetCapacity) {
if (IsHyperClock()) {
// TODO: update test & code for limited supoort
// TODO: update test & code for limited support
ROCKSDB_GTEST_BYPASS(
"HyperClockCache doesn't support arbitrary capacity "
"adjustments.");
+258 -294
View File
@@ -38,13 +38,11 @@ namespace ROCKSDB_NAMESPACE {
namespace clock_cache {
namespace {
inline uint64_t GetRefcount(uint64_t meta) {
return ((meta >> ClockHandle::kAcquireCounterShift) -
(meta >> ClockHandle::kReleaseCounterShift)) &
ClockHandle::kCounterMask;
}
using SlotMeta = ClockHandle::SlotMeta;
using AcquireCounter = SlotMeta::AcquireCounter;
using ReleaseCounter = SlotMeta::ReleaseCounter;
inline uint64_t GetInitialCountdown(Cache::Priority priority) {
inline uint32_t GetInitialCountdown(Cache::Priority priority) {
// Set initial clock data from priority
// TODO: configuration parameters for priority handling and clock cycle
// count?
@@ -65,11 +63,11 @@ inline uint64_t GetInitialCountdown(Cache::Priority priority) {
inline void MarkEmpty(ClockHandle& h) {
#ifndef NDEBUG
// Mark slot as empty, with assertion
uint64_t meta = h.meta.Exchange(0);
assert(meta >> ClockHandle::kStateShift == ClockHandle::kStateConstruction);
auto old_meta = h.meta.Exchange({});
assert(old_meta.IsUnderConstruction());
#else
// Mark slot as empty
h.meta.Store(0);
h.meta.Store({});
#endif
}
@@ -85,18 +83,20 @@ inline void FreeDataMarkEmpty(ClockHandle& h, MemoryAllocator* allocator) {
// Called to undo the effect of referencing an entry for internal purposes,
// so it should not be marked as having been used.
inline void Unref(const ClockHandle& h, uint64_t count = 1) {
inline void Unref(const ClockHandle& h, uint32_t count = 1) {
// Pretend we never took the reference
// WART: there's a tiny chance we release last ref to invisible
// entry here. If that happens, we let eviction take care of it.
uint64_t old_meta = h.meta.FetchSub(ClockHandle::kAcquireIncrement * count);
assert(GetRefcount(old_meta) != 0);
SlotMeta old_meta;
h.meta.Apply(AcquireCounter::MinusTransformPromiseNoUnderflow(count),
&old_meta);
assert(old_meta.GetRefcount() != 0);
(void)old_meta;
}
inline bool ClockUpdate(ClockHandle& h, BaseClockTable::EvictionData* data,
bool* purgeable = nullptr) {
uint64_t meta;
SlotMeta meta;
if (purgeable) {
assert(*purgeable == false);
// In AutoHCC, our eviction process follows the chain structure, so we
@@ -110,46 +110,40 @@ inline bool ClockUpdate(ClockHandle& h, BaseClockTable::EvictionData* data,
meta = h.meta.LoadRelaxed();
}
if (((meta >> ClockHandle::kStateShift) & ClockHandle::kStateShareableBit) ==
0) {
if (!meta.IsShareable()) {
// Only clock update Shareable entries
if (purgeable) {
*purgeable = true;
// AutoHCC only: make sure we only attempt to update non-empty slots
assert((meta >> ClockHandle::kStateShift) &
ClockHandle::kStateOccupiedBit);
assert(!meta.IsEmpty());
}
return false;
}
uint64_t acquire_count =
(meta >> ClockHandle::kAcquireCounterShift) & ClockHandle::kCounterMask;
uint64_t release_count =
(meta >> ClockHandle::kReleaseCounterShift) & ClockHandle::kCounterMask;
uint32_t acquire_count = meta.GetAcquireCounter();
uint32_t release_count = meta.GetReleaseCounter();
if (acquire_count != release_count) {
// Only clock update entries with no outstanding refs
data->seen_pinned_count++;
return false;
}
if ((meta >> ClockHandle::kStateShift == ClockHandle::kStateVisible) &&
acquire_count > 0) {
if (meta.IsVisible() && acquire_count > 0) {
// Decrement clock
uint64_t new_count =
std::min(acquire_count - 1, uint64_t{ClockHandle::kMaxCountdown} - 1);
uint32_t new_count =
std::min(acquire_count - 1, uint32_t{ClockHandle::kMaxCountdown} - 1);
// Compare-exchange in the decremented clock info, but
// not aggressively
uint64_t new_meta =
(uint64_t{ClockHandle::kStateVisible} << ClockHandle::kStateShift) |
(meta & ClockHandle::kHitBitMask) |
(new_count << ClockHandle::kReleaseCounterShift) |
(new_count << ClockHandle::kAcquireCounterShift);
SlotMeta new_meta = meta;
new_meta.SetReleaseCounter(new_count);
new_meta.SetAcquireCounter(new_count);
h.meta.CasStrongRelaxed(meta, new_meta);
return false;
}
// Otherwise, remove entry (either unreferenced invisible or
// unreferenced and expired visible).
if (h.meta.CasStrong(meta, (uint64_t{ClockHandle::kStateConstruction}
<< ClockHandle::kStateShift) |
(meta & ClockHandle::kHitBitMask))) {
SlotMeta construction_meta;
construction_meta.SetUnderConstruction();
construction_meta.SetHit(meta.GetHit());
if (h.meta.CasStrong(meta, construction_meta)) {
// Took ownership.
data->freed_charge += h.GetTotalCharge();
data->freed_count += 1;
@@ -215,39 +209,39 @@ inline bool ClockUpdate(ClockHandle& h, BaseClockTable::EvictionData* data,
// counter to reach "high" state again and bumped back to "medium." (This
// motivates only checking for release counter in high state, not both in high
// state.)
inline void CorrectNearOverflow(uint64_t old_meta,
AcqRelAtomic<uint64_t>& meta) {
inline void CorrectNearOverflow(SlotMeta old_meta,
BitFieldsAtomic<SlotMeta>& meta) {
// We clear both top-most counter bits at the same time.
constexpr uint64_t kCounterTopBit = uint64_t{1}
<< (ClockHandle::kCounterNumBits - 1);
constexpr uint64_t kClearBits =
(kCounterTopBit << ClockHandle::kAcquireCounterShift) |
(kCounterTopBit << ClockHandle::kReleaseCounterShift);
// A simple check that allows us to initiate clearing the top bits for
// a large portion of the "high" state space on release counter.
constexpr uint64_t kCheckBits =
(kCounterTopBit | (ClockHandle::kMaxCountdown + 1))
<< ClockHandle::kReleaseCounterShift;
constexpr uint32_t kCounterTopBit = uint32_t{1}
<< (SlotMeta::kCounterNumBits - 1);
// The threshold for correcting "near overflow" is to ensure
// (a) the value has a top bit set that can be cleared
// (b) when we clear the top bit, the eviction state will be preserved
// (everything >= kMaxCountdown is treated equivalently)
// As mentioned above, we only check the release count.
constexpr uint32_t kThreshold = kCounterTopBit + ClockHandle::kMaxCountdown;
if (UNLIKELY(old_meta & kCheckBits)) {
meta.FetchAndRelaxed(~kClearBits);
if (UNLIKELY(old_meta.GetReleaseCounter() > kThreshold)) {
auto clear_transform = AcquireCounter::AndTransform(kCounterTopBit - 1) +
ReleaseCounter::AndTransform(kCounterTopBit - 1);
meta.ApplyRelaxed(clear_transform);
}
}
inline bool BeginSlotInsert(const ClockHandleBasicData& proto, ClockHandle& h,
uint64_t initial_countdown, bool* already_matches) {
uint32_t initial_countdown, bool* already_matches) {
assert(*already_matches == false);
// Optimistically transition the slot from "empty" to
// "under construction" (no effect on other states)
uint64_t old_meta = h.meta.FetchOr(uint64_t{ClockHandle::kStateOccupiedBit}
<< ClockHandle::kStateShift);
uint64_t old_state = old_meta >> ClockHandle::kStateShift;
auto set_occupied = SlotMeta::OccupiedFlag::SetTransform();
SlotMeta old_meta;
h.meta.Apply(set_occupied, &old_meta);
if (old_state == ClockHandle::kStateEmpty) {
if (old_meta.IsEmpty()) {
// We've started inserting into an available slot, and taken
// ownership.
return true;
} else if (old_state != ClockHandle::kStateVisible) {
} else if (!old_meta.IsVisible()) {
// Slot not usable / touchable now
return false;
}
@@ -255,15 +249,17 @@ inline bool BeginSlotInsert(const ClockHandleBasicData& proto, ClockHandle& h,
// But first, we need to acquire a ref to read it. In fact, number of
// refs for initial countdown, so that we boost the clock state if
// this is a match.
old_meta =
h.meta.FetchAdd(ClockHandle::kAcquireIncrement * initial_countdown);
auto add_acquire =
AcquireCounter::PlusTransformPromiseNoOverflow(initial_countdown);
h.meta.Apply(add_acquire, &old_meta);
// Like Lookup
if ((old_meta >> ClockHandle::kStateShift) == ClockHandle::kStateVisible) {
if (old_meta.IsVisible()) {
// Acquired a read reference
if (h.hashed_key == proto.hashed_key) {
// Match. Release in a way that boosts the clock state
old_meta =
h.meta.FetchAdd(ClockHandle::kReleaseIncrement * initial_countdown);
auto add_release =
ReleaseCounter::PlusTransformPromiseNoOverflow(initial_countdown);
h.meta.Apply(add_release, &old_meta);
// Correct for possible (but rare) overflow
CorrectNearOverflow(old_meta, h.meta);
// Insert detached instead (only if return handle needed)
@@ -273,8 +269,7 @@ inline bool BeginSlotInsert(const ClockHandleBasicData& proto, ClockHandle& h,
// Mismatch.
Unref(h, initial_countdown);
}
} else if (UNLIKELY((old_meta >> ClockHandle::kStateShift) ==
ClockHandle::kStateInvisible)) {
} else if (UNLIKELY(old_meta.IsInvisible())) {
// Pretend we never took the reference
Unref(h, initial_countdown);
} else {
@@ -286,25 +281,23 @@ inline bool BeginSlotInsert(const ClockHandleBasicData& proto, ClockHandle& h,
}
inline void FinishSlotInsert(const ClockHandleBasicData& proto, ClockHandle& h,
uint64_t initial_countdown, bool keep_ref) {
uint32_t initial_countdown, bool keep_ref) {
// Save data fields
ClockHandleBasicData* h_alias = &h;
*h_alias = proto;
// Transition from "under construction" state to "visible" state
uint64_t new_meta = uint64_t{ClockHandle::kStateVisible}
<< ClockHandle::kStateShift;
SlotMeta new_meta;
new_meta.SetVisible();
// Maybe with an outstanding reference
new_meta |= initial_countdown << ClockHandle::kAcquireCounterShift;
new_meta |= (initial_countdown - keep_ref)
<< ClockHandle::kReleaseCounterShift;
new_meta.SetAcquireCounter(initial_countdown);
new_meta.SetReleaseCounter(initial_countdown - (keep_ref ? 1 : 0));
#ifndef NDEBUG
// Save the state transition, with assertion
uint64_t old_meta = h.meta.Exchange(new_meta);
assert(old_meta >> ClockHandle::kStateShift ==
ClockHandle::kStateConstruction);
auto old_meta = h.meta.Exchange(new_meta);
assert(old_meta.IsUnderConstruction());
#else
// Save the state transition
h.meta.Store(new_meta);
@@ -312,7 +305,7 @@ inline void FinishSlotInsert(const ClockHandleBasicData& proto, ClockHandle& h,
}
bool TryInsert(const ClockHandleBasicData& proto, ClockHandle& h,
uint64_t initial_countdown, bool keep_ref,
uint32_t initial_countdown, bool keep_ref,
bool* already_matches) {
bool b = BeginSlotInsert(proto, h, initial_countdown, already_matches);
if (b) {
@@ -326,35 +319,32 @@ template <class HandleImpl, class Func>
void ConstApplyToEntriesRange(const Func& func, const HandleImpl* begin,
const HandleImpl* end,
bool apply_if_will_be_deleted) {
uint64_t check_state_mask = ClockHandle::kStateShareableBit;
if (!apply_if_will_be_deleted) {
check_state_mask |= ClockHandle::kStateVisibleBit;
}
for (const HandleImpl* h = begin; h < end; ++h) {
// Note: to avoid using compare_exchange, we have to be extra careful.
uint64_t old_meta = h->meta.LoadRelaxed();
SlotMeta old_meta = h->meta.LoadRelaxed();
// Check if it's an entry visible to lookups
if ((old_meta >> ClockHandle::kStateShift) & check_state_mask) {
// Increment acquire counter. Note: it's possible that the entry has
// completely changed since we loaded old_meta, but incrementing acquire
// count is always safe. (Similar to optimistic Lookup here.)
old_meta = h->meta.FetchAdd(ClockHandle::kAcquireIncrement);
// Check whether we actually acquired a reference.
if ((old_meta >> ClockHandle::kStateShift) &
ClockHandle::kStateShareableBit) {
// Apply func if appropriate
if ((old_meta >> ClockHandle::kStateShift) & check_state_mask) {
func(*h);
if (apply_if_will_be_deleted || old_meta.IsVisible()) {
if (old_meta.IsShareable()) {
// Increment acquire counter. Note: it's possible that the entry has
// completely changed since we loaded old_meta, but incrementing acquire
// count is always safe. (Similar to optimistic Lookup here.)
auto add_acquire = AcquireCounter::PlusTransformPromiseNoOverflow(1);
h->meta.Apply(add_acquire, &old_meta);
// Check whether we actually acquired a reference.
if (old_meta.IsShareable()) {
// Apply func if appropriate
if (apply_if_will_be_deleted || old_meta.IsVisible()) {
func(*h);
}
// Pretend we never took the reference
Unref(*h);
// No net change, so don't need to check for overflow
} else {
// For other states, incrementing the acquire counter has no effect
// so we don't need to undo it. Furthermore, we cannot safely undo
// it because we did not acquire a read reference to lock the
// entry in a Shareable state.
}
// Pretend we never took the reference
Unref(*h);
// No net change, so don't need to check for overflow
} else {
// For other states, incrementing the acquire counter has no effect
// so we don't need to undo it. Furthermore, we cannot safely undo
// it because we did not acquire a read reference to lock the
// entry in a Shareable state.
}
}
}
@@ -399,9 +389,9 @@ HandleImpl* BaseClockTable::StandaloneInsert(
h->SetStandalone();
// Single reference (standalone entries only created if returning a refed
// Handle back to user)
uint64_t meta = uint64_t{ClockHandle::kStateInvisible}
<< ClockHandle::kStateShift;
meta |= uint64_t{1} << ClockHandle::kAcquireCounterShift;
SlotMeta meta;
meta.SetInvisible();
meta.SetAcquireCounter(1);
h->meta.Store(meta);
// Keep track of how much of usage is standalone
standalone_usage_.FetchAddRelaxed(proto.GetTotalCharge());
@@ -564,11 +554,10 @@ void BaseClockTable::TrackAndReleaseEvictedEntry(ClockHandle* h) {
if (eviction_callback_) {
// For key reconstructed from hash
UniqueId64x2 unhashed;
took_value_ownership =
eviction_callback_(ClockCacheShard<FixedHyperClockTable>::ReverseHash(
h->GetHash(), &unhashed, hash_seed_),
static_cast<Cache::Handle*>(h),
h->meta.LoadRelaxed() & ClockHandle::kHitBitMask);
took_value_ownership = eviction_callback_(
ClockCacheShard<FixedHyperClockTable>::ReverseHash(
h->GetHash(), &unhashed, hash_seed_),
static_cast<Cache::Handle*>(h), h->meta.LoadRelaxed().GetHit());
}
if (!took_value_ownership) {
h->FreeData(allocator_);
@@ -648,7 +637,7 @@ Status BaseClockTable::Insert(const ClockHandleBasicData& proto,
// * Have to insert into a suboptimal location (more probes) so that the
// old entry can be kept around as well.
uint64_t initial_countdown = GetInitialCountdown(priority);
uint32_t initial_countdown = GetInitialCountdown(priority);
assert(initial_countdown > 0);
HandleImpl* e =
@@ -693,34 +682,34 @@ Status BaseClockTable::Insert(const ClockHandleBasicData& proto,
void BaseClockTable::Ref(ClockHandle& h) {
// Increment acquire counter
uint64_t old_meta = h.meta.FetchAdd(ClockHandle::kAcquireIncrement);
SlotMeta old_meta;
h.meta.Apply(AcquireCounter::PlusTransformPromiseNoOverflow(1), &old_meta);
assert((old_meta >> ClockHandle::kStateShift) &
ClockHandle::kStateShareableBit);
assert(old_meta.IsShareable());
// Must have already had a reference
assert(GetRefcount(old_meta) > 0);
assert(old_meta.GetRefcount() > 0);
(void)old_meta;
}
#ifndef NDEBUG
void BaseClockTable::TEST_RefN(ClockHandle& h, size_t n) {
void BaseClockTable::TEST_RefN(ClockHandle& h, uint32_t n) {
// Increment acquire counter
uint64_t old_meta = h.meta.FetchAdd(n * ClockHandle::kAcquireIncrement);
SlotMeta old_meta;
h.meta.Apply(AcquireCounter::PlusTransformPromiseNoOverflow(n), &old_meta);
assert((old_meta >> ClockHandle::kStateShift) &
ClockHandle::kStateShareableBit);
assert(old_meta.IsShareable());
(void)old_meta;
}
void BaseClockTable::TEST_ReleaseNMinus1(ClockHandle* h, size_t n) {
void BaseClockTable::TEST_ReleaseNMinus1(ClockHandle* h, uint32_t n) {
assert(n > 0);
// Like n-1 Releases, but assumes one more will happen in the caller to take
// care of anything like erasing an unreferenced, invisible entry.
uint64_t old_meta =
h->meta.FetchAdd((n - 1) * ClockHandle::kReleaseIncrement);
assert((old_meta >> ClockHandle::kStateShift) &
ClockHandle::kStateShareableBit);
SlotMeta old_meta;
h->meta.Apply(ReleaseCounter::PlusTransformPromiseNoOverflow(n - 1),
&old_meta);
assert(old_meta.IsShareable());
(void)old_meta;
}
#endif
@@ -754,23 +743,20 @@ FixedHyperClockTable::~FixedHyperClockTable() {
// in the table.
for (size_t i = 0; i < GetTableSize(); i++) {
HandleImpl& h = array_[i];
switch (h.meta.LoadRelaxed() >> ClockHandle::kStateShift) {
case ClockHandle::kStateEmpty:
// noop
break;
case ClockHandle::kStateInvisible: // rare but possible
case ClockHandle::kStateVisible:
assert(GetRefcount(h.meta.LoadRelaxed()) == 0);
h.FreeData(allocator_);
SlotMeta meta = h.meta.LoadRelaxed();
if (meta.IsShareable()) {
// NOTE: Reaching here invisible is rare but possible
assert(meta.GetRefcount() == 0);
h.FreeData(allocator_);
#ifndef NDEBUG
Rollback(h.hashed_key, &h);
ReclaimEntryUsage(h.GetTotalCharge());
Rollback(h.hashed_key, &h);
ReclaimEntryUsage(h.GetTotalCharge());
#endif
break;
// otherwise
default:
assert(false);
break;
} else {
// Should be no transient "under construction" states unless a thread
// was killed or we are being destructed while another thread is still
// operating on the structure
assert(meta.IsEmpty());
}
}
@@ -792,7 +778,7 @@ bool FixedHyperClockTable::GrowIfNeeded(size_t new_occupancy, InsertState&) {
}
FixedHyperClockTable::HandleImpl* FixedHyperClockTable::DoInsert(
const ClockHandleBasicData& proto, uint64_t initial_countdown,
const ClockHandleBasicData& proto, uint32_t initial_countdown,
bool keep_ref, InsertState&) {
bool already_matches = false;
HandleImpl* e = FindSlot(
@@ -843,47 +829,46 @@ FixedHyperClockTable::HandleImpl* FixedHyperClockTable::Lookup(
HandleImpl* e = FindSlot(
hashed_key,
[&](HandleImpl* h) {
SlotMeta old_meta;
// Mostly branch-free version (similar performance)
/*
uint64_t old_meta = h->meta.FetchAdd(ClockHandle::kAcquireIncrement,
std::memory_order_acquire);
bool Shareable = (old_meta >> (ClockHandle::kStateShift + 1)) & 1U;
bool visible = (old_meta >> ClockHandle::kStateShift) & 1U;
bool match = (h->key == key) & visible;
h->meta.FetchSub(static_cast<uint64_t>(Shareable & !match) <<
ClockHandle::kAcquireCounterShift); return
match;
h->meta.Apply(AcquireCounter::PlusTransformPromiseNoOverflow(1),
&old_meta);
bool shareable = old_meta.IsShareable();
bool visible = old_meta.IsVisible();
bool match = (h->hashed_key == hashed_key) & visible;
h->meta.Apply(AcquireCounter::MinusTransformPromiseNoUnderflow(
uint32_t{shareable} & uint32_t{!match}));
h->meta.Apply(SlotMeta::HitFlag::Or(match));
return match;
*/
// Optimistic lookup should pay off when the table is relatively
// sparse.
constexpr bool kOptimisticLookup = true;
uint64_t old_meta;
if (!kOptimisticLookup) {
old_meta = h->meta.Load();
if ((old_meta >> ClockHandle::kStateShift) !=
ClockHandle::kStateVisible) {
if (!old_meta.IsVisible()) {
return false;
}
}
// (Optimistically) increment acquire counter
old_meta = h->meta.FetchAdd(ClockHandle::kAcquireIncrement);
h->meta.Apply(AcquireCounter::PlusTransformPromiseNoOverflow(1),
&old_meta);
// Check if it's an entry visible to lookups
if ((old_meta >> ClockHandle::kStateShift) ==
ClockHandle::kStateVisible) {
if (old_meta.IsVisible()) {
// Acquired a read reference
if (h->hashed_key == hashed_key) {
// Match
// Update the hit bit
if (eviction_callback_) {
h->meta.FetchOrRelaxed(uint64_t{1} << ClockHandle::kHitBitShift);
h->meta.ApplyRelaxed(SlotMeta::HitFlag::SetTransform());
}
return true;
} else {
// Mismatch. Pretend we never took the reference
Unref(*h);
}
} else if (UNLIKELY((old_meta >> ClockHandle::kStateShift) ==
ClockHandle::kStateInvisible)) {
} else if (UNLIKELY(old_meta.IsInvisible())) {
// Pretend we never took the reference
Unref(*h);
} else {
@@ -907,53 +892,49 @@ bool FixedHyperClockTable::Release(HandleImpl* h, bool useful,
// is only freed up by EvictFromClock (called by Insert when space is needed)
// and Erase. We do this to avoid an extra atomic read of the variable usage_.
uint64_t old_meta;
SlotMeta old_meta;
if (useful) {
// Increment release counter to indicate was used
old_meta = h->meta.FetchAdd(ClockHandle::kReleaseIncrement);
auto add_release = ReleaseCounter::PlusTransformPromiseNoOverflow(1);
h->meta.Apply(add_release, &old_meta);
} else {
// Decrement acquire counter to pretend it never happened
old_meta = h->meta.FetchSub(ClockHandle::kAcquireIncrement);
auto sub_acquire = AcquireCounter::MinusTransformPromiseNoUnderflow(1);
h->meta.Apply(sub_acquire, &old_meta);
}
assert((old_meta >> ClockHandle::kStateShift) &
ClockHandle::kStateShareableBit);
assert(old_meta.IsShareable());
// No underflow
assert(((old_meta >> ClockHandle::kAcquireCounterShift) &
ClockHandle::kCounterMask) !=
((old_meta >> ClockHandle::kReleaseCounterShift) &
ClockHandle::kCounterMask));
assert(old_meta.GetAcquireCounter() != old_meta.GetReleaseCounter());
if (erase_if_last_ref || UNLIKELY(old_meta >> ClockHandle::kStateShift ==
ClockHandle::kStateInvisible)) {
if (erase_if_last_ref || UNLIKELY(old_meta.IsInvisible())) {
// FIXME: There's a chance here that another thread could replace this
// entry and we end up erasing the wrong one.
// Update for last FetchAdd op
// Update for last Apply op
if (useful) {
old_meta += ClockHandle::kReleaseIncrement;
old_meta.SetReleaseCounter(old_meta.GetReleaseCounter() + 1);
} else {
old_meta -= ClockHandle::kAcquireIncrement;
old_meta.SetAcquireCounter(old_meta.GetAcquireCounter() - 1);
}
// Take ownership if no refs
SlotMeta construction_meta;
construction_meta.SetUnderConstruction();
do {
if (GetRefcount(old_meta) != 0) {
if (old_meta.GetRefcount() != 0) {
// Not last ref at some point in time during this Release call
// Correct for possible (but rare) overflow
CorrectNearOverflow(old_meta, h->meta);
return false;
}
if ((old_meta & (uint64_t{ClockHandle::kStateShareableBit}
<< ClockHandle::kStateShift)) == 0) {
if (!old_meta.IsShareable()) {
// Someone else took ownership
return false;
}
// Note that there's a small chance that we release, another thread
// replaces this entry with another, reaches zero refs, and then we end
// up erasing that other entry. That's an acceptable risk / imprecision.
} while (
!h->meta.CasWeak(old_meta, uint64_t{ClockHandle::kStateConstruction}
<< ClockHandle::kStateShift));
} while (!h->meta.CasWeak(old_meta, construction_meta));
// Took ownership
size_t total_charge = h->GetTotalCharge();
if (UNLIKELY(h->IsStandalone())) {
@@ -976,7 +957,7 @@ bool FixedHyperClockTable::Release(HandleImpl* h, bool useful,
}
#ifndef NDEBUG
void FixedHyperClockTable::TEST_ReleaseN(HandleImpl* h, size_t n) {
void FixedHyperClockTable::TEST_ReleaseN(HandleImpl* h, uint32_t n) {
if (n > 0) {
// Do n-1 simple releases first
TEST_ReleaseNMinus1(h, n);
@@ -993,30 +974,29 @@ void FixedHyperClockTable::Erase(const UniqueId64x2& hashed_key) {
[&](HandleImpl* h) {
// Could be multiple entries in rare cases. Erase them all.
// Optimistically increment acquire counter
uint64_t old_meta = h->meta.FetchAdd(ClockHandle::kAcquireIncrement);
auto add_acquire = AcquireCounter::PlusTransformPromiseNoOverflow(1);
SlotMeta old_meta, meta;
h->meta.Apply(add_acquire, &old_meta, &meta);
// Check if it's an entry visible to lookups
if ((old_meta >> ClockHandle::kStateShift) ==
ClockHandle::kStateVisible) {
if (meta.IsVisible()) {
// Acquired a read reference
if (h->hashed_key == hashed_key) {
// Match. Set invisible.
old_meta =
h->meta.FetchAnd(~(uint64_t{ClockHandle::kStateVisibleBit}
<< ClockHandle::kStateShift));
// Apply update to local copy
old_meta &= ~(uint64_t{ClockHandle::kStateVisibleBit}
<< ClockHandle::kStateShift);
// Match. Take ownership if no other refs, or set invisible other
// refs exist.
for (;;) {
uint64_t refcount = GetRefcount(old_meta);
uint32_t refcount = meta.GetRefcount();
assert(refcount > 0);
if (refcount > 1) {
// Not last ref at some point in time during this Erase call
// Pretend we never took the reference
// Set invisible
h->meta.Apply(SlotMeta::VisibleFlag::ClearTransform());
// And pretend we never took the reference
Unref(*h);
break;
} else if (h->meta.CasWeak(
old_meta, uint64_t{ClockHandle::kStateConstruction}
<< ClockHandle::kStateShift)) {
}
SlotMeta construction_meta;
construction_meta.SetUnderConstruction();
if (h->meta.CasWeak(meta, construction_meta)) {
// Took ownership
assert(hashed_key == h->hashed_key);
size_t total_charge = h->GetTotalCharge();
@@ -1032,8 +1012,7 @@ void FixedHyperClockTable::Erase(const UniqueId64x2& hashed_key) {
// Mismatch. Pretend we never took the reference
Unref(*h);
}
} else if (UNLIKELY((old_meta >> ClockHandle::kStateShift) ==
ClockHandle::kStateInvisible)) {
} else if (UNLIKELY(old_meta.IsInvisible())) {
// Pretend we never took the reference
Unref(*h);
} else {
@@ -1050,17 +1029,17 @@ void FixedHyperClockTable::EraseUnRefEntries() {
for (size_t i = 0; i <= this->length_bits_mask_; i++) {
HandleImpl& h = array_[i];
uint64_t old_meta = h.meta.LoadRelaxed();
if (old_meta & (uint64_t{ClockHandle::kStateShareableBit}
<< ClockHandle::kStateShift) &&
GetRefcount(old_meta) == 0 &&
h.meta.CasStrong(old_meta, uint64_t{ClockHandle::kStateConstruction}
<< ClockHandle::kStateShift)) {
// Took ownership
size_t total_charge = h.GetTotalCharge();
Rollback(h.hashed_key, &h);
FreeDataMarkEmpty(h, allocator_);
ReclaimEntryUsage(total_charge);
SlotMeta old_meta = h.meta.LoadRelaxed();
if (old_meta.IsShareable() && old_meta.GetRefcount() == 0) {
SlotMeta construction_meta;
construction_meta.SetUnderConstruction();
if (h.meta.CasStrong(old_meta, construction_meta)) {
// Took ownership
size_t total_charge = h.GetTotalCharge();
Rollback(h.hashed_key, &h);
FreeDataMarkEmpty(h, allocator_);
ReclaimEntryUsage(total_charge);
}
}
}
}
@@ -1320,12 +1299,12 @@ bool ClockCacheShard<Table>::Release(HandleImpl* handle, bool useful,
#ifndef NDEBUG
template <class Table>
void ClockCacheShard<Table>::TEST_RefN(HandleImpl* h, size_t n) {
void ClockCacheShard<Table>::TEST_RefN(HandleImpl* h, uint32_t n) {
table_.TEST_RefN(*h, n);
}
template <class Table>
void ClockCacheShard<Table>::TEST_ReleaseN(HandleImpl* h, size_t n) {
void ClockCacheShard<Table>::TEST_ReleaseN(HandleImpl* h, uint32_t n) {
table_.TEST_ReleaseN(h, n);
}
#endif
@@ -1373,8 +1352,8 @@ size_t ClockCacheShard<Table>::GetPinnedUsage() const {
metadata_charge_policy_ == kFullChargeCacheMetadata;
ConstApplyToEntriesRange(
[&table_pinned_usage, charge_metadata](const HandleImpl& h) {
uint64_t meta = h.meta.LoadRelaxed();
uint64_t refcount = GetRefcount(meta);
SlotMeta meta = h.meta.LoadRelaxed();
uint32_t refcount = meta.GetRefcount();
// Holding one ref for ConstApplyToEntriesRange
assert(refcount > 0);
if (refcount > 1) {
@@ -1494,7 +1473,7 @@ void AddShardEvaluation(const FixedHyperClockCache::Shard& shard,
}
bool IsSlotOccupied(const ClockHandle& h) {
return (h.meta.LoadRelaxed() >> ClockHandle::kStateShift) != 0;
return !h.meta.LoadRelaxed().IsEmpty();
}
} // namespace
@@ -1759,12 +1738,12 @@ inline bool MatchAndRef(const UniqueId64x2* hashed_key, const ClockHandle& h,
// Must be at least something to match
assert(hashed_key || shift > 0);
uint64_t old_meta;
SlotMeta old_meta, new_meta;
// (Optimistically) increment acquire counter.
old_meta = h.meta.FetchAdd(ClockHandle::kAcquireIncrement);
auto add_acquire = AcquireCounter::PlusTransformPromiseNoOverflow(1);
h.meta.Apply(add_acquire, &old_meta, &new_meta);
// Check if it's a referencable (sharable) entry
if ((old_meta & (uint64_t{ClockHandle::kStateShareableBit}
<< ClockHandle::kStateShift)) == 0) {
if (!old_meta.IsShareable()) {
// For non-sharable states, incrementing the acquire counter has no effect
// so we don't need to undo it. Furthermore, we cannot safely undo
// it because we did not acquire a read reference to lock the
@@ -1775,10 +1754,9 @@ inline bool MatchAndRef(const UniqueId64x2* hashed_key, const ClockHandle& h,
return false;
}
// Else acquired a read reference
assert(GetRefcount(old_meta + ClockHandle::kAcquireIncrement) > 0);
assert(new_meta.GetRefcount() > 0);
if (hashed_key && h.hashed_key == *hashed_key &&
LIKELY(old_meta & (uint64_t{ClockHandle::kStateVisibleBit}
<< ClockHandle::kStateShift))) {
LIKELY(old_meta.IsVisible())) {
// Match on full key, visible
if (full_match_or_unknown) {
*full_match_or_unknown = true;
@@ -1946,7 +1924,7 @@ class AutoHyperClockTable::ChainRewriteLock {
}
}
AcqRelBitFieldsAtomic<NextWithShift>* head_ptr_;
BitFieldsAtomic<NextWithShift>* head_ptr_;
NextWithShift saved_head_;
};
@@ -2051,7 +2029,7 @@ AutoHyperClockTable::~AutoHyperClockTable() {
HandleImpl::kUnusedMarker);
assert(array_[i].chain_next_with_shift.LoadRelaxed() ==
HandleImpl::kUnusedMarker);
assert(array_[i].meta.LoadRelaxed() == 0);
assert(array_[i].meta.LoadRelaxed() == SlotMeta{});
}
#endif // MUST_FREE_HEAP_ALLOCATIONS
#ifndef NDEBUG // Extra invariant checking
@@ -2060,30 +2038,27 @@ AutoHyperClockTable::~AutoHyperClockTable() {
#endif // !NDEBUG
for (size_t i = 0; i < used_end; i++) {
HandleImpl& h = array_[i];
switch (h.meta.LoadRelaxed() >> ClockHandle::kStateShift) {
case ClockHandle::kStateEmpty:
// noop
break;
case ClockHandle::kStateInvisible: // rare but possible
case ClockHandle::kStateVisible:
assert(GetRefcount(h.meta.LoadRelaxed()) == 0);
h.FreeData(allocator_);
SlotMeta meta = h.meta.LoadRelaxed();
if (meta.IsShareable()) {
// NOTE: Reaching here invisible is rare but possible
assert(meta.GetRefcount() == 0);
h.FreeData(allocator_);
#ifndef NDEBUG // Extra invariant checking
usage_.FetchSubRelaxed(h.total_charge);
occupancy_.FetchSubRelaxed(1U);
was_populated[i] = true;
if (!h.chain_next_with_shift.LoadRelaxed().IsEnd()) {
assert(!h.chain_next_with_shift.LoadRelaxed().IsLocked());
size_t next = h.chain_next_with_shift.LoadRelaxed().GetNext();
assert(!was_pointed_to[next]);
was_pointed_to[next] = true;
}
usage_.FetchSubRelaxed(h.total_charge);
occupancy_.FetchSubRelaxed(1U);
was_populated[i] = true;
if (!h.chain_next_with_shift.LoadRelaxed().IsEnd()) {
assert(!h.chain_next_with_shift.LoadRelaxed().IsLocked());
size_t next = h.chain_next_with_shift.LoadRelaxed().GetNext();
assert(!was_pointed_to[next]);
was_pointed_to[next] = true;
}
#endif // !NDEBUG
break;
// otherwise
default:
assert(false);
break;
} else {
// Should be no transient "under construction" states unless a thread
// was killed or we are being destructed while another thread is still
// operating on the structure
assert(meta.IsEmpty());
}
#ifndef NDEBUG // Extra invariant checking
if (!h.head_next_with_shift.LoadRelaxed().IsEnd()) {
@@ -2691,20 +2666,17 @@ void AutoHyperClockTable::PurgeImplLocked(OpData* op_data,
op_data->push_back(h);
// Entries for eviction become purgeable
purgeable = true;
assert((h->meta.Load() >> ClockHandle::kStateShift) ==
ClockHandle::kStateConstruction);
assert(h->meta.Load().IsUnderConstruction());
}
} else {
(void)op_data;
(void)data;
purgeable = ((h->meta.Load() >> ClockHandle::kStateShift) &
ClockHandle::kStateShareableBit) == 0;
purgeable = !h->meta.Load().IsShareable();
}
}
if (purgeable) {
assert((h->meta.Load() >> ClockHandle::kStateShift) ==
ClockHandle::kStateConstruction);
assert(h->meta.Load().IsUnderConstruction());
pending_purge = true;
} else if (pending_purge) {
if (prev_to_keep) {
@@ -2864,7 +2836,7 @@ void AutoHyperClockTable::PurgeImpl(OpData* op_data, size_t home,
}
AutoHyperClockTable::HandleImpl* AutoHyperClockTable::DoInsert(
const ClockHandleBasicData& proto, uint64_t initial_countdown,
const ClockHandleBasicData& proto, uint32_t initial_countdown,
bool take_ref, InsertState& state) {
size_t home;
int orig_home_shift;
@@ -3149,14 +3121,14 @@ AutoHyperClockTable::HandleImpl* AutoHyperClockTable::Lookup(
#endif
if (probably_equal) {
// Increment acquire counter for definitive check
uint64_t old_meta = h->meta.FetchAdd(ClockHandle::kAcquireIncrement);
auto add_acquire = AcquireCounter::PlusTransformPromiseNoOverflow(1);
SlotMeta old_meta, new_meta;
h->meta.Apply(add_acquire, &old_meta, &new_meta);
// Check if it's a referencable (sharable) entry
if (LIKELY(old_meta & (uint64_t{ClockHandle::kStateShareableBit}
<< ClockHandle::kStateShift))) {
assert(GetRefcount(old_meta + ClockHandle::kAcquireIncrement) > 0);
if (LIKELY(old_meta.IsShareable())) {
assert(new_meta.GetRefcount() > 0);
if (LIKELY(h->hashed_key == hashed_key) &&
LIKELY(old_meta & (uint64_t{ClockHandle::kStateVisibleBit}
<< ClockHandle::kStateShift))) {
LIKELY(old_meta.IsVisible())) {
return h;
} else {
Unref(*h);
@@ -3277,7 +3249,7 @@ AutoHyperClockTable::HandleImpl* AutoHyperClockTable::Lookup(
}
// Update the hit bit
if (eviction_callback_) {
h->meta.FetchOrRelaxed(uint64_t{1} << ClockHandle::kHitBitShift);
h->meta.ApplyRelaxed(SlotMeta::HitFlag::SetTransform());
}
// All done.
return h;
@@ -3317,8 +3289,7 @@ AutoHyperClockTable::HandleImpl* AutoHyperClockTable::Lookup(
}
void AutoHyperClockTable::Remove(HandleImpl* h) {
assert((h->meta.Load() >> ClockHandle::kStateShift) ==
ClockHandle::kStateConstruction);
assert(h->meta.Load().IsUnderConstruction());
const HandleImpl& c_h = *h;
PurgeImpl(&c_h.hashed_key);
@@ -3326,26 +3297,23 @@ void AutoHyperClockTable::Remove(HandleImpl* h) {
bool AutoHyperClockTable::TryEraseHandle(HandleImpl* h, bool holding_ref,
bool mark_invisible) {
uint64_t meta;
if (mark_invisible) {
// Set invisible
meta = h->meta.FetchAnd(
~(uint64_t{ClockHandle::kStateVisibleBit} << ClockHandle::kStateShift));
// To local variable also
meta &=
~(uint64_t{ClockHandle::kStateVisibleBit} << ClockHandle::kStateShift);
} else {
meta = h->meta.Load();
}
SlotMeta meta = h->meta.Load();
assert(!holding_ref || meta.IsShareable());
// Take ownership if no other refs
// Take ownership if no other refs, or set invisible if other refs exist (and
// mark_invisible is set).
SlotMeta construction_meta;
construction_meta.SetUnderConstruction();
do {
if (GetRefcount(meta) != uint64_t{holding_ref}) {
if (meta.GetRefcount() != uint32_t{holding_ref}) {
// Not last ref at some point in time during this call
if (mark_invisible) {
// Set invisible
h->meta.Apply(SlotMeta::VisibleFlag::ClearTransform());
}
return false;
}
if ((meta & (uint64_t{ClockHandle::kStateShareableBit}
<< ClockHandle::kStateShift)) == 0) {
if (!meta.IsShareable()) {
// Someone else took ownership
return false;
}
@@ -3353,8 +3321,7 @@ bool AutoHyperClockTable::TryEraseHandle(HandleImpl* h, bool holding_ref,
// another thread replaces this entry with another, reaches zero refs, and
// then we end up erasing that other entry. That's an acceptable risk /
// imprecision.
} while (!h->meta.CasWeak(meta, uint64_t{ClockHandle::kStateConstruction}
<< ClockHandle::kStateShift));
} while (!h->meta.CasWeak(meta, construction_meta));
// Took ownership
// TODO? Delay freeing?
h->FreeData(allocator_);
@@ -3381,27 +3348,24 @@ bool AutoHyperClockTable::Release(HandleImpl* h, bool useful,
// is needed) and Erase. We do this to avoid an extra atomic read of the
// variable usage_.
uint64_t old_meta;
SlotMeta old_meta;
if (useful) {
// Increment release counter to indicate was used
old_meta = h->meta.FetchAdd(ClockHandle::kReleaseIncrement);
auto add_release = ReleaseCounter::PlusTransformPromiseNoOverflow(1);
h->meta.Apply(add_release, &old_meta);
// Correct for possible (but rare) overflow
CorrectNearOverflow(old_meta, h->meta);
} else {
// Decrement acquire counter to pretend it never happened
old_meta = h->meta.FetchSub(ClockHandle::kAcquireIncrement);
auto sub_acquire = AcquireCounter::MinusTransformPromiseNoUnderflow(1);
h->meta.Apply(sub_acquire, &old_meta);
}
assert((old_meta >> ClockHandle::kStateShift) &
ClockHandle::kStateShareableBit);
assert(old_meta.IsShareable());
// No underflow
assert(((old_meta >> ClockHandle::kAcquireCounterShift) &
ClockHandle::kCounterMask) !=
((old_meta >> ClockHandle::kReleaseCounterShift) &
ClockHandle::kCounterMask));
assert(old_meta.GetAcquireCounter() != old_meta.GetReleaseCounter());
if ((erase_if_last_ref || UNLIKELY(old_meta >> ClockHandle::kStateShift ==
ClockHandle::kStateInvisible))) {
if ((erase_if_last_ref || UNLIKELY(old_meta.IsInvisible()))) {
// FIXME: There's a chance here that another thread could replace this
// entry and we end up erasing the wrong one.
return TryEraseHandle(h, /*holding_ref=*/false, /*mark_invisible=*/false);
@@ -3411,7 +3375,7 @@ bool AutoHyperClockTable::Release(HandleImpl* h, bool useful,
}
#ifndef NDEBUG
void AutoHyperClockTable::TEST_ReleaseN(HandleImpl* h, size_t n) {
void AutoHyperClockTable::TEST_ReleaseN(HandleImpl* h, uint32_t n) {
if (n > 0) {
// Do n-1 simple releases first
TEST_ReleaseNMinus1(h, n);
@@ -3441,20 +3405,20 @@ void AutoHyperClockTable::EraseUnRefEntries() {
for (size_t i = 0; i < usable_size; i++) {
HandleImpl& h = array_[i];
uint64_t old_meta = h.meta.LoadRelaxed();
if (old_meta & (uint64_t{ClockHandle::kStateShareableBit}
<< ClockHandle::kStateShift) &&
GetRefcount(old_meta) == 0 &&
h.meta.CasStrong(old_meta, uint64_t{ClockHandle::kStateConstruction}
<< ClockHandle::kStateShift)) {
// Took ownership
h.FreeData(allocator_);
usage_.FetchSubRelaxed(h.total_charge);
// NOTE: could be more efficient with a dedicated variant of
// PurgeImpl, but this is not a common operation
Remove(&h);
MarkEmpty(h);
occupancy_.FetchSub(1U);
SlotMeta old_meta = h.meta.LoadRelaxed();
if (old_meta.IsShareable() && old_meta.GetRefcount() == 0) {
SlotMeta construction_meta;
construction_meta.SetUnderConstruction();
if (h.meta.CasStrong(old_meta, construction_meta)) {
// Took ownership
h.FreeData(allocator_);
usage_.FetchSubRelaxed(h.total_charge);
// NOTE: could be more efficient with a dedicated variant of
// PurgeImpl, but this is not a common operation
Remove(&h);
MarkEmpty(h);
occupancy_.FetchSub(1U);
}
}
}
}
+93 -44
View File
@@ -317,40 +317,89 @@ struct ClockHandle : public ClockHandleBasicData {
// | acquire counter | release counter | hit bit | state marker |
// -----------------------------------------------------------------------
// For reading or updating counters in meta word.
static constexpr uint8_t kCounterNumBits = 30;
static constexpr uint64_t kCounterMask = (uint64_t{1} << kCounterNumBits) - 1;
struct SlotMeta : public BitFields<uint64_t, SlotMeta> {
// For reading or updating counters in meta word.
static constexpr uint8_t kCounterNumBits = 30;
// Number of times the a reference has been acquired (or attempted)
// since last reset by eviction processing
using AcquireCounter =
UnsignedBitField<SlotMeta, kCounterNumBits, NoPrevBitField>;
// Number of times the a reference has been released (or attempted)
// since last reset by eviction processing
using ReleaseCounter =
UnsignedBitField<SlotMeta, kCounterNumBits, AcquireCounter>;
// Metadata bit in support of secondary cache
using HitFlag = BoolBitField<SlotMeta, ReleaseCounter>;
// Occupied means any state other than empty
using OccupiedFlag = BoolBitField<SlotMeta, HitFlag>;
// Shareable means the entry is reference counted (visible or invisible)
// (only set if also occupied)
using ShareableFlag = BoolBitField<SlotMeta, OccupiedFlag>;
// Visible is only set if also shareable (invisible can't be found by
// Lookup)
using VisibleFlag = BoolBitField<SlotMeta, ShareableFlag>;
static constexpr uint8_t kAcquireCounterShift = 0;
static constexpr uint64_t kAcquireIncrement = uint64_t{1}
<< kAcquireCounterShift;
static constexpr uint8_t kReleaseCounterShift = kCounterNumBits;
static constexpr uint64_t kReleaseIncrement = uint64_t{1}
<< kReleaseCounterShift;
// Convenience functions
uint32_t GetAcquireCounter() const { return Get<AcquireCounter>(); }
void SetAcquireCounter(uint32_t val) { Set<AcquireCounter>(val); }
uint32_t GetReleaseCounter() const { return Get<ReleaseCounter>(); }
void SetReleaseCounter(uint32_t val) { Set<ReleaseCounter>(val); }
uint32_t GetRefcount() const {
return Get<AcquireCounter>() - Get<ReleaseCounter>();
}
bool GetHit() const { return Get<HitFlag>(); }
void SetHit(bool val) { Set<HitFlag>(val); }
// For setting the hit bit
static constexpr uint8_t kHitBitShift = 2U * kCounterNumBits;
static constexpr uint64_t kHitBitMask = uint64_t{1} << kHitBitShift;
// Some distinct states for the various state flags
bool IsEmpty() const {
bool rv = !Get<OccupiedFlag>();
if (rv) {
assert(!Get<ShareableFlag>());
assert(!Get<VisibleFlag>());
}
return rv;
}
// For reading or updating the state marker in meta word
static constexpr uint8_t kStateShift = kHitBitShift + 1;
bool IsUnderConstruction() const {
bool rv = Get<OccupiedFlag>() && !Get<ShareableFlag>();
if (rv) {
assert(!Get<VisibleFlag>());
}
return rv;
}
void SetUnderConstruction() {
Set<OccupiedFlag>(true);
Set<ShareableFlag>(false);
Set<VisibleFlag>(false);
}
// Bits contribution to state marker.
// Occupied means any state other than empty
static constexpr uint8_t kStateOccupiedBit = 0b100;
// Shareable means the entry is reference counted (visible or invisible)
// (only set if also occupied)
static constexpr uint8_t kStateShareableBit = 0b010;
// Visible is only set if also shareable
static constexpr uint8_t kStateVisibleBit = 0b001;
bool IsShareable() const { return Get<ShareableFlag>(); }
bool IsInvisible() const {
bool rv = Get<ShareableFlag>() && !Get<VisibleFlag>();
if (rv) {
assert(Get<OccupiedFlag>());
}
return rv;
}
void SetInvisible() {
Set<OccupiedFlag>(true);
Set<ShareableFlag>(true);
Set<VisibleFlag>(false);
}
// Complete state markers (not shifted into full word)
static constexpr uint8_t kStateEmpty = 0b000;
static constexpr uint8_t kStateConstruction = kStateOccupiedBit;
static constexpr uint8_t kStateInvisible =
kStateOccupiedBit | kStateShareableBit;
static constexpr uint8_t kStateVisible =
kStateOccupiedBit | kStateShareableBit | kStateVisibleBit;
bool IsVisible() const {
bool rv = Get<ShareableFlag>() && Get<VisibleFlag>();
if (rv) {
assert(Get<OccupiedFlag>());
}
return rv;
}
void SetVisible() {
Set<OccupiedFlag>(true);
Set<ShareableFlag>(true);
Set<VisibleFlag>(true);
}
};
// Constants for initializing the countdown clock. (Countdown clock is only
// in effect with zero refs, acquire counter == release counter, and in that
@@ -364,7 +413,7 @@ struct ClockHandle : public ClockHandleBasicData {
// TODO: make these coundown values tuning parameters for eviction?
// See above. Mutable for read reference counting.
mutable AcqRelAtomic<uint64_t> meta{};
mutable BitFieldsAtomic<SlotMeta> meta{};
}; // struct ClockHandle
class BaseClockTable {
@@ -431,9 +480,9 @@ class BaseClockTable {
bool IsEvictionEffortExceeded(const BaseClockTable::EvictionData& data) const;
#ifndef NDEBUG
// Acquire N references
void TEST_RefN(ClockHandle& handle, size_t n);
void TEST_RefN(ClockHandle& handle, uint32_t n);
// Helper for TEST_ReleaseN
void TEST_ReleaseNMinus1(ClockHandle* handle, size_t n);
void TEST_ReleaseNMinus1(ClockHandle* handle, uint32_t n);
#endif
private: // fns
@@ -489,13 +538,13 @@ class BaseClockTable {
// TODO: is this separation needed if we don't do background evictions?
ALIGN_AS(CACHE_LINE_SIZE)
// Number of elements in the table.
AcqRelAtomic<size_t> occupancy_{};
Atomic<size_t> occupancy_{};
// Memory usage by entries tracked by the cache (including standalone)
AcqRelAtomic<size_t> usage_{};
Atomic<size_t> usage_{};
// Part of usage by standalone entries (not in table)
AcqRelAtomic<size_t> standalone_usage_{};
Atomic<size_t> standalone_usage_{};
// Maximum total charge of all elements stored in the table.
// (Relaxed: eventual consistency/update is OK)
@@ -586,7 +635,7 @@ class FixedHyperClockTable : public BaseClockTable {
bool GrowIfNeeded(size_t new_occupancy, InsertState& state);
HandleImpl* DoInsert(const ClockHandleBasicData& proto,
uint64_t initial_countdown, bool take_ref,
uint32_t initial_countdown, bool take_ref,
InsertState& state);
// Runs the clock eviction algorithm trying to reclaim at least
@@ -614,7 +663,7 @@ class FixedHyperClockTable : public BaseClockTable {
}
// Release N references
void TEST_ReleaseN(HandleImpl* handle, size_t n);
void TEST_ReleaseN(HandleImpl* handle, uint32_t n);
#endif
// The load factor p is a real number in (0, 1) such that at all
@@ -850,8 +899,8 @@ class AutoHyperClockTable : public BaseClockTable {
// See above. The head pointer is logically independent of the rest of
// the entry, including the chain next pointer.
AcqRelBitFieldsAtomic<NextWithShift> head_next_with_shift{kUnusedMarker};
AcqRelBitFieldsAtomic<NextWithShift> chain_next_with_shift{kUnusedMarker};
BitFieldsAtomic<NextWithShift> head_next_with_shift{kUnusedMarker};
BitFieldsAtomic<NextWithShift> chain_next_with_shift{kUnusedMarker};
// For supporting CreateStandalone and some fallback cases.
inline bool IsStandalone() const {
@@ -897,7 +946,7 @@ class AutoHyperClockTable : public BaseClockTable {
bool GrowIfNeeded(size_t new_occupancy, InsertState& state);
HandleImpl* DoInsert(const ClockHandleBasicData& proto,
uint64_t initial_countdown, bool take_ref,
uint32_t initial_countdown, bool take_ref,
InsertState& state);
// Runs the clock eviction algorithm trying to reclaim at least
@@ -925,7 +974,7 @@ class AutoHyperClockTable : public BaseClockTable {
}
// Release N references
void TEST_ReleaseN(HandleImpl* handle, size_t n);
void TEST_ReleaseN(HandleImpl* handle, uint32_t n);
#endif
// Maximum ratio of number of occupied slots to number of usable slots. The
@@ -1007,7 +1056,7 @@ class AutoHyperClockTable : public BaseClockTable {
// To maximize parallelization of Grow() operations, this field is only
// updated opportunistically after Grow() operations and in DoInsert() where
// it is found to be out-of-date. See CatchUpLengthInfoNoWait().
AcqRelAtomic<uint64_t> length_info_;
Atomic<uint64_t> length_info_;
// An already-computed version of the usable length times the max load
// factor. Could be slightly out of date but GrowIfNeeded()/Grow() handle
@@ -1130,8 +1179,8 @@ class ALIGN_AS(CACHE_LINE_SIZE) ClockCacheShard final : public CacheShardBase {
return table_.TEST_MutableOccupancyLimit();
}
// Acquire/release N references
void TEST_RefN(HandleImpl* handle, size_t n);
void TEST_ReleaseN(HandleImpl* handle, size_t n);
void TEST_RefN(HandleImpl* handle, uint32_t n);
void TEST_ReleaseN(HandleImpl* handle, uint32_t n);
#endif
private: // data
+1 -5
View File
@@ -50,8 +50,7 @@ CompressedSecondaryCache::CompressedSecondaryCache(
std::make_shared<CacheReservationManagerImpl<CacheEntryRole::kMisc>>(
cache_))),
disable_cache_(opts.capacity == 0) {
auto mgr =
GetBuiltinCompressionManager(cache_options_.compress_format_version);
auto mgr = GetBuiltinV2CompressionManager();
compressor_ = mgr->GetCompressor(cache_options_.compression_opts,
cache_options_.compression_type);
decompressor_ =
@@ -356,9 +355,6 @@ std::string CompressedSecondaryCache::GetPrintableOptions() const {
const_cast<CompressionOptions&>(cache_options_.compression_opts))
.c_str());
ret.append(buffer);
snprintf(buffer, kBufferSize, " compress_format_version : %d\n",
cache_options_.compress_format_version);
ret.append(buffer);
return ret;
}
+2 -3
View File
@@ -856,8 +856,7 @@ TEST_P(CompressedSecondaryCacheTestWithCompressionParam, BasicTestFromString) {
if (LZ4_Supported()) {
sec_cache_uri =
"compressed_secondary_cache://"
"capacity=2048;num_shard_bits=0;compression_type=kLZ4Compression;"
"compress_format_version=2";
"capacity=2048;num_shard_bits=0;compression_type=kLZ4Compression";
} else {
ROCKSDB_GTEST_SKIP("This test requires LZ4 support.");
sec_cache_uri =
@@ -888,7 +887,7 @@ TEST_P(CompressedSecondaryCacheTestWithCompressionParam,
sec_cache_uri =
"compressed_secondary_cache://"
"capacity=2048;num_shard_bits=0;compression_type=kLZ4Compression;"
"compress_format_version=2;enable_custom_split_merge=true";
"enable_custom_split_merge=true";
} else {
ROCKSDB_GTEST_SKIP("This test requires LZ4 support.");
sec_cache_uri =
+18 -18
View File
@@ -1503,7 +1503,7 @@ TEST_P(BasicSecondaryCacheTest, FullCapacityTest) {
/*context*/ this, Cache::Priority::LOW);
ASSERT_EQ(handle1, nullptr);
// k1 promotion can fail with strict_capacit_limit=true, but Lookup still
// k1 promotion can fail with strict_capacity_limit=true, but Lookup still
// succeeds using a standalone handle
handle1 = cache->Lookup(k1.AsSlice(), GetHelper(),
/*context*/ this, Cache::Priority::LOW);
@@ -1680,7 +1680,7 @@ TEST_P(DBSecondaryCacheTest, TestSecondaryCacheCorrectness2) {
// After Flush is successful, RocksDB will do the paranoid check for the new
// SST file. Meta blocks are always cached in the block cache and they
// will not be evicted. When block_2 is cache miss and read out, it is
// inserted to the block cache. Thefore, block_1 is evicted from block
// inserted to the block cache. Therefore, block_1 is evicted from block
// cache and successfully inserted to the secondary cache. Here are 2
// lookups in the secondary cache for block_1 and block_2.
ASSERT_EQ(secondary_cache->num_inserts(), 1u);
@@ -1721,7 +1721,7 @@ TEST_P(DBSecondaryCacheTest, TestSecondaryCacheCorrectness2) {
v = Get(Key(0));
ASSERT_EQ(1007, v.size());
// This Get needs to access block_1, since block_1 is not in block cache
// there is one econdary cache lookup. Then, block_1 is cached in the
// there is one secondary cache lookup. Then, block_1 is cached in the
// block cache.
ASSERT_EQ(secondary_cache->num_inserts(), 2u);
ASSERT_EQ(secondary_cache->num_lookups(), 5u);
@@ -1785,7 +1785,7 @@ TEST_P(DBSecondaryCacheTest, NoSecondaryCacheInsertion) {
std::string v = Get(Key(0));
ASSERT_EQ(1000, v.size());
// Since the block cache is large enough, all the blocks are cached. we
// do not need to lookup the seondary cache.
// do not need to lookup the secondary cache.
ASSERT_EQ(secondary_cache->num_inserts(), 0u);
ASSERT_EQ(secondary_cache->num_lookups(), 2u);
@@ -2150,7 +2150,7 @@ TEST_P(DBSecondaryCacheTest, LRUCacheDumpLoadBasic) {
ASSERT_OK(Flush());
Compact("a", "z");
// do th eread for all the key value pairs, so all the blocks should be in
// do the read for all the key value pairs, so all the blocks should be in
// cache
uint32_t start_insert = cache->GetInsertCount();
uint32_t start_lookup = cache->GetLookupcount();
@@ -2179,7 +2179,7 @@ TEST_P(DBSecondaryCacheTest, LRUCacheDumpLoadBasic) {
&cache_dumper);
ASSERT_OK(s);
std::vector<DB*> db_list;
db_list.push_back(db_);
db_list.push_back(db_.get());
s = cache_dumper->SetDumpFilter(db_list);
ASSERT_OK(s);
s = cache_dumper->DumpCacheEntriesToWriter();
@@ -2263,11 +2263,11 @@ TEST_P(DBSecondaryCacheTest, LRUCacheDumpLoadWithFilter) {
options.env = fault_env_.get();
std::string dbname1 = test::PerThreadDBPath("db_1");
ASSERT_OK(DestroyDB(dbname1, options));
DB* db1 = nullptr;
std::unique_ptr<DB> db1;
ASSERT_OK(DB::Open(options, dbname1, &db1));
std::string dbname2 = test::PerThreadDBPath("db_2");
ASSERT_OK(DestroyDB(dbname2, options));
DB* db2 = nullptr;
std::unique_ptr<DB> db2;
ASSERT_OK(DB::Open(options, dbname2, &db2));
fault_fs_->SetFailGetUniqueId(true);
@@ -2335,7 +2335,7 @@ TEST_P(DBSecondaryCacheTest, LRUCacheDumpLoadWithFilter) {
&cache_dumper);
ASSERT_OK(s);
std::vector<DB*> db_list;
db_list.push_back(db1);
db_list.push_back(db1.get());
s = cache_dumper->SetDumpFilter(db_list);
ASSERT_OK(s);
s = cache_dumper->DumpCacheEntriesToWriter();
@@ -2377,7 +2377,7 @@ TEST_P(DBSecondaryCacheTest, LRUCacheDumpLoadWithFilter) {
ASSERT_OK(s);
ASSERT_OK(db1->Close());
delete db1;
db1.reset();
ASSERT_OK(DB::Open(options, dbname1, &db1));
// After load, we do the Get again. To validate the cache, we do not allow any
@@ -2406,8 +2406,8 @@ TEST_P(DBSecondaryCacheTest, LRUCacheDumpLoadWithFilter) {
ASSERT_EQ(256, static_cast<int>(block_lookup));
fault_fs_->SetFailGetUniqueId(false);
fault_fs_->SetFilesystemActive(true);
delete db1;
delete db2;
db1.reset();
db2.reset();
ASSERT_OK(DestroyDB(dbname1, options));
ASSERT_OK(DestroyDB(dbname2, options));
}
@@ -2464,7 +2464,7 @@ TEST_P(DBSecondaryCacheTest, TestSecondaryCacheOptionBasic) {
std::string v = Get(Key(0));
ASSERT_EQ(1007, v.size());
// Check the data in first block. Cache miss, direclty read from SST file.
// Check the data in first block. Cache miss, directly read from SST file.
ASSERT_EQ(secondary_cache->num_inserts(), 0u);
ASSERT_EQ(secondary_cache->num_lookups(), 0u);
@@ -2598,7 +2598,7 @@ TEST_P(DBSecondaryCacheTest, TestSecondaryCacheOptionChange) {
}
// Two DB test. We create 2 DBs sharing the same block cache and secondary
// cache. We diable the secondary cache option for DB2.
// cache. We disable the secondary cache option for DB2.
TEST_P(DBSecondaryCacheTest, TestSecondaryCacheOptionTwoDB) {
if (IsHyperClock()) {
ROCKSDB_GTEST_BYPASS("Test depends on LRUCache-specific behaviors");
@@ -2619,11 +2619,11 @@ TEST_P(DBSecondaryCacheTest, TestSecondaryCacheOptionTwoDB) {
options.paranoid_file_checks = true;
std::string dbname1 = test::PerThreadDBPath("db_t_1");
ASSERT_OK(DestroyDB(dbname1, options));
DB* db1 = nullptr;
std::unique_ptr<DB> db1;
ASSERT_OK(DB::Open(options, dbname1, &db1));
std::string dbname2 = test::PerThreadDBPath("db_t_2");
ASSERT_OK(DestroyDB(dbname2, options));
DB* db2 = nullptr;
std::unique_ptr<DB> db2;
Options options2 = options;
options2.lowest_used_cache_tier = CacheTier::kVolatileTier;
ASSERT_OK(DB::Open(options2, dbname2, &db2));
@@ -2700,8 +2700,8 @@ TEST_P(DBSecondaryCacheTest, TestSecondaryCacheOptionTwoDB) {
fault_fs_->SetFailGetUniqueId(false);
fault_fs_->SetFilesystemActive(true);
delete db1;
delete db2;
db1.reset();
db2.reset();
ASSERT_OK(DestroyDB(dbname1, options));
ASSERT_OK(DestroyDB(dbname2, options));
}
+512
View File
@@ -0,0 +1,512 @@
# Adding New Options to RocksDB Public API
This document provides guidance on how to add new options to RocksDB's public API. There are two main categories of options:
1. **Standard Column Family Options** (Options/DBOptions/AdvancedColumnFamilyOptions)
2. **BlockBasedTableOptions** (options specific to block-based table format)
## Overview of Files to Modify
### For Standard Column Family Options
| File | Purpose |
|------|---------|
| `include/rocksdb/advanced_options.h` | Define the option with documentation |
| `include/rocksdb/options.h` | Add reference in related option groups if needed |
| `options/cf_options.h` | Add to `MutableCFOptions` or `ImmutableCFOptions` struct |
| `options/cf_options.cc` | Register option for serialization/deserialization and logging |
| `options/options_helper.cc` | Add to `UpdateColumnFamilyOptions()` for mutable options |
| `options/options_settable_test.cc` | Add to test string for option parsing |
| `db_stress_tool/db_stress_common.h` | Declare gflag |
| `db_stress_tool/db_stress_gflags.cc` | Define gflag with default value |
| `db_stress_tool/db_stress_test_base.cc` | Apply flag to options |
| `tools/db_bench_tool.cc` | Add flag definition and apply to options |
| `tools/db_crashtest.py` | Add randomized values for stress testing |
| `unreleased_history/new_features/` | Add release note markdown file |
### For BlockBasedTableOptions
| File | Purpose |
|------|---------|
| `include/rocksdb/table.h` | Define the option in `BlockBasedTableOptions` struct |
| `table/block_based/block_based_table_factory.cc` | Register for serialization, validation, and printing |
| `options/options_settable_test.cc` | Add to `BlockBasedTableOptionsAllFieldsSettable` test |
| `options/options_test.cc` | Add to `MutableCFOptions` test if applicable |
| `db_stress_tool/db_stress_common.h` | Declare gflag |
| `db_stress_tool/db_stress_gflags.cc` | Define gflag |
| `db_stress_tool/db_stress_test_base.cc` | Apply flag to `block_based_options` |
| `tools/db_bench_tool.cc` | Add flag definition and apply to `block_based_options` |
| `tools/db_crashtest.py` | Add randomized values |
| `java/src/main/java/org/rocksdb/BlockBasedTableConfig.java` | Java API |
| `java/rocksjni/portal.h` | JNI portal for Java bindings |
| `java/rocksjni/table.cc` | JNI implementation |
| `java/src/test/java/org/rocksdb/BlockBasedTableConfigTest.java` | Java unit test |
---
## Pattern 1: Adding a Standard Column Family Option
Example reference: commit `94e65a2e0b4f817aa4bfa4c96cdf867e7980d7bc` (memtable_veirfy_per_key_checksum_on_seek)
### Step 1: Define the Option in Public Header
**File: `include/rocksdb/advanced_options.h`**
Add the option with documentation in `AdvancedColumnFamilyOptions` struct:
```cpp
// Enables additional integrity checks during seek.
// Specifically, for skiplist-based memtables, key checksum validation could
// be enabled during seek optionally. This is helpful to detect corrupted
// memtable keys during reads. Enabling this feature incurs a performance
// overhead due to additional key checksum validation during memtable seek
// operation.
// This option depends on memtable_protection_bytes_per_key to be non zero.
// If memtable_protection_bytes_per_key is zero, no validation is performed.
bool memtable_veirfy_per_key_checksum_on_seek = false;
```
### Step 2: Add to Internal Options Structs
**File: `options/cf_options.h`**
Add to `MutableCFOptions` struct (or `ImmutableCFOptions` for immutable options):
```cpp
// In MutableCFOptions constructor from Options:
memtable_veirfy_per_key_checksum_on_seek(
options.memtable_veirfy_per_key_checksum_on_seek),
// In MutableCFOptions default constructor:
memtable_veirfy_per_key_checksum_on_seek(false),
// In MutableCFOptions struct member declarations:
bool memtable_veirfy_per_key_checksum_on_seek;
```
### Step 3: Register for Serialization/Deserialization
**File: `options/cf_options.cc`**
Add to the options type info map for serialization:
```cpp
{"memtable_veirfy_per_key_checksum_on_seek",
{offsetof(struct MutableCFOptions,
memtable_veirfy_per_key_checksum_on_seek),
OptionType::kBoolean, OptionVerificationType::kNormal,
OptionTypeFlags::kMutable}},
```
Add logging in `MutableCFOptions::Dump()`:
```cpp
ROCKS_LOG_INFO(log, "memtable_veirfy_per_key_checksum_on_seek: %d",
memtable_veirfy_per_key_checksum_on_seek);
```
### Step 4: Update Options Helper
**File: `options/options_helper.cc`**
Add to `UpdateColumnFamilyOptions()`:
```cpp
cf_opts->memtable_veirfy_per_key_checksum_on_seek =
moptions.memtable_veirfy_per_key_checksum_on_seek;
```
### Step 5: Add to Options Settable Test
**File: `options/options_settable_test.cc`**
Add to the test string in `ColumnFamilyOptionsAllFieldsSettable`:
```cpp
"memtable_veirfy_per_key_checksum_on_seek=1;"
```
### Step 6: Add db_stress Support
**File: `db_stress_tool/db_stress_common.h`**
```cpp
DECLARE_bool(memtable_veirfy_per_key_checksum_on_seek);
```
**File: `db_stress_tool/db_stress_gflags.cc`**
```cpp
DEFINE_bool(
memtable_veirfy_per_key_checksum_on_seek,
ROCKSDB_NAMESPACE::Options().memtable_veirfy_per_key_checksum_on_seek,
"Sets CF option memtable_veirfy_per_key_checksum_on_seek.");
```
**File: `db_stress_tool/db_stress_test_base.cc`**
```cpp
options.memtable_veirfy_per_key_checksum_on_seek =
FLAGS_memtable_veirfy_per_key_checksum_on_seek;
```
### Step 7: Add db_bench Support
**File: `tools/db_bench_tool.cc`**
```cpp
// Flag definition (near related flags):
DEFINE_bool(memtable_veirfy_per_key_checksum_on_seek, false,
"Sets CF option memtable_veirfy_per_key_checksum_on_seek");
// Apply flag to options (in InitializeOptionsFromFlags or similar):
options.memtable_veirfy_per_key_checksum_on_seek =
FLAGS_memtable_veirfy_per_key_checksum_on_seek;
```
### Step 8: Add Crash Test Support
**File: `tools/db_crashtest.py`**
```python
"memtable_veirfy_per_key_checksum_on_seek": lambda: random.choice([0] * 7 + [1]),
```
Also add constraint handling in `finalize_and_sanitize()` if needed:
```python
# only skip list memtable representation supports paranoid memory checks
if dest_params.get("memtablerep") != "skip_list":
dest_params["memtable_veirfy_per_key_checksum_on_seek"] = 0
```
### Step 9: Add Release Note
**File: `unreleased_history/new_features/<descriptive_name>.md`**
```markdown
A new flag memtable_veirfy_per_key_checksum_on_seek is added to AdvancedColumnFamilyOptions. When it is enabled, it will validate key checksum along the binary search path on skiplist based memtable during seek operation.
```
---
## Pattern 2: Adding a BlockBasedTableOptions Option
Example reference: commit `742741b175c5f238374c1714f9db3340d49de569` (super_block_alignment_size)
### Step 1: Define the Option in Public Header
**File: `include/rocksdb/table.h`**
Add to `BlockBasedTableOptions` struct with documentation:
```cpp
// Align data blocks on super block alignment. Avoid a data block split across
// super block boundaries. Works with/without compression.
//
// Here a "super block" refers to an aligned unit of underlying Filesystem
// storage for which there is an extra cost when a random read involves two
// such super blocks instead of just one. Configuring that size here suggests
// inserting padding in the SST file to avoid a single SST block splitting
// across two super blocks. Only power-of-two sizes are supported. See also
// super_block_alignment_space_overhead_ratio. Default to 0, which means super
// block alignment is disabled.
size_t super_block_alignment_size = 0;
// This option controls the storage space overhead of super block alignment.
// It is used to calculate the max padding size allowed for super block
// alignment. It is calculated in this way. If super_block_alignment_size is
// 2MB, and super_block_alignment_overhead_ratio is 128, then the max padding
// size allowed for super block alignment is 2MB / 128 = 16KB.
// Note that, when it is set to 0, super block alignment is disabled.
size_t super_block_alignment_space_overhead_ratio = 128;
```
### Step 2: Register for Serialization in Table Factory
**File: `table/block_based/block_based_table_factory.cc`**
Add to the type info map:
```cpp
{"super_block_alignment_size",
{offsetof(struct BlockBasedTableOptions, super_block_alignment_size),
OptionType::kSizeT, OptionVerificationType::kNormal}},
{"super_block_alignment_space_overhead_ratio",
{offsetof(struct BlockBasedTableOptions,
super_block_alignment_space_overhead_ratio),
OptionType::kSizeT, OptionVerificationType::kNormal}},
```
Add validation in `ValidateOptions()`:
```cpp
if ((table_options_.super_block_alignment_size &
(table_options_.super_block_alignment_size - 1))) {
return Status::InvalidArgument(
"Super Block alignment requested but super block alignment size is not "
"a power of 2");
}
if (table_options_.super_block_alignment_size >
std::numeric_limits<uint32_t>::max()) {
return Status::InvalidArgument(
"Super block alignment size exceeds maximum number (4GiB) allowed");
}
```
Add printing in `GetPrintableOptions()`:
```cpp
snprintf(buffer, kBufferSize,
" super_block_alignment_size: %" ROCKSDB_PRIszt "\n",
table_options_.super_block_alignment_size);
ret.append(buffer);
```
### Step 3: Add to Options Settable Test
**File: `options/options_settable_test.cc`**
Add to `BlockBasedTableOptionsAllFieldsSettable` test:
```cpp
"super_block_alignment_size=65536;"
"super_block_alignment_space_overhead_ratio=4096;"
```
### Step 4: Add to Options Test
**File: `options/options_test.cc`**
```cpp
ASSERT_OK(GetColumnFamilyOptionsFromString(
config_options, cf_opts,
"block_based_table_factory.super_block_alignment_size=65536; "
"block_based_table_factory.super_block_alignment_space_overhead_ratio=4096;",
&cf_opts));
ASSERT_EQ(bbto->super_block_alignment_size, 65536);
ASSERT_EQ(bbto->super_block_alignment_space_overhead_ratio, 4096);
```
### Step 5: Add db_stress Support
**File: `db_stress_tool/db_stress_common.h`**
```cpp
DECLARE_uint64(super_block_alignment_size);
DECLARE_uint64(super_block_alignment_space_overhead_ratio);
```
**File: `db_stress_tool/db_stress_gflags.cc`**
```cpp
DEFINE_uint64(
super_block_alignment_size,
ROCKSDB_NAMESPACE::BlockBasedTableOptions().super_block_alignment_size,
"BlockBasedTableOptions.super_block_alignment_size");
DEFINE_uint64(
super_block_alignment_space_overhead_ratio,
ROCKSDB_NAMESPACE::BlockBasedTableOptions()
.super_block_alignment_space_overhead_ratio,
"BlockBasedTableOptions.super_block_alignment_space_overhead_ratio");
```
**File: `db_stress_tool/db_stress_test_base.cc`**
```cpp
block_based_options.super_block_alignment_size =
fLU64::FLAGS_super_block_alignment_size;
block_based_options.super_block_alignment_space_overhead_ratio =
fLU64::FLAGS_super_block_alignment_space_overhead_ratio;
```
### Step 6: Add db_bench Support
**File: `tools/db_bench_tool.cc`**
```cpp
// Flag definitions:
DEFINE_uint64(
super_block_alignment_size,
ROCKSDB_NAMESPACE::BlockBasedTableOptions().super_block_alignment_size,
"Configure super block size");
DEFINE_uint64(super_block_alignment_space_overhead_ratio,
ROCKSDB_NAMESPACE::BlockBasedTableOptions()
.super_block_alignment_space_overhead_ratio,
"Configure space overhead for super block alignment");
// Apply to block_based_options (in the block where other options are set):
block_based_options.super_block_alignment_size = FLAGS_super_block_alignment_size;
block_based_options.super_block_alignment_space_overhead_ratio =
FLAGS_super_block_alignment_space_overhead_ratio;
```
### Step 7: Add Crash Test Support
**File: `tools/db_crashtest.py`**
```python
"super_block_alignment_size": lambda: random.choice(
[0, 128 * 1024, 512 * 1024, 2 * 1024 * 1024]
),
"super_block_alignment_space_overhead_ratio": lambda: random.choice([0, 32, 4096]),
```
### Step 8: Add Java API Support
**File: `java/src/main/java/org/rocksdb/BlockBasedTableConfig.java`**
Add getter and setter methods:
```java
/**
* Get the super block alignment size.
*
* @return the super block alignment size.
*/
public long superBlockAlignmentSize() {
return superBlockAlignmentSize;
}
/**
* Set the super block alignment size.
* When set to 0, super block alignment is disabled.
*
* @param superBlockAlignmentSize the super block alignment size.
*
* @return the reference to the current option.
*/
public BlockBasedTableConfig setSuperBlockAlignmentSize(final long superBlockAlignmentSize) {
this.superBlockAlignmentSize = superBlockAlignmentSize;
return this;
}
```
Add member variable:
```java
private long superBlockAlignmentSize;
```
Update constructor and native method signature.
**File: `java/rocksjni/portal.h`**
Update `GetMethodID` signature and add fields to Java object construction.
**File: `java/rocksjni/table.cc`**
Add parameters to JNI function and apply to options.
**File: `java/src/test/java/org/rocksdb/BlockBasedTableConfigTest.java`**
Add unit tests:
```java
@Test
public void superBlockAlignmentSize() {
final BlockBasedTableConfig blockBasedTableConfig = new BlockBasedTableConfig();
blockBasedTableConfig.setSuperBlockAlignmentSize(1024 * 1024);
assertThat(blockBasedTableConfig.superBlockAlignmentSize()).isEqualTo(1024 * 1024);
}
```
---
## Pattern 3: Adding C API for Existing Option
Example reference: commit `429b36c22d76403d275dd0e6877b08d4cea2bc90` (block_align C API)
If an option already exists but needs C API support:
**File: `db/c.cc`**
```cpp
void rocksdb_block_based_options_set_block_align(
rocksdb_block_based_table_options_t* options, unsigned char v) {
options->rep.block_align = v;
}
```
**File: `include/rocksdb/c.h`**
```cpp
extern ROCKSDB_LIBRARY_API void rocksdb_block_based_options_set_block_align(
rocksdb_block_based_table_options_t*, unsigned char);
```
---
## Unit Testing Guidelines
### For Standard Options
Add tests in appropriate test files (e.g., `db/db_memtable_test.cc`, `db/db_options_test.cc`):
```cpp
TEST_F(DBMemTableTest, YourOptionTest) {
Options options;
options.your_new_option = true;
Reopen(options);
// Test the behavior
}
```
### For BlockBasedTableOptions
Add tests in `db/db_flush_test.cc`, `table/block_based/block_based_table_reader_test.cc`, or `table/table_test.cc`:
```cpp
TEST_P(DBFlushYourFeatureTest, YourFeature) {
Options options;
BlockBasedTableOptions block_options;
block_options.your_new_option = some_value;
options.table_factory.reset(NewBlockBasedTableFactory(block_options));
ASSERT_OK(options.table_factory->ValidateOptions(
DBOptions(options), ColumnFamilyOptions(options)));
Reopen(options);
// Test the behavior
}
```
---
## Option Type Reference
Common option types used in serialization:
| OptionType | C++ Type | Example |
|------------|----------|---------|
| `kBoolean` | `bool` | `paranoid_memory_checks` |
| `kInt` | `int` | `max_write_buffer_number` |
| `kInt32T` | `int32_t` | `level0_file_num_compaction_trigger` |
| `kUInt32T` | `uint32_t` | `memtable_protection_bytes_per_key` |
| `kUInt64T` | `uint64_t` | `target_file_size_base` |
| `kSizeT` | `size_t` | `block_size` |
| `kDouble` | `double` | `compression_ratio` |
| `kString` | `std::string` | `db_log_dir` |
---
## Checklist Summary
- [ ] Public header file with option definition and documentation
- [ ] Internal options struct (MutableCFOptions or ImmutableCFOptions)
- [ ] Options serialization/deserialization registration
- [ ] Options logging in Dump() method
- [ ] UpdateColumnFamilyOptions() for mutable options
- [ ] options_settable_test.cc
- [ ] db_stress_common.h (DECLARE)
- [ ] db_stress_gflags.cc (DEFINE)
- [ ] db_stress_test_base.cc (apply flag)
- [ ] db_bench_tool.cc (DEFINE and apply)
- [ ] db_crashtest.py (randomized values)
- [ ] Unit tests
- [ ] unreleased_history markdown file
- [ ] Java API (for BlockBasedTableOptions)
- [ ] C API (if needed)
+504
View File
@@ -0,0 +1,504 @@
# RocksDB API Development Guide
This document provides guidance for adding new public APIs to RocksDB, following the established patterns used by existing APIs like `CompactRange`.
## API Layer Architecture
RocksDB exposes public APIs through multiple layers. Users can access RocksDB through any of the three public APIs: C++ headers, C headers, or Java bindings.
Here is an example for public header db.h:
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ Level 1: Public APIs (User Entry Points) │
├───────────────────────┬─────────────────────────┬───────────────────────────┤
│ C++ Public API │ C API Bindings │ Java/JNI API │
│ include/rocksdb/db.h │ include/rocksdb/c.h │ java/src/.../RocksDB.java │
│ include/rocksdb/*.h │ │ java/src/.../*.java │
└───────────────────────┴────────────┬────────────┴───────────────────────────┘
↓
┌─────────────────────────────────────────────────────────────────────────────┐
│ Level 2: C++ Implementation (Internal Core) │
│ db/db_impl/db_impl*.cc, db/c.cc, java/rocksjni/*.cc │
└─────────────────────────────────────────────────────────────────────────────┘
```
## Step-by-Step Guide: Adding a New Public API
### Step 1: Define the C++ Public Interface
**File:** `include/rocksdb/db.h`
Add the virtual method declaration in the `DB` class:
\`\`\`cpp
// Pure virtual - must be implemented by DBImpl
virtual Status YourNewAPI(const YourAPIOptions& options,
ColumnFamilyHandle* column_family,
/* other params */) = 0;
// Convenience overload for default column family
virtual Status YourNewAPI(const YourAPIOptions& options,
/* other params */) {
return YourNewAPI(options, DefaultColumnFamily(), /* other params */);
}
\`\`\`
**Key Patterns:**
- Use `Status` return type for error handling
- Use `OptSlice` to avoid unnecessary levels of indirection and use of raw pointers.
- Use `ColumnFamilyHandle*` for column family support
- Provide convenience overloads for the default column family
### Step 2: Define Options Struct (If Needed)
**File:** `include/rocksdb/options.h`
If your API has multiple configuration options, define an options struct:
\`\`\`cpp
struct YourAPIOptions {
// Document each option with clear comments
bool some_boolean_option = false;
// Default value explanation
int some_int_option = -1;
// Pointer options require careful lifetime management
std::atomic<bool>* canceled = nullptr;
// Enum options for multi-choice settings
YourEnumType some_enum = YourEnumType::kDefault;
};
\`\`\`
**Key Patterns:**
- Use sensible default values specified inline (e.g., `= false`, `= -1`)
- Do NOT redundantly document the default value in comments; instead, document the rationale (why this default), historical context, and how different values are interpreted
- Group related options logically
- Consider thread-safety for pointer options
### Step 3: Implement in DBImpl
**Header:** `db/db_impl/db_impl.h`
\`\`\`cpp
using DB::YourNewAPI;
Status YourNewAPI(const YourAPIOptions& options,
ColumnFamilyHandle* column_family,
/* other params */) override;
// Private internal implementation if needed
Status YourNewAPIInternal(const YourAPIOptions& options,
ColumnFamilyHandle* column_family,
/* other params */);
\`\`\`
**Implementation:** `db/db_impl/db_impl_<category>.cc`
Choose the appropriate implementation file based on functionality:
- `db_impl_compaction_flush.cc` - Compaction and flush operations
- `db_impl_write.cc` - Write operations
- `db_impl_open.cc` - DB opening/closing
- `db_impl_files.cc` - File operations
- `db_impl.cc` - General operations
\`\`\`cpp
Status DBImpl::YourNewAPI(const YourAPIOptions& options,
ColumnFamilyHandle* column_family,
/* other params */) {
// 1. Input validation
if (/* invalid input */) {
return Status::InvalidArgument("Error message");
}
// 2. Check for cancellation/abort conditions
if (options.canceled && options.canceled->load(std::memory_order_acquire)) {
return Status::Incomplete(Status::SubCode::kManualCompactionPaused);
}
// 3. Get column family data
auto cfh = static_cast<ColumnFamilyHandleImpl*>(column_family);
auto cfd = cfh->cfd();
// 4. Core implementation logic
// ...
return Status::OK();
}
\`\`\`
### Step 4: Handle Special DB Types
**StackableDB (Wrapper DBs):**
**File:** `include/rocksdb/utilities/stackable_db.h`
\`\`\`cpp
using DB::YourNewAPI;
Status YourNewAPI(const YourAPIOptions& options,
ColumnFamilyHandle* column_family,
/* other params */) override {
return db_->YourNewAPI(options, column_family, /* other params */);
}
\`\`\`
**Secondary DB (Read-Only):**
**File:** `db/db_impl/db_impl_secondary.h`
\`\`\`cpp
using DBImpl::YourNewAPI;
Status YourNewAPI(const YourAPIOptions& /*options*/,
ColumnFamilyHandle* /*column_family*/,
/* other params */) override {
return Status::NotSupported("Not supported in secondary DB");
}
\`\`\`
**CompactedDB (Read-Only):**
**File:** `db/db_impl/compacted_db_impl.h`
\`\`\`cpp
using DBImpl::YourNewAPI;
Status YourNewAPI(const YourAPIOptions& /*options*/,
ColumnFamilyHandle* /*column_family*/,
/* other params */) override {
return Status::NotSupported("Not supported for read-only DB");
}
\`\`\`
### Step 5: Add C API Bindings
**Header:** `include/rocksdb/c.h`
\`\`\`c
// Basic version
extern ROCKSDB_LIBRARY_API void rocksdb_your_new_api(
rocksdb_t* db,
const char* start_key, size_t start_key_len,
const char* limit_key, size_t limit_key_len);
// Column family version
extern ROCKSDB_LIBRARY_API void rocksdb_your_new_api_cf(
rocksdb_t* db, rocksdb_column_family_handle_t* column_family,
const char* start_key, size_t start_key_len,
const char* limit_key, size_t limit_key_len);
// With options and error handling
extern ROCKSDB_LIBRARY_API void rocksdb_your_new_api_opt(
rocksdb_t* db, rocksdb_your_api_options_t* opt,
const char* start_key, size_t start_key_len,
const char* limit_key, size_t limit_key_len,
char** errptr);
\`\`\`
**Implementation:** `db/c.cc`
\`\`\`cpp
void rocksdb_your_new_api(rocksdb_t* db, const char* start_key,
size_t start_key_len, const char* limit_key,
size_t limit_key_len) {
Slice a, b;
db->rep->YourNewAPI(
YourAPIOptions(), // Default options
(start_key ? (a = Slice(start_key, start_key_len), &a) : nullptr),
(limit_key ? (b = Slice(limit_key, limit_key_len), &b) : nullptr));
}
void rocksdb_your_new_api_cf(rocksdb_t* db,
rocksdb_column_family_handle_t* column_family,
const char* start_key, size_t start_key_len,
const char* limit_key, size_t limit_key_len) {
Slice a, b;
db->rep->YourNewAPI(
YourAPIOptions(),
column_family->rep,
(start_key ? (a = Slice(start_key, start_key_len), &a) : nullptr),
(limit_key ? (b = Slice(limit_key, limit_key_len), &b) : nullptr));
}
\`\`\`
**If you have options, also add:**
\`\`\`cpp
// Options struct wrapper
struct rocksdb_your_api_options_t {
YourAPIOptions rep;
};
rocksdb_your_api_options_t* rocksdb_your_api_options_create() {
return new rocksdb_your_api_options_t;
}
void rocksdb_your_api_options_destroy(rocksdb_your_api_options_t* opt) {
delete opt;
}
void rocksdb_your_api_options_set_some_option(
rocksdb_your_api_options_t* opt, unsigned char value) {
opt->rep.some_boolean_option = value;
}
\`\`\`
### Step 6: Add Java Bindings
**Java API:** `java/src/main/java/org/rocksdb/RocksDB.java`
\`\`\`java
// Basic version
public void yourNewAPI() throws RocksDBException {
yourNewAPI(null);
}
// Column family version
public void yourNewAPI(ColumnFamilyHandle columnFamilyHandle)
throws RocksDBException {
yourNewAPI(nativeHandle_, null, -1, null, -1, 0,
columnFamilyHandle == null ? 0 : columnFamilyHandle.nativeHandle_);
}
// Range version
public void yourNewAPI(final byte[] begin, final byte[] end)
throws RocksDBException {
yourNewAPI(null, begin, end);
}
// Full-featured version with options
public void yourNewAPI(ColumnFamilyHandle columnFamilyHandle,
final byte[] begin, final byte[] end,
final YourAPIOptions options)
throws RocksDBException {
yourNewAPI(nativeHandle_,
begin, begin == null ? -1 : begin.length,
end, end == null ? -1 : end.length,
options.nativeHandle_,
columnFamilyHandle == null ? 0 : columnFamilyHandle.nativeHandle_);
}
// Native method declaration
private static native void yourNewAPI(final long handle,
/* @Nullable */ final byte[] begin, final int beginLen,
/* @Nullable */ final byte[] end, final int endLen,
final long optionsHandle,
final long cfHandle);
\`\`\`
**Options Class:** `java/src/main/java/org/rocksdb/YourAPIOptions.java`
\`\`\`java
public class YourAPIOptions extends RocksObject {
public YourAPIOptions() {
super(newYourAPIOptions());
}
// Builder pattern setters
public YourAPIOptions setSomeBooleanOption(boolean value) {
setSomeBooleanOption(nativeHandle_, value);
return this;
}
// Getters
public boolean someBooleanOption() {
return someBooleanOption(nativeHandle_);
}
// Native method declarations
private static native long newYourAPIOptions();
private static native void disposeInternalJni(long handle);
private static native void setSomeBooleanOption(long handle, boolean value);
private static native boolean someBooleanOption(long handle);
@Override
protected final void disposeInternal(final long handle) {
disposeInternalJni(handle);
}
}
\`\`\`
**JNI Implementation:** `java/rocksjni/rocksjni.cc`
\`\`\`cpp
void Java_org_rocksdb_RocksDB_yourNewAPI(
JNIEnv* env, jclass,
jlong jdb_handle, jbyteArray jbegin, jint jbegin_len,
jbyteArray jend, jint jend_len,
jlong joptions_handle, jlong jcf_handle) {
// 1. Convert Java byte arrays to C++ strings
jboolean has_exception = JNI_FALSE;
std::string str_begin;
if (jbegin_len > 0) {
str_begin = ROCKSDB_NAMESPACE::JniUtil::byteString<std::string>(
env, jbegin, jbegin_len,
[](const char* str, const size_t len) { return std::string(str, len); },
&has_exception);
if (has_exception == JNI_TRUE) return;
}
std::string str_end;
if (jend_len > 0) {
str_end = ROCKSDB_NAMESPACE::JniUtil::byteString<std::string>(
env, jend, jend_len,
[](const char* str, const size_t len) { return std::string(str, len); },
&has_exception);
if (has_exception == JNI_TRUE) return;
}
// 2. Get or create options
ROCKSDB_NAMESPACE::YourAPIOptions* options = nullptr;
if (joptions_handle == 0) {
options = new ROCKSDB_NAMESPACE::YourAPIOptions();
} else {
options = reinterpret_cast<ROCKSDB_NAMESPACE::YourAPIOptions*>(joptions_handle);
}
// 3. Unwrap handles
auto* db = reinterpret_cast<ROCKSDB_NAMESPACE::DB*>(jdb_handle);
ROCKSDB_NAMESPACE::ColumnFamilyHandle* cf_handle =
jcf_handle == 0 ? db->DefaultColumnFamily()
: reinterpret_cast<ROCKSDB_NAMESPACE::ColumnFamilyHandle*>(jcf_handle);
// 4. Create Slices
std::unique_ptr<ROCKSDB_NAMESPACE::Slice> begin;
std::unique_ptr<ROCKSDB_NAMESPACE::Slice> end;
if (jbegin_len > 0) begin.reset(new ROCKSDB_NAMESPACE::Slice(str_begin));
if (jend_len > 0) end.reset(new ROCKSDB_NAMESPACE::Slice(str_end));
// 5. Call C++ API
ROCKSDB_NAMESPACE::Status s = db->YourNewAPI(*options, cf_handle, begin.get(), end.get());
// 6. Cleanup if we created options
if (joptions_handle == 0) delete options;
// 7. Throw Java exception on error
ROCKSDB_NAMESPACE::RocksDBExceptionJni::ThrowNew(env, s);
}
\`\`\`
**Options JNI:** `java/rocksjni/your_api_options.cc`
\`\`\`cpp
jlong Java_org_rocksdb_YourAPIOptions_newYourAPIOptions(JNIEnv*, jclass) {
auto* options = new ROCKSDB_NAMESPACE::YourAPIOptions();
return GET_CPLUSPLUS_POINTER(options);
}
void Java_org_rocksdb_YourAPIOptions_disposeInternalJni(JNIEnv*, jclass, jlong jhandle) {
auto* options = reinterpret_cast<ROCKSDB_NAMESPACE::YourAPIOptions*>(jhandle);
delete options;
}
void Java_org_rocksdb_YourAPIOptions_setSomeBooleanOption(
JNIEnv*, jclass, jlong jhandle, jboolean value) {
auto* options = reinterpret_cast<ROCKSDB_NAMESPACE::YourAPIOptions*>(jhandle);
options->some_boolean_option = static_cast<bool>(value);
}
jboolean Java_org_rocksdb_YourAPIOptions_someBooleanOption(JNIEnv*, jclass, jlong jhandle) {
auto* options = reinterpret_cast<ROCKSDB_NAMESPACE::YourAPIOptions*>(jhandle);
return static_cast<jboolean>(options->some_boolean_option);
}
\`\`\`
### Step 7: Update Build Files
**Java CMakeLists.txt:** `java/CMakeLists.txt`
Add your new Java source files:
\`\`\`cmake
src/main/java/org/rocksdb/YourAPIOptions.java
src/test/java/org/rocksdb/YourAPIOptionsTest.java
\`\`\`
### Step 8: Add Release Notes
**Directory:** `unreleased_history/`
RocksDB uses individual files in the `unreleased_history/` directory rather than directly editing `HISTORY.md`. This avoids merge conflicts and ensures changes are attributed to the correct release version.
Add a file to the appropriate subdirectory:
- `unreleased_history/new_features/` - For new functionality
- `unreleased_history/public_api_changes/` - For API changes
- `unreleased_history/behavior_changes/` - For behavior modifications
- `unreleased_history/bug_fixes/` - For bug fixes
**Example:** `unreleased_history/new_features/your_new_api.md`
\`\`\`markdown
Added `YourNewAPI()` to support [describe functionality]. See `YourAPIOptions` for configuration.
\`\`\`
**Example:** `unreleased_history/public_api_changes/your_api_options.md`
**Note:** Files should contain one line of markdown. The "* " prefix is automatically added if not included. These files are compiled into `HISTORY.md` during the release process.
### Step 9: Add Tests
**C++ Unit Tests:** `db/db_your_api_test.cc` or add to existing test file
\`\`\`cpp
TEST_F(DBTest, YourNewAPIBasic) {
Options options = CurrentOptions();
CreateAndReopenWithCF({"pikachu"}, options);
// Setup test data
ASSERT_OK(Put(1, "key1", "value1"));
ASSERT_OK(Put(1, "key2", "value2"));
// Test your API
YourAPIOptions api_options;
api_options.some_boolean_option = true;
ASSERT_OK(db_->YourNewAPI(api_options, handles_[1], nullptr, nullptr));
// Verify results
// ...
}
\`\`\`
**Java Tests:** `java/src/test/java/org/rocksdb/YourAPIOptionsTest.java`
\`\`\`java
public class YourAPIOptionsTest {
@Test
public void yourAPIOptions() {
try (final YourAPIOptions options = new YourAPIOptions()) {
assertFalse(options.someBooleanOption());
options.setSomeBooleanOption(true);
assertTrue(options.someBooleanOption());
}
}
}
\`\`\`
## File Summary Checklist
| Component | File(s) | Required |
|-----------|---------|----------|
| C++ Public Interface | `include/rocksdb/db.h` | ✓ |
| Options Struct | `include/rocksdb/options.h` | If needed |
| DBImpl Declaration | `db/db_impl/db_impl.h` | ✓ |
| DBImpl Implementation | `db/db_impl/db_impl_*.cc` | ✓ |
| StackableDB | `include/rocksdb/utilities/stackable_db.h` | ✓ |
| Secondary DB | `db/db_impl/db_impl_secondary.h` | If not supported |
| Compacted DB | `db/db_impl/compacted_db_impl.h` | If not supported |
| C API Header | `include/rocksdb/c.h` | ✓ |
| C API Implementation | `db/c.cc` | ✓ |
| Java API | `java/src/main/java/org/rocksdb/RocksDB.java` | ✓ |
| Java Options | `java/src/main/java/org/rocksdb/YourAPIOptions.java` | If needed |
| JNI Implementation | `java/rocksjni/rocksjni.cc` | ✓ |
| JNI Options | `java/rocksjni/your_api_options.cc` | If needed |
| Java CMake | `java/CMakeLists.txt` | If new files |
| Changelog | `unreleased_history/*.md` | ✓ |
| C++ Tests | `db/db_*_test.cc` | ✓ |
| Java Tests | `java/src/test/java/org/rocksdb/*Test.java` | ✓ |
## Best Practices
1. **Error Handling**: Always return `Status` objects in C++, throw exceptions in Java
2. **Default Values**: Provide sensible defaults for all options
3. **Documentation**: Add clear comments for all public methods and options
4. **Column Family Support**: Always support column family operations
5. **Thread Safety**: Document thread-safety guarantees
6. **Backward Compatibility**: Avoid breaking existing API contracts
7. **Testing**: Add comprehensive unit tests for all code paths
+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/`
+5 -1
View File
@@ -8,7 +8,7 @@ DB_STRESS_CMD?=./db_stress
include common.mk
CRASHTEST_MAKE=$(MAKE) -f crash_test.mk
CRASHTEST_PY=$(PYTHON) -u tools/db_crashtest.py --stress_cmd=$(DB_STRESS_CMD) --cleanup_cmd='$(DB_CLEANUP_CMD)'
CRASHTEST_PY=$(PYTHON) -u tools/db_crashtest.py --stress_cmd=$(DB_STRESS_CMD) --cleanup_cmd='$(DB_CLEANUP_CMD)' --destroy_db_initially=1
.PHONY: crash_test crash_test_with_atomic_flush crash_test_with_txn \
crash_test_with_wc_txn crash_test_with_wp_txn crash_test_with_wup_txn \
@@ -34,6 +34,7 @@ CRASHTEST_PY=$(PYTHON) -u tools/db_crashtest.py --stress_cmd=$(DB_STRESS_CMD) --
whitebox_crash_test_with_txn whitebox_crash_test_with_ts \
whitebox_crash_test_with_optimistic_txn \
whitebox_crash_test_with_tiered_storage \
crash_test_db_cleanup \
crash_test: $(DB_STRESS_CMD)
# Do not parallelize
@@ -161,6 +162,9 @@ whitebox_crash_test_with_optimistic_txn: $(DB_STRESS_CMD)
$(CRASHTEST_PY) --optimistic_txn whitebox --random_kill_odd \
$(CRASH_TEST_KILL_ODD) $(CRASH_TEST_EXT_ARGS)
crash_test_db_cleanup: $(DB_STRESS_CMD)
$(DB_STRESS_CMD) --delete_dir_and_exit=$(TEST_TMPDIR)
# Old names DEPRECATED
crash_test_with_txn: crash_test_with_wc_txn
whitebox_crash_test_with_txn: whitebox_crash_test_with_wc_txn
+23 -22
View File
@@ -67,6 +67,16 @@ BlobFileBuilder::BlobFileBuilder(
min_blob_size_(mutable_cf_options->min_blob_size),
blob_file_size_(mutable_cf_options->blob_file_size),
blob_compression_type_(mutable_cf_options->blob_compression_type),
// TODO: support most CompressionOptions with a new CF option
// blob_compression_opts
// TODO with schema change: support custom compression manager and options
// such as max_compressed_bytes_per_kb
// NOTE: returns nullptr for kNoCompression
blob_compressor_(GetBuiltinV2CompressionManager()->GetCompressor(
CompressionOptions{}, blob_compression_type_)),
blob_compressor_wa_(blob_compressor_
? blob_compressor_->ObtainWorkingArea()
: Compressor::ManagedWorkingArea{}),
prepopulate_blob_cache_(mutable_cf_options->prepopulate_blob_cache),
file_options_(file_options),
write_options_(write_options),
@@ -113,7 +123,7 @@ Status BlobFileBuilder::Add(const Slice& key, const Slice& value,
}
Slice blob = value;
std::string compressed_blob;
GrowableBuffer compressed_blob;
{
const Status s = CompressBlobIfNeeded(&blob, &compressed_blob);
@@ -254,36 +264,27 @@ Status BlobFileBuilder::OpenBlobFileIfNeeded() {
}
Status BlobFileBuilder::CompressBlobIfNeeded(
Slice* blob, std::string* compressed_blob) const {
Slice* blob, GrowableBuffer* compressed_blob) const {
assert(blob);
assert(compressed_blob);
assert(compressed_blob->empty());
assert(immutable_options_);
if (blob_compression_type_ == kNoCompression) {
if (!blob_compressor_) {
assert(blob_compression_type_ == kNoCompression);
return Status::OK();
}
assert(blob_compression_type_ != kNoCompression);
// TODO: allow user CompressionOptions, including max_compressed_bytes_per_kb
CompressionOptions opts;
CompressionContext context(blob_compression_type_, opts);
// WART: always stored as compressed even when that increases the size.
CompressionInfo info(opts, context, CompressionDict::GetEmptyDict(),
blob_compression_type_);
constexpr uint32_t compression_format_version = 2;
bool success = false;
{
StopWatch stop_watch(immutable_options_->clock, immutable_options_->stats,
BLOB_DB_COMPRESSION_MICROS);
success = OLD_CompressData(*blob, info, compression_format_version,
compressed_blob);
}
if (!success) {
return Status::Corruption("Error compressing blob");
Status s;
StopWatch stop_watch(immutable_options_->clock, immutable_options_->stats,
BLOB_DB_COMPRESSION_MICROS);
s = LegacyForceBuiltinCompression(*blob_compressor_, &blob_compressor_wa_,
*blob, compressed_blob);
if (!s.ok()) {
return s;
}
*blob = Slice(*compressed_blob);
+6 -1
View File
@@ -10,12 +10,14 @@
#include <string>
#include <vector>
#include "rocksdb/advanced_compression.h"
#include "rocksdb/advanced_options.h"
#include "rocksdb/compression_type.h"
#include "rocksdb/env.h"
#include "rocksdb/options.h"
#include "rocksdb/rocksdb_namespace.h"
#include "rocksdb/types.h"
#include "util/aligned_buffer.h"
namespace ROCKSDB_NAMESPACE {
@@ -76,7 +78,8 @@ class BlobFileBuilder {
private:
bool IsBlobFileOpen() const;
Status OpenBlobFileIfNeeded();
Status CompressBlobIfNeeded(Slice* blob, std::string* compressed_blob) const;
Status CompressBlobIfNeeded(Slice* blob,
GrowableBuffer* compressed_blob) const;
Status WriteBlobToFile(const Slice& key, const Slice& blob,
uint64_t* blob_file_number, uint64_t* blob_offset);
Status CloseBlobFile();
@@ -91,6 +94,8 @@ class BlobFileBuilder {
uint64_t min_blob_size_;
uint64_t blob_file_size_;
CompressionType blob_compression_type_;
std::unique_ptr<Compressor> blob_compressor_;
mutable Compressor::ManagedWorkingArea blob_compressor_wa_;
PrepopulateBlobCache prepopulate_blob_cache_;
const FileOptions* file_options_;
const WriteOptions* write_options_;
+14 -16
View File
@@ -403,22 +403,19 @@ TEST_F(BlobFileBuilderTest, Compression) {
ASSERT_EQ(blob_file_addition.GetBlobFileNumber(), blob_file_number);
ASSERT_EQ(blob_file_addition.GetTotalBlobCount(), 1);
CompressionOptions opts;
CompressionContext context(kSnappyCompression, opts);
CompressionInfo info(opts, context, CompressionDict::GetEmptyDict(),
kSnappyCompression);
std::string compressed_value;
ASSERT_TRUE(Snappy_Compress(info, uncompressed_value.data(),
uncompressed_value.size(), &compressed_value));
auto compressor =
GetBuiltinV2CompressionManager()->GetCompressor({}, kSnappyCompression);
GrowableBuffer compressed_value;
ASSERT_OK(LegacyForceBuiltinCompression(*compressor, /*working_area=*/nullptr,
uncompressed_value,
&compressed_value));
ASSERT_EQ(blob_file_addition.GetTotalBlobBytes(),
BlobLogRecord::kHeaderSize + key_size + compressed_value.size());
// Verify the contents of the new blob file as well as the blob reference
std::vector<std::pair<std::string, std::string>> expected_key_value_pairs{
{key, compressed_value}};
{key, compressed_value.AsSlice().ToString()}};
std::vector<std::string> blob_indexes{blob_index};
VerifyBlobFile(blob_file_number, blob_file_path, column_family_id,
@@ -457,11 +454,12 @@ TEST_F(BlobFileBuilderTest, CompressionError) {
nullptr /*IOTracer*/, nullptr /*BlobFileCompletionCallback*/,
BlobFileCreationReason::kFlush, &blob_file_paths, &blob_file_additions);
SyncPoint::GetInstance()->SetCallBack("CompressData:TamperWithReturnValue",
[](void* arg) {
bool* ret = static_cast<bool*>(arg);
*ret = false;
});
SyncPoint::GetInstance()->SetCallBack(
"LegacyForceBuiltinCompression:TamperWithStatus", [](void* arg) {
Status* ret = static_cast<Status*>(arg);
ASSERT_OK(*ret);
*ret = Status::Corruption("Tampered result");
});
SyncPoint::GetInstance()->EnableProcessing();
constexpr char key[] = "1";
@@ -469,7 +467,7 @@ TEST_F(BlobFileBuilderTest, CompressionError) {
std::string blob_index;
ASSERT_TRUE(builder.Add(key, value, &blob_index).IsCorruption());
ASSERT_EQ(builder.Add(key, value, &blob_index).code(), Status::kCorruption);
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
+38 -26
View File
@@ -17,10 +17,10 @@
#include "rocksdb/file_system.h"
#include "rocksdb/slice.h"
#include "rocksdb/status.h"
#include "table/format.h"
#include "table/multiget_context.h"
#include "test_util/sync_point.h"
#include "util/compression.h"
#include "util/crc32c.h"
#include "util/stop_watch.h"
namespace ROCKSDB_NAMESPACE {
@@ -69,9 +69,16 @@ Status BlobFileReader::Create(
}
}
blob_file_reader->reset(
new BlobFileReader(std::move(file_reader), file_size, compression_type,
immutable_options.clock, statistics));
std::shared_ptr<Decompressor> decompressor;
if (compression_type != kNoCompression) {
// The blob format has always used compression format 2
decompressor = GetBuiltinV2CompressionManager()->GetDecompressorOptimizeFor(
compression_type);
}
blob_file_reader->reset(new BlobFileReader(
std::move(file_reader), file_size, compression_type,
std::move(decompressor), immutable_options.clock, statistics));
return Status::OK();
}
@@ -282,11 +289,13 @@ Status BlobFileReader::ReadFromFile(const RandomAccessFileReader* file_reader,
BlobFileReader::BlobFileReader(
std::unique_ptr<RandomAccessFileReader>&& file_reader, uint64_t file_size,
CompressionType compression_type, SystemClock* clock,
CompressionType compression_type,
std::shared_ptr<Decompressor> decompressor, SystemClock* clock,
Statistics* statistics)
: file_reader_(std::move(file_reader)),
file_size_(file_size),
compression_type_(compression_type),
decompressor_(std::move(decompressor)),
clock_(clock),
statistics_(statistics) {
assert(file_reader_);
@@ -375,8 +384,9 @@ Status BlobFileReader::GetBlob(
const Slice value_slice(record_slice.data() + adjustment, value_size);
{
const Status s = UncompressBlobIfNeeded(
value_slice, compression_type, allocator, clock_, statistics_, result);
const Status s = UncompressBlobIfNeeded(value_slice, compression_type,
decompressor_.get(), allocator,
clock_, statistics_, result);
if (!s.ok()) {
return s;
}
@@ -523,10 +533,10 @@ void BlobFileReader::MultiGetBlob(
}
// Uncompress blob if needed
Slice value_slice(record_slice.data() + adjustments[i], req->len);
*req->status =
UncompressBlobIfNeeded(value_slice, compression_type_, allocator,
clock_, statistics_, &blob_reqs[i].second);
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);
if (req->status->ok()) {
total_bytes += record_slice.size();
}
@@ -583,8 +593,8 @@ Status BlobFileReader::VerifyBlob(const Slice& record_slice,
Status BlobFileReader::UncompressBlobIfNeeded(
const Slice& value_slice, CompressionType compression_type,
MemoryAllocator* allocator, SystemClock* clock, Statistics* statistics,
std::unique_ptr<BlobContents>* result) {
Decompressor* decompressor, MemoryAllocator* allocator, SystemClock* clock,
Statistics* statistics, std::unique_ptr<BlobContents>* result) {
assert(result);
if (compression_type == kNoCompression) {
@@ -593,31 +603,33 @@ Status BlobFileReader::UncompressBlobIfNeeded(
return Status::OK();
}
UncompressionContext context(compression_type);
UncompressionInfo info(context, UncompressionDict::GetEmptyDict(),
compression_type);
assert(decompressor);
size_t uncompressed_size = 0;
constexpr uint32_t compression_format_version = 2;
Decompressor::Args args;
args.compression_type = compression_type;
args.compressed_data = value_slice;
CacheAllocationPtr output;
Status s = decompressor->ExtractUncompressedSize(args);
if (!s.ok()) {
return Status::Corruption(s.ToString());
}
CacheAllocationPtr output = AllocateBlock(args.uncompressed_size, allocator);
{
PERF_TIMER_GUARD(blob_decompress_time);
StopWatch stop_watch(clock, statistics, BLOB_DB_DECOMPRESSION_MICROS);
output = OLD_UncompressData(info, value_slice.data(), value_slice.size(),
&uncompressed_size, compression_format_version,
allocator);
s = decompressor->DecompressBlock(args, output.get());
}
TEST_SYNC_POINT_CALLBACK(
"BlobFileReader::UncompressBlobIfNeeded:TamperWithResult", &output);
"BlobFileReader::UncompressBlobIfNeeded:TamperWithResult", &s);
if (!output) {
return Status::Corruption("Unable to uncompress blob");
if (!s.ok()) {
return Status::Corruption(s.ToString());
}
result->reset(new BlobContents(std::move(output), uncompressed_size));
result->reset(new BlobContents(std::move(output), args.uncompressed_size));
return Status::OK();
}
+5 -1
View File
@@ -10,6 +10,7 @@
#include "db/blob/blob_read_request.h"
#include "file/random_access_file_reader.h"
#include "rocksdb/advanced_compression.h"
#include "rocksdb/compression_type.h"
#include "rocksdb/rocksdb_namespace.h"
#include "util/autovector.h"
@@ -64,7 +65,8 @@ class BlobFileReader {
private:
BlobFileReader(std::unique_ptr<RandomAccessFileReader>&& file_reader,
uint64_t file_size, CompressionType compression_type,
SystemClock* clock, Statistics* statistics);
std::shared_ptr<Decompressor> decompressor, SystemClock* clock,
Statistics* statistics);
static Status OpenFile(const ImmutableOptions& immutable_options,
const FileOptions& file_opts,
@@ -96,6 +98,7 @@ class BlobFileReader {
static Status UncompressBlobIfNeeded(const Slice& value_slice,
CompressionType compression_type,
Decompressor* decompressor,
MemoryAllocator* allocator,
SystemClock* clock,
Statistics* statistics,
@@ -104,6 +107,7 @@ class BlobFileReader {
std::unique_ptr<RandomAccessFileReader> file_reader_;
uint64_t file_size_;
CompressionType compression_type_;
std::shared_ptr<Decompressor> decompressor_;
SystemClock* clock_;
Statistics* statistics_;
};
+101 -18
View File
@@ -65,7 +65,7 @@ void WriteBlobFile(const ImmutableOptions& immutable_options,
ASSERT_OK(blob_log_writer.WriteHeader(WriteOptions(), header));
std::vector<std::string> compressed_blobs(num);
std::vector<GrowableBuffer> compressed_blobs(num);
std::vector<Slice> blobs_to_write(num);
if (kNoCompression == compression) {
for (size_t i = 0; i < num; ++i) {
@@ -73,16 +73,13 @@ void WriteBlobFile(const ImmutableOptions& immutable_options,
blob_sizes[i] = blobs[i].size();
}
} else {
CompressionOptions opts;
CompressionContext context(compression, opts);
CompressionInfo info(opts, context, CompressionDict::GetEmptyDict(),
compression);
constexpr uint32_t compression_format_version = 2;
auto compressor =
GetBuiltinV2CompressionManager()->GetCompressor({}, compression);
for (size_t i = 0; i < num; ++i) {
ASSERT_TRUE(OLD_CompressData(blobs[i], info, compression_format_version,
&compressed_blobs[i]));
ASSERT_OK(LegacyForceBuiltinCompression(*compressor,
/*working_area=*/nullptr,
blobs[i], &compressed_blobs[i]));
blobs_to_write[i] = compressed_blobs[i];
blob_sizes[i] = compressed_blobs[i].size();
}
@@ -809,11 +806,10 @@ TEST_F(BlobFileReaderTest, UncompressionError) {
SyncPoint::GetInstance()->SetCallBack(
"BlobFileReader::UncompressBlobIfNeeded:TamperWithResult", [](void* arg) {
CacheAllocationPtr* const output =
static_cast<CacheAllocationPtr*>(arg);
assert(output);
auto* result = static_cast<Status*>(arg);
assert(result);
output->reset();
*result = Status::Corruption("Injected result");
});
SyncPoint::GetInstance()->EnableProcessing();
@@ -824,11 +820,12 @@ TEST_F(BlobFileReaderTest, UncompressionError) {
std::unique_ptr<BlobContents> value;
uint64_t bytes_read = 0;
ASSERT_TRUE(reader
->GetBlob(ReadOptions(), key, blob_offset, blob_size,
kSnappyCompression, prefetch_buffer, allocator,
&value, &bytes_read)
.IsCorruption());
ASSERT_EQ(reader
->GetBlob(ReadOptions(), key, blob_offset, blob_size,
kSnappyCompression, prefetch_buffer, allocator,
&value, &bytes_read)
.code(),
Status::Code::kCorruption);
ASSERT_EQ(value, nullptr);
ASSERT_EQ(bytes_read, 0);
@@ -1014,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) {
+12
View File
@@ -137,6 +137,18 @@ class BlobIndex {
return oss.str();
}
// Encode this blob index into dst based on its type.
void EncodeTo(std::string* dst) const {
if (IsInlined()) {
EncodeInlinedTTL(dst, expiration_, value_);
} else if (HasTTL()) {
EncodeBlobTTL(dst, expiration_, file_number_, offset_, size_,
compression_);
} else {
EncodeBlob(dst, file_number_, offset_, size_, compression_);
}
}
static void EncodeInlinedTTL(std::string* dst, uint64_t expiration,
const Slice& value) {
assert(dst != nullptr);
+6 -9
View File
@@ -67,7 +67,7 @@ void WriteBlobFile(const ImmutableOptions& immutable_options,
ASSERT_OK(blob_log_writer.WriteHeader(WriteOptions(), header));
std::vector<std::string> compressed_blobs(num);
std::vector<GrowableBuffer> compressed_blobs(num);
std::vector<Slice> blobs_to_write(num);
if (kNoCompression == compression) {
for (size_t i = 0; i < num; ++i) {
@@ -75,16 +75,13 @@ void WriteBlobFile(const ImmutableOptions& immutable_options,
blob_sizes[i] = blobs[i].size();
}
} else {
CompressionOptions opts;
CompressionContext context(compression, opts);
CompressionInfo info(opts, context, CompressionDict::GetEmptyDict(),
compression);
constexpr uint32_t compression_format_version = 2;
auto compressor =
GetBuiltinV2CompressionManager()->GetCompressor({}, compression);
for (size_t i = 0; i < num; ++i) {
ASSERT_TRUE(OLD_CompressData(blobs[i], info, compression_format_version,
&compressed_blobs[i]));
ASSERT_OK(LegacyForceBuiltinCompression(*compressor,
/*working_area=*/nullptr,
blobs[i], &compressed_blobs[i]));
blobs_to_write[i] = compressed_blobs[i];
blob_sizes[i] = compressed_blobs[i].size();
}
+23
View File
@@ -56,6 +56,18 @@ TableBuilder* NewTableBuilder(const TableBuilderOptions& tboptions,
return tboptions.moptions.table_factory->NewTableBuilder(tboptions, file);
}
void ExtractTimestampFromTableProperties(const TableProperties& tp,
FileMetaData* meta) {
auto min_ts_iter = tp.user_collected_properties.find("rocksdb.timestamp_min");
if (min_ts_iter != tp.user_collected_properties.end()) {
meta->min_timestamp = min_ts_iter->second;
}
auto max_ts_iter = tp.user_collected_properties.find("rocksdb.timestamp_max");
if (max_ts_iter != tp.user_collected_properties.end()) {
meta->max_timestamp = max_ts_iter->second;
}
}
Status BuildTable(
const std::string& dbname, VersionSet* versions,
const ImmutableDBOptions& db_options, const TableBuilderOptions& tboptions,
@@ -310,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());
@@ -318,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) {
@@ -355,6 +377,7 @@ Status BuildTable(
assert(meta->fd.GetFileSize() > 0);
tp = builder
->GetTableProperties(); // refresh now that builder is finished
ExtractTimestampFromTableProperties(tp, meta);
if (memtable_payload_bytes != nullptr &&
memtable_garbage_bytes != nullptr) {
const CompactionIterationStats& ci_stats = c_iter.iter_stats();
+7
View File
@@ -41,6 +41,13 @@ class BlobFileCompletionCallback;
TableBuilder* NewTableBuilder(const TableBuilderOptions& tboptions,
WritableFileWriter* file);
// Extract min/max timestamps from table properties and populate FileMetaData.
// This is used by both flush (BuildTable) and compaction (CompactionOutputs)
// to populate timestamp range in FileMetaData from the TimestampTableProperties
// collector output.
void ExtractTimestampFromTableProperties(const TableProperties& tp,
FileMetaData* meta);
// Build a Table file from the contents of *iter. The generated file
// will be named according to number specified in meta. On success, the rest of
// *meta will be filled with metadata about the generated table.
+373 -57
View File
@@ -11,6 +11,7 @@
#include <cstdlib>
#include <map>
#include <memory>
#include <unordered_set>
#include <vector>
@@ -89,10 +90,12 @@ using ROCKSDB_NAMESPACE::EnvOptions;
using ROCKSDB_NAMESPACE::EventListener;
using ROCKSDB_NAMESPACE::ExportImportFilesMetaData;
using ROCKSDB_NAMESPACE::ExternalFileIngestionInfo;
using ROCKSDB_NAMESPACE::FileChecksumGenFactory;
using ROCKSDB_NAMESPACE::FileLock;
using ROCKSDB_NAMESPACE::FilterPolicy;
using ROCKSDB_NAMESPACE::FlushJobInfo;
using ROCKSDB_NAMESPACE::FlushOptions;
using ROCKSDB_NAMESPACE::GetFileChecksumGenCrc32cFactory;
using ROCKSDB_NAMESPACE::HistogramData;
using ROCKSDB_NAMESPACE::HyperClockCacheOptions;
using ROCKSDB_NAMESPACE::ImportColumnFamilyOptions;
@@ -112,6 +115,7 @@ using ROCKSDB_NAMESPACE::NewCompactOnDeletionCollectorFactory;
using ROCKSDB_NAMESPACE::NewGenericRateLimiter;
using ROCKSDB_NAMESPACE::NewLRUCache;
using ROCKSDB_NAMESPACE::NewRibbonFilterPolicy;
using ROCKSDB_NAMESPACE::NewSstPartitionerFixedPrefixFactory;
using ROCKSDB_NAMESPACE::OpenAndCompactOptions;
using ROCKSDB_NAMESPACE::OptimisticTransactionDB;
using ROCKSDB_NAMESPACE::OptimisticTransactionOptions;
@@ -133,9 +137,11 @@ using ROCKSDB_NAMESPACE::Snapshot;
using ROCKSDB_NAMESPACE::SstFileManager;
using ROCKSDB_NAMESPACE::SstFileMetaData;
using ROCKSDB_NAMESPACE::SstFileWriter;
using ROCKSDB_NAMESPACE::SstPartitionerFactory;
using ROCKSDB_NAMESPACE::Status;
using ROCKSDB_NAMESPACE::StderrLogger;
using ROCKSDB_NAMESPACE::SubcompactionJobInfo;
using ROCKSDB_NAMESPACE::TableFactory;
using ROCKSDB_NAMESPACE::TablePropertiesCollectorFactory;
using ROCKSDB_NAMESPACE::Transaction;
using ROCKSDB_NAMESPACE::TransactionDB;
@@ -235,6 +241,15 @@ struct rocksdb_filelock_t {
struct rocksdb_logger_t {
std::shared_ptr<Logger> rep;
};
struct rocksdb_file_checksum_gen_factory_t {
std::shared_ptr<FileChecksumGenFactory> rep;
};
struct rocksdb_sst_partitioner_factory_t {
std::shared_ptr<SstPartitionerFactory> rep;
};
struct rocksdb_table_properties_collector_factory_t {
std::shared_ptr<TablePropertiesCollectorFactory> rep;
};
struct rocksdb_lru_cache_options_t {
LRUCacheOptions rep;
};
@@ -581,7 +596,6 @@ struct rocksdb_slicetransform_t : public SliceTransform {
char* (*transform_)(void*, const char* key, size_t length,
size_t* dst_length);
unsigned char (*in_domain_)(void*, const char* key, size_t length);
unsigned char (*in_range_)(void*, const char* key, size_t length);
~rocksdb_slicetransform_t() override { (*destructor_)(state_); }
@@ -596,10 +610,6 @@ struct rocksdb_slicetransform_t : public SliceTransform {
bool InDomain(const Slice& src) const override {
return (*in_domain_)(state_, src.data(), src.size());
}
bool InRange(const Slice& src) const override {
return (*in_range_)(state_, src.data(), src.size());
}
};
struct rocksdb_universal_compaction_options_t {
@@ -920,6 +930,38 @@ rocksdb_compaction_service_options_override_create() {
return new rocksdb_compaction_service_options_override_t;
}
rocksdb_compaction_service_options_override_t*
rocksdb_compaction_service_options_override_create_from_options(
rocksdb_options_t* options) {
if (!options) {
return nullptr;
}
rocksdb_compaction_service_options_override_t* override_opts =
new rocksdb_compaction_service_options_override_t;
// Copy all relevant options from rocksdb_options_t
override_opts->rep.env = options->rep.env;
override_opts->rep.file_checksum_gen_factory =
options->rep.file_checksum_gen_factory;
override_opts->rep.comparator = options->rep.comparator;
override_opts->rep.merge_operator = options->rep.merge_operator;
override_opts->rep.compaction_filter = options->rep.compaction_filter;
override_opts->rep.compaction_filter_factory =
options->rep.compaction_filter_factory;
override_opts->rep.prefix_extractor = options->rep.prefix_extractor;
override_opts->rep.table_factory = options->rep.table_factory;
override_opts->rep.sst_partitioner_factory =
options->rep.sst_partitioner_factory;
override_opts->rep.listeners = options->rep.listeners;
override_opts->rep.statistics = options->rep.statistics;
override_opts->rep.info_log = options->rep.info_log;
override_opts->rep.table_properties_collector_factories =
options->rep.table_properties_collector_factories;
return override_opts;
}
void rocksdb_compaction_service_options_override_destroy(
rocksdb_compaction_service_options_override_t* override_options) {
if (override_options) {
@@ -943,6 +985,111 @@ void rocksdb_compaction_service_options_override_set_comparator(
}
}
void rocksdb_compaction_service_options_override_set_merge_operator(
rocksdb_compaction_service_options_override_t* override_options,
rocksdb_mergeoperator_t* merge_operator) {
if (override_options && merge_operator) {
override_options->rep.merge_operator =
std::shared_ptr<MergeOperator>(merge_operator);
}
}
void rocksdb_compaction_service_options_override_set_compaction_filter(
rocksdb_compaction_service_options_override_t* override_options,
rocksdb_compactionfilter_t* compaction_filter) {
if (override_options && compaction_filter) {
override_options->rep.compaction_filter = compaction_filter;
}
}
void rocksdb_compaction_service_options_override_set_compaction_filter_factory(
rocksdb_compaction_service_options_override_t* override_options,
rocksdb_compactionfilterfactory_t* compaction_filter_factory) {
if (override_options && compaction_filter_factory) {
override_options->rep.compaction_filter_factory =
std::shared_ptr<CompactionFilterFactory>(compaction_filter_factory);
}
}
void rocksdb_compaction_service_options_override_set_prefix_extractor(
rocksdb_compaction_service_options_override_t* override_options,
rocksdb_slicetransform_t* prefix_extractor) {
if (override_options && prefix_extractor) {
override_options->rep.prefix_extractor =
std::shared_ptr<const SliceTransform>(prefix_extractor);
}
}
void rocksdb_compaction_service_options_override_set_block_based_table_factory(
rocksdb_compaction_service_options_override_t* override_options,
rocksdb_block_based_table_options_t* table_options) {
if (override_options && table_options) {
override_options->rep.table_factory = std::shared_ptr<TableFactory>(
NewBlockBasedTableFactory(table_options->rep));
}
}
void rocksdb_compaction_service_options_override_set_cuckoo_table_factory(
rocksdb_compaction_service_options_override_t* override_options,
rocksdb_cuckoo_table_options_t* table_options) {
if (override_options && table_options) {
override_options->rep.table_factory = std::shared_ptr<TableFactory>(
NewCuckooTableFactory(table_options->rep));
}
}
// Note: add_event_listener is defined later after rocksdb_eventlistener_t
// struct
void rocksdb_compaction_service_options_override_set_statistics(
rocksdb_compaction_service_options_override_t* override_options,
rocksdb_options_t* options) {
if (override_options && options) {
override_options->rep.statistics = options->rep.statistics;
}
}
void rocksdb_compaction_service_options_override_set_info_log(
rocksdb_compaction_service_options_override_t* override_options,
rocksdb_logger_t* logger) {
if (override_options && logger) {
override_options->rep.info_log = logger->rep;
}
}
void rocksdb_compaction_service_options_override_set_option(
rocksdb_compaction_service_options_override_t* override_options,
const char* key, const char* value) {
if (override_options && key && value) {
override_options->rep.options_map[std::string(key)] = std::string(value);
}
}
void rocksdb_compaction_service_options_override_set_file_checksum_gen_factory(
rocksdb_compaction_service_options_override_t* override_options,
rocksdb_file_checksum_gen_factory_t* factory) {
if (override_options && factory) {
override_options->rep.file_checksum_gen_factory = factory->rep;
}
}
void rocksdb_compaction_service_options_override_set_sst_partitioner_factory(
rocksdb_compaction_service_options_override_t* override_options,
rocksdb_sst_partitioner_factory_t* factory) {
if (override_options && factory) {
override_options->rep.sst_partitioner_factory = factory->rep;
}
}
void rocksdb_compaction_service_options_override_add_table_properties_collector_factory(
rocksdb_compaction_service_options_override_t* override_options,
rocksdb_table_properties_collector_factory_t* factory) {
if (override_options && factory) {
override_options->rep.table_properties_collector_factories.push_back(
factory->rep);
}
}
// Atomic bool management for cancellation
unsigned char* rocksdb_open_and_compact_canceled_create() {
return reinterpret_cast<unsigned char*>(new std::atomic<bool>(false));
@@ -1071,12 +1218,12 @@ char* rocksdb_open_and_compact_with_options(
rocksdb_t* rocksdb_open(const rocksdb_options_t* options, const char* name,
char** errptr) {
DB* db;
if (SaveError(errptr, DB::Open(options->rep, std::string(name), &db))) {
std::unique_ptr<DB> dbptr;
if (SaveError(errptr, DB::Open(options->rep, std::string(name), &dbptr))) {
return nullptr;
}
rocksdb_t* result = new rocksdb_t;
result->rep = db;
result->rep = dbptr.release();
return result;
}
@@ -1096,13 +1243,14 @@ rocksdb_t* rocksdb_open_for_read_only(const rocksdb_options_t* options,
const char* name,
unsigned char error_if_wal_file_exists,
char** errptr) {
DB* db;
if (SaveError(errptr, DB::OpenForReadOnly(options->rep, std::string(name),
&db, error_if_wal_file_exists))) {
std::unique_ptr<DB> dbptr;
if (SaveError(errptr,
DB::OpenForReadOnly(options->rep, std::string(name), &dbptr,
error_if_wal_file_exists))) {
return nullptr;
}
rocksdb_t* result = new rocksdb_t;
result->rep = db;
result->rep = dbptr.release();
return result;
}
@@ -1110,14 +1258,14 @@ rocksdb_t* rocksdb_open_as_secondary(const rocksdb_options_t* options,
const char* name,
const char* secondary_path,
char** errptr) {
DB* db;
std::unique_ptr<DB> dbptr;
if (SaveError(errptr,
DB::OpenAsSecondary(options->rep, std::string(name),
std::string(secondary_path), &db))) {
std::string(secondary_path), &dbptr))) {
return nullptr;
}
rocksdb_t* result = new rocksdb_t;
result->rep = db;
result->rep = dbptr.release();
return result;
}
@@ -1431,11 +1579,11 @@ rocksdb_t* rocksdb_open_and_trim_history(
std::string trim_ts_(trim_ts, trim_tslen);
DB* db;
std::unique_ptr<DB> dbptr;
std::vector<ColumnFamilyHandle*> handles;
if (SaveError(errptr, DB::OpenAndTrimHistory(
DBOptions(db_options->rep), std::string(name),
column_families, &handles, &db, trim_ts_))) {
column_families, &handles, &dbptr, trim_ts_))) {
return nullptr;
}
@@ -1447,7 +1595,7 @@ rocksdb_t* rocksdb_open_and_trim_history(
column_family_handles[i] = c_handle;
}
rocksdb_t* result = new rocksdb_t;
result->rep = db;
result->rep = dbptr.release();
return result;
}
@@ -1463,10 +1611,10 @@ rocksdb_t* rocksdb_open_column_families(
ColumnFamilyOptions(column_family_options[i]->rep));
}
DB* db;
std::unique_ptr<DB> dbptr;
std::vector<ColumnFamilyHandle*> handles;
if (SaveError(errptr, DB::Open(DBOptions(db_options->rep), std::string(name),
column_families, &handles, &db))) {
column_families, &handles, &dbptr))) {
return nullptr;
}
@@ -1478,7 +1626,7 @@ rocksdb_t* rocksdb_open_column_families(
column_family_handles[i] = c_handle;
}
rocksdb_t* result = new rocksdb_t;
result->rep = db;
result->rep = dbptr.release();
return result;
}
@@ -1531,12 +1679,12 @@ rocksdb_t* rocksdb_open_for_read_only_column_families(
ColumnFamilyOptions(column_family_options[i]->rep));
}
DB* db;
std::unique_ptr<DB> dbptr;
std::vector<ColumnFamilyHandle*> handles;
if (SaveError(errptr,
DB::OpenForReadOnly(DBOptions(db_options->rep),
std::string(name), column_families,
&handles, &db, error_if_wal_file_exists))) {
if (SaveError(errptr, DB::OpenForReadOnly(DBOptions(db_options->rep),
std::string(name), column_families,
&handles, &dbptr,
error_if_wal_file_exists))) {
return nullptr;
}
@@ -1548,7 +1696,7 @@ rocksdb_t* rocksdb_open_for_read_only_column_families(
column_family_handles[i] = c_handle;
}
rocksdb_t* result = new rocksdb_t;
result->rep = db;
result->rep = dbptr.release();
return result;
}
@@ -1564,12 +1712,12 @@ rocksdb_t* rocksdb_open_as_secondary_column_families(
std::string(column_family_names[i]),
ColumnFamilyOptions(column_family_options[i]->rep));
}
DB* db;
std::unique_ptr<DB> dbptr;
std::vector<ColumnFamilyHandle*> handles;
if (SaveError(errptr, DB::OpenAsSecondary(DBOptions(db_options->rep),
std::string(name),
std::string(secondary_path),
column_families, &handles, &db))) {
if (SaveError(errptr, DB::OpenAsSecondary(
DBOptions(db_options->rep), std::string(name),
std::string(secondary_path), column_families,
&handles, &dbptr))) {
return nullptr;
}
for (size_t i = 0; i != handles.size(); ++i) {
@@ -1580,7 +1728,7 @@ rocksdb_t* rocksdb_open_as_secondary_column_families(
column_family_handles[i] = c_handle;
}
rocksdb_t* result = new rocksdb_t;
result->rep = db;
result->rep = dbptr.release();
return result;
}
@@ -1611,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;
}
@@ -2891,12 +3043,19 @@ class H : public WriteBatch::Handler {
void* state_;
void (*put_)(void*, const char* k, size_t klen, const char* v, size_t vlen);
void (*deleted_)(void*, const char* k, size_t klen);
void (*log_data_)(void*, const char* blob, size_t blob_len);
void Put(const Slice& key, const Slice& value) override {
(*put_)(state_, key.data(), key.size(), value.data(), value.size());
}
void Delete(const Slice& key) override {
(*deleted_)(state_, key.data(), key.size());
}
void LogData(const Slice& blob) override {
if (log_data_) {
(*log_data_)(state_, blob.data(), blob.size());
}
}
};
class HCF : public WriteBatch::Handler {
@@ -2907,6 +3066,8 @@ class HCF : public WriteBatch::Handler {
void (*deleted_cf_)(void*, uint32_t cfid, const char* k, size_t klen);
void (*merge_cf_)(void*, uint32_t cfid, const char* k, size_t klen,
const char* v, size_t vlen);
void (*log_data_)(void*, const char* blob, size_t blob_len);
Status PutCF(uint32_t column_family_id, const Slice& key,
const Slice& value) override {
(*put_cf_)(state_, column_family_id, key.data(), key.size(), value.data(),
@@ -2923,6 +3084,11 @@ class HCF : public WriteBatch::Handler {
value.size());
return Status::OK();
}
void LogData(const Slice& blob) override {
if (log_data_) {
(*log_data_)(state_, blob.data(), blob.size());
}
}
};
void rocksdb_writebatch_iterate(rocksdb_writebatch_t* b, void* state,
@@ -2934,6 +3100,20 @@ void rocksdb_writebatch_iterate(rocksdb_writebatch_t* b, void* state,
handler.state_ = state;
handler.put_ = put;
handler.deleted_ = deleted;
handler.log_data_ = nullptr;
b->rep.Iterate(&handler);
}
void rocksdb_writebatch_iterate_ld(
rocksdb_writebatch_t* b, void* state,
void (*put)(void*, const char* k, size_t klen, const char* v, size_t vlen),
void (*deleted)(void*, const char* k, size_t klen),
void (*log_data)(void*, const char* blob, size_t blob_len)) {
H handler;
handler.state_ = state;
handler.put_ = put;
handler.deleted_ = deleted;
handler.log_data_ = log_data;
b->rep.Iterate(&handler);
}
@@ -2949,6 +3129,24 @@ void rocksdb_writebatch_iterate_cf(
handler.put_cf_ = put_cf;
handler.deleted_cf_ = deleted_cf;
handler.merge_cf_ = merge_cf;
handler.log_data_ = nullptr;
b->rep.Iterate(&handler);
}
void rocksdb_writebatch_iterate_cf_ld(
rocksdb_writebatch_t* b, void* state,
void (*put_cf)(void*, uint32_t cfid, const char* k, size_t klen,
const char* v, size_t vlen),
void (*deleted_cf)(void*, uint32_t cfid, const char* k, size_t klen),
void (*merge_cf)(void*, uint32_t cfid, const char* k, size_t klen,
const char* v, size_t vlen),
void (*log_data)(void*, const char* blob, size_t blob_len)) {
HCF handler;
handler.state_ = state;
handler.put_cf_ = put_cf;
handler.deleted_cf_ = deleted_cf;
handler.merge_cf_ = merge_cf;
handler.log_data_ = log_data;
b->rep.Iterate(&handler);
}
@@ -3549,6 +3747,11 @@ void rocksdb_block_based_options_set_format_version(
options->rep.format_version = v;
}
void rocksdb_block_based_options_set_separate_key_value_in_data_block(
rocksdb_block_based_table_options_t* options, unsigned char v) {
options->rep.separate_key_value_in_data_block = v;
}
void rocksdb_block_based_options_set_index_type(
rocksdb_block_based_table_options_t* options, int v) {
options->rep.index_type = static_cast<BlockBasedTableOptions::IndexType>(v);
@@ -3560,6 +3763,12 @@ void rocksdb_block_based_options_set_data_block_index_type(
static_cast<BlockBasedTableOptions::DataBlockIndexType>(v);
}
void rocksdb_block_based_options_set_index_block_search_type(
rocksdb_block_based_table_options_t* options, int v) {
options->rep.index_block_search_type =
static_cast<BlockBasedTableOptions::BlockSearchType>(v);
}
void rocksdb_block_based_options_set_data_block_hash_ratio(
rocksdb_block_based_table_options_t* options, double v) {
options->rep.data_block_hash_table_util_ratio = v;
@@ -3612,6 +3821,11 @@ void rocksdb_block_based_options_set_unpartitioned_pinning_tier(
static_cast<ROCKSDB_NAMESPACE::PinningTier>(v);
}
void rocksdb_block_based_options_set_block_align(
rocksdb_block_based_table_options_t* options, unsigned char v) {
options->rep.block_align = v;
}
/* FlushJobInfo */
const char* rocksdb_flushjobinfo_cf_name(const rocksdb_flushjobinfo_t* info,
@@ -3967,6 +4181,15 @@ void rocksdb_options_add_eventlistener(rocksdb_options_t* opt,
opt->rep.listeners.emplace_back(std::shared_ptr<EventListener>(t));
}
void rocksdb_compaction_service_options_override_add_event_listener(
rocksdb_compaction_service_options_override_t* override_options,
rocksdb_eventlistener_t* event_listener) {
if (override_options && event_listener) {
override_options->rep.listeners.emplace_back(
std::shared_ptr<EventListener>(event_listener));
}
}
rocksdb_cuckoo_table_options_t* rocksdb_cuckoo_options_create() {
return new rocksdb_cuckoo_table_options_t;
}
@@ -4132,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) {
@@ -4178,6 +4410,65 @@ rocksdb_logger_t* rocksdb_logger_create_callback_logger(
void rocksdb_logger_destroy(rocksdb_logger_t* logger) { delete logger; }
/* File Checksum Gen Factory */
rocksdb_file_checksum_gen_factory_t*
rocksdb_file_checksum_gen_crc32c_factory_create() {
rocksdb_file_checksum_gen_factory_t* factory =
new rocksdb_file_checksum_gen_factory_t;
factory->rep = GetFileChecksumGenCrc32cFactory();
return factory;
}
void rocksdb_file_checksum_gen_factory_destroy(
rocksdb_file_checksum_gen_factory_t* factory) {
delete factory;
}
void rocksdb_options_set_file_checksum_gen_factory(
rocksdb_options_t* opt, rocksdb_file_checksum_gen_factory_t* factory) {
if (opt && factory) {
opt->rep.file_checksum_gen_factory = factory->rep;
}
}
/* SST Partitioner Factory */
rocksdb_sst_partitioner_factory_t*
rocksdb_sst_partitioner_fixed_prefix_factory_create(size_t prefix_len) {
rocksdb_sst_partitioner_factory_t* factory =
new rocksdb_sst_partitioner_factory_t;
factory->rep = NewSstPartitionerFixedPrefixFactory(prefix_len);
return factory;
}
void rocksdb_sst_partitioner_factory_destroy(
rocksdb_sst_partitioner_factory_t* factory) {
delete factory;
}
void rocksdb_options_set_sst_partitioner_factory(
rocksdb_options_t* opt, rocksdb_sst_partitioner_factory_t* factory) {
if (opt && factory) {
opt->rep.sst_partitioner_factory = factory->rep;
}
}
/* Table Properties Collector Factory */
void rocksdb_table_properties_collector_factory_destroy(
rocksdb_table_properties_collector_factory_t* factory) {
delete factory;
}
void rocksdb_options_add_table_properties_collector_factory(
rocksdb_options_t* opt,
rocksdb_table_properties_collector_factory_t* factory) {
if (opt && factory) {
opt->rep.table_properties_collector_factories.push_back(factory->rep);
}
}
void rocksdb_options_set_env(rocksdb_options_t* opt, rocksdb_env_t* env) {
opt->rep.env = (env ? env->rep : nullptr);
}
@@ -4394,16 +4685,6 @@ unsigned char rocksdb_options_get_skip_stats_update_on_db_open(
return opt->rep.skip_stats_update_on_db_open;
}
void rocksdb_options_set_skip_checking_sst_file_sizes_on_db_open(
rocksdb_options_t* opt, unsigned char val) {
opt->rep.skip_checking_sst_file_sizes_on_db_open = val;
}
unsigned char rocksdb_options_get_skip_checking_sst_file_sizes_on_db_open(
rocksdb_options_t* opt) {
return opt->rep.skip_checking_sst_file_sizes_on_db_open;
}
/* Blob Options Settings */
void rocksdb_options_set_enable_blob_files(rocksdb_options_t* opt,
unsigned char val) {
@@ -5521,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;
}
@@ -5819,11 +6110,6 @@ unsigned char rocksdb_readoptions_get_tailing(rocksdb_readoptions_t* opt) {
return opt->rep.tailing;
}
void rocksdb_readoptions_set_managed(rocksdb_readoptions_t* opt,
unsigned char v) {
opt->rep.managed = v;
}
void rocksdb_readoptions_set_readahead_size(rocksdb_readoptions_t* opt,
size_t v) {
opt->rep.readahead_size = v;
@@ -6624,14 +6910,12 @@ rocksdb_slicetransform_t* rocksdb_slicetransform_create(
char* (*transform)(void*, const char* key, size_t length,
size_t* dst_length),
unsigned char (*in_domain)(void*, const char* key, size_t length),
unsigned char (*in_range)(void*, const char* key, size_t length),
const char* (*name)(void*)) {
rocksdb_slicetransform_t* result = new rocksdb_slicetransform_t;
result->state_ = state;
result->destructor_ = destructor;
result->transform_ = transform;
result->in_domain_ = in_domain;
result->in_range_ = in_range;
result->name_ = name;
return result;
}
@@ -6647,7 +6931,6 @@ struct SliceTransformWrapper : public rocksdb_slicetransform_t {
return rep_->Transform(src);
}
bool InDomain(const Slice& src) const override { return rep_->InDomain(src); }
bool InRange(const Slice& src) const override { return rep_->InRange(src); }
static void DoNothing(void*) {}
};
@@ -6771,6 +7054,27 @@ uint64_t rocksdb_fifo_compaction_options_get_max_table_files_size(
return fifo_opts->rep.max_table_files_size;
}
void rocksdb_fifo_compaction_options_set_max_data_files_size(
rocksdb_fifo_compaction_options_t* fifo_opts, uint64_t size) {
fifo_opts->rep.max_data_files_size = size;
}
uint64_t rocksdb_fifo_compaction_options_get_max_data_files_size(
rocksdb_fifo_compaction_options_t* fifo_opts) {
return fifo_opts->rep.max_data_files_size;
}
void rocksdb_fifo_compaction_options_set_use_kv_ratio_compaction(
rocksdb_fifo_compaction_options_t* fifo_opts,
unsigned char use_kv_ratio_compaction) {
fifo_opts->rep.use_kv_ratio_compaction = use_kv_ratio_compaction;
}
unsigned char rocksdb_fifo_compaction_options_get_use_kv_ratio_compaction(
rocksdb_fifo_compaction_options_t* fifo_opts) {
return fifo_opts->rep.use_kv_ratio_compaction;
}
void rocksdb_fifo_compaction_options_destroy(
rocksdb_fifo_compaction_options_t* fifo_opts) {
delete fifo_opts;
@@ -7244,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;
}
@@ -8346,6 +8654,14 @@ void rocksdb_enable_manual_compaction(rocksdb_t* db) {
db->rep->EnableManualCompaction();
}
void rocksdb_abort_all_compactions(rocksdb_t* db) {
db->rep->AbortAllCompactions();
}
void rocksdb_resume_all_compactions(rocksdb_t* db) {
db->rep->ResumeAllCompactions();
}
rocksdb_statistics_histogram_data_t*
rocksdb_statistics_histogram_data_create() {
return new rocksdb_statistics_histogram_data_t{};
+101 -1
View File
@@ -3596,6 +3596,14 @@ int main(int argc, char** argv) {
100000 ==
rocksdb_fifo_compaction_options_get_max_table_files_size(fco));
rocksdb_fifo_compaction_options_set_max_data_files_size(fco, 200000);
CheckCondition(
200000 == rocksdb_fifo_compaction_options_get_max_data_files_size(fco));
rocksdb_fifo_compaction_options_set_use_kv_ratio_compaction(fco, 1);
CheckCondition(
1 == rocksdb_fifo_compaction_options_get_use_kv_ratio_compaction(fco));
rocksdb_fifo_compaction_options_destroy(fco);
}
@@ -4447,7 +4455,7 @@ int main(int argc, char** argv) {
StartPhase("statistics");
{
const uint32_t BYTES_WRITTEN_TICKER = 60;
const uint32_t BYTES_WRITTEN_TICKER = 61;
const uint32_t DB_WRITE_HIST = 1;
rocksdb_statistics_histogram_data_t* hist =
@@ -4717,9 +4725,86 @@ int main(int argc, char** argv) {
rocksdb_compaction_service_options_override_set_comparator(override_opts,
cmp);
// Test file checksum gen factory
rocksdb_file_checksum_gen_factory_t* checksum_factory =
rocksdb_file_checksum_gen_crc32c_factory_create();
CheckCondition(checksum_factory != NULL);
rocksdb_compaction_service_options_override_set_file_checksum_gen_factory(
override_opts, checksum_factory);
// Test SST partitioner factory
rocksdb_sst_partitioner_factory_t* partitioner_factory =
rocksdb_sst_partitioner_fixed_prefix_factory_create(4);
CheckCondition(partitioner_factory != NULL);
rocksdb_compaction_service_options_override_set_sst_partitioner_factory(
override_opts, partitioner_factory);
// Test merge operator
rocksdb_compaction_service_options_override_set_merge_operator(
override_opts, NULL);
// Test compaction filter
rocksdb_compaction_service_options_override_set_compaction_filter(
override_opts, NULL);
// Test prefix extractor
rocksdb_compaction_service_options_override_set_prefix_extractor(
override_opts, NULL);
// Test table factory - block based
rocksdb_block_based_table_options_t* table_opts =
rocksdb_block_based_options_create();
rocksdb_compaction_service_options_override_set_block_based_table_factory(
override_opts, table_opts);
rocksdb_block_based_options_destroy(table_opts);
// Test statistics via options
rocksdb_options_t* stats_opts = rocksdb_options_create();
rocksdb_options_enable_statistics(stats_opts);
rocksdb_compaction_service_options_override_set_statistics(override_opts,
stats_opts);
rocksdb_options_destroy(stats_opts);
// Test info log
rocksdb_logger_t* logger =
rocksdb_logger_create_stderr_logger(1, "test_prefix");
rocksdb_compaction_service_options_override_set_info_log(override_opts,
logger);
rocksdb_logger_destroy(logger);
// Test options map
rocksdb_compaction_service_options_override_set_option(
override_opts, "max_bytes_for_level_base", "67108864");
// Cleanup
rocksdb_file_checksum_gen_factory_destroy(checksum_factory);
rocksdb_sst_partitioner_factory_destroy(partitioner_factory);
rocksdb_compaction_service_options_override_destroy(override_opts);
}
StartPhase("factory_options_on_regular_options");
{
// Test that the new factory types work with regular rocksdb_options_t
rocksdb_options_t* test_opts = rocksdb_options_create();
// Test file checksum gen factory on regular options
rocksdb_file_checksum_gen_factory_t* checksum_factory =
rocksdb_file_checksum_gen_crc32c_factory_create();
CheckCondition(checksum_factory != NULL);
rocksdb_options_set_file_checksum_gen_factory(test_opts, checksum_factory);
// Test SST partitioner factory on regular options
rocksdb_sst_partitioner_factory_t* partitioner_factory =
rocksdb_sst_partitioner_fixed_prefix_factory_create(8);
CheckCondition(partitioner_factory != NULL);
rocksdb_options_set_sst_partitioner_factory(test_opts, partitioner_factory);
// Cleanup
rocksdb_file_checksum_gen_factory_destroy(checksum_factory);
rocksdb_sst_partitioner_factory_destroy(partitioner_factory);
rocksdb_options_destroy(test_opts);
}
StartPhase("remote_compaction_null_callback_handling");
{
// Test that NULL callback returns are handled gracefully
@@ -4795,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);
+31 -3
View File
@@ -401,7 +401,13 @@ ColumnFamilyOptions SanitizeCfOptions(const ImmutableDBOptions& db_options,
}
if (result.max_compaction_bytes == 0) {
result.max_compaction_bytes = result.target_file_size_base * 25;
// For FIFO with use_kv_ratio_compaction, leave max_compaction_bytes as 0
// to signal "auto-calculate target from capacity and SST/blob ratio."
// When explicitly set by the user, it overrides the auto-calculated target.
if (result.compaction_style != kCompactionStyleFIFO ||
!result.compaction_options_fifo.use_kv_ratio_compaction) {
result.max_compaction_bytes = result.target_file_size_base * 25;
}
}
bool is_block_based_table = (result.table_factory->IsInstanceOf(
@@ -1248,7 +1254,7 @@ Compaction* ColumnFamilyData::PickCompaction(
auto* result = compaction_picker_->PickCompaction(
GetName(), mutable_options, mutable_db_options, existing_snapshots,
snapshot_checker, current_->storage_info(), log_buffer,
require_max_output_level);
GetFullHistoryTsLow(), require_max_output_level);
if (result != nullptr) {
result->FinalizeInputInfo(current_);
}
@@ -1336,7 +1342,7 @@ Compaction* ColumnFamilyData::CompactRange(
GetName(), mutable_cf_options, mutable_db_options,
current_->storage_info(), input_level, output_level,
compact_range_options, begin, end, compaction_end, conflict,
max_file_num_to_ignore, trim_ts);
max_file_num_to_ignore, trim_ts, GetFullHistoryTsLow());
if (result != nullptr) {
result->FinalizeInputInfo(current_);
}
@@ -1563,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) ==
+10 -13
View File
@@ -118,8 +118,7 @@ class ColumnFamilyTestBase : public testing::Test {
for (int i = 0; i < n; i++) {
if (flush_every != 0 && i != 0 && i % flush_every == 0) {
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
dbi->TEST_FlushMemTable();
dbfull()->TEST_FlushMemTable();
}
int keyi = base + i;
@@ -177,8 +176,7 @@ class ColumnFamilyTestBase : public testing::Test {
}
handles_.clear();
names_.clear();
delete db_;
db_ = nullptr;
db_.reset();
}
Status TryOpen(std::vector<std::string> cf,
@@ -218,7 +216,7 @@ class ColumnFamilyTestBase : public testing::Test {
void Open() { Open({"default"}); }
DBImpl* dbfull() { return static_cast_with_check<DBImpl>(db_); }
DBImpl* dbfull() { return static_cast_with_check<DBImpl>(db_.get()); }
int GetProperty(int cf, std::string property) {
std::string value;
@@ -500,7 +498,7 @@ class ColumnFamilyTestBase : public testing::Test {
ColumnFamilyOptions column_family_options_;
DBOptions db_options_;
std::string dbname_;
DB* db_ = nullptr;
std::unique_ptr<DB> db_;
EnvCounter* env_;
std::shared_ptr<Env> env_guard_;
Random rnd_;
@@ -517,7 +515,7 @@ class ColumnFamilyTest
INSTANTIATE_TEST_CASE_P(FormatDef, ColumnFamilyTest,
testing::Values(test::kDefaultFormatVersion));
INSTANTIATE_TEST_CASE_P(FormatLatest, ColumnFamilyTest,
testing::Values(kLatestFormatVersion));
testing::Values(kLatestBbtFormatVersion));
TEST_P(ColumnFamilyTest, DontReuseColumnFamilyID) {
for (int iter = 0; iter < 3; ++iter) {
@@ -707,8 +705,8 @@ INSTANTIATE_TEST_CASE_P(
std::make_tuple(test::kDefaultFormatVersion, false)));
INSTANTIATE_TEST_CASE_P(
FormatLatest, FlushEmptyCFTestWithParam,
testing::Values(std::make_tuple(kLatestFormatVersion, true),
std::make_tuple(kLatestFormatVersion, false)));
testing::Values(std::make_tuple(kLatestBbtFormatVersion, true),
std::make_tuple(kLatestBbtFormatVersion, false)));
TEST_P(ColumnFamilyTest, AddDrop) {
Open();
@@ -3542,11 +3540,10 @@ TEST_P(ColumnFamilyTest, MultipleCFPathsTest) {
// Re-open and verify the keys.
Reopen({ColumnFamilyOptions(), cf_opt1, cf_opt2});
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
for (int cf = 1; cf != 3; ++cf) {
ReadOptions read_options;
read_options.readahead_size = 0;
auto it = dbi->NewIterator(read_options, handles_[cf]);
auto it = db_->NewIterator(read_options, handles_[cf]);
for (it->SeekToFirst(); it->Valid(); it->Next()) {
ASSERT_OK(it->status());
Slice key(it->key());
@@ -3636,7 +3633,7 @@ TEST(ColumnFamilyTest, ValidateMemtableKVChecksumOption) {
// the behavior of manual flush is that it skips retaining UDTs.
class ColumnFamilyRetainUDTTest : public ColumnFamilyTestBase {
public:
ColumnFamilyRetainUDTTest() : ColumnFamilyTestBase(kLatestFormatVersion) {}
ColumnFamilyRetainUDTTest() : ColumnFamilyTestBase(kLatestBbtFormatVersion) {}
void SetUp() override {
db_options_.allow_concurrent_memtable_write = false;
@@ -3886,7 +3883,7 @@ TEST_F(ManualFlushSkipRetainUDTTest, FlushRemovesStaleEntries) {
static_cast_with_check<ColumnFamilyHandleImpl>(cfh)->cfd();
for (int version = 0; version < 100; version++) {
if (version == 50) {
ASSERT_OK(static_cast_with_check<DBImpl>(db_)->TEST_SwitchMemtable(cfd));
ASSERT_OK(dbfull()->TEST_SwitchMemtable(cfd));
}
ASSERT_OK(
Put(0, "foo", EncodeAsUint64(version), "v" + std::to_string(version)));
+35 -48
View File
@@ -75,10 +75,9 @@ TEST_F(CompactFilesTest, L0ConflictsFiles) {
options.level0_file_num_compaction_trigger = kLevel0Trigger;
options.compression = kNoCompression;
DB* db = nullptr;
std::unique_ptr<DB> db;
ASSERT_OK(DestroyDB(db_name_, options));
Status s = DB::Open(options, db_name_, &db);
assert(s.ok());
ASSERT_OK(DB::Open(options, db_name_, &db));
assert(db);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency({
@@ -114,7 +113,6 @@ TEST_F(CompactFilesTest, L0ConflictsFiles) {
}
}
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
delete db;
}
TEST_F(CompactFilesTest, MultipleLevel) {
@@ -128,11 +126,11 @@ TEST_F(CompactFilesTest, MultipleLevel) {
FlushedFileCollector* collector = new FlushedFileCollector();
options.listeners.emplace_back(collector);
DB* db = nullptr;
std::unique_ptr<DB> db;
ASSERT_OK(DestroyDB(db_name_, options));
Status s = DB::Open(options, db_name_, &db);
ASSERT_OK(s);
ASSERT_NE(db, nullptr);
ASSERT_NE(db.get(), nullptr);
// create couple files in L0, L3, L4 and L5
for (int i = 5; i > 2; --i) {
@@ -141,7 +139,8 @@ TEST_F(CompactFilesTest, MultipleLevel) {
ASSERT_OK(db->Flush(FlushOptions()));
// Ensure background work is fully finished including listener callbacks
// before accessing listener state.
ASSERT_OK(static_cast_with_check<DBImpl>(db)->TEST_WaitForBackgroundWork());
ASSERT_OK(
static_cast_with_check<DBImpl>(db.get())->TEST_WaitForBackgroundWork());
auto l0_files = collector->GetFlushedFiles();
ASSERT_OK(db->CompactFiles(CompactionOptions(), l0_files, i));
@@ -191,8 +190,6 @@ TEST_F(CompactFilesTest, MultipleLevel) {
ASSERT_OK(db->CompactFiles(CompactionOptions(), files, 5));
SyncPoint::GetInstance()->DisableProcessing();
thread.join();
delete db;
}
TEST_F(CompactFilesTest, ObsoleteFiles) {
@@ -212,11 +209,11 @@ TEST_F(CompactFilesTest, ObsoleteFiles) {
FlushedFileCollector* collector = new FlushedFileCollector();
options.listeners.emplace_back(collector);
DB* db = nullptr;
std::unique_ptr<DB> db;
ASSERT_OK(DestroyDB(db_name_, options));
Status s = DB::Open(options, db_name_, &db);
ASSERT_OK(s);
ASSERT_NE(db, nullptr);
ASSERT_NE(db.get(), nullptr);
// create couple files
for (int i = 1000; i < 2000; ++i) {
@@ -226,13 +223,12 @@ TEST_F(CompactFilesTest, ObsoleteFiles) {
auto l0_files = collector->GetFlushedFiles();
ASSERT_OK(db->CompactFiles(CompactionOptions(), l0_files, 1));
ASSERT_OK(static_cast_with_check<DBImpl>(db)->TEST_WaitForCompact());
ASSERT_OK(static_cast_with_check<DBImpl>(db.get())->TEST_WaitForCompact());
// verify all compaction input files are deleted
for (const auto& fname : l0_files) {
ASSERT_EQ(Status::NotFound(), env_->FileExists(fname));
}
delete db;
}
TEST_F(CompactFilesTest, NotCutOutputOnLevel0) {
@@ -251,10 +247,9 @@ TEST_F(CompactFilesTest, NotCutOutputOnLevel0) {
FlushedFileCollector* collector = new FlushedFileCollector();
options.listeners.emplace_back(collector);
DB* db = nullptr;
std::unique_ptr<DB> db;
ASSERT_OK(DestroyDB(db_name_, options));
Status s = DB::Open(options, db_name_, &db);
assert(s.ok());
ASSERT_OK(DB::Open(options, db_name_, &db));
assert(db);
// create couple files
@@ -262,19 +257,20 @@ TEST_F(CompactFilesTest, NotCutOutputOnLevel0) {
ASSERT_OK(db->Put(WriteOptions(), std::to_string(i),
std::string(1000, 'a' + (i % 26))));
}
ASSERT_OK(static_cast_with_check<DBImpl>(db)->TEST_WaitForFlushMemTable());
ASSERT_OK(
static_cast_with_check<DBImpl>(db.get())->TEST_WaitForFlushMemTable());
auto l0_files_1 = collector->GetFlushedFiles();
collector->ClearFlushedFiles();
for (int i = 0; i < 500; ++i) {
ASSERT_OK(db->Put(WriteOptions(), std::to_string(i),
std::string(1000, 'a' + (i % 26))));
}
ASSERT_OK(static_cast_with_check<DBImpl>(db)->TEST_WaitForFlushMemTable());
ASSERT_OK(
static_cast_with_check<DBImpl>(db.get())->TEST_WaitForFlushMemTable());
auto l0_files_2 = collector->GetFlushedFiles();
ASSERT_OK(db->CompactFiles(CompactionOptions(), l0_files_1, 0));
ASSERT_OK(db->CompactFiles(CompactionOptions(), l0_files_2, 0));
// no assertion failure
delete db;
}
TEST_F(CompactFilesTest, CapturingPendingFiles) {
@@ -289,7 +285,7 @@ TEST_F(CompactFilesTest, CapturingPendingFiles) {
FlushedFileCollector* collector = new FlushedFileCollector();
options.listeners.emplace_back(collector);
DB* db = nullptr;
std::unique_ptr<DB> db;
ASSERT_OK(DestroyDB(db_name_, options));
Status s = DB::Open(options, db_name_, &db);
ASSERT_OK(s);
@@ -303,7 +299,8 @@ TEST_F(CompactFilesTest, CapturingPendingFiles) {
// Ensure background work is fully finished including listener callbacks
// before accessing listener state.
ASSERT_OK(static_cast_with_check<DBImpl>(db)->TEST_WaitForBackgroundWork());
ASSERT_OK(
static_cast_with_check<DBImpl>(db.get())->TEST_WaitForBackgroundWork());
auto l0_files = collector->GetFlushedFiles();
EXPECT_EQ(5, l0_files.size());
@@ -327,13 +324,12 @@ TEST_F(CompactFilesTest, CapturingPendingFiles) {
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
delete db;
db.reset();
// Make sure we can reopen the DB.
s = DB::Open(options, db_name_, &db);
ASSERT_OK(s);
assert(db);
delete db;
}
TEST_F(CompactFilesTest, CompactionFilterWithGetSv) {
@@ -365,12 +361,12 @@ TEST_F(CompactFilesTest, CompactionFilterWithGetSv) {
options.create_if_missing = true;
options.compaction_filter = cf.get();
DB* db = nullptr;
std::unique_ptr<DB> db;
ASSERT_OK(DestroyDB(db_name_, options));
Status s = DB::Open(options, db_name_, &db);
ASSERT_OK(s);
cf->SetDB(db);
cf->SetDB(db.get());
// Write one L0 file
ASSERT_OK(db->Put(WriteOptions(), "K1", "V1"));
@@ -384,8 +380,6 @@ TEST_F(CompactFilesTest, CompactionFilterWithGetSv) {
ASSERT_OK(
db->CompactFiles(ROCKSDB_NAMESPACE::CompactionOptions(), {fname}, 0));
}
delete db;
}
TEST_F(CompactFilesTest, SentinelCompressionType) {
@@ -413,7 +407,7 @@ TEST_F(CompactFilesTest, SentinelCompressionType) {
options.create_if_missing = true;
FlushedFileCollector* collector = new FlushedFileCollector();
options.listeners.emplace_back(collector);
DB* db = nullptr;
std::unique_ptr<DB> db;
ASSERT_OK(DB::Open(options, db_name_, &db));
ASSERT_OK(db->Put(WriteOptions(), "key", "val"));
@@ -421,7 +415,8 @@ TEST_F(CompactFilesTest, SentinelCompressionType) {
// Ensure background work is fully finished including listener callbacks
// before accessing listener state.
ASSERT_OK(static_cast_with_check<DBImpl>(db)->TEST_WaitForBackgroundWork());
ASSERT_OK(
static_cast_with_check<DBImpl>(db.get())->TEST_WaitForBackgroundWork());
auto l0_files = collector->GetFlushedFiles();
ASSERT_EQ(1, l0_files.size());
@@ -433,10 +428,10 @@ TEST_F(CompactFilesTest, SentinelCompressionType) {
ROCKSDB_NAMESPACE::TablePropertiesCollection all_tables_props;
ASSERT_OK(db->GetPropertiesOfAllTables(&all_tables_props));
for (const auto& name_and_table_props : all_tables_props) {
ASSERT_EQ(CompressionTypeToString(CompressionType::kZlibCompression),
name_and_table_props.second->compression_name);
// As of format_version 7, more elaborate information is encoded into the
// compression_name property
ASSERT_EQ("BuiltinV2;02;", name_and_table_props.second->compression_name);
}
delete db;
}
}
@@ -461,11 +456,7 @@ TEST_F(CompactFilesTest, CompressionWithBlockAlign) {
}
std::unique_ptr<DB> db;
{
DB* _db = nullptr;
ASSERT_OK(DB::Open(options, db_name_, &_db));
db.reset(_db);
}
ASSERT_OK(DB::Open(options, db_name_, &db));
ASSERT_OK(db->Put(WriteOptions(), "key", "val"));
ASSERT_OK(db->Flush(FlushOptions()));
@@ -504,7 +495,7 @@ TEST_F(CompactFilesTest, GetCompactionJobInfo) {
FlushedFileCollector* collector = new FlushedFileCollector();
options.listeners.emplace_back(collector);
DB* db = nullptr;
std::unique_ptr<DB> db;
ASSERT_OK(DestroyDB(db_name_, options));
Status s = DB::Open(options, db_name_, &db);
ASSERT_OK(s);
@@ -515,7 +506,8 @@ TEST_F(CompactFilesTest, GetCompactionJobInfo) {
ASSERT_OK(db->Put(WriteOptions(), std::to_string(i),
std::string(1000, 'a' + (i % 26))));
}
ASSERT_OK(static_cast_with_check<DBImpl>(db)->TEST_WaitForFlushMemTable());
ASSERT_OK(
static_cast_with_check<DBImpl>(db.get())->TEST_WaitForFlushMemTable());
auto l0_files_1 = collector->GetFlushedFiles();
CompactionOptions co;
co.compression = CompressionType::kLZ4Compression;
@@ -531,7 +523,6 @@ TEST_F(CompactFilesTest, GetCompactionJobInfo) {
ASSERT_EQ(compaction_job_info.output_level, 0);
ASSERT_OK(compaction_job_info.status);
// no assertion failure
delete db;
}
// Helper function to generate zero-padded keys
@@ -547,11 +538,11 @@ TEST_F(CompactFilesTest, TrivialMoveNonOverlappingFiles) {
options.compression = kNoCompression;
options.level_compaction_dynamic_level_bytes = false;
DB* db = nullptr;
std::unique_ptr<DB> db;
ASSERT_OK(DestroyDB(db_name_, options));
Status s = DB::Open(options, db_name_, &db);
ASSERT_OK(s);
ASSERT_NE(db, nullptr);
ASSERT_NE(db.get(), nullptr);
// Create 3 non-overlapping files in L0
// File 1: keys [a00-a99]
@@ -664,8 +655,6 @@ TEST_F(CompactFilesTest, TrivialMoveNonOverlappingFiles) {
ASSERT_OK(db->Get(ReadOptions(), key, &value));
ASSERT_EQ(value, "value_" + key);
}
delete db;
}
TEST_F(CompactFilesTest, TrivialMoveBlockedByOverlap) {
@@ -676,11 +665,11 @@ TEST_F(CompactFilesTest, TrivialMoveBlockedByOverlap) {
options.level_compaction_dynamic_level_bytes = false;
options.num_levels = 7;
DB* db = nullptr;
std::unique_ptr<DB> db;
ASSERT_OK(DestroyDB(db_name_, options));
Status s = DB::Open(options, db_name_, &db);
ASSERT_OK(s);
ASSERT_NE(db, nullptr);
ASSERT_NE(db.get(), nullptr);
// Create a file in L6 with keys [m00-m99] (wide range)
for (int i = 0; i < 100; i++) {
@@ -756,8 +745,6 @@ TEST_F(CompactFilesTest, TrivialMoveBlockedByOverlap) {
ASSERT_OK(db->Get(ReadOptions(), key, &value));
ASSERT_EQ(value, "updated_value_" + key);
}
delete db;
}
} // namespace ROCKSDB_NAMESPACE
+13 -5
View File
@@ -1310,14 +1310,14 @@ void CompactionIterator::PrepareOutput() {
validity_info_.rep);
assert(false);
}
ikey_.sequence = 0;
last_key_seq_zeroed_ = true;
TEST_SYNC_POINT_CALLBACK("CompactionIterator::PrepareOutput:ZeroingSeq",
&ikey_);
bool zeroed_seqno = false;
if (!timestamp_size_) {
current_key_.UpdateInternalKey(0, ikey_.type);
zeroed_seqno = true;
} else if (full_history_ts_low_ && cmp_with_history_ts_low_ < 0) {
// We can also zero out timestamp for better compression.
// For UDT, the seqno and timestamp could only be zeroed out after the
// key is below history_ts_low_.
// For the same user key (excluding timestamp), the timestamp-based
// history can be collapsed to save some space if the timestamp is
// older than *full_history_ts_low_.
@@ -1325,6 +1325,14 @@ void CompactionIterator::PrepareOutput() {
const Slice ts_slice = kTsMin;
ikey_.SetTimestamp(ts_slice);
current_key_.UpdateInternalKey(0, ikey_.type, &ts_slice);
zeroed_seqno = true;
}
if (zeroed_seqno) {
ikey_.sequence = 0;
last_key_seq_zeroed_ = true;
TEST_SYNC_POINT_CALLBACK("CompactionIterator::PrepareOutput:ZeroingSeq",
&ikey_);
}
}
}
+201 -39
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"
@@ -128,6 +129,10 @@ const char* GetCompactionProximalOutputRangeTypeString(
}
}
// Static constant for compaction abort flag - always false, used for
// compaction service jobs that don't support abort signaling
const std::atomic<int> CompactionJob::kCompactionAbortedFalse{0};
CompactionJob::CompactionJob(
int job_id, Compaction* compaction, const ImmutableDBOptions& db_options,
const MutableDBOptions& mutable_db_options, const FileOptions& file_options,
@@ -141,10 +146,10 @@ CompactionJob::CompactionJob(
CompactionJobStats* compaction_job_stats, Env::Priority thread_pri,
const std::shared_ptr<IOTracer>& io_tracer,
const std::atomic<bool>& manual_compaction_canceled,
const std::string& db_id, const std::string& db_session_id,
std::string full_history_ts_low, std::string trim_ts,
BlobFileCompletionCallback* blob_callback, int* bg_compaction_scheduled,
int* bg_bottom_compaction_scheduled)
const std::atomic<int>& compaction_aborted, const std::string& db_id,
const std::string& db_session_id, std::string full_history_ts_low,
std::string trim_ts, BlobFileCompletionCallback* blob_callback,
int* bg_compaction_scheduled, int* bg_bottom_compaction_scheduled)
: compact_(new CompactionState(compaction)),
internal_stats_(compaction->compaction_reason(), 1),
db_options_(db_options),
@@ -168,6 +173,7 @@ CompactionJob::CompactionJob(
versions_(versions),
shutting_down_(shutting_down),
manual_compaction_canceled_(manual_compaction_canceled),
compaction_aborted_(compaction_aborted),
db_directory_(db_directory),
blob_output_directory_(blob_output_directory),
db_mutex_(db_mutex),
@@ -708,6 +714,7 @@ void CompactionJob::InitializeCompactionRun() {
}
void CompactionJob::RunSubcompactions() {
TEST_SYNC_POINT("CompactionJob::RunSubcompactions:BeforeStart");
const size_t num_threads = compact_->sub_compact_states.size();
assert(num_threads > 0);
compact_->compaction->GetOrInitInputTableProperties();
@@ -753,6 +760,71 @@ void CompactionJob::RemoveEmptyOutputs() {
}
}
void CompactionJob::CleanupAbortedSubcompactions() {
ColumnFamilyData* cfd = compact_->compaction->column_family_data();
uint64_t total_sst_files_deleted = 0;
uint64_t total_blob_files_deleted = 0;
// Track the first file deletion error to report at the end
Status first_error;
int deletion_errors = 0;
// Mark all subcompactions as aborted and delete their output files
for (auto& sub_compact : compact_->sub_compact_states) {
// Mark this subcompaction as aborted
sub_compact.status =
Status::Incomplete(Status::SubCode::kCompactionAborted);
// Delete all files (SST and blob) tracked during compaction.
// GetOutputFilePaths() contains ALL file paths created, including
// in-progress files that may have been removed from outputs_ or
// blob_file_additions_.
for (const bool is_proximal_level : {false, true}) {
if (is_proximal_level &&
!compact_->compaction->SupportsPerKeyPlacement()) {
continue;
}
for (const std::string& file_path :
sub_compact.Outputs(is_proximal_level)->GetOutputFilePaths()) {
Status s = env_->DeleteFile(file_path);
if (s.ok()) {
// Count SST vs blob files by checking extension
if (file_path.find(".sst") != std::string::npos) {
total_sst_files_deleted++;
} else if (file_path.find(".blob") != std::string::npos) {
total_blob_files_deleted++;
}
} else if (!s.IsNotFound()) {
if (first_error.ok()) {
first_error = s;
}
deletion_errors++;
}
}
}
sub_compact.CleanupOutputs();
}
if (stats_) {
RecordTick(stats_, COMPACTION_ABORTED);
}
ROCKS_LOG_INFO(db_options_.info_log,
"[%s] [JOB %d] Compaction aborted: deleted %" PRIu64
" SST files and %" PRIu64 " blob files",
cfd->GetName().c_str(), job_id_, total_sst_files_deleted,
total_blob_files_deleted);
if (!first_error.ok()) {
ROCKS_LOG_ERROR(db_options_.info_log,
"[%s] [JOB %d] Cleanup completed with %d file deletion "
"errors. First error: %s",
cfd->GetName().c_str(), job_id_, deletion_errors,
first_error.ToString().c_str());
}
}
bool CompactionJob::HasNewBlobFiles() const {
for (const auto& state : compact_->sub_compact_states) {
if (state.Current().HasBlobFileAdditions()) {
@@ -832,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,
@@ -870,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) {
@@ -896,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);
}
}
}
}
@@ -1004,6 +1099,15 @@ Status CompactionJob::Run() {
Status status = CollectSubcompactionErrors();
// If compaction was aborted or manually paused, clean up any output files
// from completed subcompactions to prevent orphaned files on disk.
// Skip cleanup for resumable compaction (when progress writer is set)
// because the output files are needed for resumption.
if ((status.IsCompactionAborted() || status.IsManualCompactionPaused()) &&
compaction_progress_writer_ == nullptr) {
CleanupAbortedSubcompactions();
}
if (status.ok()) {
status = SyncOutputDirectories();
}
@@ -1012,6 +1116,9 @@ Status CompactionJob::Run() {
status = VerifyOutputFiles();
}
TEST_SYNC_POINT_CALLBACK("CompactionJob::Run():AfterVerifyOutputFiles",
&status);
if (status.ok()) {
SetOutputTableProperties();
}
@@ -1415,10 +1522,10 @@ InternalIterator* CompactionJob::CreateInputIterator(
return input;
}
void CompactionJob::CreateBlobFileBuilder(SubcompactionState* sub_compact,
ColumnFamilyData* cfd,
BlobFileResources& blob_resources,
const WriteOptions& write_options) {
void CompactionJob::CreateBlobFileBuilder(
SubcompactionState* sub_compact, ColumnFamilyData* cfd,
std::unique_ptr<BlobFileBuilder>& blob_file_builder,
const WriteOptions& write_options) {
const auto& mutable_cf_options =
sub_compact->compaction->mutable_cf_options();
@@ -1427,24 +1534,24 @@ void CompactionJob::CreateBlobFileBuilder(SubcompactionState* sub_compact,
if (mutable_cf_options.enable_blob_files &&
sub_compact->compaction->output_level() >=
mutable_cf_options.blob_file_starting_level) {
blob_resources.blob_file_builder = std::make_unique<BlobFileBuilder>(
blob_file_builder = std::make_unique<BlobFileBuilder>(
versions_, fs_.get(), &sub_compact->compaction->immutable_options(),
&mutable_cf_options, &file_options_, &write_options, db_id_,
db_session_id_, job_id_, cfd->GetID(), cfd->GetName(), write_hint_,
io_tracer_, blob_callback_, BlobFileCreationReason::kCompaction,
&blob_resources.blob_file_paths,
sub_compact->Current().GetOutputFilePathsPtr(),
sub_compact->Current().GetBlobFileAdditionsPtr());
} else {
blob_resources.blob_file_builder = nullptr;
blob_file_builder = nullptr;
}
}
std::unique_ptr<CompactionIterator> CompactionJob::CreateCompactionIterator(
SubcompactionState* sub_compact, ColumnFamilyData* cfd,
InternalIterator* input, const CompactionFilter* compaction_filter,
MergeHelper& merge, BlobFileResources& blob_resources,
MergeHelper& merge, std::unique_ptr<BlobFileBuilder>& blob_file_builder,
const WriteOptions& write_options) {
CreateBlobFileBuilder(sub_compact, cfd, blob_resources, write_options);
CreateBlobFileBuilder(sub_compact, cfd, blob_file_builder, write_options);
const std::string* const full_history_ts_low =
full_history_ts_low_.empty() ? nullptr : &full_history_ts_low_;
@@ -1456,7 +1563,7 @@ std::unique_ptr<CompactionIterator> CompactionJob::CreateCompactionIterator(
job_context_->earliest_write_conflict_snapshot,
job_context_->GetJobSnapshotSequence(), job_context_->snapshot_checker,
env_, ShouldReportDetailedTime(env_, stats_), sub_compact->RangeDelAgg(),
blob_resources.blob_file_builder.get(), db_options_.allow_data_in_errors,
blob_file_builder.get(), db_options_.allow_data_in_errors,
db_options_.enforce_single_del_contracts, manual_compaction_canceled_,
sub_compact->compaction
->DoesInputReferenceBlobFiles() /* must_count_input_entries */,
@@ -1495,10 +1602,17 @@ Status CompactionJob::ProcessKeyValue(
SubcompactionState* sub_compact, ColumnFamilyData* cfd,
CompactionIterator* c_iter, const CompactionFileOpenFunc& open_file_func,
const CompactionFileCloseFunc& close_file_func, uint64_t& prev_cpu_micros) {
Status status;
const uint64_t kRecordStatsEvery = 1000;
// Cron interval for periodic operations: stats update, abort check,
// and sync points. Uses 1024 (power of 2) for efficient bitwise check.
const uint64_t kCronEveryMask = (1 << 10) - 1;
[[maybe_unused]] const std::optional<const Slice> end = sub_compact->end;
// Check for abort signal before starting key processing
if (compaction_aborted_.load(std::memory_order_acquire) > 0) {
return Status::Incomplete(Status::SubCode::kCompactionAborted);
}
Status status;
IterKey prev_iter_output_key;
ParsedInternalKey prev_iter_output_internal_key;
@@ -1511,8 +1625,16 @@ Status CompactionJob::ProcessKeyValue(
assert(!end.has_value() ||
cfd->user_comparator()->Compare(c_iter->user_key(), *end) < 0);
if (c_iter->iter_stats().num_input_records % kRecordStatsEvery ==
kRecordStatsEvery - 1) {
const uint64_t num_records = c_iter->iter_stats().num_input_records;
// Periodic cron operations: stats update, abort check.
if ((num_records & kCronEveryMask) == kCronEveryMask) {
// Check for abort signal periodically
if (compaction_aborted_.load(std::memory_order_acquire) > 0) {
status = Status::Incomplete(Status::SubCode::kCompactionAborted);
break;
}
UpdateSubcompactionJobStatsIncrementally(
c_iter, &sub_compact->compaction_job_stats,
db_options_.clock->CPUMicros(), prev_cpu_micros);
@@ -1719,6 +1841,7 @@ Status CompactionJob::FinalizeBlobFiles(SubcompactionState* sub_compact,
}
void CompactionJob::ProcessKeyValueCompaction(SubcompactionState* sub_compact) {
TEST_SYNC_POINT("CompactionJob::ProcessKeyValueCompaction:Start");
assert(sub_compact);
assert(sub_compact->compaction);
@@ -1772,11 +1895,11 @@ void CompactionJob::ProcessKeyValueCompaction(SubcompactionState* sub_compact) {
false /* internal key corruption is expected */,
job_context_->GetLatestSnapshotSequence(), job_context_->snapshot_checker,
compact_->compaction->level(), db_options_.stats);
BlobFileResources blob_resources;
std::unique_ptr<BlobFileBuilder> blob_file_builder;
auto c_iter =
CreateCompactionIterator(sub_compact, cfd, input_iter, compaction_filter,
merge, blob_resources, write_options);
merge, blob_file_builder, write_options);
assert(c_iter);
c_iter->SeekToFirst();
@@ -1794,9 +1917,8 @@ void CompactionJob::ProcessKeyValueCompaction(SubcompactionState* sub_compact) {
status = FinalizeProcessKeyValueStatus(cfd, input_iter, c_iter.get(), status);
FinalizeSubcompaction(sub_compact, status, open_file_func, close_file_func,
blob_resources.blob_file_builder.get(), c_iter.get(),
input_iter, start_cpu_micros, prev_cpu_micros,
io_stats);
blob_file_builder.get(), c_iter.get(), input_iter,
start_cpu_micros, prev_cpu_micros, io_stats);
NotifyOnSubcompactionCompleted(sub_compact);
}
@@ -2295,6 +2417,10 @@ Status CompactionJob::OpenCompactionOutputFile(SubcompactionState* sub_compact,
Status s;
IOStatus io_s = NewWritableFile(fs_.get(), fname, &writable_file, fo_copy);
s = io_s;
if (io_s.ok()) {
// Track the SST file path for cleanup on abort.
outputs.AddOutputFilePath(fname);
}
if (sub_compact->io_status.ok()) {
sub_compact->io_status = io_s;
// Since this error is really a copy of the io_s that is checked below as s,
@@ -2371,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());
@@ -2413,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;
@@ -2438,6 +2572,32 @@ bool CompactionJob::UpdateInternalStatsFromInputFiles(
bool has_error = false;
const ReadOptions read_options(Env::IOActivity::kCompaction);
const auto& input_table_properties = compaction->GetInputTableProperties();
// Check all input files for old block-based SST format_version. Why? Old
// block-based SST files from roughly version 5.0 to 5.18 could produce
// inaccurate num_entries counts due to the evolution of its handling along
// with num_range_deletions. We have to disable some paranoid checks when
// compacting files from such an old release. However, we don't have great
// information to identify those files, so we heuristically over-approximate
// that set of files using
// (a) format_version < 5, which will be true for any files from RocksDB <
// 6.6.0 and should not be true for any recent production files
// (b) to avoid including non-block-based SST files (which still use older
// format_version markers, and do not support DeleteRange), we also require
// the presence of the user property "rocksdb.block.based.table.index.type",
// which was added in RocksDB 2.8 and is always present in block-based tables.
for (const auto& tp_pair : input_table_properties) {
if (tp_pair.second && tp_pair.second->format_version < 5) {
// Check for block-based table by looking for its index type property
const auto& user_props = tp_pair.second->user_collected_properties;
if (user_props.find(BlockBasedTablePropertyNames::kIndexType) !=
user_props.end()) {
job_stats_->has_accurate_num_input_records = false;
break;
}
}
}
for (int input_level = 0;
input_level < static_cast<int>(compaction->num_input_levels());
++input_level) {
@@ -2682,7 +2842,10 @@ Status CompactionJob::ReadTablePropertiesDirectly(
std::shared_ptr<const TableProperties>* tp) {
std::unique_ptr<FSRandomAccessFile> file;
std::string file_name = GetTableFileName(file_meta->fd.GetNumber());
Status s = ioptions.fs->NewRandomAccessFile(file_name, file_options_, &file,
FileOptions fopts = file_options_;
fopts.file_checksum = file_meta->file_checksum;
fopts.file_checksum_func_name = file_meta->file_checksum_func_name;
Status s = ioptions.fs->NewRandomAccessFile(file_name, fopts, &file,
nullptr /* dbg */);
if (!s.ok()) {
return s;
@@ -2776,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]);
@@ -2804,12 +2972,6 @@ void CompactionJob::RestoreCompactionOutputs(
// - Status::NotFound(): No valid progress to resume from
// - Status::Corruption(): Resume key is invalid, beyond input range, or output
// restoration failed
// - Other non-OK status: Iterator errors or file system issues during
// restoration
//
// The caller must check for Status::IsIncomplete() to distinguish between
// "no resume needed" (proceed with `InternalIterator::SeekToFirst()`) vs
// "resume failed" scenarios.
Status CompactionJob::MaybeResumeSubcompactionProgressOnInputIterator(
SubcompactionState* sub_compact, InternalIterator* input_iter) {
const ReadOptions read_options(Env::IOActivity::kCompaction);
+15 -10
View File
@@ -142,6 +142,9 @@ class SubcompactionState;
class CompactionJob {
public:
// Constant false aborted flag, used for compaction service jobs
static const std::atomic<int> kCompactionAbortedFalse;
CompactionJob(int job_id, Compaction* compaction,
const ImmutableDBOptions& db_options,
const MutableDBOptions& mutable_db_options,
@@ -157,6 +160,7 @@ class CompactionJob {
Env::Priority thread_pri,
const std::shared_ptr<IOTracer>& io_tracer,
const std::atomic<bool>& manual_compaction_canceled,
const std::atomic<int>& compaction_aborted,
const std::string& db_id = "",
const std::string& db_session_id = "",
std::string full_history_ts_low = "", std::string trim_ts = "",
@@ -250,6 +254,10 @@ class CompactionJob {
// @param num_input_range_del if non-null, will be set to the number of range
// deletion entries in this compaction input.
//
// If any input file has potentially unreliable num_entries count (old SST
// files - details in implementation),
// job_stats_->has_accurate_num_input_records is set to false.
//
// Returns true iff internal_stats_.output_level_stats.num_input_records and
// num_input_range_del are calculated successfully.
//
@@ -299,6 +307,7 @@ class CompactionJob {
void RunSubcompactions();
void UpdateTimingStats(uint64_t start_micros);
void RemoveEmptyOutputs();
void CleanupAbortedSubcompactions();
bool HasNewBlobFiles() const;
Status CollectSubcompactionErrors();
Status SyncOutputDirectories();
@@ -363,11 +372,6 @@ class CompactionJob {
std::unique_ptr<InternalIterator> trim_history_iter;
};
struct BlobFileResources {
std::vector<std::string> blob_file_paths;
std::unique_ptr<BlobFileBuilder> blob_file_builder;
};
bool ShouldUseLocalCompaction(SubcompactionState* sub_compact);
CompactionIOStatsSnapshot InitializeIOStats();
Status SetupAndValidateCompactionFilter(
@@ -382,14 +386,14 @@ class CompactionJob {
SubcompactionState* sub_compact, ColumnFamilyData* cfd,
SubcompactionInternalIterators& iterators,
SubcompactionKeyBoundaries& boundaries, ReadOptions& read_options);
void CreateBlobFileBuilder(SubcompactionState* sub_compact,
ColumnFamilyData* cfd,
BlobFileResources& blob_resources,
const WriteOptions& write_options);
void CreateBlobFileBuilder(
SubcompactionState* sub_compact, ColumnFamilyData* cfd,
std::unique_ptr<BlobFileBuilder>& blob_file_builder,
const WriteOptions& write_options);
std::unique_ptr<CompactionIterator> CreateCompactionIterator(
SubcompactionState* sub_compact, ColumnFamilyData* cfd,
InternalIterator* input_iter, const CompactionFilter* compaction_filter,
MergeHelper& merge, BlobFileResources& blob_resources,
MergeHelper& merge, std::unique_ptr<BlobFileBuilder>& blob_file_builder,
const WriteOptions& write_options);
std::pair<CompactionFileOpenFunc, CompactionFileCloseFunc> CreateFileHandlers(
SubcompactionState* sub_compact, SubcompactionKeyBoundaries& boundaries);
@@ -461,6 +465,7 @@ class CompactionJob {
VersionSet* versions_;
const std::atomic<bool>* shutting_down_;
const std::atomic<bool>& manual_compaction_canceled_;
const std::atomic<int>& compaction_aborted_;
FSDirectory* db_directory_;
FSDirectory* blob_output_directory_;
InstrumentedMutex* db_mutex_;
+7 -7
View File
@@ -82,7 +82,7 @@ class CompactionJobStatsTest : public testing::Test,
std::string dbname_;
std::string alternative_wal_dir_;
Env* env_;
DB* db_;
std::unique_ptr<DB> db_;
std::vector<ColumnFamilyHandle*> handles_;
uint32_t max_subcompactions_;
@@ -123,7 +123,7 @@ class CompactionJobStatsTest : public testing::Test,
static void SetUpTestCase() {}
static void TearDownTestCase() {}
DBImpl* dbfull() { return static_cast_with_check<DBImpl>(db_); }
DBImpl* dbfull() { return static_cast_with_check<DBImpl>(db_.get()); }
void CreateColumnFamilies(const std::vector<std::string>& cfs,
const Options& options) {
@@ -162,7 +162,8 @@ class CompactionJobStatsTest : public testing::Test,
column_families.emplace_back(cfs[i], options[i]);
}
DBOptions db_opts = DBOptions(options[0]);
return DB::Open(db_opts, dbname_, column_families, &handles_, &db_);
auto s = DB::Open(db_opts, dbname_, column_families, &handles_, &db_);
return s;
}
Status TryReopenWithColumnFamilies(const std::vector<std::string>& cfs,
@@ -179,8 +180,7 @@ class CompactionJobStatsTest : public testing::Test,
delete h;
}
handles_.clear();
delete db_;
db_ = nullptr;
db_.reset();
}
void DestroyAndReopen(const Options& options) {
@@ -743,7 +743,7 @@ TEST_P(CompactionJobStatsTest, CompactionJobStatsTest) {
}
ASSERT_OK(Flush(1));
ASSERT_OK(static_cast_with_check<DBImpl>(db_)->TEST_WaitForCompact());
ASSERT_OK(dbfull()->TEST_WaitForCompact());
stats_checker->set_verify_next_comp_io_stats(true);
std::atomic<bool> first_prepare_write(true);
@@ -944,7 +944,7 @@ TEST_P(CompactionJobStatsTest, UniversalCompactionTest) {
start_key += key_base) {
MakeTableWithKeyValues(&rnd, start_key, start_key + key_base - 1, kKeySize,
kValueSize, key_interval, compression_ratio, 1);
ASSERT_OK(static_cast_with_check<DBImpl>(db_)->TEST_WaitForCompact());
ASSERT_OK(dbfull()->TEST_WaitForCompact());
}
ASSERT_EQ(stats_checker->NumberOfUnverifiedStats(), 0U);
}
+2 -455
View File
@@ -676,8 +676,8 @@ class CompactionJobTestBase : public testing::Test {
&event_logger, false, false, dbname_, &compaction_job_stats_,
Env::Priority::USER, nullptr /* IOTracer */,
/*manual_compaction_canceled=*/kManualCompactionCanceledFalse,
env_->GenerateUniqueId(), DBImpl::GenerateDbSessionId(nullptr),
full_history_ts_low_);
CompactionJob::kCompactionAbortedFalse, env_->GenerateUniqueId(),
DBImpl::GenerateDbSessionId(nullptr), full_history_ts_low_);
VerifyInitializationOfCompactionJobStats(compaction_job_stats_);
compaction_job.Prepare(std::nullopt /*subcompact to be computed*/);
@@ -2409,459 +2409,6 @@ TEST_F(CompactionJobIOPriorityTest, GetRateLimiterPriority) {
kMaxSequenceNumber, 1, false, {kInvalidBlobFileNumber}, true,
Env::IO_LOW, Env::IO_LOW);
}
class ResumableCompactionJobTest : public CompactionJobTestBase {
public:
ResumableCompactionJobTest()
: CompactionJobTestBase(
test::PerThreadDBPath("allow_resumption_job_test"),
BytewiseComparator(), [](uint64_t /*ts*/) { return ""; },
/*test_io_priority=*/false, TableTypeForTest::kBlockBasedTable) {}
protected:
static constexpr const char* kCancelBeforeThisKey = "cancel_before_this_key";
std::string progress_dir_;
bool enable_cancel_ = false;
std::atomic<int> stop_count_{0};
std::atomic<bool> cancel_{false};
SequenceNumber cancel_before_seqno = kMaxSequenceNumber;
void SetUp() override {
CompactionJobTestBase::SetUp();
SyncPoint::GetInstance()->SetCallBack(
"CompactionOutputs::ShouldStopBefore::manual_decision",
[this](void* p) {
auto* pair = static_cast<std::pair<bool*, const Slice>*>(p);
*(pair->first) = true;
// Cancel after outputting a specific key
if (enable_cancel_) {
ParsedInternalKey parsed_key;
if (ParseInternalKey(pair->second, &parsed_key, true).ok()) {
if (parsed_key.user_key == kCancelBeforeThisKey &&
(cancel_before_seqno == kMaxSequenceNumber ||
parsed_key.sequence == cancel_before_seqno)) {
cancel_.store(true);
}
}
}
});
SyncPoint::GetInstance()->EnableProcessing();
}
void TearDown() override {
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
if (env_->FileExists(progress_dir_).ok()) {
std::vector<std::string> files;
EXPECT_OK(env_->GetChildren(progress_dir_, &files));
for (const auto& file : files) {
if (file != "." && file != "..") {
EXPECT_OK(env_->DeleteFile(progress_dir_ + "/" + file));
}
}
EXPECT_OK(env_->DeleteDir(progress_dir_));
}
CompactionJobTestBase::TearDown();
}
void NewDB() {
if (env_->FileExists(progress_dir_).ok()) {
std::vector<std::string> files;
EXPECT_OK(env_->GetChildren(progress_dir_, &files));
for (const auto& file : files) {
if (file != "." && file != "..") {
EXPECT_OK(env_->DeleteFile(progress_dir_ + "/" + file));
}
}
EXPECT_OK(env_->DeleteDir(progress_dir_));
}
CompactionJobTestBase::NewDB();
progress_dir_ = test::PerThreadDBPath("compaction_progress");
ASSERT_OK(env_->CreateDirIfMissing(progress_dir_));
}
void EnableCompactionCancel() { enable_cancel_ = true; }
void DisableCompactionCancel() {
enable_cancel_ = false;
cancel_.store(false);
}
std::unique_ptr<log::Writer> CreateCompactionProgressWriter(
const std::string& compaction_progress_file) {
std::unique_ptr<FSWritableFile> file;
EXPECT_OK(fs_->NewWritableFile(compaction_progress_file, FileOptions(),
&file, nullptr));
auto file_writer = std::make_unique<WritableFileWriter>(
std::move(file), compaction_progress_file, FileOptions());
auto compaction_progress_writer =
std::make_unique<log::Writer>(std::move(file_writer), 0, false);
return compaction_progress_writer;
}
Status RunCompactionWithProgressTracking(
const CompactionProgress& compaction_progress,
log::Writer* compaction_progress_writer,
std::vector<SequenceNumber> snapshots = {},
std::shared_ptr<Statistics> stats = nullptr) {
mutex_.Lock();
auto cfd = versions_->GetColumnFamilySet()->GetDefault();
auto files = cfd->current()->storage_info()->LevelFiles(0);
db_options_.statistics = stats;
db_options_.stats = db_options_.statistics.get();
std::vector<CompactionInputFiles> compaction_input_files;
CompactionInputFiles level;
level.level = 0;
level.files = files;
compaction_input_files.push_back(level);
Compaction compaction(
cfd->current()->storage_info(), cfd->ioptions(),
cfd->GetLatestMutableCFOptions(), mutable_db_options_,
compaction_input_files, 1, mutable_cf_options_.target_file_size_base,
mutable_cf_options_.max_compaction_bytes, 0, kNoCompression,
cfd->GetLatestMutableCFOptions().compression_opts,
Temperature::kUnknown, 0, {}, std::nullopt, nullptr,
CompactionReason::kManualCompaction);
compaction.FinalizeInputInfo(cfd->current());
LogBuffer log_buffer(InfoLogLevel::INFO_LEVEL, db_options_.info_log.get());
EventLogger event_logger(db_options_.info_log.get());
JobContext job_context(1, false);
job_context.InitSnapshotContext(nullptr, nullptr, kMaxSequenceNumber,
std::move(snapshots));
CompactionJobStats job_stats;
CompactionJob compaction_job(
0, &compaction, db_options_, mutable_db_options_, env_options_,
versions_.get(), &shutting_down_, &log_buffer, nullptr, nullptr,
nullptr, stats.get(), &mutex_, &error_handler_, &job_context,
table_cache_, &event_logger, false, false, dbname_, &job_stats,
Env::Priority::USER, nullptr, cancel_, env_->GenerateUniqueId(),
DBImpl::GenerateDbSessionId(nullptr), "");
compaction_job.Prepare(std::nullopt, compaction_progress,
compaction_progress_writer);
mutex_.Unlock();
compaction_job.Run().PermitUncheckedError();
EXPECT_OK(compaction_job.io_status());
mutex_.Lock();
bool compaction_released = false;
Status s = compaction_job.Install(&compaction_released);
mutex_.Unlock();
if (!compaction_released) {
compaction.ReleaseCompactionFiles(s);
}
return s;
}
SubcompactionProgress ReadAndParseProgress(
const std::string& compaction_progress_file) {
std::unique_ptr<FSSequentialFile> seq_file;
EXPECT_OK(fs_->NewSequentialFile(compaction_progress_file, FileOptions(),
&seq_file, nullptr));
auto file_reader = std::make_unique<SequentialFileReader>(
std::move(seq_file), compaction_progress_file, 0, nullptr);
log::Reader reader(nullptr, std::move(file_reader), nullptr, true, 0);
SubcompactionProgressBuilder builder;
std::string record;
Slice slice;
while (reader.ReadRecord(&slice, &record)) {
VersionEdit edit;
if (!edit.DecodeFrom(slice).ok()) {
continue;
}
builder.ProcessVersionEdit(edit);
}
EXPECT_TRUE(builder.HasAccumulatedSubcompactionProgress());
return builder.GetAccumulatedSubcompactionProgress();
}
// Test utility function to verify that compaction progress was correctly
// persisted to the progress file after compaction interruption.
//
// VERIFIES:
// - Progress file exists and has expected size (empty if no progress
// expected)
// - Next internal key to compact matches expected user key with proper format
// - Number of processed input records matches position in ordered input keys
// - Number of processed output records equals number of processed input
// records (by test design to simplify verification)
// - Each output file contains exactly one user key (by test design to
// simplify verification)
void VerifyCompactionProgressPersisted(
const std::string& compaction_progress_file,
const std::string& next_user_key_to_compact,
const std::vector<std::string>& ordered_intput_keys) {
ASSERT_OK(env_->FileExists(compaction_progress_file));
uint64_t file_size;
ASSERT_OK(env_->GetFileSize(compaction_progress_file, &file_size));
if (next_user_key_to_compact.empty()) {
ASSERT_EQ(file_size, 0);
return;
}
const auto& subcompaction_progress =
ReadAndParseProgress(compaction_progress_file);
ASSERT_FALSE(subcompaction_progress.next_internal_key_to_compact.empty());
ParsedInternalKey parsed_next_key;
ASSERT_OK(
ParseInternalKey(subcompaction_progress.next_internal_key_to_compact,
&parsed_next_key, true /* log_err_key */));
ASSERT_EQ(parsed_next_key.user_key, next_user_key_to_compact);
ASSERT_EQ(parsed_next_key.sequence, kMaxSequenceNumber);
ASSERT_EQ(parsed_next_key.type, kValueTypeForSeek);
auto it = std::find(ordered_intput_keys.begin(), ordered_intput_keys.end(),
next_user_key_to_compact);
ASSERT_TRUE(it != ordered_intput_keys.end());
auto next_key_index = std::distance(ordered_intput_keys.begin(), it);
ASSERT_EQ(subcompaction_progress.num_processed_input_records,
next_key_index);
ASSERT_EQ(subcompaction_progress.output_level_progress
.GetNumProcessedOutputRecords(),
next_key_index);
ASSERT_EQ(
subcompaction_progress.output_level_progress.GetOutputFiles().size(),
next_key_index);
for (size_t i = 0;
i <
subcompaction_progress.output_level_progress.GetOutputFiles().size();
++i) {
const auto& output_file =
subcompaction_progress.output_level_progress.GetOutputFiles()[i];
ASSERT_EQ(output_file.smallest.user_key().ToString(),
output_file.largest.user_key().ToString());
ASSERT_EQ(output_file.largest.user_key().ToString(),
ordered_intput_keys[i]);
}
}
void RunCancelAndResumeTest(
const std::initializer_list<mock::KVPair>& input_file_1,
const std::initializer_list<mock::KVPair>& input_file_2,
uint64_t last_sequence, const std::vector<uint64_t>& snapshots,
const std::string& expected_next_key_to_compact,
const std::vector<std::string>& expected_input_keys,
bool cancelled_past_mid_point = false) {
std::shared_ptr<Statistics> stats = ROCKSDB_NAMESPACE::CreateDBStatistics();
auto file1 = mock::MakeMockFile(input_file_1);
AddMockFile(file1);
auto file2 = mock::MakeMockFile(input_file_2);
AddMockFile(file2);
SetLastSequence(last_sequence);
// First compaction (will be cancelled)
std::string compaction_progress_file =
CompactionProgressFileName(progress_dir_, 123);
std::unique_ptr<log::Writer> compaction_progress_writer =
CreateCompactionProgressWriter(compaction_progress_file);
ASSERT_OK(stats->Reset());
EnableCompactionCancel();
Status status = RunCompactionWithProgressTracking(
CompactionProgress{}, compaction_progress_writer.get(), snapshots,
stats);
ASSERT_TRUE(status.IsManualCompactionPaused());
DisableCompactionCancel();
HistogramData cancelled_compaction_stats;
stats->histogramData(FILE_WRITE_COMPACTION_MICROS,
&cancelled_compaction_stats);
VerifyCompactionProgressPersisted(compaction_progress_file,
expected_next_key_to_compact,
expected_input_keys);
// Resume compaction
CompactionProgress compaction_progress;
if (expected_next_key_to_compact != "") {
compaction_progress.push_back(
ReadAndParseProgress(compaction_progress_file));
}
std::string compaction_progress_file_2 =
CompactionProgressFileName(progress_dir_, 234);
std::unique_ptr<log::Writer> compaction_progress_writer_2 =
CreateCompactionProgressWriter(compaction_progress_file_2);
ASSERT_OK(stats->Reset());
status = RunCompactionWithProgressTracking(
compaction_progress, compaction_progress_writer_2.get(),
{} /* snapshots */, stats);
ASSERT_OK(status);
if (cancelled_past_mid_point) {
HistogramData resumed_compaction_stats;
stats->histogramData(FILE_WRITE_COMPACTION_MICROS,
&resumed_compaction_stats);
ASSERT_GT(cancelled_compaction_stats.count,
resumed_compaction_stats.count);
}
}
};
TEST_F(ResumableCompactionJobTest, BasicProgressPersistence) {
NewDB();
auto file1 = mock::MakeMockFile({
{KeyStr("a", 1U, kTypeValue), "val1"},
{KeyStr("b", 2U, kTypeValue), "val2"},
});
AddMockFile(file1);
auto file2 = mock::MakeMockFile({
{KeyStr("c", 3U, kTypeValue), "val3"},
{KeyStr("d", 4U, kTypeValue), "val4"},
});
AddMockFile(file2);
SetLastSequence(4U);
std::string compaction_progress_file =
CompactionProgressFileName(progress_dir_, 123);
std::unique_ptr<log::Writer> compaction_progress_writer =
CreateCompactionProgressWriter(compaction_progress_file);
Status status = RunCompactionWithProgressTracking(
CompactionProgress(), compaction_progress_writer.get());
ASSERT_OK(status);
VerifyCompactionProgressPersisted(
compaction_progress_file, "d" /* next_user_key_to_compact */,
{"a", "b", "c", "d"} /* ordered_intput_keys */);
}
TEST_F(ResumableCompactionJobTest, BasicProgressResume) {
NewDB();
RunCancelAndResumeTest(
{{KeyStr("a", 1U, kTypeValue), "val1"},
{KeyStr("b", 2U, kTypeValue), "val2"}} /* input_file_1 */,
{{KeyStr("bb", 3U, kTypeValue), "val3"},
{KeyStr(kCancelBeforeThisKey, 4U, kTypeValue),
"val4"}} /* input_file_2 */,
4U /* last_sequence */, {} /* snapshots */,
kCancelBeforeThisKey /* expected_next_key_to_compact */,
{"a", "b", "bb", kCancelBeforeThisKey} /* expected_input_keys */,
true /* cancelled_past_mid_point */);
}
TEST_F(ResumableCompactionJobTest, NoProgressResumeOnSameKey) {
NewDB();
// `cancel_before_seqno` is set to 0U to force cancellation after
// `kCancelBeforeThisKey@1` instead of `kCancelBeforeThisKey@2`.
// The seqno is 0 because `kCancelBeforeThisKey@1` will have its sequence
// number zeroed during compaction while `kCancelBeforeThisKey@2` won't be
cancel_before_seqno = 0U;
RunCancelAndResumeTest(
{{KeyStr(kCancelBeforeThisKey, 1U, kTypeValue),
"val1"}} /* input_file_1 */,
{{KeyStr(kCancelBeforeThisKey, 2U, kTypeValue), "val11"},
{KeyStr("d", 3U, kTypeValue), "val2"}} /* input_file_2 */,
3U /* last_sequence */, {1U} /* snapshots */,
"" /* expected_next_key_to_compact */,
{kCancelBeforeThisKey, kCancelBeforeThisKey,
"d"} /* expected_input_keys */);
}
TEST_F(ResumableCompactionJobTest, NoProgressResumeOnDeleteRange) {
NewDB();
RunCancelAndResumeTest(
{{KeyStr("a", 1U, kTypeValue), "val1"},
{KeyStr("b", 2U, kTypeValue), "val2"},
{KeyStr(kCancelBeforeThisKey, 3U, kTypeValue),
"val3"}} /* input_file_1 */,
{{KeyStr(kCancelBeforeThisKey, 4U, kTypeRangeDeletion),
"range_deletion_end_key"},
{KeyStr("d", 5U, kTypeValue), "val4"}} /* input_file_2 */,
5U /* last_sequence */, {3U} /* snapshots */,
"b" /* expected_next_key_to_compact */,
{"a", "b", kCancelBeforeThisKey, kCancelBeforeThisKey,
"d"} /* expected_input_keys */);
}
TEST_F(ResumableCompactionJobTest, NoProgressResumeOnMerge) {
merge_op_ = MergeOperators::CreateStringAppendOperator();
NewDB();
RunCancelAndResumeTest(
{{KeyStr("a", 1U, kTypeValue), "val1"},
{KeyStr("b", 2U, kTypeValue), "val2"}} /* input_file_1 */,
{{KeyStr("bb", 3U, kTypeValue), "val3"},
{KeyStr(kCancelBeforeThisKey, 4U, kTypeMerge),
"val4"}} /* input_file_2 */,
4U /* last_sequence */, {} /* snapshots */,
"bb" /* expected_next_key_to_compact */,
{"a", "b", "bb", kCancelBeforeThisKey} /* expected_input_keys */);
}
TEST_F(ResumableCompactionJobTest, NoProgressResumeOnSingleDelete) {
NewDB();
RunCancelAndResumeTest(
{{KeyStr("a", 1U, kTypeValue), "val1"},
{KeyStr("b", 2U, kTypeValue), "val2"},
{KeyStr(kCancelBeforeThisKey, 3U, kTypeValue),
"val3"}} /* input_file_1 */,
{{KeyStr(kCancelBeforeThisKey, 4U, kTypeSingleDeletion), ""},
{KeyStr("d", 5U, kTypeValue), "val4"}} /* input_file_2 */,
5U /* last_sequence */, {3U} /* snapshots */,
"b" /* expected_next_key_to_compact */,
{"a", "b", kCancelBeforeThisKey, kCancelBeforeThisKey,
"d"} /* expected_input_keys */);
}
TEST_F(ResumableCompactionJobTest, NoProgressResumeOnDeletionAtBottom) {
NewDB();
RunCancelAndResumeTest(
{{KeyStr("a", 1U, kTypeValue), "val1"},
{KeyStr("b", 2U, kTypeValue), "val2"},
{KeyStr(kCancelBeforeThisKey, 3U, kTypeValue),
"val3"}} /* input_file_1 */,
{{KeyStr(kCancelBeforeThisKey, 4U, kTypeDeletion), ""},
{KeyStr("d", 5U, kTypeValue), "val4"}} /* input_file_2 */,
5U /* last_sequence */, {3U} /* snapshots */,
"b" /* expected_next_key_to_compact */,
{"a", "b", kCancelBeforeThisKey, kCancelBeforeThisKey,
"d"} /* expected_input_keys */);
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
+4 -2
View File
@@ -49,8 +49,10 @@ Status CompactionOutputs::Finish(
meta->fd.file_size = current_bytes;
meta->tail_size = builder_->GetTailSize();
meta->marked_for_compaction = builder_->NeedCompact();
meta->user_defined_timestamps_persisted = static_cast<bool>(
builder_->GetTableProperties().user_defined_timestamps_persisted);
const TableProperties& tp = builder_->GetTableProperties();
meta->user_defined_timestamps_persisted =
static_cast<bool>(tp.user_defined_timestamps_persisted);
ExtractTimestampFromTableProperties(tp, meta);
}
current_output().finished = true;
stats_.bytes_written += current_bytes;
+19
View File
@@ -84,6 +84,19 @@ class CompactionOutputs {
bool HasBlobFileAdditions() const { return !blob_file_additions_.empty(); }
// Get all file paths (SST and blob) created during compaction.
const std::vector<std::string>& GetOutputFilePaths() const {
return output_file_paths_;
}
std::vector<std::string>* GetOutputFilePathsPtr() {
return &output_file_paths_;
}
void AddOutputFilePath(const std::string& path) {
output_file_paths_.push_back(path);
}
BlobGarbageMeter* CreateBlobGarbageMeter() {
assert(!is_proximal_level_);
blob_garbage_meter_ = std::make_unique<BlobGarbageMeter>();
@@ -321,6 +334,12 @@ class CompactionOutputs {
std::vector<BlobFileAddition> blob_file_additions_;
std::unique_ptr<BlobGarbageMeter> blob_garbage_meter_;
// All file paths (SST and blob) created during compaction.
// Used for cleanup on abort - ensures orphan files are deleted even if
// they were removed from outputs_ or blob_file_additions_ (e.g., by
// RemoveLastEmptyOutput when file_size is 0 because builder was abandoned).
std::vector<std::string> output_file_paths_;
// Per level's output stat
InternalStats::CompactionStats stats_;
+72 -9
View File
@@ -27,12 +27,68 @@
namespace ROCKSDB_NAMESPACE {
bool FindIntraL0Compaction(const std::vector<FileMetaData*>& level_files,
size_t min_files_to_compact,
uint64_t max_compact_bytes_per_del_file,
uint64_t max_compaction_bytes,
CompactionInputFiles* comp_inputs) {
TEST_SYNC_POINT("FindIntraL0Compaction");
#ifndef NDEBUG
static void AssertCleanCut(const InternalKeyComparator* icmp,
VersionStorageInfo* vstorage,
CompactionInputFiles* inputs, int level,
Logger* logger) {
const std::vector<FileMetaData*>& level_files = vstorage->LevelFiles(level);
if (inputs->files.empty() || level_files.empty()) {
return;
}
const Comparator* ucmp = icmp->user_comparator();
// Find first and last input file indices in level
int first_input_idx = -1;
int last_input_idx = -1;
for (size_t i = 0; i < level_files.size(); i++) {
if (level_files[i] == inputs->files.front()) {
first_input_idx = static_cast<int>(i);
}
if (level_files[i] == inputs->files.back()) {
last_input_idx = static_cast<int>(i);
}
}
// Check file before first input
if (first_input_idx > 0) {
const FileMetaData* prev_file = level_files[first_input_idx - 1];
const FileMetaData* first_file = inputs->files.front();
int cmp = sstableKeyCompare(ucmp, prev_file->largest, first_file->smallest);
if (cmp == 0) {
ROCKS_LOG_ERROR(logger,
"Clean cut violated: L%d unselected file %" PRIu64
" adjacent to first selected file %" PRIu64,
level, prev_file->fd.GetNumber(),
first_file->fd.GetNumber());
assert(false);
}
}
// Check file after last input
if (last_input_idx >= 0 &&
static_cast<size_t>(last_input_idx) < level_files.size() - 1) {
const FileMetaData* last_file = inputs->files.back();
const FileMetaData* next_file = level_files[last_input_idx + 1];
int cmp = sstableKeyCompare(ucmp, last_file->largest, next_file->smallest);
if (cmp == 0) {
ROCKS_LOG_ERROR(logger,
"Clean cut violated: L%d unselected file %" PRIu64
" adjacent to last selected file %" PRIu64,
level, next_file->fd.GetNumber(),
last_file->fd.GetNumber());
assert(false);
}
}
}
#endif // NDEBUG
bool PickCostBasedIntraL0Compaction(
const std::vector<FileMetaData*>& level_files, size_t min_files_to_compact,
uint64_t max_compact_bytes_per_del_file, uint64_t max_compaction_bytes,
CompactionInputFiles* comp_inputs) {
TEST_SYNC_POINT("PickCostBasedIntraL0Compaction");
size_t start = 0;
@@ -250,6 +306,10 @@ bool CompactionPicker::ExpandInputsToCleanCut(const std::string& /*cf_name*/,
// inputs. thus, inputs should be non-empty here
assert(!inputs->empty());
#ifndef NDEBUG
AssertCleanCut(icmp_, vstorage, inputs, level, ioptions_.logger);
#endif // NDEBUG
// If, after the expansion, there are files that are already under
// compaction, then we must drop/cancel this compaction.
if (AreFilesInCompaction(inputs->files)) {
@@ -611,7 +671,8 @@ Compaction* CompactionPicker::PickCompactionForCompactRange(
int input_level, int output_level,
const CompactRangeOptions& compact_range_options, const InternalKey* begin,
const InternalKey* end, InternalKey** compaction_end, bool* manual_conflict,
uint64_t max_file_num_to_ignore, const std::string& trim_ts) {
uint64_t max_file_num_to_ignore, const std::string& trim_ts,
const std::string& full_history_ts_low) {
// CompactionPickerFIFO has its own implementation of compact range
assert(ioptions_.compaction_style != kCompactionStyleFIFO);
@@ -690,7 +751,8 @@ Compaction* CompactionPicker::PickCompactionForCompactRange(
compact_range_options.blob_garbage_collection_age_cutoff);
RegisterCompaction(c);
vstorage->ComputeCompactionScore(ioptions_, mutable_cf_options);
vstorage->ComputeCompactionScore(ioptions_, mutable_cf_options,
full_history_ts_low);
return c;
}
@@ -887,7 +949,8 @@ Compaction* CompactionPicker::PickCompactionForCompactRange(
// takes running compactions into account (by skipping files that are already
// being compacted). Since we just changed compaction score, we recalculate it
// here
vstorage->ComputeCompactionScore(ioptions_, mutable_cf_options);
vstorage->ComputeCompactionScore(ioptions_, mutable_cf_options,
full_history_ts_low);
return compaction;
}
+12 -10
View File
@@ -65,7 +65,8 @@ class CompactionPicker {
const MutableDBOptions& mutable_db_options,
const std::vector<SequenceNumber>& existing_snapshots,
const SnapshotChecker* snapshot_checker, VersionStorageInfo* vstorage,
LogBuffer* log_buffer, bool require_max_output_level) = 0;
LogBuffer* log_buffer, const std::string& full_history_ts_low,
bool require_max_output_level = false) = 0;
// The returned Compaction might not include the whole requested range.
// In that case, compaction_end will be set to the next key that needs
@@ -82,7 +83,8 @@ class CompactionPicker {
const CompactRangeOptions& compact_range_options,
const InternalKey* begin, const InternalKey* end,
InternalKey** compaction_end, bool* manual_conflict,
uint64_t max_file_num_to_ignore, const std::string& trim_ts);
uint64_t max_file_num_to_ignore, const std::string& trim_ts,
const std::string& full_history_ts_low);
// The maximum allowed output level. Default value is NumberLevels() - 1.
virtual int MaxOutputLevel() const { return NumberLevels() - 1; }
@@ -284,7 +286,8 @@ class NullCompactionPicker : public CompactionPicker {
const std::vector<SequenceNumber>& /*existing_snapshots*/,
const SnapshotChecker* /*snapshot_checker*/,
VersionStorageInfo* /*vstorage*/, LogBuffer* /* log_buffer */,
bool /*require_max_output_level*/ = false) override {
const std::string& /*full_history_ts_low*/,
bool /*require_max_output_level*/) override {
return nullptr;
}
@@ -298,8 +301,8 @@ class NullCompactionPicker : public CompactionPicker {
const CompactRangeOptions& /*compact_range_options*/,
const InternalKey* /*begin*/, const InternalKey* /*end*/,
InternalKey** /*compaction_end*/, bool* /*manual_conflict*/,
uint64_t /*max_file_num_to_ignore*/,
const std::string& /*trim_ts*/) override {
uint64_t /*max_file_num_to_ignore*/, const std::string& /*trim_ts*/,
const std::string& /*full_history_ts_low*/) override {
return nullptr;
}
@@ -325,11 +328,10 @@ class NullCompactionPicker : public CompactionPicker {
// files. Cannot be nullptr.
//
// @return true iff compaction was found.
bool FindIntraL0Compaction(const std::vector<FileMetaData*>& level_files,
size_t min_files_to_compact,
uint64_t max_compact_bytes_per_del_file,
uint64_t max_compaction_bytes,
CompactionInputFiles* comp_inputs);
bool PickCostBasedIntraL0Compaction(
const std::vector<FileMetaData*>& level_files, size_t min_files_to_compact,
uint64_t max_compact_bytes_per_del_file, uint64_t max_compaction_bytes,
CompactionInputFiles* comp_inputs);
CompressionType GetCompressionType(const VersionStorageInfo* vstorage,
const MutableCFOptions& mutable_cf_options,
+404 -82
View File
@@ -9,6 +9,7 @@
#include "db/compaction/compaction_picker_fifo.h"
#include <algorithm>
#include <cinttypes>
#include <string>
#include <vector>
@@ -31,6 +32,29 @@ uint64_t GetTotalFilesSize(const std::vector<FileMetaData*>& files) {
}
return total_size;
}
// Compute effective data size and capacity limit for FIFO compaction.
// When max_data_files_size > 0 (blob-aware mode), the effective size includes
// both SST and blob file sizes, and the limit is max_data_files_size.
// Otherwise, only SST sizes are used with max_table_files_size as the limit.
void GetEffectiveSizeAndLimit(const CompactionOptionsFIFO& fifo_opts,
uint64_t total_sst_size, uint64_t total_blob_size,
uint64_t* effective_size,
uint64_t* effective_max) {
*effective_size = total_sst_size;
*effective_max = fifo_opts.max_table_files_size;
if (fifo_opts.max_data_files_size > 0) {
*effective_size += total_blob_size;
*effective_max = fifo_opts.max_data_files_size;
}
}
// Return the effective capacity limit for FIFO compaction.
// Convenience wrapper when only the limit is needed (e.g., PickTTLCompaction).
uint64_t GetEffectiveMax(const CompactionOptionsFIFO& fifo_opts) {
return fifo_opts.max_data_files_size > 0 ? fifo_opts.max_data_files_size
: fifo_opts.max_table_files_size;
}
} // anonymous namespace
bool FIFOCompactionPicker::NeedsCompaction(
@@ -78,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;
@@ -98,10 +122,43 @@ Compaction* FIFOCompactionPicker::PickTTLCompaction(
// Return a nullptr and proceed to size-based FIFO compaction if:
// 1. there are no files older than ttl OR
// 2. there are a few files older than ttl, but deleting them will not bring
// the total size to be less than max_table_files_size threshold.
if (inputs[0].files.empty() ||
total_size >
mutable_cf_options.compaction_options_fifo.max_table_files_size) {
// the total size to be less than the size threshold.
uint64_t effective_max =
GetEffectiveMax(mutable_cf_options.compaction_options_fifo);
// Estimate the effective remaining data after dropping TTL-expired SSTs.
// Each dropped SST also frees a proportional share of blob data.
//
// In multi-level FIFO (migration), we must use total SST across ALL levels
// as the reference, because total_blob covers all levels. Using only L0
// SST would inflate the blob estimate.
uint64_t effective_remaining = total_size;
if (mutable_cf_options.compaction_options_fifo.max_data_files_size > 0) {
uint64_t total_blob = vstorage->GetBlobStats().total_file_size;
// Compute total SST across all levels so the reference scope matches
// total_blob's scope (all levels).
uint64_t total_sst_all_levels = GetTotalFilesSize(level_files);
for (int level = 1; level < vstorage->num_levels(); ++level) {
total_sst_all_levels += GetTotalFilesSize(vstorage->LevelFiles(level));
}
// remaining_sst_all = total_sst_all - dropped_l0_sst
// total_size is the remaining L0 SST after removing expired files;
// original L0 SST minus remaining L0 SST = dropped.
uint64_t original_l0_sst = GetTotalFilesSize(level_files);
uint64_t dropped_sst = original_l0_sst - total_size;
uint64_t remaining_sst_all = total_sst_all_levels - dropped_sst;
// Proportional blob estimate: each SST byte "owns" a proportional
// share of blob bytes. Both reference sizes must come from the same
// scope (all levels) to avoid inflated estimates.
if (total_sst_all_levels > 0 && total_blob > 0) {
effective_remaining =
remaining_sst_all +
static_cast<uint64_t>(static_cast<double>(remaining_sst_all) /
total_sst_all_levels * total_blob);
} else {
effective_remaining = remaining_sst_all;
}
}
if (inputs[0].files.empty() || effective_remaining > effective_max) {
return nullptr;
}
@@ -109,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
@@ -151,7 +209,9 @@ Compaction* FIFOCompactionPicker::PickSizeCompaction(
const std::string& cf_name, const MutableCFOptions& mutable_cf_options,
const MutableDBOptions& mutable_db_options, VersionStorageInfo* vstorage,
LogBuffer* log_buffer) {
// compute the total size and identify the last non-empty level
const auto& fifo_opts = mutable_cf_options.compaction_options_fifo;
// compute the total SST size and identify the last non-empty level
int last_level = 0;
uint64_t total_size = 0;
for (int level = 0; level < vstorage->num_levels(); ++level) {
@@ -164,52 +224,13 @@ Compaction* FIFOCompactionPicker::PickSizeCompaction(
const std::vector<FileMetaData*>& last_level_files =
vstorage->LevelFiles(last_level);
if (last_level == 0 &&
total_size <=
mutable_cf_options.compaction_options_fifo.max_table_files_size) {
// total size not exceeded, try to find intra level 0 compaction if enabled
const std::vector<FileMetaData*>& level0_files = vstorage->LevelFiles(0);
if (mutable_cf_options.compaction_options_fifo.allow_compaction &&
level0_files.size() > 0) {
CompactionInputFiles comp_inputs;
// try to prevent same files from being compacted multiple times, which
// could produce large files that may never TTL-expire. Achieve this by
// disallowing compactions with files larger than memtable (inflate its
// size by 10% to account for uncompressed L0 files that may have size
// slightly greater than memtable size limit).
size_t max_compact_bytes_per_del_file =
static_cast<size_t>(MultiplyCheckOverflow(
static_cast<uint64_t>(mutable_cf_options.write_buffer_size),
1.1));
if (FindIntraL0Compaction(
level0_files,
mutable_cf_options
.level0_file_num_compaction_trigger /* min_files_to_compact */
,
max_compact_bytes_per_del_file,
mutable_cf_options.max_compaction_bytes, &comp_inputs)) {
Compaction* c = new Compaction(
vstorage, ioptions_, mutable_cf_options, mutable_db_options,
{comp_inputs}, 0, 16 * 1024 * 1024 /* output file size limit */,
0 /* max compaction bytes, not applicable */,
0 /* output path ID */, mutable_cf_options.compression,
mutable_cf_options.compression_opts, Temperature::kUnknown,
0 /* max_subcompactions */, {},
/* earliest_snapshot */ std::nullopt,
/* snapshot_checker */ nullptr,
CompactionReason::kFIFOReduceNumFiles,
/* trim_ts */ "", vstorage->CompactionScore(0),
/* l0_files_might_overlap */ true);
return c;
}
}
// Compute effective size and limit for comparison.
uint64_t effective_size, effective_max;
GetEffectiveSizeAndLimit(fifo_opts, total_size,
vstorage->GetBlobStats().total_file_size,
&effective_size, &effective_max);
ROCKS_LOG_BUFFER(
log_buffer,
"[%s] FIFO compaction: nothing to do. Total size %" PRIu64
", max size %" PRIu64 "\n",
cf_name.c_str(), total_size,
mutable_cf_options.compaction_options_fifo.max_table_files_size);
if (last_level == 0 && effective_size <= effective_max) {
return nullptr;
}
@@ -227,11 +248,29 @@ Compaction* FIFOCompactionPicker::PickSizeCompaction(
inputs[0].level = last_level;
if (last_level == 0) {
// When using blob-aware sizing, use proportional estimation (same
// principle as EstimateTotalDataForSST): each SST "owns"
// effective_size / num_files of total data. This is an approximation
// — individual SSTs may reference different amounts of blob data,
// but uniform distribution is a reasonable estimate for FIFO dropping.
uint64_t remaining_size = effective_size;
const uint64_t num_files = last_level_files.size();
// Proportional estimate of data per file (SST + blob).
// Use max(1) to prevent stalling when effective_size < num_files.
const uint64_t data_per_file =
(fifo_opts.max_data_files_size > 0 && num_files > 0)
? std::max(effective_size / num_files, uint64_t{1})
: 0;
// In L0, right-most files are the oldest files.
for (auto ritr = last_level_files.rbegin(); ritr != last_level_files.rend();
++ritr) {
auto f = *ritr;
total_size -= f->fd.file_size;
if (fifo_opts.max_data_files_size > 0) {
remaining_size -= std::min(remaining_size, data_per_file);
} else {
remaining_size -= std::min(remaining_size, f->fd.file_size);
}
inputs[0].files.push_back(f);
char tmp_fsize[16];
AppendHumanBytes(f->fd.GetFileSize(), tmp_fsize, sizeof(tmp_fsize));
@@ -239,13 +278,11 @@ Compaction* FIFOCompactionPicker::PickSizeCompaction(
"[%s] FIFO compaction: picking file %" PRIu64
" with size %s for deletion",
cf_name.c_str(), f->fd.GetNumber(), tmp_fsize);
if (total_size <=
mutable_cf_options.compaction_options_fifo.max_table_files_size) {
if (remaining_size <= effective_max) {
break;
}
}
} else if (total_size >
mutable_cf_options.compaction_options_fifo.max_table_files_size) {
} else if (effective_size > effective_max) {
// If the last level is non-L0, we actually don't know which file is
// logically the oldest since the file creation time only represents
// when this file was compacted to this level, which is independent
@@ -255,34 +292,36 @@ Compaction* FIFOCompactionPicker::PickSizeCompaction(
// file with the smallest key will be deleted first. This design decision
// better serves a major type of FIFO use cases where smaller keys are
// associated with older data.
const uint64_t num_files = last_level_files.size();
// Proportional estimate of data per file (SST + blob), same as L0 path.
const uint64_t data_per_file =
(fifo_opts.max_data_files_size > 0 && num_files > 0)
? std::max(effective_size / num_files, uint64_t{1})
: 0;
for (const auto& f : last_level_files) {
if (f->being_compacted) {
continue;
}
total_size -= f->fd.file_size;
if (fifo_opts.max_data_files_size > 0) {
effective_size -= std::min(effective_size, data_per_file);
} else {
effective_size -= std::min(effective_size, f->fd.file_size);
}
inputs[0].files.push_back(f);
char tmp_fsize[16];
AppendHumanBytes(f->fd.GetFileSize(), tmp_fsize, sizeof(tmp_fsize));
ROCKS_LOG_BUFFER(
log_buffer,
"[%s] FIFO compaction: picking file %" PRIu64
" with size %s for deletion under total size %" PRIu64
" vs max table files size %" PRIu64,
cf_name.c_str(), f->fd.GetNumber(), tmp_fsize, total_size,
mutable_cf_options.compaction_options_fifo.max_table_files_size);
ROCKS_LOG_BUFFER(log_buffer,
"[%s] FIFO compaction: picking file %" PRIu64
" with size %s for deletion under total size %" PRIu64
" vs max size %" PRIu64,
cf_name.c_str(), f->fd.GetNumber(), tmp_fsize,
effective_size, effective_max);
if (total_size <=
mutable_cf_options.compaction_options_fifo.max_table_files_size) {
if (effective_size <= effective_max) {
break;
}
}
} else {
ROCKS_LOG_BUFFER(
log_buffer,
"[%s] FIFO compaction: nothing to do. Total size %" PRIu64
", max size %" PRIu64 "\n",
cf_name.c_str(), total_size,
mutable_cf_options.compaction_options_fifo.max_table_files_size);
return nullptr;
}
@@ -419,12 +458,282 @@ Compaction* FIFOCompactionPicker::PickTemperatureChangeCompaction(
return c;
}
Compaction* FIFOCompactionPicker::PickIntraL0Compaction(
const std::string& cf_name, const MutableCFOptions& mutable_cf_options,
const MutableDBOptions& mutable_db_options, VersionStorageInfo* vstorage,
LogBuffer* log_buffer) {
const auto& fifo_opts = mutable_cf_options.compaction_options_fifo;
if (!fifo_opts.allow_compaction) {
return nullptr;
}
const std::vector<FileMetaData*>& level0_files = vstorage->LevelFiles(0);
if (level0_files.empty()) {
return nullptr;
}
if (fifo_opts.use_kv_ratio_compaction) {
if (fifo_opts.max_data_files_size == 0) {
ROCKS_LOG_BUFFER(
log_buffer,
"[%s] FIFO kv-ratio compaction: skipping — "
"max_data_files_size is 0, cannot compute target file size. ",
cf_name.c_str());
} else if (fifo_opts.max_data_files_size < fifo_opts.max_table_files_size) {
ROCKS_LOG_BUFFER(log_buffer,
"[%s] FIFO kv-ratio compaction: skipping — "
"max_data_files_size (%" PRIu64
") < max_table_files_size "
"(%" PRIu64 ").",
cf_name.c_str(), fifo_opts.max_data_files_size,
fifo_opts.max_table_files_size);
} else {
return PickRatioBasedIntraL0Compaction(cf_name, mutable_cf_options,
mutable_db_options, vstorage,
log_buffer);
}
ROCKS_LOG_BUFFER(
log_buffer, "[%s] FIFO: falling back to cost-based intra-L0 compaction",
cf_name.c_str());
}
// Old intra-L0 path: merge small files using PickCostBasedIntraL0Compaction.
// Minimum files to compact follows level0_file_num_compaction_trigger.
// Try to prevent same files from being compacted multiple times, which
// could produce large files that may never TTL-expire. Achieve this by
// disallowing compactions with files larger than memtable (inflate its
// size by 10% to account for uncompressed L0 files that may have size
// slightly greater than memtable size limit).
CompactionInputFiles comp_inputs;
size_t max_compact_bytes_per_del_file =
static_cast<size_t>(MultiplyCheckOverflow(
static_cast<uint64_t>(mutable_cf_options.write_buffer_size), 1.1));
if (PickCostBasedIntraL0Compaction(
level0_files,
mutable_cf_options
.level0_file_num_compaction_trigger /* min_files_to_compact */,
max_compact_bytes_per_del_file,
mutable_cf_options.max_compaction_bytes, &comp_inputs)) {
Compaction* c = new Compaction(
vstorage, ioptions_, mutable_cf_options, mutable_db_options,
{comp_inputs}, 0, 16 * 1024 * 1024 /* output file size limit */,
0 /* max compaction bytes, not applicable */, 0 /* output path ID */,
mutable_cf_options.compression, mutable_cf_options.compression_opts,
Temperature::kUnknown, 0 /* max_subcompactions */, {},
/* earliest_snapshot */ std::nullopt,
/* snapshot_checker */ nullptr, CompactionReason::kFIFOReduceNumFiles,
/* trim_ts */ "", vstorage->CompactionScore(0),
/* l0_files_might_overlap */ true);
return c;
}
return nullptr;
}
Compaction* FIFOCompactionPicker::PickRatioBasedIntraL0Compaction(
const std::string& cf_name, const MutableCFOptions& mutable_cf_options,
const MutableDBOptions& mutable_db_options, VersionStorageInfo* vstorage,
LogBuffer* log_buffer) {
const auto& fifo_opts = mutable_cf_options.compaction_options_fifo;
assert(fifo_opts.use_kv_ratio_compaction);
assert(fifo_opts.max_data_files_size > 0);
// During migration from level/universal compaction to FIFO, non-L0 levels
// may still contain files. The ratio-based algorithm only operates on L0,
// so skip it until PickSizeCompaction has drained all non-L0 levels.
// Once levels collapse to L0-only, this algorithm will kick in.
for (int level = 1; level < vstorage->num_levels(); ++level) {
if (!vstorage->LevelFiles(level).empty()) {
ROCKS_LOG_BUFFER(log_buffer,
"[%s] FIFO kv-ratio compaction: skipping — non-L0 "
"level %d still has %" ROCKSDB_PRIszt
" files (migration in progress)",
cf_name.c_str(), level,
vstorage->LevelFiles(level).size());
return nullptr;
}
}
if (!level0_compactions_in_progress_.empty()) {
return nullptr;
}
const std::vector<FileMetaData*>& level0_files = vstorage->LevelFiles(0);
if (mutable_cf_options.level0_file_num_compaction_trigger <= 1) {
// trigger <= 0 is invalid; trigger == 1 means compact after every flush,
// which doesn't make sense for tiered merging (the tier boundary loop
// divides by trigger, so trigger == 1 would cause an infinite loop).
return nullptr;
}
const size_t trigger = static_cast<size_t>(
mutable_cf_options.level0_file_num_compaction_trigger);
if (level0_files.size() < trigger) {
return nullptr;
}
// Determine the target compacted file size.
//
// When max_compaction_bytes > 0 (explicitly set by user), use it directly
// as the target. This allows users to override the auto-calculated value.
//
// When max_compaction_bytes == 0 (default), auto-calculate from the data
// capacity and observed SST/blob ratio:
// target = max_data_files_size * sst_ratio / trigger
//
// This is recomputed on every PickCompaction call. The computation is
// trivial (sum file sizes + arithmetic) and PickCompaction is only called
// once per flush or compaction completion, so no caching is needed.
uint64_t target = 0;
if (mutable_cf_options.max_compaction_bytes > 0) {
// User explicitly set max_compaction_bytes — use it as target
target = mutable_cf_options.max_compaction_bytes;
} else {
// Auto-calculate from capacity and observed SST/blob ratio
uint64_t total_sst = GetTotalFilesSize(level0_files);
uint64_t total_blob = vstorage->GetBlobStats().total_file_size;
uint64_t total_data = total_sst + total_blob;
if (total_data == 0 || total_sst == 0) {
return nullptr;
}
// Compute sst_ratio (inverse of EstimateTotalDataForSST's proportion):
// when no blob files exist, sst_ratio is 1.0 and the target becomes
// max_data_files_size / trigger, which is large. The algorithm will
// naturally not find small enough files to compact.
double sst_ratio =
(total_blob > 0) ? static_cast<double>(total_sst) / total_data : 1.0;
uint64_t total_sst_at_cap =
static_cast<uint64_t>(fifo_opts.max_data_files_size * sst_ratio);
target = total_sst_at_cap / trigger;
ROCKS_LOG_BUFFER(log_buffer,
"[%s] FIFO ratio-based compaction: sst_ratio=%.4f, "
"target_file_size=%" PRIu64,
cf_name.c_str(), sst_ratio, target);
}
if (target == 0) {
return nullptr;
}
// Tiered size-based file selection.
//
// Tier boundaries form a geometric sequence descending from target:
// ..., target/trigger^2, target/trigger, target
// For each boundary (smallest first), find contiguous L0 files with
// size < boundary. If their accumulated bytes >= boundary, merge them.
// The output (~boundary bytes) advances to the next tier. Files that
// reach target are "graduated" and never compacted again.
//
// Trade-off: write amplification vs L0 file count.
//
// Write amp: O(log(target/flush) / log(trigger)) per byte, instead of
// O(target / (trigger * flush)) from flat merging. Each byte is
// rewritten once per tier crossing.
//
// L0 file count: trigger + k * (trigger - 1) at steady state, where
// k = ceil(log(target/flush) / log(trigger)). This is higher than
// the original trigger target because intermediate tier files
// accumulate while waiting for the next tier merge. The trade-off
// is explicit: more L0 files in exchange for logarithmic (instead
// of linear) write amplification.
// Build tier boundaries from smallest to largest.
// Stop at 10KB minimum — SST files of most workloads are larger than
// this, so lower boundaries would only waste CPU scanning L0 files.
// Files smaller than the lowest boundary simply merge at that boundary.
static constexpr uint64_t kMinTierBoundary = 10 * 1024; // 10KB
std::vector<uint64_t> boundaries;
for (uint64_t b = target; b >= kMinTierBoundary; b /= trigger) {
boundaries.push_back(b);
}
if (boundaries.empty()) {
// target itself is below kMinTierBoundary — use target as the
// sole boundary so we can still compact at the target size.
boundaries.push_back(target);
}
std::reverse(boundaries.begin(), boundaries.end());
// For each tier boundary (smallest first), scan L0 for mergeable batches.
// L0 files are stored newest-first; oldest is at the end.
for (const uint64_t boundary : boundaries) {
for (size_t scan = level0_files.size(); scan > 0;) {
// Skip files >= boundary (they belong to higher tiers) or in-progress
if (level0_files[scan - 1]->fd.file_size >= boundary ||
level0_files[scan - 1]->being_compacted) {
--scan;
continue;
}
// Found a file < boundary — collect contiguous batch
std::vector<FileMetaData*> batch;
uint64_t accumulated = 0;
size_t pos = scan;
while (pos > 0 && level0_files[pos - 1]->fd.file_size < boundary &&
!level0_files[pos - 1]->being_compacted) {
// Don't let output exceed 2x boundary (prevent tier-skipping)
if (accumulated >= boundary &&
accumulated + level0_files[pos - 1]->fd.file_size > boundary * 2) {
break;
}
batch.push_back(level0_files[pos - 1]);
accumulated += level0_files[pos - 1]->fd.file_size;
--pos;
}
// Viable: >= 2 files and accumulated >= boundary
if (batch.size() >= 2 && accumulated >= boundary) {
CompactionInputFiles comp_inputs;
comp_inputs.level = 0;
comp_inputs.files = std::move(batch);
ROCKS_LOG_BUFFER(
log_buffer,
"[%s] FIFO kv-ratio compaction: picking %" ROCKSDB_PRIszt
" files (%" PRIu64 " bytes) at tier boundary %" PRIu64
" for intra-L0 compaction, target=%" PRIu64,
cf_name.c_str(), comp_inputs.files.size(), accumulated, boundary,
target);
Compaction* c = new Compaction(
vstorage, ioptions_, mutable_cf_options, mutable_db_options,
{comp_inputs}, 0, boundary /* output file size limit */,
0 /* max compaction bytes, not applicable */,
0 /* output path ID */, mutable_cf_options.compression,
mutable_cf_options.compression_opts, Temperature::kUnknown,
0 /* max_subcompactions */, {},
/* earliest_snapshot */ std::nullopt,
/* snapshot_checker */ nullptr,
CompactionReason::kFIFOReduceNumFiles,
/* trim_ts */ "", vstorage->CompactionScore(0),
/* l0_files_might_overlap */ true);
return c;
}
// This batch wasn't enough — advance past it
scan = pos;
}
}
return nullptr;
}
// The full_history_ts_low parameter is used to control bottommost file marking
// for compaction when user-defined timestamps (UDT) are enabled.
// TODO leverage full_history_ts_low for FIFO compaction, by trigggerring
// compaction early for data that has already expired to achieve the goal of TTL
// enforced compliance.
Compaction* FIFOCompactionPicker::PickCompaction(
const std::string& cf_name, const MutableCFOptions& mutable_cf_options,
const MutableDBOptions& mutable_db_options,
const std::vector<SequenceNumber>& /* existing_snapshots */,
const SnapshotChecker* /* snapshot_checker */, VersionStorageInfo* vstorage,
LogBuffer* log_buffer, bool /* require_max_output_level*/) {
LogBuffer* log_buffer, const std::string& /* full_history_ts_low */,
bool /* require_max_output_level*/) {
Compaction* c = nullptr;
if (mutable_cf_options.ttl > 0) {
c = PickTTLCompaction(cf_name, mutable_cf_options, mutable_db_options,
@@ -434,10 +743,22 @@ Compaction* FIFOCompactionPicker::PickCompaction(
c = PickSizeCompaction(cf_name, mutable_cf_options, mutable_db_options,
vstorage, log_buffer);
}
// Intra-L0 compaction merges small files to reduce file count.
// It runs after size-based dropping: if PickSizeCompaction dropped files,
// it returned non-null and we skip this. Otherwise, we try to reduce
// L0 file count by merging small files together.
if (c == nullptr) {
c = PickIntraL0Compaction(cf_name, mutable_cf_options, mutable_db_options,
vstorage, log_buffer);
}
if (c == nullptr) {
c = PickTemperatureChangeCompaction(
cf_name, mutable_cf_options, mutable_db_options, vstorage, log_buffer);
}
if (c == nullptr) {
ROCKS_LOG_BUFFER(log_buffer, "[%s] FIFO compaction: no compaction picked",
cf_name.c_str());
}
RegisterCompaction(c);
return c;
}
@@ -449,7 +770,8 @@ Compaction* FIFOCompactionPicker::PickCompactionForCompactRange(
const CompactRangeOptions& /*compact_range_options*/,
const InternalKey* /*begin*/, const InternalKey* /*end*/,
InternalKey** compaction_end, bool* /*manual_conflict*/,
uint64_t /*max_file_num_to_ignore*/, const std::string& /*trim_ts*/) {
uint64_t /*max_file_num_to_ignore*/, const std::string& /*trim_ts*/,
const std::string& full_history_ts_low) {
#ifdef NDEBUG
(void)input_level;
(void)output_level;
@@ -458,10 +780,10 @@ Compaction* FIFOCompactionPicker::PickCompactionForCompactRange(
assert(output_level == 0);
*compaction_end = nullptr;
LogBuffer log_buffer(InfoLogLevel::INFO_LEVEL, ioptions_.logger);
Compaction* c =
PickCompaction(cf_name, mutable_cf_options, mutable_db_options,
/*existing_snapshots*/ {}, /*snapshot_checker*/ nullptr,
vstorage, &log_buffer);
Compaction* c = PickCompaction(
cf_name, mutable_cf_options, mutable_db_options,
/*existing_snapshots*/ {}, /*snapshot_checker*/ nullptr, vstorage,
&log_buffer, full_history_ts_low, /* require_max_output_level */ false);
log_buffer.FlushBufferToLog();
return c;
}
+25 -1
View File
@@ -24,6 +24,7 @@ class FIFOCompactionPicker : public CompactionPicker {
const std::vector<SequenceNumber>& /* existing_snapshots */,
const SnapshotChecker* /* snapshot_checker */,
VersionStorageInfo* version, LogBuffer* log_buffer,
const std::string& /* full_history_ts_low */,
bool /* require_max_output_level*/ = false) override;
Compaction* PickCompactionForCompactRange(
@@ -33,7 +34,8 @@ class FIFOCompactionPicker : public CompactionPicker {
const CompactRangeOptions& compact_range_options,
const InternalKey* begin, const InternalKey* end,
InternalKey** compaction_end, bool* manual_conflict,
uint64_t max_file_num_to_ignore, const std::string& trim_ts) override;
uint64_t max_file_num_to_ignore, const std::string& trim_ts,
const std::string& full_history_ts_low) override;
// The maximum allowed output level. Always returns 0.
int MaxOutputLevel() const override { return 0; }
@@ -53,6 +55,28 @@ class FIFOCompactionPicker : public CompactionPicker {
VersionStorageInfo* version,
LogBuffer* log_buffer);
// Intra-L0 compaction: merges small L0 files to reduce file count.
// Dispatches between two strategies based on configuration:
// - use_kv_ratio_compaction = true: PickRatioBasedIntraL0Compaction
// (BlobDB-optimized)
// - use_kv_ratio_compaction = false: PickCostBasedIntraL0Compaction
// (original)
// Only active when allow_compaction = true.
Compaction* PickIntraL0Compaction(const std::string& cf_name,
const MutableCFOptions& mutable_cf_options,
const MutableDBOptions& mutable_db_options,
VersionStorageInfo* vstorage,
LogBuffer* log_buffer);
// Capacity-derived intra-L0 compaction for BlobDB workloads.
// Uses the observed SST/blob ratio to compute a target file size,
// producing uniform files for predictable FIFO trimming.
// Called from PickIntraL0Compaction when use_kv_ratio_compaction = true.
Compaction* PickRatioBasedIntraL0Compaction(
const std::string& cf_name, const MutableCFOptions& mutable_cf_options,
const MutableDBOptions& mutable_db_options, VersionStorageInfo* vstorage,
LogBuffer* log_buffer);
// Will pick one file to compact at a time, starting from the oldest file.
Compaction* PickTemperatureChangeCompaction(
const std::string& cf_name, const MutableCFOptions& mutable_cf_options,
+23 -10
View File
@@ -61,14 +61,16 @@ class LevelCompactionBuilder {
LogBuffer* log_buffer,
const MutableCFOptions& mutable_cf_options,
const ImmutableOptions& ioptions,
const MutableDBOptions& mutable_db_options)
const MutableDBOptions& mutable_db_options,
const std::string& full_history_ts_low)
: cf_name_(cf_name),
vstorage_(vstorage),
compaction_picker_(compaction_picker),
log_buffer_(log_buffer),
mutable_cf_options_(mutable_cf_options),
ioptions_(ioptions),
mutable_db_options_(mutable_db_options) {}
mutable_db_options_(mutable_db_options),
full_history_ts_low_(full_history_ts_low) {}
// Pick and return a compaction.
Compaction* PickCompaction();
@@ -155,6 +157,7 @@ class LevelCompactionBuilder {
const MutableCFOptions& mutable_cf_options_;
const ImmutableOptions& ioptions_;
const MutableDBOptions& mutable_db_options_;
const std::string& full_history_ts_low_;
// Pick a path ID to place a newly generated file, with its level
static uint32_t GetPathId(const ImmutableCFOptions& ioptions,
const MutableCFOptions& mutable_cf_options,
@@ -401,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
@@ -571,7 +582,8 @@ Compaction* LevelCompactionBuilder::GetCompaction() {
// takes running compactions into account (by skipping files that are already
// being compacted). Since we just changed compaction score, we recalculate it
// here
vstorage_->ComputeCompactionScore(ioptions_, mutable_cf_options_);
vstorage_->ComputeCompactionScore(ioptions_, mutable_cf_options_,
full_history_ts_low_);
return c;
}
@@ -910,10 +922,10 @@ bool LevelCompactionBuilder::PickIntraL0Compaction() {
// resort to L0->L0 compaction yet.
return false;
}
return FindIntraL0Compaction(level_files, kMinFilesForIntraL0Compaction,
std::numeric_limits<uint64_t>::max(),
mutable_cf_options_.max_compaction_bytes,
&start_level_inputs_);
return PickCostBasedIntraL0Compaction(
level_files, kMinFilesForIntraL0Compaction,
std::numeric_limits<uint64_t>::max(),
mutable_cf_options_.max_compaction_bytes, &start_level_inputs_);
}
bool LevelCompactionBuilder::PickSizeBasedIntraL0Compaction() {
@@ -976,10 +988,11 @@ Compaction* LevelCompactionPicker::PickCompaction(
const MutableDBOptions& mutable_db_options,
const std::vector<SequenceNumber>& /*existing_snapshots */,
const SnapshotChecker* /*snapshot_checker*/, VersionStorageInfo* vstorage,
LogBuffer* log_buffer, bool /* require_max_output_level*/) {
LogBuffer* log_buffer, const std::string& full_history_ts_low,
bool /* require_max_output_level*/) {
LevelCompactionBuilder builder(cf_name, vstorage, this, log_buffer,
mutable_cf_options, ioptions_,
mutable_db_options);
mutable_db_options, full_history_ts_low);
return builder.PickCompaction();
}
} // namespace ROCKSDB_NAMESPACE
+1
View File
@@ -26,6 +26,7 @@ class LevelCompactionPicker : public CompactionPicker {
const std::vector<SequenceNumber>& /* existing_snapshots */,
const SnapshotChecker* /* snapshot_checker */,
VersionStorageInfo* vstorage, LogBuffer* log_buffer,
const std::string& full_history_ts_low,
bool /*require_max_output_level*/ = false) override;
bool NeedsCompaction(const VersionStorageInfo* vstorage) const override;
File diff suppressed because it is too large. Load diff
+12 -5
View File
@@ -39,7 +39,7 @@ class UniversalCompactionBuilder {
const std::vector<SequenceNumber>& existing_snapshots,
const SnapshotChecker* snapshot_checker, VersionStorageInfo* vstorage,
UniversalCompactionPicker* picker, LogBuffer* log_buffer,
bool require_max_output_level)
bool require_max_output_level, const std::string& full_history_ts_low)
: ioptions_(ioptions),
icmp_(icmp),
cf_name_(cf_name),
@@ -50,7 +50,8 @@ class UniversalCompactionBuilder {
log_buffer_(log_buffer),
require_max_output_level_(require_max_output_level),
allow_ingest_behind_(ioptions.cf_allow_ingest_behind ||
ioptions.allow_ingest_behind) {
ioptions.allow_ingest_behind),
full_history_ts_low_(full_history_ts_low) {
assert(icmp_);
const auto* ucmp = icmp_->user_comparator();
assert(ucmp);
@@ -450,6 +451,7 @@ class UniversalCompactionBuilder {
std::map<uint64_t, size_t> file_marked_for_compaction_to_sorted_run_index_;
bool require_max_output_level_;
bool allow_ingest_behind_;
const std::string& full_history_ts_low_;
std::vector<UniversalCompactionBuilder::SortedRun> CalculateSortedRuns(
const VersionStorageInfo& vstorage, int last_level,
@@ -595,16 +597,20 @@ bool UniversalCompactionPicker::NeedsCompaction(
return false;
}
// TODO leverage full_history_ts_low in universal compaction picking. It could
// help reduce the same infinite compaction loop issue found in level
// compaction.
Compaction* UniversalCompactionPicker::PickCompaction(
const std::string& cf_name, const MutableCFOptions& mutable_cf_options,
const MutableDBOptions& mutable_db_options,
const std::vector<SequenceNumber>& existing_snapshots,
const SnapshotChecker* snapshot_checker, VersionStorageInfo* vstorage,
LogBuffer* log_buffer, bool require_max_output_level) {
LogBuffer* log_buffer, const std::string& full_history_ts_low,
bool require_max_output_level) {
UniversalCompactionBuilder builder(
ioptions_, icmp_, cf_name, mutable_cf_options, mutable_db_options,
existing_snapshots, snapshot_checker, vstorage, this, log_buffer,
require_max_output_level);
require_max_output_level, full_history_ts_low);
return builder.PickCompaction();
}
@@ -825,7 +831,8 @@ Compaction* UniversalCompactionBuilder::PickCompaction() {
RecordInHistogram(ioptions_.stats, NUM_FILES_IN_SINGLE_COMPACTION, num_files);
picker_->RegisterCompaction(c);
vstorage_->ComputeCompactionScore(ioptions_, mutable_cf_options_);
vstorage_->ComputeCompactionScore(ioptions_, mutable_cf_options_,
full_history_ts_low_);
TEST_SYNC_POINT_CALLBACK("UniversalCompactionBuilder::PickCompaction:Return",
c);
+2 -1
View File
@@ -26,7 +26,8 @@ class UniversalCompactionPicker : public CompactionPicker {
const MutableDBOptions& mutable_db_options,
const std::vector<SequenceNumber>& existing_snapshots,
const SnapshotChecker* snapshot_checker, VersionStorageInfo* vstorage,
LogBuffer* log_buffer, bool require_max_output_level = false) override;
LogBuffer* log_buffer, const std::string& full_history_ts_low,
bool require_max_output_level = false) override;
int MaxOutputLevel() const override { return NumberLevels() - 1; }
bool NeedsCompaction(const VersionStorageInfo* vstorage) const override;
+19 -10
View File
@@ -117,6 +117,14 @@ CompactionJob::ProcessKeyValueCompactionWithCompactionService(
std::string debug_str_before_wait =
compaction->input_version()->DebugString(/*hex=*/true);
// TODO: Update CompactionService API to support abort and resume
// functionality. Currently, remote compaction jobs cannot be aborted via
// AbortAllCompactions() because the CompactionService interface lacks methods
// to signal abort to remote workers and to properly resume after an abort.
// The API needs to be extended with:
// - A method to signal abort to running remote compaction jobs
// - A method to resume/re-enable scheduling after an abort is lifted
ROCKS_LOG_INFO(db_options_.info_log,
"[%s] [JOB %d] Waiting for remote compaction...",
compaction->column_family_data()->GetName().c_str(), job_id_);
@@ -312,16 +320,17 @@ CompactionServiceCompactionJob::CompactionServiceCompactionJob(
std::string output_path,
const CompactionServiceInput& compaction_service_input,
CompactionServiceResult* compaction_service_result)
: CompactionJob(job_id, compaction, db_options, mutable_db_options,
file_options, versions, shutting_down, log_buffer, nullptr,
output_directory, nullptr, stats, db_mutex,
db_error_handler, job_context, std::move(table_cache),
event_logger,
compaction->mutable_cf_options().paranoid_file_checks,
compaction->mutable_cf_options().report_bg_io_stats, dbname,
&(compaction_service_result->stats), Env::Priority::USER,
io_tracer, manual_compaction_canceled, db_id, db_session_id,
compaction->column_family_data()->GetFullHistoryTsLow()),
: CompactionJob(
job_id, compaction, db_options, mutable_db_options, file_options,
versions, shutting_down, log_buffer, nullptr, output_directory,
nullptr, stats, db_mutex, db_error_handler, job_context,
std::move(table_cache), event_logger,
compaction->mutable_cf_options().paranoid_file_checks,
compaction->mutable_cf_options().report_bg_io_stats, dbname,
&(compaction_service_result->stats), Env::Priority::USER, io_tracer,
manual_compaction_canceled, CompactionJob::kCompactionAbortedFalse,
db_id, db_session_id,
compaction->column_family_data()->GetFullHistoryTsLow()),
output_path_(std::move(output_path)),
compaction_input_(compaction_service_input),
compaction_result_(compaction_service_result) {}
+500 -40
View File
@@ -195,9 +195,11 @@ class MyTestCompactionService : public CompactionService {
std::vector<std::shared_ptr<EventListener>> listeners_;
std::vector<std::shared_ptr<TablePropertiesCollectorFactory>>
table_properties_collector_factories_;
std::atomic_bool canceled_{false};
std::atomic<CompactionServiceJobStatus> final_updated_status_{
CompactionServiceJobStatus::kUseLocal};
protected:
std::atomic_bool canceled_{false};
};
class CompactionServiceTest : public DBTestBase {
@@ -407,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;
@@ -1111,6 +1141,8 @@ TEST_F(CompactionServiceTest, CorruptedOutputVerifyOutputFlags) {
VerifyOutputFlags::kVerifyBlockChecksum,
VerifyOutputFlags::kEnableForRemoteCompaction |
VerifyOutputFlags::kVerifyIteration,
VerifyOutputFlags::kEnableForRemoteCompaction |
VerifyOutputFlags::kVerifyFileChecksum,
VerifyOutputFlags::kVerifyAll}) {
SCOPED_TRACE(
"verify_output_flags=" +
@@ -1122,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();
@@ -1157,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 {
@@ -1378,8 +1414,9 @@ TEST_F(CompactionServiceTest, CancelCompactionOnPrimarySide) {
// Primary DB calls CancelAllBackgroundWork() while the compaction is running
SyncPoint::GetInstance()->SetCallBack(
"CompactionJob::Run():Inprogress",
[&](void* /*arg*/) { CancelAllBackgroundWork(db_, false /*wait*/); });
"CompactionJob::Run():Inprogress", [&](void* /*arg*/) {
CancelAllBackgroundWork(db_.get(), false /*wait*/);
});
SyncPoint::GetInstance()->EnableProcessing();
@@ -2137,6 +2174,12 @@ class ResumableCompactionService : public MyTestCompactionService {
{} /* table_properties_collector_factories */),
scenario_(scenario) {}
// Set the user key where cancellation should happen.
void SetCancelAtKey(const std::string& key, SequenceNumber seqno) {
cancel_at_key_ = key;
cancel_at_seqno_ = seqno;
}
CompactionServiceJobStatus Wait(const std::string& scheduled_job_id,
std::string* result) override {
std::string compaction_input = ExtractCompactionInput(scheduled_job_id);
@@ -2149,25 +2192,50 @@ class ResumableCompactionService : public MyTestCompactionService {
// ASSUMPTION: This makes stats.count directly proportional to keys
// processed.
SyncPoint::GetInstance()->SetCallBack(
"CompactionOutputs::ShouldStopBefore::manual_decision", [](void* p) {
"CompactionOutputs::ShouldStopBefore::manual_decision",
[this](void* p) {
auto* pair = static_cast<std::pair<bool*, const Slice>*>(p);
*(pair->first) = true;
*(pair->first) = true; // Force file cut at every key
// If cancel_at_key_ is set, cancel when we encounter that key
if (!cancel_at_key_.empty() && !already_canceled_) {
ParsedInternalKey parsed_key;
if (ParseInternalKey(pair->second, &parsed_key, true).ok()) {
if (parsed_key.user_key.ToString() == cancel_at_key_) {
// Check sequence number if specified
if (cancel_at_seqno_ == kMaxSequenceNumber ||
parsed_key.sequence == cancel_at_seqno_) {
canceled_ = true;
already_canceled_ = true;
}
}
}
}
});
// If no cancel_at_key_ is set, use the original behavior:
// Simulate cancelled compaction by overriding status at completion. So
// compaction processes all keys before this point to make stats.count
// comparison straightforward.
SyncPoint::GetInstance()->SetCallBack(
"DBImplSecondary::CompactWithoutInstallation::End", [&](void* status) {
auto s = static_cast<Status*>(status);
*s = Status::Incomplete(Status::SubCode::kManualCompactionPaused);
});
if (cancel_at_key_.empty()) {
SyncPoint::GetInstance()->SetCallBack(
"DBImplSecondary::CompactWithoutInstallation::End",
[&](void* status) {
auto s = static_cast<Status*>(status);
*s = Status::Incomplete(Status::SubCode::kManualCompactionPaused);
});
}
SyncPoint::GetInstance()->EnableProcessing();
// Phase 1: Run compaction with resumption enabled and cancel it
// - Processes all input keys
// - Processes input keys until cancellation point
// - Creates output files and saves progress
// - Status overridden to "paused"
open_and_compaction_options.allow_resumption = true;
open_and_compaction_options.canceled = &canceled_;
already_canceled_ = false;
canceled_ = false;
auto phase1_stats =
RunCancelledCompaction(open_and_compaction_options, scheduled_job_id,
compaction_input, override_options);
@@ -2188,6 +2256,9 @@ class ResumableCompactionService : public MyTestCompactionService {
EXPECT_TRUE(cleanup_status.ok());
EXPECT_OK(override_options.env->CreateDir(output_dir));
already_canceled_ = false;
canceled_ = false;
phase2_stats =
RunCancelledCompaction(open_and_compaction_options, scheduled_job_id,
compaction_input, override_options);
@@ -2199,9 +2270,6 @@ class ResumableCompactionService : public MyTestCompactionService {
EXPECT_EQ(phase2_stats.count, phase1_stats.count);
}
SyncPoint::GetInstance()->ClearCallBack(
"DBImplSecondary::CompactWithoutInstallation::End");
// Final phase: Run compaction to completion (no cancellation)
if (scenario_ == TestScenario::kMultipleCancelToggleResumption) {
// Attempt to resume but it ends up starting fresh
@@ -2220,6 +2288,12 @@ class ResumableCompactionService : public MyTestCompactionService {
EXPECT_OK(override_options.env->CreateDir(output_dir));
}
// Prevent triggering of cancellation
SyncPoint::GetInstance()->ClearCallBack(
"DBImplSecondary::CompactWithoutInstallation::End");
already_canceled_ = true;
canceled_ = false;
auto final_phase_stats =
RunCompaction(open_and_compaction_options, scheduled_job_id,
compaction_input, override_options, result);
@@ -2227,36 +2301,38 @@ class ResumableCompactionService : public MyTestCompactionService {
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
// Validate statistics based on scenario
if (scenario_ == TestScenario::kMultipleCancelToggleResumption) {
// ASSUMPTION: Phase 1 processes all keys before cancellation
EXPECT_GT(phase1_stats.count, 0);
// Validate statistics based on scenario (only when cancelling at end)
if (cancel_at_key_.empty()) {
if (scenario_ == TestScenario::kMultipleCancelToggleResumption) {
// ASSUMPTION: Phase 1 processes all keys before cancellation
EXPECT_GT(phase1_stats.count, 0);
// ASSUMPTION: Phase 2 runs with allow_resumption=false and an empty
// folder. Phase 2 then creates its own output files (but doesn't save
// progress). When Phase 3 starts with allow_resumption=true, it finds no
// progress file exists, so it cannot resume and must start from scratch,
// processing all input keys again.
// Result: Phase 3 does the same amount of work as Phase 1.
EXPECT_EQ(final_phase_stats.count, phase1_stats.count);
// ASSUMPTION: Phase 2 runs with allow_resumption=false and an empty
// folder. Phase 2 then creates its own output files (but doesn't save
// progress). When Phase 3 starts with allow_resumption=true, it finds
// no progress file exists, so it cannot resume and must start from
// scratch, processing all input keys again. Result: Phase 3 does the
// same amount of work as Phase 1.
EXPECT_EQ(final_phase_stats.count, phase1_stats.count);
} else if (scenario_ == TestScenario::kCancelThenResume) {
// ASSUMPTION: Phase 1 processes all keys before cancellation
EXPECT_GT(phase1_stats.count, 0);
} else if (scenario_ == TestScenario::kCancelThenResume) {
// ASSUMPTION: Phase 1 processes all keys before cancellation
EXPECT_GT(phase1_stats.count, 0);
// ASSUMPTION: Phase 1 processes all keys and saves progress before
// cancellation. Final phase resumes from Phase 1's saved progress.
// Since Phase 1 completed all processing before being cancelled, the
// final phase should do less work than Phase 1.
EXPECT_LT(final_phase_stats.count, phase1_stats.count);
// ASSUMPTION: Phase 1 processes all keys and saves progress before
// cancellation. Final phase resumes from Phase 1's saved progress.
// Since Phase 1 completed all processing before being cancelled, the
// final phase should do less work than Phase 1.
EXPECT_LT(final_phase_stats.count, phase1_stats.count);
} else { // kCancelThenFreshStart
// ASSUMPTION: Phase 1 processes all keys before cancellation
EXPECT_GT(phase1_stats.count, 0);
} else { // kCancelThenFreshStart
// ASSUMPTION: Phase 1 processes all keys before cancellation
EXPECT_GT(phase1_stats.count, 0);
// ASSUMPTION: Final phase starts fresh without resumption, so it
// processes all input keys again and creates the same number of files
EXPECT_EQ(final_phase_stats.count, phase1_stats.count);
// ASSUMPTION: Final phase starts fresh without resumption, so it
// processes all input keys again and creates the same number of files
EXPECT_EQ(final_phase_stats.count, phase1_stats.count);
}
}
StoreResult(*result);
@@ -2326,6 +2402,9 @@ class ResumableCompactionService : public MyTestCompactionService {
}
TestScenario scenario_;
std::string cancel_at_key_;
SequenceNumber cancel_at_seqno_ = kMaxSequenceNumber;
std::atomic<bool> already_canceled_{false};
};
class ResumableCompactionServiceTest : public CompactionServiceTest {
@@ -2432,6 +2511,387 @@ TEST_F(ResumableCompactionServiceTest,
RunCompactionCancelTest(ResumableCompactionService::TestScenario::
kMultipleCancelToggleResumption);
}
class ResumableCompactionKeyTypeTest : public CompactionServiceTest {
public:
explicit ResumableCompactionKeyTypeTest() : CompactionServiceTest() {}
protected:
void SetupResumableCompactionService(
Options& options, const std::string& cancel_at_key = "",
SequenceNumber cancel_at_seqno = kMaxSequenceNumber) {
options.disable_auto_compactions = true;
statistics_ = CreateDBStatistics();
resume_cs_ = std::make_shared<ResumableCompactionService>(
dbname_, options, statistics_,
ResumableCompactionService::TestScenario::kCancelThenResume);
if (!cancel_at_key.empty()) {
resume_cs_->SetCancelAtKey(cancel_at_key, cancel_at_seqno);
}
options.compaction_service = resume_cs_;
DestroyAndReopen(options);
}
void ResetStatistics() { ASSERT_OK(statistics_->Reset()); }
void VerifyResumeBytes() {
uint64_t resumed_bytes =
statistics_->getTickerCount(REMOTE_COMPACT_RESUMED_BYTES);
ASSERT_GT(resumed_bytes, 0);
}
private:
std::shared_ptr<ResumableCompactionService> resume_cs_;
std::shared_ptr<Statistics> statistics_;
};
// Cancel compaction right before processing key "c" to test resumption at a
// deletion at the non-bottom level. When resumed, compaction will continue
// from this deletion.
TEST_F(ResumableCompactionKeyTypeTest,
CancelAndResumeWithDeleteAtNonBottomLevel) {
Options options = CurrentOptions();
SetupResumableCompactionService(options, "c");
ASSERT_OK(Put("c", "old_value"));
ASSERT_OK(Put("c_placeholder", "placeholder"));
ASSERT_OK(Flush());
MoveFilesToLevel(options.num_levels - 1);
ASSERT_OK(Put("a", "val1"));
ASSERT_OK(Put("b", "val2"));
ASSERT_OK(Put("d", "val4"));
ASSERT_OK(Flush());
ASSERT_OK(Delete("c"));
ASSERT_OK(Flush());
std::vector<std::string> input_files;
ColumnFamilyMetaData cf_meta;
db_->GetColumnFamilyMetaData(&cf_meta);
for (const auto& file : cf_meta.levels[0].files) {
input_files.push_back(file.name);
}
ASSERT_EQ(input_files.size(), 2);
ResetStatistics();
CompactionOptions compact_options;
ASSERT_OK(
db_->CompactFiles(compact_options, input_files, 1 /* output_level*/));
ASSERT_EQ(Get("a"), "val1");
ASSERT_EQ(Get("b"), "val2");
ASSERT_EQ(Get("c"), "NOT_FOUND");
ASSERT_EQ(Get("d"), "val4");
VerifyResumeBytes();
}
// Cancel compaction right before processing key "c" to test resumption at a
// deletion at the ottom level. When resumed, compaction will continue from
// the last saved progress point before the delete.
TEST_F(ResumableCompactionKeyTypeTest, CancelAndResumeWithDeleteAtBottomLevel) {
Options options = CurrentOptions();
SetupResumableCompactionService(options, "c");
ASSERT_OK(Put("c", "old_value"));
const Snapshot* snapshot = db_->GetSnapshot();
ASSERT_OK(Delete("c"));
ASSERT_OK(Flush());
MoveFilesToLevel(options.num_levels - 1);
ASSERT_OK(Put("a", "val1"));
ASSERT_OK(Put("b", "val2"));
ASSERT_OK(Put("d", "val4"));
ASSERT_OK(Flush());
ResetStatistics();
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
ASSERT_EQ(Get("a"), "val1");
ASSERT_EQ(Get("b"), "val2");
ASSERT_EQ(Get("c"), "NOT_FOUND");
ASSERT_EQ(Get("c", snapshot), "old_value");
ASSERT_EQ(Get("d"), "val4");
db_->ReleaseSnapshot(snapshot);
VerifyResumeBytes();
}
// Cancel compaction right before processing key "c" to test resumption at a
// merge operand. When resumed, compaction will continue from the last saved
// progress point before the merge operand.
TEST_F(ResumableCompactionKeyTypeTest, CancelAndResumeWithMerge) {
Options options = CurrentOptions();
options.merge_operator = MergeOperators::CreateStringAppendOperator();
SetupResumableCompactionService(options, "c");
ASSERT_OK(Put("c", "old_value"));
ASSERT_OK(Put("c_placeholder", "placeholder"));
ASSERT_OK(Flush());
MoveFilesToLevel(options.num_levels - 1);
ASSERT_OK(Put("a", "val1"));
ASSERT_OK(Put("b", "val2"));
ASSERT_OK(Put("d", "val4"));
ASSERT_OK(Flush());
ASSERT_OK(Merge("c", "new_value"));
ASSERT_OK(Flush());
std::vector<std::string> input_files;
ColumnFamilyMetaData cf_meta;
db_->GetColumnFamilyMetaData(&cf_meta);
for (const auto& file : cf_meta.levels[0].files) {
input_files.push_back(file.name);
}
ASSERT_EQ(input_files.size(), 2);
ResetStatistics();
CompactionOptions compact_options;
ASSERT_OK(
db_->CompactFiles(compact_options, input_files, 1 /* output_level*/));
ASSERT_EQ(Get("a"), "val1");
ASSERT_EQ(Get("b"), "val2");
ASSERT_EQ(Get("c"), "old_value,new_value");
ASSERT_EQ(Get("d"), "val4");
VerifyResumeBytes();
}
// Cancel compaction right before processing key "c" to test resumption at a
// single delete. When resumed, compaction will continue from the last saved
// progress point before the single delete.
TEST_F(ResumableCompactionKeyTypeTest, CancelAndResumeWithSingleDelete) {
Options options = CurrentOptions();
SetupResumableCompactionService(options, "c");
ASSERT_OK(Put("c", "old_value"));
ASSERT_OK(Put("c_placeholder", "placeholder"));
ASSERT_OK(Flush());
MoveFilesToLevel(options.num_levels - 1);
ASSERT_OK(Put("a", "val1"));
ASSERT_OK(Put("b", "val2"));
ASSERT_OK(Put("d", "val4"));
ASSERT_OK(Flush());
ASSERT_OK(SingleDelete("c"));
ASSERT_OK(Flush());
std::vector<std::string> input_files;
ColumnFamilyMetaData cf_meta;
db_->GetColumnFamilyMetaData(&cf_meta);
for (const auto& file : cf_meta.levels[0].files) {
input_files.push_back(file.name);
}
ASSERT_EQ(input_files.size(), 2);
ResetStatistics();
CompactionOptions compact_options;
ASSERT_OK(
db_->CompactFiles(compact_options, input_files, 1 /* output_level*/));
ASSERT_EQ(Get("a"), "val1");
ASSERT_EQ(Get("b"), "val2");
ASSERT_EQ(Get("c"), "NOT_FOUND");
ASSERT_EQ(Get("d"), "val4");
VerifyResumeBytes();
}
// Cancel compaction right before processing key "c" to test resumption at a
// range delete. When resumed, compaction will continue from the last saved
// progress point before the range delete.
TEST_F(ResumableCompactionKeyTypeTest, CancelAndResumeWithRangeDelete) {
Options options = CurrentOptions();
SetupResumableCompactionService(options, "c");
ASSERT_OK(Put("c", "old_value"));
ASSERT_OK(Put("c_placeholder", "placeholder"));
ASSERT_OK(Flush());
MoveFilesToLevel(options.num_levels - 1);
ASSERT_OK(Put("a", "val1"));
ASSERT_OK(Put("b", "val2"));
ASSERT_OK(Put("d", "val4"));
ASSERT_OK(Flush());
ASSERT_OK(
db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), "c", "c_"));
ASSERT_OK(Flush());
std::vector<std::string> input_files;
ColumnFamilyMetaData cf_meta;
db_->GetColumnFamilyMetaData(&cf_meta);
for (const auto& file : cf_meta.levels[0].files) {
input_files.push_back(file.name);
}
ASSERT_EQ(input_files.size(), 2);
ResetStatistics();
CompactionOptions compact_options;
ASSERT_OK(
db_->CompactFiles(compact_options, input_files, 1 /* output_level*/));
ASSERT_EQ(Get("a"), "val1");
ASSERT_EQ(Get("b"), "val2");
ASSERT_EQ(Get("c"), "NOT_FOUND");
ASSERT_EQ(Get("d"), "val4");
VerifyResumeBytes();
}
// Test resumption when a key has multiple versions spanning across file
// boundaries (i.e., the same key exists in multiple SST files).
//
// Scenario:
// File 1 largest key: key "b"
// File 2 smallest key: key "c" with seqno=4 (older version)
// File 3 largest key: key "c" with seqno=5 (newer version)
//
// Cancel compaction right before processing the older version of key "c".
// Upon resumption, compaction continues from the saved progress point "b" and
// correctly processes both versions
TEST_F(ResumableCompactionKeyTypeTest,
CancelAndResumeWithKeySpanningFileBoundaries) {
Options options = CurrentOptions();
// Set up cancellation at the older version of the key which will have
// sequence number zero-ed out
SetupResumableCompactionService(options, "c" /*cancel_at_key*/, 0 /*seqno*/);
ASSERT_OK(Put("a", "val1"));
ASSERT_OK(Put("b", "val2"));
ASSERT_OK(Put("d", "val4"));
ASSERT_OK(Flush());
ASSERT_OK(Put("c", "old_value"));
const Snapshot* snapshot = db_->GetSnapshot();
ASSERT_OK(Put("c", "new_value"));
ASSERT_OK(Flush());
ResetStatistics();
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
ASSERT_EQ(Get("a"), "val1");
ASSERT_EQ(Get("b"), "val2");
ASSERT_EQ(Get("c"), "new_value");
ASSERT_EQ(Get("c", snapshot), "old_value");
ASSERT_EQ(Get("d"), "val4");
db_->ReleaseSnapshot(snapshot);
VerifyResumeBytes();
}
// Cancel compaction right before processing key "c" to test resumption at a
// wide column. When resumed, compaction will continue
// from the wide column.
TEST_F(ResumableCompactionKeyTypeTest, CancelAndResumeWithWideColumn) {
Options options = CurrentOptions();
SetupResumableCompactionService(options, "c" /*cancel_at_key*/);
ASSERT_OK(Put("a", "val1"));
ASSERT_OK(Put("b", "val2"));
ASSERT_OK(Put("d", "val4"));
ASSERT_OK(Flush());
WideColumns columns{{"col1", "value1"}, {"col2", "value2"}};
ASSERT_OK(
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), "c", columns));
ASSERT_OK(Flush());
ResetStatistics();
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
ASSERT_EQ(Get("a"), "val1");
ASSERT_EQ(Get("b"), "val2");
PinnableWideColumns result;
ASSERT_OK(
db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(), "c", &result));
WideColumns expected{{"col1", "value1"}, {"col2", "value2"}};
ASSERT_EQ(result.columns(), expected);
ASSERT_EQ(Get("d"), "val4");
VerifyResumeBytes();
}
// Cancel compaction right before processing key "c" to test resumption at a
// timed put. When resumed, compaction will continue
// from the timed put.
TEST_F(ResumableCompactionKeyTypeTest, CancelAndResumeWithTimedPut) {
Options options = CurrentOptions();
options.preclude_last_level_data_seconds = 86400; // Enable TimedPut feature
options.preserve_internal_time_seconds = 86400; // Preserve write time
SetupResumableCompactionService(options, "c" /*cancel_at_key*/);
ASSERT_OK(Put("c", "old_value"));
ASSERT_OK(Put("c_placeholder", "placeholder"));
ASSERT_OK(Flush());
MoveFilesToLevel(options.num_levels - 1);
ASSERT_OK(Put("a", "val1"));
ASSERT_OK(Put("b", "val2"));
ASSERT_OK(Put("d", "val4"));
ASSERT_OK(Flush());
// Use TimedPut for key "c" with current write time
uint64_t write_time = env_->NowMicros() / 1000000;
ASSERT_OK(TimedPut("c", "val3", write_time /*write_unix_time*/));
ASSERT_OK(Put("d", "val4"));
ASSERT_OK(Flush());
std::vector<std::string> input_files;
ColumnFamilyMetaData cf_meta;
db_->GetColumnFamilyMetaData(&cf_meta);
for (const auto& file : cf_meta.levels[0].files) {
input_files.push_back(file.name);
}
ASSERT_EQ(input_files.size(), 2);
ResetStatistics();
CompactionOptions compact_options;
ASSERT_OK(
db_->CompactFiles(compact_options, input_files, 1 /* output_level*/));
ASSERT_EQ(Get("a"), "val1");
ASSERT_EQ(Get("b"), "val2");
ASSERT_EQ(Get("c"), "val3");
ASSERT_EQ(Get("d"), "val4");
VerifyResumeBytes();
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
+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
+9 -1
View File
@@ -95,6 +95,14 @@ class SubcompactionState {
proximal_level_outputs_.RemoveLastEmptyOutput();
}
// Cleanup output builders for abandoning in-progress files.
void CleanupOutputs() {
compaction_outputs_.Cleanup();
if (compaction->SupportsPerKeyPlacement()) {
proximal_level_outputs_.Cleanup();
}
}
void BuildSubcompactionJobInfo(
SubcompactionJobInfo& subcompaction_job_info) const {
const Compaction* c = compaction;
@@ -162,7 +170,7 @@ class SubcompactionState {
}
}
void Cleanup(Cache* cache);
void Cleanup(Cache* cache, const Status& overall_status);
void AggregateCompactionOutputStats(
InternalStats::CompactionStatsFull& internal_stats) const;
+4 -4
View File
@@ -1717,8 +1717,8 @@ TEST_P(PrecludeLastLevelTest, MigrationFromPreserveTimePartial) {
ASSERT_EQ("0,0,0,0,0,0,1", FilesPerLevel());
std::vector<KeyVersion> key_versions;
ASSERT_OK(GetAllKeyVersions(db_, {}, {}, std::numeric_limits<size_t>::max(),
&key_versions));
ASSERT_OK(GetAllKeyVersions(
db_.get(), {}, {}, std::numeric_limits<size_t>::max(), &key_versions));
// make sure there're more than 300 keys and first 100 keys are having seqno
// zeroed out, the last 100 key seqno not zeroed out
@@ -2319,8 +2319,8 @@ TEST_P(PrecludeLastLevelTest, LastLevelOnlyCompactionPartial) {
ASSERT_GT(GetSstSizeHelper(Temperature::kUnknown), 0);
std::vector<KeyVersion> key_versions;
ASSERT_OK(GetAllKeyVersions(db_, {}, {}, std::numeric_limits<size_t>::max(),
&key_versions));
ASSERT_OK(GetAllKeyVersions(
db_.get(), {}, {}, std::numeric_limits<size_t>::max(), &key_versions));
// make sure there're more than 300 keys and first 100 keys are having seqno
// zeroed out, the last 100 key seqno not zeroed out
+7 -9
View File
@@ -258,12 +258,12 @@ class ComparatorDBTest
private:
std::string dbname_;
Env* env_;
DB* db_;
std::unique_ptr<DB> db_;
Options last_options_;
std::unique_ptr<const Comparator> comparator_guard;
public:
ComparatorDBTest() : env_(Env::Default()), db_(nullptr) {
ComparatorDBTest() : env_(Env::Default()) {
kTestComparator = BytewiseComparator();
dbname_ = test::PerThreadDBPath("comparator_db_test");
BlockBasedTableOptions toptions;
@@ -274,12 +274,12 @@ class ComparatorDBTest
}
~ComparatorDBTest() override {
delete db_;
db_.reset();
EXPECT_OK(DestroyDB(dbname_, last_options_));
kTestComparator = BytewiseComparator();
}
DB* GetDB() { return db_; }
DB* GetDB() { return db_.get(); }
void SetOwnedComparator(const Comparator* cmp, bool owner = true) {
if (owner) {
@@ -301,14 +301,12 @@ class ComparatorDBTest
}
void Destroy() {
delete db_;
db_ = nullptr;
db_.reset();
ASSERT_OK(DestroyDB(dbname_, last_options_));
}
Status TryReopen() {
delete db_;
db_ = nullptr;
db_.reset();
last_options_.create_if_missing = true;
return DB::Open(last_options_, dbname_, &db_);
@@ -318,7 +316,7 @@ class ComparatorDBTest
INSTANTIATE_TEST_CASE_P(FormatDef, ComparatorDBTest,
testing::Values(test::kDefaultFormatVersion));
INSTANTIATE_TEST_CASE_P(FormatLatest, ComparatorDBTest,
testing::Values(kLatestFormatVersion));
testing::Values(kLatestBbtFormatVersion));
TEST_P(ComparatorDBTest, Bytewise) {
for (int rand_seed = 301; rand_seed < 306; rand_seed++) {
+4 -4
View File
@@ -65,7 +65,7 @@ Status VerifySstFileChecksum(const Options& options,
}
Status VerifySstFileChecksumInternal(const Options& options,
const EnvOptions& env_options,
const FileOptions& file_options,
const ReadOptions& read_options,
const std::string& file_path,
const SequenceNumber& largest_seqno) {
@@ -74,8 +74,8 @@ Status VerifySstFileChecksumInternal(const Options& options,
InternalKeyComparator internal_comparator(options.comparator);
ImmutableOptions ioptions(options);
Status s = ioptions.fs->NewRandomAccessFile(
file_path, FileOptions(env_options), &file, nullptr);
Status s =
ioptions.fs->NewRandomAccessFile(file_path, file_options, &file, nullptr);
if (s.ok()) {
s = ioptions.fs->GetFileSize(file_path, IOOptions(), &file_size, nullptr);
} else {
@@ -94,7 +94,7 @@ Status VerifySstFileChecksumInternal(const Options& options,
const bool kImmortal = true;
auto reader_options = TableReaderOptions(
ioptions, options.prefix_extractor, options.compression_manager.get(),
env_options, internal_comparator, options.block_protection_bytes_per_key,
file_options, internal_comparator, options.block_protection_bytes_per_key,
false /* skip_filters */, !kImmortal, false /* force_direct_prefetch */,
-1 /* level */);
reader_options.largest_seqno = largest_seqno;
+2 -1
View File
@@ -5,10 +5,11 @@
#pragma once
#include "rocksdb/db.h"
#include "rocksdb/file_system.h"
namespace ROCKSDB_NAMESPACE {
Status VerifySstFileChecksumInternal(const Options& options,
const EnvOptions& env_options,
const FileOptions& file_options,
const ReadOptions& read_options,
const std::string& file_path,
const SequenceNumber& largest_seqno = 0);
+60 -88
View File
@@ -73,7 +73,7 @@ class CorruptionTest : public testing::Test {
std::string dbname_;
std::shared_ptr<Cache> tiny_cache_;
Options options_;
DB* db_;
std::unique_ptr<DB> db_;
CorruptionTest() {
// If LRU cache shard bit is smaller than 2 (or -1 which will automatically
@@ -105,8 +105,7 @@ class CorruptionTest : public testing::Test {
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->LoadDependency({});
SyncPoint::GetInstance()->ClearAllCallBacks();
delete db_;
db_ = nullptr;
db_.reset();
if (getenv("KEEP_DB")) {
fprintf(stdout, "db is still at %s\n", dbname_.c_str());
} else {
@@ -116,14 +115,12 @@ class CorruptionTest : public testing::Test {
}
}
void CloseDb() {
delete db_;
db_ = nullptr;
}
void CloseDb() { db_.reset(); }
DBImpl* dbfull() { return static_cast_with_check<DBImpl>(db_.get()); }
Status TryReopen(Options* options = nullptr) {
delete db_;
db_ = nullptr;
db_.reset();
Options opt = (options ? *options : options_);
if (opt.env == Options().env) {
// If env is not overridden, replace it with ErrorEnv.
@@ -141,8 +138,7 @@ class CorruptionTest : public testing::Test {
void Reopen(Options* options = nullptr) { ASSERT_OK(TryReopen(options)); }
void RepairDB() {
delete db_;
db_ = nullptr;
db_.reset();
ASSERT_OK(::ROCKSDB_NAMESPACE::RepairDB(dbname_, options_));
}
@@ -151,8 +147,7 @@ class CorruptionTest : public testing::Test {
WriteBatch batch;
for (int i = 0; i < n; i++) {
if (flush_every != 0 && i != 0 && i % flush_every == 0) {
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
ASSERT_OK(dbi->TEST_FlushMemTable());
ASSERT_OK(dbfull()->TEST_FlushMemTable());
}
// if ((i % 100) == 0) fprintf(stderr, "@ %d of %d\n", i, n);
Slice key = Key(i + start, &key_space);
@@ -436,14 +431,14 @@ TEST_F(CorruptionTest, NewFileErrorDuringWrite) {
TEST_F(CorruptionTest, TableFile) {
Build(100);
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
DBImpl* dbi = dbfull();
ASSERT_OK(dbi->TEST_FlushMemTable());
ASSERT_OK(dbi->TEST_CompactRange(0, nullptr, nullptr));
ASSERT_OK(dbi->TEST_CompactRange(1, nullptr, nullptr));
Corrupt(kTableFile, 100, 1);
Check(99, 99);
ASSERT_NOK(dbi->VerifyChecksum());
ASSERT_NOK(db_->VerifyChecksum());
}
TEST_F(CorruptionTest, VerifyChecksumReadahead) {
@@ -460,14 +455,14 @@ TEST_F(CorruptionTest, VerifyChecksumReadahead) {
Reopen(&options);
Build(10000);
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
DBImpl* dbi = dbfull();
ASSERT_OK(dbi->TEST_FlushMemTable());
ASSERT_OK(dbi->TEST_CompactRange(0, nullptr, nullptr));
ASSERT_OK(dbi->TEST_CompactRange(1, nullptr, nullptr));
senv.count_random_reads_ = true;
senv.random_read_counter_.Reset();
ASSERT_OK(dbi->VerifyChecksum());
ASSERT_OK(db_->VerifyChecksum());
// Make sure the counter is enabled.
ASSERT_GT(senv.random_read_counter_.Read(), 0);
@@ -480,7 +475,7 @@ TEST_F(CorruptionTest, VerifyChecksumReadahead) {
senv.random_read_bytes_counter_ = 0;
ReadOptions ro;
ro.readahead_size = size_t{32 * 1024};
ASSERT_OK(dbi->VerifyChecksum(ro));
ASSERT_OK(db_->VerifyChecksum(ro));
// The SST file is about 10MB. We set readahead size to 32KB.
// Give 0 to 20 reads for metadata blocks, and allow real read
// to range from 24KB to 48KB. The lower bound would be:
@@ -494,8 +489,7 @@ TEST_F(CorruptionTest, VerifyChecksumReadahead) {
// disabled).
options.allow_mmap_reads = true;
Reopen(&options);
dbi = static_cast<DBImpl*>(db_);
ASSERT_OK(dbi->VerifyChecksum(ro));
ASSERT_OK(db_->VerifyChecksum(ro));
CloseDb();
}
@@ -508,18 +502,16 @@ TEST_F(CorruptionTest, TableFileIndexData) {
Reopen(&options);
// build 2 tables, flush at 5000
Build(10000, 5000);
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
ASSERT_OK(dbi->TEST_FlushMemTable());
ASSERT_OK(dbfull()->TEST_FlushMemTable());
// corrupt an index block of an entire file
Corrupt(kTableFile, -2000, 500);
options.paranoid_checks = false;
Reopen(&options);
dbi = static_cast_with_check<DBImpl>(db_);
// one full file may be readable, since only one was corrupted
// the other file should be fully non-readable, since index was corrupted
Check(0, 5000, ReadOptions(true, true));
ASSERT_NOK(dbi->VerifyChecksum());
ASSERT_NOK(db_->VerifyChecksum());
// In paranoid mode, the db cannot be opened due to the corrupted file.
ASSERT_TRUE(TryReopen().IsCorruption());
@@ -527,8 +519,7 @@ TEST_F(CorruptionTest, TableFileIndexData) {
TEST_F(CorruptionTest, TableFileFooterMagic) {
Build(100);
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
ASSERT_OK(dbi->TEST_FlushMemTable());
ASSERT_OK(dbfull()->TEST_FlushMemTable());
Check(100, 100);
// Corrupt the whole footer
Corrupt(kTableFile, -100, 100);
@@ -543,8 +534,7 @@ TEST_F(CorruptionTest, TableFileFooterMagic) {
TEST_F(CorruptionTest, TableFileFooterNotMagic) {
Build(100);
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
ASSERT_OK(dbi->TEST_FlushMemTable());
ASSERT_OK(dbfull()->TEST_FlushMemTable());
Check(100, 100);
// Corrupt footer except magic number
Corrupt(kTableFile, -100, 92);
@@ -579,8 +569,7 @@ TEST_F(CorruptionTest, DBOpenWithWrongFileSize) {
for (auto* cfh : cfhs) {
delete cfh;
}
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
ASSERT_OK(dbi->TEST_FlushMemTable());
ASSERT_OK(dbfull()->TEST_FlushMemTable());
// ********************************************
// Corrupt the file by making the file bigger
@@ -601,7 +590,8 @@ TEST_F(CorruptionTest, DBOpenWithWrongFileSize) {
// true
options_.paranoid_checks = true;
std::vector<ColumnFamilyHandle*> cfhs;
auto s = DB::Open(options_, dbname_, cf_descs, &cfhs, &db_);
Status s;
s = DB::Open(options_, dbname_, cf_descs, &cfhs, &db_);
ASSERT_TRUE(s.IsCorruption());
ASSERT_TRUE(s.ToString().find("file size mismatch") != std::string::npos);
@@ -626,8 +616,7 @@ TEST_F(CorruptionTest, DBOpenWithWrongFileSize) {
TEST_F(CorruptionTest, TableFileWrongSize) {
Build(100);
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
ASSERT_OK(dbi->TEST_FlushMemTable());
ASSERT_OK(dbfull()->TEST_FlushMemTable());
Check(100, 100);
// ********************************************
@@ -710,12 +699,11 @@ TEST_F(CorruptionTest, SequenceNumberRecovery) {
TEST_F(CorruptionTest, CorruptedDescriptor) {
ASSERT_OK(db_->Put(WriteOptions(), "foo", "hello"));
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
ASSERT_OK(dbi->TEST_FlushMemTable());
ASSERT_OK(dbfull()->TEST_FlushMemTable());
CompactRangeOptions cro;
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
ASSERT_OK(
dbi->CompactRange(cro, dbi->DefaultColumnFamily(), nullptr, nullptr));
db_->CompactRange(cro, db_->DefaultColumnFamily(), nullptr, nullptr));
Corrupt(kDescriptorFile, 0, 1000);
Status s = TryReopen();
@@ -734,7 +722,7 @@ TEST_F(CorruptionTest, CompactionInputError) {
options.env = env_.get();
Reopen(&options);
Build(10);
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
DBImpl* dbi = dbfull();
ASSERT_OK(dbi->TEST_FlushMemTable());
ASSERT_OK(dbi->TEST_CompactRange(0, nullptr, nullptr));
ASSERT_OK(dbi->TEST_CompactRange(1, nullptr, nullptr));
@@ -742,12 +730,12 @@ TEST_F(CorruptionTest, CompactionInputError) {
Corrupt(kTableFile, 100, 1);
Check(9, 9);
ASSERT_NOK(dbi->VerifyChecksum());
ASSERT_NOK(db_->VerifyChecksum());
// Force compactions by writing lots of values
Build(10000);
Check(10000, 10000);
ASSERT_NOK(dbi->VerifyChecksum());
ASSERT_NOK(db_->VerifyChecksum());
}
TEST_F(CorruptionTest, CompactionInputErrorParanoid) {
@@ -758,14 +746,14 @@ TEST_F(CorruptionTest, CompactionInputErrorParanoid) {
options.write_buffer_size = 131072;
options.max_write_buffer_number = 2;
Reopen(&options);
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
DBImpl* dbi = dbfull();
// Fill levels >= 1
for (int level = 1; level < dbi->NumberLevels(); level++) {
ASSERT_OK(dbi->Put(WriteOptions(), "", "begin"));
ASSERT_OK(dbi->Put(WriteOptions(), "~", "end"));
for (int level = 1; level < db_->NumberLevels(); level++) {
ASSERT_OK(db_->Put(WriteOptions(), "", "begin"));
ASSERT_OK(db_->Put(WriteOptions(), "~", "end"));
ASSERT_OK(dbi->TEST_FlushMemTable());
for (int comp_level = 0; comp_level < dbi->NumberLevels() - level;
for (int comp_level = 0; comp_level < db_->NumberLevels() - level;
++comp_level) {
ASSERT_OK(dbi->TEST_CompactRange(comp_level, nullptr, nullptr));
}
@@ -773,7 +761,7 @@ TEST_F(CorruptionTest, CompactionInputErrorParanoid) {
Reopen(&options);
dbi = static_cast_with_check<DBImpl>(db_);
dbi = dbfull();
Build(10);
ASSERT_OK(dbi->TEST_FlushMemTable());
ASSERT_OK(dbi->TEST_WaitForCompact());
@@ -781,7 +769,7 @@ TEST_F(CorruptionTest, CompactionInputErrorParanoid) {
CorruptTableFileAtLevel(0, 100, 1);
Check(9, 9);
ASSERT_NOK(dbi->VerifyChecksum());
ASSERT_NOK(db_->VerifyChecksum());
// Write must eventually fail because of corrupted table
Status s;
@@ -800,17 +788,16 @@ TEST_F(CorruptionTest, CompactionInputErrorParanoid) {
TEST_F(CorruptionTest, UnrelatedKeys) {
Build(10);
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
ASSERT_OK(dbi->TEST_FlushMemTable());
ASSERT_OK(dbfull()->TEST_FlushMemTable());
Corrupt(kTableFile, 100, 1);
ASSERT_NOK(dbi->VerifyChecksum());
ASSERT_NOK(db_->VerifyChecksum());
std::string tmp1, tmp2;
ASSERT_OK(db_->Put(WriteOptions(), Key(1000, &tmp1), Value(1000, &tmp2)));
std::string v;
ASSERT_OK(db_->Get(ReadOptions(), Key(1000, &tmp1), &v));
ASSERT_EQ(Value(1000, &tmp2).ToString(), v);
ASSERT_OK(dbi->TEST_FlushMemTable());
ASSERT_OK(dbfull()->TEST_FlushMemTable());
ASSERT_OK(db_->Get(ReadOptions(), Key(1000, &tmp1), &v));
ASSERT_EQ(Value(1000, &tmp2).ToString(), v);
}
@@ -857,14 +844,12 @@ TEST_F(CorruptionTest, FileSystemStateCorrupted) {
Reopen(&options);
Build(10);
ASSERT_OK(db_->Flush(FlushOptions()));
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
std::vector<LiveFileMetaData> metadata;
dbi->GetLiveFilesMetaData(&metadata);
db_->GetLiveFilesMetaData(&metadata);
ASSERT_GT(metadata.size(), 0);
std::string filename = dbname_ + metadata[0].name;
delete db_;
db_ = nullptr;
db_.reset();
if (iter == 0) { // corrupt file size
std::unique_ptr<WritableFile> file;
@@ -896,8 +881,7 @@ TEST_F(CorruptionTest, ParanoidFileChecksOnFlush) {
options.create_if_missing = true;
Status s;
for (const auto& mode : corruption_modes) {
delete db_;
db_ = nullptr;
db_.reset();
s = DestroyDB(dbname_, options);
ASSERT_OK(s);
std::shared_ptr<mock::MockTableFactory> mock =
@@ -924,8 +908,7 @@ TEST_F(CorruptionTest, ParanoidFileChecksOnCompact) {
options.create_if_missing = true;
Status s;
for (const auto& mode : corruption_modes) {
delete db_;
db_ = nullptr;
db_.reset();
s = DestroyDB(dbname_, options);
ASSERT_OK(s);
std::shared_ptr<mock::MockTableFactory> mock =
@@ -934,12 +917,11 @@ TEST_F(CorruptionTest, ParanoidFileChecksOnCompact) {
ASSERT_OK(DB::Open(options, dbname_, &db_));
assert(db_ != nullptr); // suppress false clang-analyze report
Build(100, 2);
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
ASSERT_OK(dbi->TEST_FlushMemTable());
ASSERT_OK(dbfull()->TEST_FlushMemTable());
mock->SetCorruptionMode(mode);
CompactRangeOptions cro;
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
s = dbi->CompactRange(cro, dbi->DefaultColumnFamily(), nullptr, nullptr);
s = db_->CompactRange(cro, db_->DefaultColumnFamily(), nullptr, nullptr);
if (mode == mock::MockTableFactory::kCorruptNone) {
ASSERT_OK(s);
} else {
@@ -955,8 +937,7 @@ TEST_F(CorruptionTest, ParanoidFileChecksWithDeleteRangeFirst) {
options.paranoid_file_checks = true;
options.create_if_missing = true;
for (bool do_flush : {true, false}) {
delete db_;
db_ = nullptr;
db_.reset();
ASSERT_OK(DestroyDB(dbname_, options));
ASSERT_OK(DB::Open(options, dbname_, &db_));
std::string start, end;
@@ -973,12 +954,11 @@ TEST_F(CorruptionTest, ParanoidFileChecksWithDeleteRangeFirst) {
if (do_flush) {
ASSERT_OK(db_->Flush(FlushOptions()));
} else {
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
ASSERT_OK(dbi->TEST_FlushMemTable());
ASSERT_OK(dbfull()->TEST_FlushMemTable());
CompactRangeOptions cro;
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
ASSERT_OK(
dbi->CompactRange(cro, dbi->DefaultColumnFamily(), nullptr, nullptr));
db_->CompactRange(cro, db_->DefaultColumnFamily(), nullptr, nullptr));
}
db_->ReleaseSnapshot(snap);
}
@@ -991,8 +971,7 @@ TEST_F(CorruptionTest, ParanoidFileChecksWithDeleteRange) {
options.paranoid_file_checks = true;
options.create_if_missing = true;
for (bool do_flush : {true, false}) {
delete db_;
db_ = nullptr;
db_.reset();
ASSERT_OK(DestroyDB(dbname_, options));
ASSERT_OK(DB::Open(options, dbname_, &db_));
assert(db_ != nullptr); // suppress false clang-analyze report
@@ -1012,12 +991,11 @@ TEST_F(CorruptionTest, ParanoidFileChecksWithDeleteRange) {
if (do_flush) {
ASSERT_OK(db_->Flush(FlushOptions()));
} else {
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
ASSERT_OK(dbi->TEST_FlushMemTable());
ASSERT_OK(dbfull()->TEST_FlushMemTable());
CompactRangeOptions cro;
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
ASSERT_OK(
dbi->CompactRange(cro, dbi->DefaultColumnFamily(), nullptr, nullptr));
db_->CompactRange(cro, db_->DefaultColumnFamily(), nullptr, nullptr));
}
db_->ReleaseSnapshot(snap);
}
@@ -1030,8 +1008,7 @@ TEST_F(CorruptionTest, ParanoidFileChecksWithDeleteRangeLast) {
options.paranoid_file_checks = true;
options.create_if_missing = true;
for (bool do_flush : {true, false}) {
delete db_;
db_ = nullptr;
db_.reset();
ASSERT_OK(DestroyDB(dbname_, options));
ASSERT_OK(DB::Open(options, dbname_, &db_));
assert(db_ != nullptr); // suppress false clang-analyze report
@@ -1048,12 +1025,11 @@ TEST_F(CorruptionTest, ParanoidFileChecksWithDeleteRangeLast) {
if (do_flush) {
ASSERT_OK(db_->Flush(FlushOptions()));
} else {
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
ASSERT_OK(dbi->TEST_FlushMemTable());
ASSERT_OK(dbfull()->TEST_FlushMemTable());
CompactRangeOptions cro;
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
ASSERT_OK(
dbi->CompactRange(cro, dbi->DefaultColumnFamily(), nullptr, nullptr));
db_->CompactRange(cro, db_->DefaultColumnFamily(), nullptr, nullptr));
}
db_->ReleaseSnapshot(snap);
}
@@ -1066,8 +1042,7 @@ TEST_F(CorruptionTest, LogCorruptionErrorsInCompactionIterator) {
options.create_if_missing = true;
options.allow_data_in_errors = true;
auto mode = mock::MockTableFactory::kCorruptKey;
delete db_;
db_ = nullptr;
db_.reset();
ASSERT_OK(DestroyDB(dbname_, options));
std::shared_ptr<mock::MockTableFactory> mock =
@@ -1079,12 +1054,11 @@ TEST_F(CorruptionTest, LogCorruptionErrorsInCompactionIterator) {
assert(db_ != nullptr); // suppress false clang-analyze report
Build(100, 2);
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
ASSERT_OK(dbi->TEST_FlushMemTable());
ASSERT_OK(dbfull()->TEST_FlushMemTable());
CompactRangeOptions cro;
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
Status s =
dbi->CompactRange(cro, dbi->DefaultColumnFamily(), nullptr, nullptr);
db_->CompactRange(cro, db_->DefaultColumnFamily(), nullptr, nullptr);
ASSERT_NOK(s);
ASSERT_TRUE(s.IsCorruption());
}
@@ -1095,8 +1069,7 @@ TEST_F(CorruptionTest, CompactionKeyOrderCheck) {
options.env = env_.get();
options.paranoid_file_checks = false;
options.create_if_missing = true;
delete db_;
db_ = nullptr;
db_.reset();
ASSERT_OK(DestroyDB(dbname_, options));
std::shared_ptr<mock::MockTableFactory> mock =
std::make_shared<mock::MockTableFactory>();
@@ -1105,14 +1078,13 @@ TEST_F(CorruptionTest, CompactionKeyOrderCheck) {
assert(db_ != nullptr); // suppress false clang-analyze report
mock->SetCorruptionMode(mock::MockTableFactory::kCorruptReorderKey);
Build(100, 2);
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
ASSERT_OK(dbi->TEST_FlushMemTable());
ASSERT_OK(dbfull()->TEST_FlushMemTable());
mock->SetCorruptionMode(mock::MockTableFactory::kCorruptNone);
CompactRangeOptions cro;
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
ASSERT_NOK(
dbi->CompactRange(cro, dbi->DefaultColumnFamily(), nullptr, nullptr));
db_->CompactRange(cro, db_->DefaultColumnFamily(), nullptr, nullptr));
}
TEST_F(CorruptionTest, FlushKeyOrderCheck) {
@@ -1139,7 +1111,7 @@ TEST_F(CorruptionTest, FlushKeyOrderCheck) {
}
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
Status s = static_cast_with_check<DBImpl>(db_)->TEST_FlushMemTable();
Status s = dbfull()->TEST_FlushMemTable();
ASSERT_NOK(s);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
@@ -1263,7 +1235,7 @@ TEST_P(CrashDuringRecoveryWithCorruptionTest, CrashDuringRecovery) {
// while other don't.
{
ASSERT_OK(DB::Open(options, dbname_, cf_descs, &handles, &db_));
auto* dbimpl = static_cast_with_check<DBImpl>(db_);
auto* dbimpl = dbfull();
assert(dbimpl);
// Write one key to test_cf.
+10 -12
View File
@@ -21,18 +21,18 @@ class CuckooTableDBTest : public testing::Test {
private:
std::string dbname_;
Env* env_;
DB* db_;
std::unique_ptr<DB> db_;
public:
CuckooTableDBTest() : env_(Env::Default()) {
dbname_ = test::PerThreadDBPath("cuckoo_table_db_test");
EXPECT_OK(DestroyDB(dbname_, Options()));
db_ = nullptr;
db_.reset();
Reopen();
}
~CuckooTableDBTest() override {
delete db_;
db_.reset();
EXPECT_OK(DestroyDB(dbname_, Options()));
}
@@ -47,12 +47,11 @@ class CuckooTableDBTest : public testing::Test {
return options;
}
DBImpl* dbfull() { return static_cast_with_check<DBImpl>(db_); }
DBImpl* dbfull() { return static_cast_with_check<DBImpl>(db_.get()); }
// The following util methods are copied from plain_table_db_test.
void Reopen(Options* options = nullptr) {
delete db_;
db_ = nullptr;
db_.reset();
Options opts;
if (options != nullptr) {
opts = *options;
@@ -66,8 +65,7 @@ class CuckooTableDBTest : public testing::Test {
void DestroyAndReopen(Options* options) {
assert(options);
ASSERT_OK(db_->Close());
delete db_;
db_ = nullptr;
db_.reset();
ASSERT_OK(DestroyDB(dbname_, *options));
Reopen(options);
}
@@ -130,7 +128,7 @@ TEST_F(CuckooTableDBTest, Flush) {
ASSERT_OK(dbfull()->TEST_FlushMemTable());
TablePropertiesCollection ptc;
ASSERT_OK(static_cast<DB*>(dbfull())->GetPropertiesOfAllTables(&ptc));
ASSERT_OK(dbfull()->GetPropertiesOfAllTables(&ptc));
VerifySstUniqueIds(ptc);
ASSERT_EQ(1U, ptc.size());
ASSERT_EQ(3U, ptc.begin()->second->num_entries);
@@ -147,7 +145,7 @@ TEST_F(CuckooTableDBTest, Flush) {
ASSERT_OK(Put("key6", "v6"));
ASSERT_OK(dbfull()->TEST_FlushMemTable());
ASSERT_OK(static_cast<DB*>(dbfull())->GetPropertiesOfAllTables(&ptc));
ASSERT_OK(dbfull()->GetPropertiesOfAllTables(&ptc));
VerifySstUniqueIds(ptc);
ASSERT_EQ(2U, ptc.size());
auto row = ptc.begin();
@@ -165,7 +163,7 @@ TEST_F(CuckooTableDBTest, Flush) {
ASSERT_OK(Delete("key5"));
ASSERT_OK(Delete("key4"));
ASSERT_OK(dbfull()->TEST_FlushMemTable());
ASSERT_OK(static_cast<DB*>(dbfull())->GetPropertiesOfAllTables(&ptc));
ASSERT_OK(dbfull()->GetPropertiesOfAllTables(&ptc));
VerifySstUniqueIds(ptc);
ASSERT_EQ(3U, ptc.size());
row = ptc.begin();
@@ -190,7 +188,7 @@ TEST_F(CuckooTableDBTest, FlushWithDuplicateKeys) {
ASSERT_OK(dbfull()->TEST_FlushMemTable());
TablePropertiesCollection ptc;
ASSERT_OK(static_cast<DB*>(dbfull())->GetPropertiesOfAllTables(&ptc));
ASSERT_OK(dbfull()->GetPropertiesOfAllTables(&ptc));
VerifySstUniqueIds(ptc);
ASSERT_EQ(1U, ptc.size());
ASSERT_EQ(2U, ptc.begin()->second->num_entries);
+471 -40
View File
@@ -91,17 +91,15 @@ class DBBasicTest : public DBTestBase {
TEST_F(DBBasicTest, OpenWhenOpen) {
Options options = CurrentOptions();
options.env = env_;
DB* db2 = nullptr;
std::unique_ptr<DB> db2;
Status s = DB::Open(options, dbname_, &db2);
ASSERT_NOK(s) << [db2]() {
delete db2;
ASSERT_NOK(s) << [&db2]() {
db2.reset();
return "db2 open: ok";
}();
ASSERT_EQ(Status::Code::kIOError, s.code());
ASSERT_EQ(Status::SubCode::kNone, s.subcode());
ASSERT_TRUE(strstr(s.getState(), "lock ") != nullptr);
delete db2;
}
TEST_F(DBBasicTest, EnableDirectIOWithZeroBuf) {
@@ -242,6 +240,43 @@ TEST_F(DBBasicTest, ReadOnlyDB) {
Status::Code::kNotSupported);
}
TEST_F(DBBasicTest, ReadOnlyDBFlushWAL) {
// Test that FlushWAL returns NotSupported on read-only DB, and that
// GetLiveFilesStorageInfo works correctly even with manual_wal_flush=true.
// This is a regression test for a bug where GetLiveFilesStorageInfo would
// crash on read-only DBs with manual_wal_flush=true because FlushWAL
// accessed logs_.back() on an empty deque.
auto options = CurrentOptions();
options.manual_wal_flush = true;
DestroyAndReopen(options);
ASSERT_OK(Put("foo", "v1"));
ASSERT_OK(Put("bar", "v2"));
ASSERT_OK(Flush());
ASSERT_OK(Put("baz", "v3")); // Unflushed data in WAL
Close();
// Reopen as read-only
ASSERT_OK(ReadOnlyReopen(options));
ASSERT_EQ("v1", Get("foo"));
ASSERT_EQ("v2", Get("bar"));
ASSERT_EQ("v3", Get("baz"));
// FlushWAL should return NotSupported (not crash)
ASSERT_EQ(db_->FlushWAL(/*sync=*/false).code(), Status::Code::kNotSupported);
ASSERT_EQ(db_->FlushWAL(/*sync=*/true).code(), Status::Code::kNotSupported);
// GetLiveFilesStorageInfo should succeed (previously crashed with
// manual_wal_flush=true because it called FlushWAL which accessed
// logs_.back() on empty deque)
LiveFilesStorageInfoOptions lfsi_opts;
lfsi_opts.wal_size_for_flush = 0;
std::vector<LiveFileStorageInfo> files;
ASSERT_OK(db_->GetLiveFilesStorageInfo(lfsi_opts, &files));
ASSERT_GT(files.size(), 0);
Close();
}
TEST_F(DBBasicTest, ReadOnlyDBWithWriteDBIdToManifestSet) {
auto options = CurrentOptions();
options.write_dbid_to_manifest = false;
@@ -549,14 +584,14 @@ TEST_F(DBBasicTest, GetSnapshot) {
TEST_F(DBBasicTest, CheckLock) {
do {
DB* localdb = nullptr;
std::unique_ptr<DB> localdb;
Options options = CurrentOptions();
ASSERT_OK(TryReopen(options));
// second open should fail
Status s = DB::Open(options, dbname_, &localdb);
ASSERT_NOK(s) << [localdb]() {
delete localdb;
ASSERT_NOK(s) << [&localdb]() {
localdb.reset();
return "localdb open: ok";
}();
#ifdef OS_LINUX
@@ -825,7 +860,7 @@ TEST_F(DBBasicTest, Snapshot) {
ASSERT_OK(Put(1, "foo", "1v3"));
{
ManagedSnapshot s3(db_);
ManagedSnapshot s3(db_.get());
ASSERT_EQ(3U, GetNumSnapshots());
ASSERT_EQ(time_snap1, GetTimeOldestSnapshots());
ASSERT_EQ(GetSequenceOldestSnapshots(), s1->GetSequenceNumber());
@@ -948,7 +983,7 @@ TEST_F(DBBasicTest, DBOpen_Options) {
Destroy(options);
// Does not exist, and create_if_missing == false: error
DB* db = nullptr;
std::unique_ptr<DB> db;
options.create_if_missing = false;
Status s = DB::Open(options, dbname_, &db);
ASSERT_TRUE(strstr(s.ToString().c_str(), "does not exist") != nullptr);
@@ -960,8 +995,7 @@ TEST_F(DBBasicTest, DBOpen_Options) {
ASSERT_OK(s);
ASSERT_TRUE(db != nullptr);
delete db;
db = nullptr;
db.reset();
// Does exist, and error_if_exists == true: error
options.create_if_missing = false;
@@ -977,8 +1011,7 @@ TEST_F(DBBasicTest, DBOpen_Options) {
ASSERT_OK(s);
ASSERT_TRUE(db != nullptr);
delete db;
db = nullptr;
db.reset();
}
TEST_F(DBBasicTest, CompactOnFlush) {
@@ -1283,7 +1316,7 @@ TEST_F(DBBasicTest, DBClose) {
std::string dbname = test::PerThreadDBPath("db_close_test");
ASSERT_OK(DestroyDB(dbname, options));
DB* db = nullptr;
std::unique_ptr<DB> db;
TestEnv* env = new TestEnv(env_);
std::unique_ptr<TestEnv> local_env_guard(env);
options.create_if_missing = true;
@@ -1296,14 +1329,14 @@ TEST_F(DBBasicTest, DBClose) {
ASSERT_EQ(env->GetCloseCount(), 1);
ASSERT_EQ(s, Status::IOError());
delete db;
db.reset();
ASSERT_EQ(env->GetCloseCount(), 1);
// Do not call DB::Close() and ensure our logger Close() still gets called
s = DB::Open(options, dbname, &db);
ASSERT_OK(s);
ASSERT_TRUE(db != nullptr);
delete db;
db.reset();
ASSERT_EQ(env->GetCloseCount(), 2);
// close by WaitForCompact() with close_db option
@@ -1318,7 +1351,7 @@ TEST_F(DBBasicTest, DBClose) {
// see TestLogger::CloseHelper()
ASSERT_EQ(s, Status::IOError());
delete db;
db.reset();
ASSERT_EQ(env->GetCloseCount(), 3);
// Provide our own logger and ensure DB::Close() does not close it
@@ -1329,7 +1362,7 @@ TEST_F(DBBasicTest, DBClose) {
s = db->Close();
ASSERT_EQ(s, Status::OK());
delete db;
db.reset();
ASSERT_EQ(env->GetCloseCount(), 3);
options.info_log.reset();
ASSERT_EQ(env->GetCloseCount(), 4);
@@ -1347,7 +1380,7 @@ TEST_F(DBBasicTest, DBCloseAllDirectoryFDs) {
ASSERT_OK(DestroyDB(dbname, options));
DB* db = nullptr;
std::unique_ptr<DB> db;
std::unique_ptr<Env> env = NewCompositeEnv(
std::make_shared<CountedFileSystem>(FileSystem::Default()));
options.create_if_missing = true;
@@ -1365,7 +1398,7 @@ TEST_F(DBBasicTest, DBCloseAllDirectoryFDs) {
ASSERT_EQ(counted_fs->counters()->dir_opens,
counted_fs->counters()->dir_closes);
ASSERT_OK(s);
delete db;
db.reset();
}
TEST_F(DBBasicTest, DBCloseFlushError) {
@@ -1427,7 +1460,7 @@ TEST_P(DBMultiGetTestWithParam, MultiGetMultiCF) {
}
int get_sv_count = 0;
ROCKSDB_NAMESPACE::DBImpl* db = static_cast_with_check<DBImpl>(db_);
ROCKSDB_NAMESPACE::DBImpl* db = dbfull();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"DBImpl::MultiCFSnapshot::AfterRefSV", [&](void* /*arg*/) {
if (++get_sv_count == 2) {
@@ -1499,10 +1532,9 @@ TEST_P(DBMultiGetTestWithParam, MultiGetMultiCF) {
ASSERT_EQ(values[2], std::get<2>(cf_kv_vec[1]) + "_2");
for (int cf = 0; cf < 8; ++cf) {
auto* cfd =
static_cast_with_check<ColumnFamilyHandleImpl>(
static_cast_with_check<DBImpl>(db_)->GetColumnFamilyHandle(cf))
->cfd();
auto* cfd = static_cast_with_check<ColumnFamilyHandleImpl>(
dbfull()->GetColumnFamilyHandle(cf))
->cfd();
ASSERT_NE(cfd->TEST_GetLocalSV()->Get(), SuperVersion::kSVInUse);
ASSERT_NE(cfd->TEST_GetLocalSV()->Get(), SuperVersion::kSVObsolete);
}
@@ -1588,10 +1620,9 @@ TEST_P(DBMultiGetTestWithParam, MultiGetMultiCFMutex) {
"cf" + std::to_string(j) + "_val" + std::to_string(retries));
}
for (int i = 0; i < 8; ++i) {
auto* cfd =
static_cast_with_check<ColumnFamilyHandleImpl>(
static_cast_with_check<DBImpl>(db_)->GetColumnFamilyHandle(i))
->cfd();
auto* cfd = static_cast_with_check<ColumnFamilyHandleImpl>(
dbfull()->GetColumnFamilyHandle(i))
->cfd();
ASSERT_NE(cfd->TEST_GetLocalSV()->Get(), SuperVersion::kSVInUse);
}
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
@@ -1615,7 +1646,7 @@ TEST_P(DBMultiGetTestWithParam, MultiGetMultiCFSnapshot) {
}
int get_sv_count = 0;
ROCKSDB_NAMESPACE::DBImpl* db = static_cast_with_check<DBImpl>(db_);
ROCKSDB_NAMESPACE::DBImpl* db = dbfull();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"DBImpl::MultiCFSnapshot::AfterRefSV", [&](void* /*arg*/) {
if (++get_sv_count == 2) {
@@ -1656,10 +1687,9 @@ TEST_P(DBMultiGetTestWithParam, MultiGetMultiCFSnapshot) {
ASSERT_EQ(values[j], "cf" + std::to_string(j) + "_val");
}
for (int i = 0; i < 8; ++i) {
auto* cfd =
static_cast_with_check<ColumnFamilyHandleImpl>(
static_cast_with_check<DBImpl>(db_)->GetColumnFamilyHandle(i))
->cfd();
auto* cfd = static_cast_with_check<ColumnFamilyHandleImpl>(
dbfull()->GetColumnFamilyHandle(i))
->cfd();
ASSERT_NE(cfd->TEST_GetLocalSV()->Get(), SuperVersion::kSVInUse);
}
}
@@ -2505,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()));
@@ -3264,8 +3607,8 @@ TEST_F(DBBasicTest, GetAllKeyVersions) {
ASSERT_OK(Delete(std::to_string(i)));
}
std::vector<KeyVersion> key_versions;
ASSERT_OK(GetAllKeyVersions(db_, {}, {}, std::numeric_limits<size_t>::max(),
&key_versions));
ASSERT_OK(GetAllKeyVersions(
db_.get(), {}, {}, std::numeric_limits<size_t>::max(), &key_versions));
ASSERT_EQ(kNumInserts + kNumDeletes + kNumUpdates, key_versions.size());
for (size_t i = 0; i < kNumInserts + kNumDeletes + kNumUpdates; i++) {
if (i % 3 == 0) {
@@ -3274,7 +3617,7 @@ TEST_F(DBBasicTest, GetAllKeyVersions) {
ASSERT_EQ(key_versions[i].GetTypeName(), "TypeValue");
}
}
ASSERT_OK(GetAllKeyVersions(db_, handles_[0], {}, {},
ASSERT_OK(GetAllKeyVersions(db_.get(), handles_[0], {}, {},
std::numeric_limits<size_t>::max(),
&key_versions));
ASSERT_EQ(kNumInserts + kNumDeletes + kNumUpdates, key_versions.size());
@@ -3289,14 +3632,14 @@ TEST_F(DBBasicTest, GetAllKeyVersions) {
for (size_t i = 0; i + 1 != kNumDeletes; ++i) {
ASSERT_OK(Delete(1, std::to_string(i)));
}
ASSERT_OK(GetAllKeyVersions(db_, handles_[1], {}, {},
ASSERT_OK(GetAllKeyVersions(db_.get(), handles_[1], {}, {},
std::numeric_limits<size_t>::max(),
&key_versions));
ASSERT_EQ(kNumInserts + kNumDeletes + kNumUpdates - 3, key_versions.size());
// Change from historical behavior: empty key is now interpreted literally as
// a legal key (rather than as a "not present" key)
ASSERT_OK(GetAllKeyVersions(db_, handles_[1], Slice(), Slice(),
ASSERT_OK(GetAllKeyVersions(db_.get(), handles_[1], Slice(), Slice(),
std::numeric_limits<size_t>::max(),
&key_versions));
ASSERT_EQ(key_versions.size(), 0);
@@ -5437,6 +5780,94 @@ INSTANTIATE_TEST_CASE_P(DBBasicTestDeadline, DBBasicTestDeadline,
::testing::Values(std::make_tuple(true, false),
std::make_tuple(false, true),
std::make_tuple(true, true)));
// FileSystemWrapper that captures FileOptions passed to NewRandomAccessFile
// for .sst files, so we can verify file_checksum fields are populated.
class ChecksumCapturingFS : public FileSystemWrapper {
public:
explicit ChecksumCapturingFS(const std::shared_ptr<FileSystem>& base)
: FileSystemWrapper(base) {}
static const char* kClassName() { return "ChecksumCapturingFS"; }
const char* Name() const override { return kClassName(); }
IOStatus NewRandomAccessFile(const std::string& fname,
const FileOptions& opts,
std::unique_ptr<FSRandomAccessFile>* result,
IODebugContext* dbg) override {
if (fname.find(".sst") != std::string::npos) {
std::lock_guard<std::mutex> lock(mu_);
captured_file_checksum_ = opts.file_checksum;
captured_file_checksum_func_name_ = opts.file_checksum_func_name;
capture_count_++;
}
return target()->NewRandomAccessFile(fname, opts, result, dbg);
}
std::string GetCapturedFileChecksum() {
std::lock_guard<std::mutex> lock(mu_);
return captured_file_checksum_;
}
std::string GetCapturedFileChecksumFuncName() {
std::lock_guard<std::mutex> lock(mu_);
return captured_file_checksum_func_name_;
}
int GetCaptureCount() {
std::lock_guard<std::mutex> lock(mu_);
return capture_count_;
}
void Reset() {
std::lock_guard<std::mutex> lock(mu_);
captured_file_checksum_.clear();
captured_file_checksum_func_name_.clear();
capture_count_ = 0;
}
private:
std::mutex mu_;
std::string captured_file_checksum_;
std::string captured_file_checksum_func_name_;
int capture_count_ = 0;
};
TEST_F(DBBasicTest, FileChecksumInFileOptions) {
// Verify that file_checksum and file_checksum_func_name from FileMetaData
// are propagated through FileOptions when opening SST files.
auto capturing_fs =
std::make_shared<ChecksumCapturingFS>(env_->GetFileSystem());
std::unique_ptr<Env> env(new CompositeEnvWrapper(env_, capturing_fs));
Options options = GetDefaultOptions();
options.create_if_missing = true;
options.env = env.get();
options.file_checksum_gen_factory = GetFileChecksumGenCrc32cFactory();
DestroyAndReopen(options);
// Write data and flush to create an SST with a file checksum.
ASSERT_OK(Put("key1", "value1"));
ASSERT_OK(Flush());
// Reset captures, then reopen to trigger TableCache SST open.
capturing_fs->Reset();
Reopen(options);
// Read to trigger SST open through TableCache::GetTableReader.
ASSERT_EQ("value1", Get("key1"));
// Verify that checksum fields were populated.
ASSERT_GT(capturing_fs->GetCaptureCount(), 0);
ASSERT_FALSE(capturing_fs->GetCapturedFileChecksum().empty());
ASSERT_NE(capturing_fs->GetCapturedFileChecksumFuncName(),
capturing_fs->GetCapturedFileChecksum());
ASSERT_EQ(capturing_fs->GetCapturedFileChecksumFuncName(),
"FileChecksumCrc32c");
Close();
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
+116 -2
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)
@@ -1661,7 +1775,7 @@ TEST_P(DBBlockCacheKeyTest, StableCacheKeys) {
std::string export_files_dir = dbname_ + "/exported";
ExportImportFilesMetaData* metadata_ptr_ = nullptr;
Checkpoint* checkpoint;
ASSERT_OK(Checkpoint::Create(db_, &checkpoint));
ASSERT_OK(Checkpoint::Create(db_.get(), &checkpoint));
ASSERT_OK(checkpoint->ExportColumnFamily(handles_[1], export_files_dir,
&metadata_ptr_));
ASSERT_NE(metadata_ptr_, nullptr);
@@ -1698,7 +1812,7 @@ TEST_P(DBBlockCacheKeyTest, StableCacheKeys) {
// StableCacheKeyTestFS, Checkpoint will resort to full copy not hard link.
// (Checkpoint not available in LITE mode to test this.)
auto db_copy_name = dbname_ + "-copy";
ASSERT_OK(Checkpoint::Create(db_, &checkpoint));
ASSERT_OK(Checkpoint::Create(db_.get(), &checkpoint));
ASSERT_OK(checkpoint->CreateCheckpoint(db_copy_name));
delete checkpoint;
+5 -15
View File
@@ -137,11 +137,6 @@ class SliceTransformLimitedDomainGeneric : public SliceTransform {
// prefix will be x????
return src.size() >= 5;
}
bool InRange(const Slice& dst) const override {
// prefix will be x????
return dst.size() == 5;
}
};
// KeyMayExist can lead to a few false positives, but not false negatives.
@@ -922,14 +917,14 @@ INSTANTIATE_TEST_CASE_P(
::testing::Values(
std::make_tuple(kAutoBloom,
FilterPartitioning::kCoupledPartitionedFilter,
kLatestFormatVersion),
kLatestBbtFormatVersion),
std::make_tuple(kAutoBloom,
FilterPartitioning::kDecoupledPartitionedFilter,
kLatestFormatVersion),
kLatestBbtFormatVersion),
std::make_tuple(kAutoBloom, FilterPartitioning::kUnpartitionedFilter,
kLatestFormatVersion),
kLatestBbtFormatVersion),
std::make_tuple(kAutoRibbon, FilterPartitioning::kUnpartitionedFilter,
kLatestFormatVersion)));
kLatestBbtFormatVersion)));
#endif // !defined(ROCKSDB_VALGRIND_RUN) || defined(ROCKSDB_FULL_VALGRIND_RUN)
TEST_F(DBBloomFilterTest, BloomFilterRate) {
@@ -2077,11 +2072,6 @@ class SliceTransformLimitedDomain : public SliceTransform {
// prefix will be x????
return src.size() >= 5 && src[0] == 'x';
}
bool InRange(const Slice& dst) const override {
// prefix will be x????
return dst.size() == 5 && dst[0] == 'x';
}
};
TEST_F(DBBloomFilterTest, PrefixExtractorWithFilter1) {
@@ -4145,7 +4135,7 @@ TEST_F(DBBloomFilterTest, SstQueryFilter) {
using Keys = std::vector<std::string>;
auto RangeQuery =
[factory, db = db_](
[factory, db = db_.get()](
std::string lb, std::string ub,
std::shared_ptr<SstQueryFilterConfigsManager::Factory> alt_factory =
nullptr) {
+993
View File
@@ -0,0 +1,993 @@
// 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 <atomic>
#include <set>
#include <thread>
#include <unordered_map>
#include "db/compaction/compaction_job.h"
#include "db/db_impl/db_impl_secondary.h"
#include "db/db_test_util.h"
#include "options/options_helper.h"
#include "port/stack_trace.h"
#include "rocksdb/db.h"
#include "rocksdb/sst_file_writer.h"
#include "test_util/sync_point.h"
#include "test_util/testharness.h"
namespace ROCKSDB_NAMESPACE {
// Helper class to manage abort synchronization in tests.
//
// Compaction abort could happen at various stage of compaction.
// To test this, we need to trigger abort at different stage. This requires
// precise control on the timing of abort API invocation. To achieve this in a
// consistent way across various tests, we invoke AbortAllCompactions() within
// the sync point callback, that is added at various stages of compaction.
// However as the abort API is a blocking call, calling it within the sync point
// callback on the compaction thread would cause deadlock. This test helper
// class is designed to solve this challenge.
//
// 1. Abort must happen from a different thread:
// AbortAllCompactions() is typically called from the compaction thread
// via a sync point callback, so that we could precisely control the time of
// API invocation to simulate abort at different stage of compaction.
// However, we can't block the compaction thread waiting for the abort to
// complete - the compaction needs to continue executing to actually check
// the abort flag and exit. So we spawn a separate thread to call
// AbortAllCompactions().
//
// 2. We need to know when abort completes:
// After compaction returns (with aborted status), we often need to:
// - Verify state (e.g., no output files created)
// - Call ResumeAllCompactions()
// - Run compaction again to verify it succeeds
// We must wait for the abort thread to finish before proceeding, otherwise
// we might call Resume before Abort completes, causing race conditions.
//
// 3. Sync point callbacks may fire multiple times:
// With multiple subcompactions, a callback like
// "CompactionJob::ProcessKeyValueCompaction:Start" fires once per
// subcompaction. We only want to trigger abort once, so we use
// abort_triggered_ as a guard.
//
// 4. Tests may need multiple abort cycles:
// Some tests (e.g., MultipleAbortResumeSequence) do abort->resume->abort
// multiple times. The class supports this by auto-resetting when a
// previous abort has completed.
class AbortSynchronizer {
public:
AbortSynchronizer() : abort_cv_(&abort_mutex_) {}
~AbortSynchronizer() {
// Join the thread if it was started - ensures clean shutdown
if (abort_thread_.joinable()) {
abort_thread_.join();
}
}
// Non-copyable, non-movable due to thread member
AbortSynchronizer(const AbortSynchronizer&) = delete;
AbortSynchronizer& operator=(const AbortSynchronizer&) = delete;
// Trigger abort from a separate thread.
// - Safe to call multiple times; only first call in each cycle spawns thread
// - If a previous abort has completed, automatically resets state first
// - The spawned thread calls AbortAllCompactions() and signals completion
void TriggerAbort(DBImpl* db) {
// If previous abort completed, reset state to allow new abort
if (abort_triggered_.load() && abort_completed_.load()) {
Reset();
}
if (!abort_triggered_.exchange(true)) {
abort_thread_ = std::thread([this, db]() {
db->AbortAllCompactions();
SignalAbortCompleted();
});
}
}
// Wait for the abort thread to complete.
// Call this AFTER compaction returns to ensure the abort thread has finished
// before proceeding with Resume or other operations.
void WaitForAbortCompletion() {
MutexLock l(&abort_mutex_);
while (!abort_completed_.load()) {
abort_cv_.Wait();
}
}
// Reset state for reuse. Joins any previous thread first.
// Called automatically by TriggerAbort() if previous abort completed,
// but can also be called explicitly for clarity.
void Reset() {
if (abort_thread_.joinable()) {
abort_thread_.join();
}
abort_triggered_.store(false);
abort_completed_.store(false);
}
bool IsAbortTriggered() const { return abort_triggered_.load(); }
private:
void SignalAbortCompleted() {
MutexLock l(&abort_mutex_);
abort_completed_.store(true);
abort_cv_.SignalAll();
}
std::atomic<bool> abort_triggered_{false}; // Guards against multiple spawns
std::atomic<bool> abort_completed_{false}; // Signals thread completion
port::Mutex abort_mutex_;
port::CondVar abort_cv_;
std::thread abort_thread_; // The thread that calls AbortAllCompactions()
};
// Helper to clean up SyncPoint state after tests
inline void CleanupSyncPoints() {
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
}
// Helper class that combines AbortSynchronizer with sync point setup for
// deterministic abort triggering. This adds sync point coordination on top
// of AbortSynchronizer:
//
// This is useful when you need deterministic timing - the callback won't
// return until AbortAllCompactions() has actually set the abort flag,
// guaranteeing the compaction will see it on the next check.
class SyncPointAbortHelper {
public:
explicit SyncPointAbortHelper(const std::string& trigger_point)
: trigger_point_(trigger_point) {}
// Set up sync points and callbacks. Call this before starting compaction.
void Setup(DBImpl* db_impl) {
db_impl_ = db_impl;
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency({
{"DBImpl::AbortAllCompactions:FlagSet", kWaitPointName},
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
trigger_point_, [this](void* /*arg*/) {
// Use AbortSynchronizer to handle the abort in a separate thread
abort_sync_.TriggerAbort(db_impl_);
// Wait for abort flag to be set via sync point dependency
// This ensures deterministic timing - compaction will see the flag
TEST_SYNC_POINT_CALLBACK(kWaitPointName, nullptr);
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
}
// Wait for the abort to complete. Call this after compaction returns.
void WaitForAbortCompletion() { abort_sync_.WaitForAbortCompletion(); }
// Clean up sync points and wait for abort completion in one call
void CleanupAndWait() {
CleanupSyncPoints();
WaitForAbortCompletion();
}
private:
static constexpr const char* kWaitPointName =
"SyncPointAbortHelper::WaitForAbort";
std::string trigger_point_;
DBImpl* db_impl_{nullptr};
AbortSynchronizer abort_sync_;
};
class DBCompactionAbortTest : public DBTestBase {
public:
DBCompactionAbortTest()
: DBTestBase("db_compaction_abort_test", /*env_do_fsync=*/false) {}
protected:
// Map to track the latest value of each key for verification
std::unordered_map<std::string, std::string> expected_values_;
// Statistics object for verifying compaction metrics
std::shared_ptr<Statistics> stats_;
// Get current options with statistics enabled
Options GetOptionsWithStats() {
Options options = CurrentOptions();
stats_ = CreateDBStatistics();
options.statistics = stats_;
return options;
}
// Populate database with test data.
// If overlapping=true, uses the same key range (0 to keys_per_file-1) in each
// file to ensure compaction has work to do.
// If overlapping=false, uses non-overlapping keys across files.
void PopulateData(int num_files, int keys_per_file, int value_size,
bool overlapping = true, int seed = 301) {
Random rnd(seed);
for (int i = 0; i < num_files; ++i) {
for (int j = 0; j < keys_per_file; ++j) {
int key_index = overlapping ? j : (j + i * keys_per_file);
std::string key = Key(key_index);
std::string value = rnd.RandomString(value_size);
ASSERT_OK(Put(key, value));
expected_values_[key] = value;
}
ASSERT_OK(Flush());
}
}
// Verify data integrity by reading all keys and comparing with expected
// values
void VerifyDataIntegrity(int num_keys, int start_key = 0) {
std::string val;
for (int j = start_key; j < start_key + num_keys; ++j) {
std::string key = Key(j);
ASSERT_OK(dbfull()->Get(ReadOptions(), key, &val));
auto it = expected_values_.find(key);
if (it != expected_values_.end()) {
ASSERT_EQ(it->second, val) << "Value mismatch for key: " << key;
}
}
}
// Clear expected values (useful when reopening DB or between tests)
void ClearExpectedValues() { expected_values_.clear(); }
// Run the common abort test pattern with SyncPointAbortHelper:
// 1. Set up sync point abort helper
// 2. Run compaction and verify it's aborted
// 3. Verify COMPACTION_ABORTED stat increased (if stats enabled)
// 4. Clean up, resume, and verify compaction succeeds
// 5. Verify COMPACT_WRITE_BYTES increased (if stats enabled)
void RunSyncPointAbortTest(const std::string& trigger_point,
CompactRangeOptions cro = CompactRangeOptions()) {
// Capture stats and file counts before abort
uint64_t aborted_before = 0;
uint64_t write_bytes_before = 0;
if (stats_) {
aborted_before = stats_->getTickerCount(COMPACTION_ABORTED);
write_bytes_before = stats_->getTickerCount(COMPACT_WRITE_BYTES);
}
SyncPointAbortHelper helper(trigger_point);
helper.Setup(dbfull());
Status s = dbfull()->CompactRange(cro, nullptr, nullptr);
ASSERT_TRUE(s.IsIncomplete());
ASSERT_TRUE(s.IsCompactionAborted());
// Verify abort was counted
if (stats_) {
uint64_t aborted_after = stats_->getTickerCount(COMPACTION_ABORTED);
ASSERT_GT(aborted_after, aborted_before)
<< "COMPACTION_ABORTED stat should increase after abort";
}
helper.CleanupAndWait();
dbfull()->ResumeAllCompactions();
ASSERT_OK(dbfull()->CompactRange(cro, nullptr, nullptr));
// Verify compaction completed and wrote bytes
if (stats_) {
uint64_t write_bytes_after = stats_->getTickerCount(COMPACT_WRITE_BYTES);
ASSERT_GT(write_bytes_after, write_bytes_before)
<< "COMPACT_WRITE_BYTES should increase after successful compaction";
}
}
};
// Parameterized test for abort with different number of max subcompactions.
// This consolidates tests that were essentially duplicates with different
// max_subcompactions values
class DBCompactionAbortSubcompactionTest
: public DBCompactionAbortTest,
public ::testing::WithParamInterface<int> {};
TEST_P(DBCompactionAbortSubcompactionTest, AbortWithVaryingSubcompactions) {
int max_subcompactions = GetParam();
Options options = GetOptionsWithStats();
options.level0_file_num_compaction_trigger = 4;
options.max_subcompactions = max_subcompactions;
options.disable_auto_compactions = true;
Reopen(options);
PopulateData(/*num_files=*/4, /*keys_per_file=*/100, /*value_size=*/100);
RunSyncPointAbortTest("CompactionJob::RunSubcompactions:BeforeStart");
VerifyDataIntegrity(/*num_keys=*/100);
}
INSTANTIATE_TEST_CASE_P(SubcompactionVariants,
DBCompactionAbortSubcompactionTest,
::testing::Values(1, 2, 4),
[](const ::testing::TestParamInfo<int>& param_info) {
return "MaxSubcompactionCount_" +
std::to_string(param_info.param);
});
// Parameterized test for abort with different compaction styles
// This consolidates tests for Level, Universal, and FIFO compaction styles
class DBCompactionAbortStyleTest
: public DBCompactionAbortTest,
public ::testing::WithParamInterface<CompactionStyle> {
protected:
// Configure options based on compaction style
void ConfigureOptionsForStyle(Options& options, CompactionStyle style) {
options.compaction_style = style;
options.level0_file_num_compaction_trigger = 4;
options.disable_auto_compactions = true;
switch (style) {
case kCompactionStyleLevel:
// Level compaction uses default settings
break;
case kCompactionStyleUniversal:
options.compaction_options_universal.size_ratio = 10;
break;
case kCompactionStyleFIFO:
// Set a large max_table_files_size to avoid deletion compaction
options.compaction_options_fifo.max_table_files_size =
100 * 1024 * 1024;
// Enable intra-L0 compaction which goes through normal compaction path
options.compaction_options_fifo.allow_compaction = true;
options.max_open_files = -1; // Required for FIFO compaction
break;
default:
break;
}
}
};
TEST_P(DBCompactionAbortStyleTest, AbortCompaction) {
CompactionStyle style = GetParam();
Options options = GetOptionsWithStats();
options.max_subcompactions = 1;
ConfigureOptionsForStyle(options, style);
Reopen(options);
PopulateData(/*num_files=*/4, /*keys_per_file=*/100, /*value_size=*/100);
RunSyncPointAbortTest("CompactionJob::RunSubcompactions:BeforeStart");
VerifyDataIntegrity(/*num_keys=*/100);
}
INSTANTIATE_TEST_CASE_P(
CompactionStyleVariants, DBCompactionAbortStyleTest,
::testing::Values(kCompactionStyleLevel, kCompactionStyleUniversal,
kCompactionStyleFIFO),
[](const ::testing::TestParamInfo<CompactionStyle>& param_info) {
return OptionsHelper::compaction_style_to_string.at(param_info.param);
});
TEST_F(DBCompactionAbortTest, AbortManualCompaction) {
Options options = GetOptionsWithStats();
options.level0_file_num_compaction_trigger = 10;
options.disable_auto_compactions = true;
Reopen(options);
PopulateData(/*num_files=*/5, /*keys_per_file=*/100, /*value_size=*/1000);
CompactRangeOptions cro;
cro.exclusive_manual_compaction = true;
RunSyncPointAbortTest("CompactionJob::ProcessKeyValueCompaction:Start", cro);
VerifyDataIntegrity(/*num_keys=*/100);
}
TEST_F(DBCompactionAbortTest, AbortAutomaticCompaction) {
Options options = CurrentOptions();
options.level0_file_num_compaction_trigger = 4;
options.max_subcompactions = 2;
options.disable_auto_compactions = false;
Reopen(options);
Random rnd(301);
AbortSynchronizer abort_sync;
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"CompactionJob::ProcessKeyValueCompaction:Start",
[&](void* /*arg*/) { abort_sync.TriggerAbort(dbfull()); });
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
for (int i = 0; i < 4; ++i) {
for (int j = 0; j < 100; ++j) {
ASSERT_OK(Put(Key(j), rnd.RandomString(1000)));
}
ASSERT_OK(Flush());
}
ASSERT_OK(dbfull()->TEST_WaitForCompact());
CleanupSyncPoints();
abort_sync.WaitForAbortCompletion();
dbfull()->ResumeAllCompactions();
for (int j = 0; j < 100; ++j) {
ASSERT_OK(Put(Key(j), rnd.RandomString(1000)));
}
ASSERT_OK(Flush());
ASSERT_OK(dbfull()->TEST_WaitForCompact());
std::string val;
for (int j = 0; j < 100; ++j) {
ASSERT_OK(dbfull()->Get(ReadOptions(), Key(j), &val));
}
}
TEST_F(DBCompactionAbortTest, AbortAndVerifyNoOutputFiles) {
Options options = CurrentOptions();
options.level0_file_num_compaction_trigger = 4;
options.max_subcompactions = 2;
options.disable_auto_compactions = true;
Reopen(options);
PopulateData(/*num_files=*/4, /*keys_per_file=*/100, /*value_size=*/1000);
int num_l0_files_before = NumTableFilesAtLevel(0);
int num_l1_files_before = NumTableFilesAtLevel(1);
SyncPointAbortHelper helper("CompactionJob::ProcessKeyValueCompaction:Start");
helper.Setup(dbfull());
CompactRangeOptions cro;
Status s = dbfull()->CompactRange(cro, nullptr, nullptr);
ASSERT_TRUE(s.IsIncomplete());
ASSERT_TRUE(s.IsCompactionAborted());
CleanupSyncPoints();
int num_l0_files_after = NumTableFilesAtLevel(0);
int num_l1_files_after = NumTableFilesAtLevel(1);
ASSERT_EQ(num_l0_files_before, num_l0_files_after);
ASSERT_EQ(num_l1_files_before, num_l1_files_after);
helper.WaitForAbortCompletion();
dbfull()->ResumeAllCompactions();
ASSERT_OK(dbfull()->CompactRange(cro, nullptr, nullptr));
int num_l0_files_final = NumTableFilesAtLevel(0);
int num_l1_files_final = NumTableFilesAtLevel(1);
ASSERT_EQ(0, num_l0_files_final);
ASSERT_GT(num_l1_files_final, 0);
VerifyDataIntegrity(/*num_keys=*/100);
}
TEST_F(DBCompactionAbortTest, MultipleAbortResumeSequence) {
Options options = CurrentOptions();
options.level0_file_num_compaction_trigger = 4;
options.max_subcompactions = 2;
options.disable_auto_compactions = true;
Reopen(options);
PopulateData(/*num_files=*/4, /*keys_per_file=*/100, /*value_size=*/1000);
for (int round = 0; round < 3; ++round) {
// Use SyncPointAbortHelper for deterministic abort timing - it waits
// for the abort flag to be set via sync point dependency
SyncPointAbortHelper helper(
"CompactionJob::ProcessKeyValueCompaction:Start");
helper.Setup(dbfull());
CompactRangeOptions cro;
Status s = dbfull()->CompactRange(cro, nullptr, nullptr);
ASSERT_TRUE(s.IsIncomplete());
ASSERT_TRUE(s.IsCompactionAborted());
helper.CleanupAndWait();
dbfull()->ResumeAllCompactions();
}
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), nullptr, nullptr));
VerifyDataIntegrity(/*num_keys=*/100);
}
TEST_F(DBCompactionAbortTest, AbortWithOutputFilesCleanup) {
Options options = CurrentOptions();
options.num_levels = 2; // Ensure compaction output goes to L1
options.level0_file_num_compaction_trigger = 4;
options.max_subcompactions = 2;
options.disable_auto_compactions = true;
options.target_file_size_base = 50 * 1024;
Reopen(options);
PopulateData(/*num_files=*/4, /*keys_per_file=*/100, /*value_size=*/100);
SyncPointAbortHelper helper("CompactionJob::RunSubcompactions:BeforeStart");
helper.Setup(dbfull());
CompactRangeOptions cro;
Status s = dbfull()->CompactRange(cro, nullptr, nullptr);
ASSERT_TRUE(s.IsIncomplete());
ASSERT_TRUE(s.IsCompactionAborted());
CleanupSyncPoints();
int num_l1_files_after_abort = NumTableFilesAtLevel(1);
ASSERT_EQ(0, num_l1_files_after_abort);
helper.WaitForAbortCompletion();
dbfull()->ResumeAllCompactions();
ASSERT_OK(dbfull()->CompactRange(cro, nullptr, nullptr));
// Verify L0 files are compacted and L1 has output files
int num_l0_files_final = NumTableFilesAtLevel(0);
int num_l1_files_final = NumTableFilesAtLevel(1);
ASSERT_EQ(0, num_l0_files_final)
<< "L0 should be empty after successful compaction";
ASSERT_GT(num_l1_files_final, 0)
<< "L1 should have files after successful compaction";
VerifyDataIntegrity(/*num_keys=*/100);
}
TEST_F(DBCompactionAbortTest, NestedAbortResumeCalls) {
// Test that nested AbortAllCompactions() calls work correctly with the
// counter
Options options = CurrentOptions();
options.level0_file_num_compaction_trigger = 4;
options.max_subcompactions = 2;
options.disable_auto_compactions = true;
Reopen(options);
PopulateData(/*num_files=*/4, /*keys_per_file=*/100, /*value_size=*/1000);
// First abort call
dbfull()->AbortAllCompactions();
// Nested abort call (counter should be 2)
dbfull()->AbortAllCompactions();
// Compaction should still be blocked after one resume
dbfull()->ResumeAllCompactions();
// Compaction should still return aborted because counter is still 1
CompactRangeOptions cro;
Status s = dbfull()->CompactRange(cro, nullptr, nullptr);
ASSERT_TRUE(s.IsIncomplete());
ASSERT_TRUE(s.IsCompactionAborted());
// Second resume - counter should be 0 now
dbfull()->ResumeAllCompactions();
// Compaction should succeed now
ASSERT_OK(dbfull()->CompactRange(cro, nullptr, nullptr));
VerifyDataIntegrity(/*num_keys=*/100);
}
TEST_F(DBCompactionAbortTest, AbortCompactFilesAPI) {
// Test that AbortAllCompactions works with CompactFiles API
Options options = CurrentOptions();
options.level0_file_num_compaction_trigger = 100; // Disable auto compaction
options.disable_auto_compactions = true;
Reopen(options);
PopulateData(/*num_files=*/4, /*keys_per_file=*/100, /*value_size=*/1000);
// Get the L0 file names
std::vector<std::string> files_to_compact;
ColumnFamilyMetaData cf_meta;
dbfull()->GetColumnFamilyMetaData(dbfull()->DefaultColumnFamily(), &cf_meta);
for (const auto& file : cf_meta.levels[0].files) {
files_to_compact.push_back(file.name);
}
ASSERT_GE(files_to_compact.size(), 2);
SyncPointAbortHelper helper("CompactionJob::ProcessKeyValueCompaction:Start");
helper.Setup(dbfull());
CompactionOptions compact_options;
Status s = dbfull()->CompactFiles(compact_options, files_to_compact, 1);
ASSERT_TRUE(s.IsIncomplete());
ASSERT_TRUE(s.IsCompactionAborted());
helper.CleanupAndWait();
dbfull()->ResumeAllCompactions();
// CompactFiles should work after resume
ASSERT_OK(dbfull()->CompactFiles(compact_options, files_to_compact, 1));
VerifyDataIntegrity(/*num_keys=*/100);
}
TEST_F(DBCompactionAbortTest, AbortDoesNotAffectFlush) {
// Test that AbortAllCompactions does not affect flush operations
Options options = CurrentOptions();
options.level0_file_num_compaction_trigger = 100;
options.disable_auto_compactions = true;
Reopen(options);
Random rnd(301);
for (int j = 0; j < 100; ++j) {
ASSERT_OK(Put(Key(j), rnd.RandomString(1000)));
}
// Abort compactions
dbfull()->AbortAllCompactions();
// Flush should still work
ASSERT_OK(Flush());
// Write more data
for (int j = 100; j < 200; ++j) {
ASSERT_OK(Put(Key(j), rnd.RandomString(1000)));
}
// Flush should still work
ASSERT_OK(Flush());
// Resume compactions
dbfull()->ResumeAllCompactions();
VerifyDataIntegrity(/*num_keys=*/200);
}
TEST_F(DBCompactionAbortTest, AbortBeforeCompactionStarts) {
// Test aborting before any compaction has started
Options options = CurrentOptions();
options.level0_file_num_compaction_trigger = 4;
options.disable_auto_compactions = true;
Reopen(options);
PopulateData(/*num_files=*/4, /*keys_per_file=*/100, /*value_size=*/1000);
// Abort before starting compaction
dbfull()->AbortAllCompactions();
// Compaction should immediately return aborted
CompactRangeOptions cro;
Status s = dbfull()->CompactRange(cro, nullptr, nullptr);
ASSERT_TRUE(s.IsIncomplete());
ASSERT_TRUE(s.IsCompactionAborted());
// Resume
dbfull()->ResumeAllCompactions();
// Now compaction should work
ASSERT_OK(dbfull()->CompactRange(cro, nullptr, nullptr));
// Verify L0 files are compacted
ASSERT_EQ(0, NumTableFilesAtLevel(0));
}
// Test that in-progress blob and SST files are properly cleaned up when
// compaction is aborted. This specifically tests the case where abort happens
// while files are being written (opened but not yet completed/closed).
// This catches the bug where files exist on disk but are removed from the
// outputs_ vector (e.g., by RemoveLastEmptyOutput when file_size is 0 because
// the builder was abandoned), leaving orphan files.
TEST_F(DBCompactionAbortTest, AbortWithInProgressFileCleanup) {
Options options = CurrentOptions();
options.level0_file_num_compaction_trigger = 4;
options.max_subcompactions =
1; // Single subcompaction for deterministic behavior
options.disable_auto_compactions = true;
options.target_file_size_base = 32 * 1024; // 32KB
// Enable BlobDB with garbage collection to force blob rewriting during
// compaction
options.enable_blob_files = true;
options.min_blob_size = 0; // All values go to blob files
options.blob_file_size =
1024 * 1024; // 1MB - large enough to not close during test
// Enable blob garbage collection - this forces blob data to be rewritten
// during compaction, creating new blob files
options.enable_blob_garbage_collection = true;
options.blob_garbage_collection_age_cutoff = 1.0; // Include all blob files
options.blob_garbage_collection_force_threshold = 0.0; // Always force GC
Reopen(options);
// Write enough data to trigger the periodic abort check (every 1000 records).
// 4 files * 2000 keys = 2000 unique overlapping keys processed during
// compaction. The sync point triggers at 999, 1999, etc.
PopulateData(/*num_files=*/4, /*keys_per_file=*/2000, /*value_size=*/500);
// Helper function to get blob files on disk with their names
auto GetBlobFilesOnDisk = [this]() -> std::vector<std::string> {
std::vector<std::string> blob_files;
std::vector<std::string> files;
EXPECT_OK(env_->GetChildren(dbname_, &files));
for (const auto& f : files) {
if (f.find(".blob") != std::string::npos) {
blob_files.push_back(f);
}
}
std::sort(blob_files.begin(), blob_files.end());
return blob_files;
};
// Helper function to get blob file count in metadata
auto GetBlobFilesInMetadata = [this]() -> std::vector<uint64_t> {
std::vector<uint64_t> blob_file_numbers;
ColumnFamilyMetaData cf_meta;
dbfull()->GetColumnFamilyMetaData(db_->DefaultColumnFamily(), &cf_meta);
for (const auto& blob_meta : cf_meta.blob_files) {
blob_file_numbers.push_back(blob_meta.blob_file_number);
}
std::sort(blob_file_numbers.begin(), blob_file_numbers.end());
return blob_file_numbers;
};
// Helper function to get SST files on disk
auto GetSstFilesOnDisk = [this]() -> std::vector<std::string> {
std::vector<std::string> sst_files;
std::vector<std::string> files;
EXPECT_OK(env_->GetChildren(dbname_, &files));
for (const auto& f : files) {
if (f.find(".sst") != std::string::npos) {
sst_files.push_back(f);
}
}
std::sort(sst_files.begin(), sst_files.end());
return sst_files;
};
// Helper function to get SST file numbers in metadata
auto GetSstFilesInMetadata = [this]() -> std::vector<uint64_t> {
std::vector<uint64_t> sst_file_numbers;
ColumnFamilyMetaData cf_meta;
dbfull()->GetColumnFamilyMetaData(db_->DefaultColumnFamily(), &cf_meta);
for (const auto& level : cf_meta.levels) {
for (const auto& file : level.files) {
// Extract file number from the file name (e.g., "000010.sst" -> 10)
uint64_t file_num = 0;
std::string fname = file.name;
// Remove leading path separators if present
size_t pos = fname.rfind('/');
if (pos != std::string::npos) {
fname = fname.substr(pos + 1);
}
if (sscanf(fname.c_str(), "%" PRIu64, &file_num) == 1) {
sst_file_numbers.push_back(file_num);
}
}
}
std::sort(sst_file_numbers.begin(), sst_file_numbers.end());
return sst_file_numbers;
};
std::vector<std::string> initial_blob_files = GetBlobFilesOnDisk();
std::vector<uint64_t> initial_meta_blobs = GetBlobFilesInMetadata();
std::vector<std::string> initial_sst_files = GetSstFilesOnDisk();
std::vector<uint64_t> initial_meta_ssts = GetSstFilesInMetadata();
ASSERT_GT(initial_blob_files.size(), 0u) << "Expected initial blob files";
ASSERT_EQ(initial_blob_files.size(), initial_meta_blobs.size())
<< "Initial blob files should match between disk and metadata";
ASSERT_GT(initial_sst_files.size(), 0u) << "Expected initial SST files";
ASSERT_EQ(initial_sst_files.size(), initial_meta_ssts.size())
<< "Initial SST files should match between disk and metadata";
// Tracking variables for blob file lifecycle
std::atomic<int> blob_writes{0};
std::atomic<bool> abort_triggered{false};
AbortSynchronizer abort_sync;
// Set up dependency: the wait point will block until FlagSet is hit
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency({
{"DBImpl::AbortAllCompactions:FlagSet",
"DBCompactionAbortTest::InProgressBlob:WaitForAbort"},
});
// Trigger abort after some blob writes during compaction output.
// This ensures we have an in-progress blob file when abort happens.
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"BlobFileBuilder::WriteBlobToFile:AddRecord", [&](void* /*arg*/) {
int count = blob_writes.fetch_add(1) + 1;
// Trigger abort after 100 blob writes - this ensures:
// 1. A blob file has been opened (for writing)
// 2. Some data has been written to it
// 3. But it's not yet completed (blob_file_size is 1MB)
if (count == 100 && !abort_triggered.exchange(true)) {
abort_sync.TriggerAbort(dbfull());
// Wait for abort flag to be set - this sync point blocks until
// FlagSet is processed
TEST_SYNC_POINT_CALLBACK(
"DBCompactionAbortTest::InProgressBlob:WaitForAbort", nullptr);
}
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
// Run compaction - it should be aborted while blob file is in-progress
CompactRangeOptions cro;
Status s = dbfull()->CompactRange(cro, nullptr, nullptr);
ASSERT_TRUE(s.IsIncomplete())
<< "Expected compaction to be aborted, got: " << s.ToString();
CleanupSyncPoints();
abort_sync.WaitForAbortCompletion();
// Check state after abort
std::vector<std::string> post_abort_disk_blobs = GetBlobFilesOnDisk();
std::vector<uint64_t> post_abort_meta_blobs = GetBlobFilesInMetadata();
std::vector<std::string> post_abort_disk_ssts = GetSstFilesOnDisk();
std::vector<uint64_t> post_abort_meta_ssts = GetSstFilesInMetadata();
// This is the key assertion for blob files: files on disk should match
// metadata. If the in-progress blob file was NOT cleaned up, there will be an
// extra file on disk that's not in metadata (orphan).
ASSERT_EQ(post_abort_disk_blobs.size(), post_abort_meta_blobs.size())
<< "Orphan blob file detected! In-progress blob file was not cleaned up "
"after abort. Files on disk: "
<< post_abort_disk_blobs.size()
<< ", Files in metadata: " << post_abort_meta_blobs.size()
<< ". The difference indicates orphaned in-progress blob file(s).";
// This is the key assertion for SST files: files on disk should match
// metadata. If the in-progress SST file was NOT cleaned up, there will be an
// extra file on disk that's not in metadata (orphan).
ASSERT_EQ(post_abort_disk_ssts.size(), post_abort_meta_ssts.size())
<< "Orphan SST file detected! In-progress SST file was not cleaned up "
"after abort. Files on disk: "
<< post_abort_disk_ssts.size()
<< ", Files in metadata: " << post_abort_meta_ssts.size()
<< ". The difference indicates orphaned in-progress SST file(s).";
// Resume and complete compaction to verify DB is still functional
dbfull()->ResumeAllCompactions();
ASSERT_OK(dbfull()->CompactRange(cro, nullptr, nullptr));
// Verify data integrity - we wrote 4 files * 2000 keys with overlapping keys
VerifyDataIntegrity(/*num_keys=*/2000);
}
TEST_F(DBCompactionAbortTest, AbortBottommostLevelCompaction) {
Options options = CurrentOptions();
options.num_levels = 4;
options.level0_file_num_compaction_trigger = 2;
options.max_bytes_for_level_base = 1024 * 10; // 10KB
options.max_bytes_for_level_multiplier = 2;
options.disable_auto_compactions = true;
Reopen(options);
// Write data to fill multiple levels (non-overlapping keys)
PopulateData(/*num_files=*/6, /*keys_per_file=*/100,
/*value_size=*/500, /*overlapping=*/false);
// First compact to push data to lower levels
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), nullptr, nullptr));
// Write more data to L0 (overlapping keys)
PopulateData(/*num_files=*/4, /*keys_per_file=*/100, /*value_size=*/500);
SyncPointAbortHelper helper("CompactionJob::ProcessKeyValueCompaction:Start");
helper.Setup(dbfull());
// Trigger bottommost level compaction
CompactRangeOptions cro;
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
Status s = dbfull()->CompactRange(cro, nullptr, nullptr);
ASSERT_TRUE(s.IsIncomplete());
ASSERT_TRUE(s.IsCompactionAborted());
helper.CleanupAndWait();
dbfull()->ResumeAllCompactions();
ASSERT_OK(dbfull()->CompactRange(cro, nullptr, nullptr));
VerifyDataIntegrity(/*num_keys=*/600);
}
// Test that while compactions are aborted, atomic range replace
// (IngestExternalFiles with atomic_replace_range) works correctly.
// This verifies that the abort state doesn't block other write operations
// like atomic range replace.
TEST_F(DBCompactionAbortTest, AbortThenAtomicRangeReplace) {
Options options = CurrentOptions();
options.level0_file_num_compaction_trigger = 4;
options.max_subcompactions = 2;
options.disable_auto_compactions = true;
Reopen(options);
// Create a directory for SST files
std::string sst_files_dir = dbname_ + "_sst_files/";
ASSERT_OK(env_->CreateDirIfMissing(sst_files_dir));
// Populate initial data with overlapping keys
PopulateData(/*num_files=*/4, /*keys_per_file=*/100, /*value_size=*/500);
// Verify initial data
VerifyDataIntegrity(/*num_keys=*/100);
// Trigger compaction and abort it
SyncPointAbortHelper helper("CompactionJob::ProcessKeyValueCompaction:Start");
helper.Setup(dbfull());
CompactRangeOptions cro;
Status s = dbfull()->CompactRange(cro, nullptr, nullptr);
ASSERT_TRUE(s.IsIncomplete());
ASSERT_TRUE(s.IsCompactionAborted());
helper.CleanupAndWait();
// While compaction is still aborted, perform atomic range replace using
// IngestExternalFiles with atomic_replace_range. This verifies that the
// abort state doesn't block other write operations.
// Using RangeOpt() (empty range) means replace everything in the CF.
// Create an SST file with new data for keys 0-49 (replacing keys 0-99)
std::string sst_file_path = sst_files_dir + "atomic_replace_1.sst";
SstFileWriter sst_file_writer(EnvOptions(), options);
ASSERT_OK(sst_file_writer.Open(sst_file_path));
// Write new values for keys 0-49
Random rnd(42);
std::unordered_map<std::string, std::string> new_values;
for (int j = 0; j < 50; ++j) {
std::string key = Key(j);
std::string value = "replaced_" + rnd.RandomString(100);
ASSERT_OK(sst_file_writer.Put(key, value));
new_values[key] = value;
}
ASSERT_OK(sst_file_writer.Finish());
// Perform atomic range replace for the entire column family.
// Using RangeOpt() (default constructor) means replace everything in the CF.
IngestExternalFileArg arg;
arg.column_family = db_->DefaultColumnFamily();
arg.external_files = {sst_file_path};
arg.atomic_replace_range = RangeOpt();
// snapshot_consistency must be false when using atomic_replace_range
arg.options.snapshot_consistency = false;
// Atomic range replace should work even while compactions are aborted
ASSERT_OK(db_->IngestExternalFiles({arg}));
// Now resume compactions after the atomic range replace
dbfull()->ResumeAllCompactions();
// Verify that the atomic range replace worked correctly:
// 1. Keys 0-49 should have new replaced values
std::string val;
for (int j = 0; j < 50; ++j) {
std::string key = Key(j);
ASSERT_OK(db_->Get(ReadOptions(), key, &val));
auto it = new_values.find(key);
ASSERT_NE(it, new_values.end());
ASSERT_EQ(it->second, val) << "Value mismatch for replaced key: " << key;
}
// 2. Keys 50-99 should not exist (they were replaced/deleted by atomic
// replace)
for (int j = 50; j < 100; ++j) {
std::string key = Key(j);
Status get_status = db_->Get(ReadOptions(), key, &val);
ASSERT_TRUE(get_status.IsNotFound())
<< "Key " << key << " should not exist after full CF replace";
}
// Clean up SST files directory
ASSERT_OK(DestroyDir(env_, sst_files_dir));
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
ROCKSDB_NAMESPACE::port::InstallStackTraceHandler();
::testing::InitGoogleTest(&argc, argv);
return RUN_ALL_TESTS();
}
+329 -29
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
@@ -2208,7 +2218,8 @@ TEST_P(DBDeleteFileRangeTest, DeleteFileRange) {
std::string end_string = Key(2000);
Slice begin(begin_string);
Slice end(end_string);
ASSERT_OK(DeleteFilesInRange(db_, db_->DefaultColumnFamily(), &begin, &end));
ASSERT_OK(
DeleteFilesInRange(db_.get(), db_->DefaultColumnFamily(), &begin, &end));
int32_t deleted_count = 0;
for (int32_t i = 0; i < 4300; i++) {
@@ -2229,8 +2240,8 @@ TEST_P(DBDeleteFileRangeTest, DeleteFileRange) {
Slice begin1(begin_string);
Slice end1(end_string);
// Try deleting files in range which contain no keys
ASSERT_OK(
DeleteFilesInRange(db_, db_->DefaultColumnFamily(), &begin1, &end1));
ASSERT_OK(DeleteFilesInRange(db_.get(), db_->DefaultColumnFamily(), &begin1,
&end1));
// Push data from level 0 to level 1 to force all data to be deleted
// Note that we don't delete level 0 files
@@ -2239,8 +2250,8 @@ TEST_P(DBDeleteFileRangeTest, DeleteFileRange) {
ASSERT_OK(db_->CompactRange(compact_options, nullptr, nullptr));
ASSERT_OK(dbfull()->TEST_WaitForCompact());
ASSERT_OK(
DeleteFilesInRange(db_, db_->DefaultColumnFamily(), nullptr, nullptr));
ASSERT_OK(DeleteFilesInRange(db_.get(), db_->DefaultColumnFamily(), nullptr,
nullptr));
int32_t deleted_count2 = 0;
for (int32_t i = 0; i < 4300; i++) {
@@ -2308,7 +2319,7 @@ TEST_P(DBDeleteFileRangeTest, DeleteFilesInRanges) {
ranges.emplace_back(begin_str1, end_str1);
ranges.emplace_back(begin_str2, end_str2);
ranges.emplace_back(begin_str3, end_str3);
ASSERT_OK(DeleteFilesInRanges(db_, db_->DefaultColumnFamily(),
ASSERT_OK(DeleteFilesInRanges(db_.get(), db_->DefaultColumnFamily(),
ranges.data(), ranges.size()));
ASSERT_EQ("0,3,7", FilesPerLevel(0));
@@ -2335,7 +2346,7 @@ TEST_P(DBDeleteFileRangeTest, DeleteFilesInRanges) {
ranges.emplace_back(&begin1, &end1);
ranges.emplace_back(&begin2, &end2);
ranges.emplace_back(&begin3, &end3);
ASSERT_OK(DeleteFilesInRanges(db_, db_->DefaultColumnFamily(),
ASSERT_OK(DeleteFilesInRanges(db_.get(), db_->DefaultColumnFamily(),
ranges.data(), ranges.size(), false));
ASSERT_EQ("0,1,4", FilesPerLevel(0));
@@ -2356,7 +2367,8 @@ TEST_P(DBDeleteFileRangeTest, DeleteFilesInRanges) {
// Delete all files.
{
RangeOpt range;
ASSERT_OK(DeleteFilesInRanges(db_, db_->DefaultColumnFamily(), &range, 1));
ASSERT_OK(
DeleteFilesInRanges(db_.get(), db_->DefaultColumnFamily(), &range, 1));
ASSERT_EQ("", FilesPerLevel(0));
for (auto i = 0; i < 1000; i++) {
@@ -2418,7 +2430,8 @@ TEST_P(DBDeleteFileRangeTest, DeleteFileRangeFileEndpointsOverlapBug) {
// "1 -> vals[0]" to reappear.
std::string begin_str = Key(0), end_str = Key(1);
Slice begin = begin_str, end = end_str;
ASSERT_OK(DeleteFilesInRange(db_, db_->DefaultColumnFamily(), &begin, &end));
ASSERT_OK(
DeleteFilesInRange(db_.get(), db_->DefaultColumnFamily(), &begin, &end));
ASSERT_EQ(vals[1], GetValue(Key(1)));
db_->ReleaseSnapshot(snapshot);
@@ -3657,7 +3670,7 @@ TEST_F(DBCompactionTest, SuggestCompactRangeNoTwoLevel0Compactions) {
GenerateNewRandomFile(&rnd, /* nowait */ true);
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
ASSERT_OK(experimental::SuggestCompactRange(db_, nullptr, nullptr));
ASSERT_OK(experimental::SuggestCompactRange(db_.get(), nullptr, nullptr));
for (int num = 0; num < options.level0_file_num_compaction_trigger + 1;
num++) {
GenerateNewRandomFile(&rnd, /* nowait */ true);
@@ -4533,7 +4546,8 @@ TEST_F(DBCompactionTest, DeleteFilesInRangeConflictWithCompaction) {
std::string end_string = Key(kMaxKey + 1);
Slice begin(begin_string);
Slice end(end_string);
ASSERT_OK(DeleteFilesInRange(db_, db_->DefaultColumnFamily(), &begin, &end));
ASSERT_OK(
DeleteFilesInRange(db_.get(), db_->DefaultColumnFamily(), &begin, &end));
SyncPoint::GetInstance()->DisableProcessing();
}
@@ -7840,7 +7854,7 @@ class DBCompactionTestL0FilesMisorderCorruption : public DBCompactionTest {
options_.level0_file_num_compaction_trigger = 3;
CompactionOptionsFIFO fifo_options;
if (compaction_path_to_test == "FindIntraL0Compaction" ||
if (compaction_path_to_test == "PickCostBasedIntraL0Compaction" ||
compaction_path_to_test == "CompactRange") {
fifo_options.allow_compaction = true;
} else if (compaction_path_to_test == "CompactFile") {
@@ -7940,7 +7954,7 @@ class DBCompactionTestL0FilesMisorderCorruption : public DBCompactionTest {
void SetupSyncPoints(const std::string& compaction_path_to_test) {
compaction_path_sync_point_called_.store(false);
if (compaction_path_to_test == "FindIntraL0Compaction" &&
if (compaction_path_to_test == "PickCostBasedIntraL0Compaction" &&
options_.compaction_style == CompactionStyle::kCompactionStyleLevel) {
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"PostPickFileToCompact", [&](void* arg) {
@@ -7950,7 +7964,7 @@ class DBCompactionTestL0FilesMisorderCorruption : public DBCompactionTest {
*picked_file_to_compact = false;
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"FindIntraL0Compaction", [&](void* /*arg*/) {
"PickCostBasedIntraL0Compaction", [&](void* /*arg*/) {
compaction_path_sync_point_called_.store(true);
});
@@ -7986,12 +8000,12 @@ class DBCompactionTestL0FilesMisorderCorruption : public DBCompactionTest {
"PickDeleteTriggeredCompactionReturnNonnullptr", [&](void* /*arg*/) {
compaction_path_sync_point_called_.store(true);
});
} else if ((compaction_path_to_test == "FindIntraL0Compaction" ||
} else if ((compaction_path_to_test == "PickCostBasedIntraL0Compaction" ||
compaction_path_to_test == "CompactRange") &&
options_.compaction_style ==
CompactionStyle::kCompactionStyleFIFO) {
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"FindIntraL0Compaction", [&](void* /*arg*/) {
"PickCostBasedIntraL0Compaction", [&](void* /*arg*/) {
compaction_path_sync_point_called_.store(true);
});
}
@@ -8063,7 +8077,7 @@ TEST_F(DBCompactionTest, CompactFilesSupportKeyPlacementRangeConflict) {
ASSERT_OK(Flush());
ASSERT_OK(Put("k4", "v"));
ASSERT_OK(Flush());
ASSERT_OK(experimental::PromoteL0(db_, db_->DefaultColumnFamily(), 1));
ASSERT_OK(experimental::PromoteL0(db_.get(), db_->DefaultColumnFamily(), 1));
ASSERT_EQ("0,2,1", FilesPerLevel());
ASSERT_OK(Put("k2", "v"));
@@ -8151,7 +8165,7 @@ TEST_F(DBCompactionTestL0FilesMisorderCorruption,
IngestOneKeyValue(dbfull(), Key(i), "new", options_);
}
SetupSyncPoints("FindIntraL0Compaction");
SetupSyncPoints("PickCostBasedIntraL0Compaction");
ResumeCompactionThread();
ASSERT_OK(dbfull()->TEST_WaitForCompact());
@@ -8284,7 +8298,8 @@ TEST_F(DBCompactionTestL0FilesMisorderCorruption,
TEST_F(DBCompactionTestL0FilesMisorderCorruption,
FlushAfterIntraL0FIFOCompactionWithIngestedFile) {
for (const std::string compaction_path_to_test : {"FindIntraL0Compaction"}) {
for (const std::string compaction_path_to_test :
{"PickCostBasedIntraL0Compaction"}) {
SetupOptions(CompactionStyle::kCompactionStyleFIFO,
compaction_path_to_test);
DestroyAndReopen(options_);
@@ -11483,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
@@ -11511,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
@@ -11658,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) {
+155 -13
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,
@@ -101,7 +113,7 @@ TEST_F(DBFlushTest, SyncFail) {
TEST_SYNC_POINT("DBFlushTest::SyncFail:2");
fault_injection_env->SetFilesystemActive(true);
// Now the background job will do the flush; wait for it.
// Returns the IO error happend during flush.
// Returns the IO error happened during flush.
ASSERT_NOK(dbfull()->TEST_WaitForFlushMemTable());
ASSERT_EQ("", FilesPerLevel()); // flush failed.
Destroy(options);
@@ -518,11 +530,11 @@ TEST_F(DBFlushTest, StatisticsGarbageInsertAndDeletes) {
// Note : one set of delete for KEY1, KEY2, KEY3 is written to
// SSTable to propagate the delete operations to K-V pairs
// that could have been inserted into the database during past Flush
// opeartions.
// operations.
EXPECTED_MEMTABLE_GARBAGE_BYTES_AT_FLUSH -=
KEY1.size() + KEY2.size() + KEY3.size() + 3 * sizeof(uint64_t);
// Additional useful paylaod.
// Additional useful payload.
ASSERT_OK(Delete(KEY4));
ASSERT_OK(Delete(KEY5));
ASSERT_OK(Delete(KEY6));
@@ -614,7 +626,7 @@ TEST_F(DBFlushTest, StatisticsGarbageRangeDeletes) {
// Note : one set of deleteRange for (KEY1, KEY2) and (KEY2, KEY3) is written
// to SSTable to propagate the deleteRange operations to K-V pairs that could
// have been inserted into the database during past Flush opeartions.
// have been inserted into the database during past Flush operations.
EXPECTED_MEMTABLE_GARBAGE_BYTES_AT_FLUSH -=
(KEY1.size() + KEY2.size() + sizeof(uint64_t)) +
(KEY2.size() + KEY3.size() + sizeof(uint64_t));
@@ -709,7 +721,7 @@ class TestFlushListener : public EventListener {
// that assumption does not hold (see the test case MultiDBMultiListeners
// below).
ASSERT_TRUE(test_);
if (db == test_->db_) {
if (db == test_->db_.get()) {
std::vector<std::vector<FileMetaData>> files_by_level;
test_->dbfull()->TEST_GetFilesMetaData(db->DefaultColumnFamily(),
&files_by_level);
@@ -842,7 +854,7 @@ TEST_F(DBFlushTest, FixFlushReasonRaceFromConcurrentFlushes) {
ASSERT_OK(Put(1, Key(idx), std::string(1, 'v')));
}
// To coerce a manual flush happenning in the middle of GetLiveFiles's flush,
// To coerce a manual flush happening in the middle of GetLiveFiles's flush,
// we need to pause background flush thread and enable it later.
std::shared_ptr<test::SleepingBackgroundTask> sleeping_task =
std::make_shared<test::SleepingBackgroundTask>();
@@ -851,7 +863,7 @@ TEST_F(DBFlushTest, FixFlushReasonRaceFromConcurrentFlushes) {
sleeping_task.get(), Env::Priority::HIGH);
sleeping_task->WaitUntilSleeping();
// Coerce a manual flush happenning in the middle of GetLiveFiles's flush
// Coerce a manual flush happening in the middle of GetLiveFiles's flush
bool get_live_files_paused_at_sync_point = false;
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"DBImpl::AtomicFlushMemTables:AfterScheduleFlush", [&](void* /* arg */) {
@@ -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,10 +1439,11 @@ TEST_F(DBFlushTest, MemPurgeDeleteAndDeleteRange) {
delete iter;
}
ASSERT_OK(WaitForFlushCallbacks());
Close();
}
// Create a Compaction Fitler that will be invoked
// Create a Compaction Filter that will be invoked
// at flush time and will update the value of a KV pair
// if the key string is "lower" than the filter_key_ string.
class ConditionalUpdateFilter : public CompactionFilter {
@@ -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;
@@ -2533,7 +2627,7 @@ TEST_F(DBFlushTest, TombstoneVisibleInSnapshot) {
ASSERT_OK(db_->Put(WriteOptions(), "foo", "value0"));
ManagedSnapshot snapshot_guard(db_);
ManagedSnapshot snapshot_guard(db_.get());
ColumnFamilyHandle* default_cf = db_->DefaultColumnFamily();
ASSERT_OK(db_->Flush(FlushOptions(), default_cf));
@@ -2574,7 +2668,7 @@ TEST_P(DBAtomicFlushTest, ManualFlushUnder2PC) {
txn_db_opts.write_policy = TxnDBWritePolicy::WRITE_COMMITTED;
ASSERT_OK(TransactionDB::Open(options, txn_db_opts, dbname_, &txn_db));
ASSERT_NE(txn_db, nullptr);
db_ = txn_db;
db_.reset(txn_db);
// Create two more columns other than default CF.
std::vector<std::string> cfs = {"puppy", "kitty"};
@@ -2638,9 +2732,8 @@ TEST_P(DBAtomicFlushTest, ManualFlushUnder2PC) {
// it means atomic flush didn't write the min_log_number_to_keep to MANIFEST.
cfs.push_back(kDefaultColumnFamilyName);
ASSERT_OK(TryReopenWithColumnFamilies(cfs, options));
DBImpl* db_impl = static_cast<DBImpl*>(db_);
ASSERT_TRUE(db_impl->allow_2pc());
ASSERT_NE(db_impl->MinLogNumberToKeep(), 0);
ASSERT_TRUE(dbfull()->allow_2pc());
ASSERT_NE(dbfull()->MinLogNumberToKeep(), 0);
}
TEST_P(DBAtomicFlushTest, ManualAtomicFlush) {
@@ -3707,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
+234 -71
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_)),
@@ -320,6 +321,22 @@ Status DBImpl::ResumeImpl(DBRecoverContext context) {
WaitForBackgroundWork();
TEST_SYNC_POINT("DBImpl::ResumeImpl:Start");
// With two_write_queues=true, sequence numbers are allocated via
// FetchAddLastAllocatedSequence() before writes complete, but only
// published via SetLastSequence() after success. If we're recovering from
// an error, there may be allocated-but-not-published sequence numbers.
// We must sync last_sequence_ with last_allocated_sequence_ before creating
// any new memtables/WALs, otherwise the new WAL could start with a sequence
// number lower than what was already written, causing "sequence number
// going backwards" corruption on subsequent recovery.
if (immutable_db_options_.two_write_queues) {
versions_->SyncLastSequenceWithAllocated();
}
TEST_SYNC_POINT("DBImpl::ResumeImpl:AfterSyncSeq");
Status s;
if (shutdown_initiated_) {
// Returning shutdown status to SFM during auto recovery will cause it
@@ -456,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();
}
}
@@ -545,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();
@@ -591,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);
@@ -768,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;
@@ -776,7 +869,7 @@ Status DBImpl::StartPeriodicTaskScheduler() {
"DBImpl::StartPeriodicTaskScheduler:DisableScheduler",
&disable_scheduler);
if (disable_scheduler) {
return Status::OK();
return s;
}
{
@@ -787,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,
@@ -797,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,
@@ -807,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;
}
@@ -1177,23 +1271,38 @@ FSDirectory* DBImpl::GetDataDir(ColumnFamilyData* cfd, size_t path_id) const {
}
Status DBImpl::SetOptions(
ColumnFamilyHandle* column_family,
const std::unordered_map<std::string, std::string>& options_map) {
const std::unordered_map<ColumnFamilyHandle*,
std::unordered_map<std::string, std::string>>&
column_families_opts_map) {
// TODO: plumb Env::IOActivity, Env::IOPriority
const ReadOptions read_options;
const WriteOptions write_options;
auto* cfd =
static_cast_with_check<ColumnFamilyHandleImpl>(column_family)->cfd();
if (options_map.empty()) {
ROCKS_LOG_WARN(immutable_db_options_.info_log,
"SetOptions() on column family [%s], empty input",
cfd->GetName().c_str());
return Status::InvalidArgument("empty input");
if (column_families_opts_map.empty()) {
return Status::OK();
}
for (const auto& cf_opts : column_families_opts_map) {
if (cf_opts.second.empty()) {
ROCKS_LOG_WARN(immutable_db_options_.info_log,
"SetOptions() on column family [%s], empty input",
cf_opts.first->GetName().c_str());
return Status::InvalidArgument("empty input");
}
}
autovector<std::pair<ColumnFamilyData*,
const std::unordered_map<std::string, std::string>*>>
column_family_datas;
for (const auto& cf_opts : column_families_opts_map) {
column_family_datas.push_back(
{static_cast_with_check<ColumnFamilyHandleImpl>(cf_opts.first)->cfd(),
&cf_opts.second});
}
InstrumentedMutexLock ol(&options_mutex_);
MutableCFOptions new_options_copy; // For logging outside of DB mutex
autovector<MutableCFOptions>
new_options_copy; // For logging outside of DB mutex
Status s;
Status persist_options_status;
SuperVersionContext sv_context(/* create_superversion */ true);
@@ -1216,68 +1325,104 @@ Status DBImpl::SetOptions(
// Thus aren't releasing the DB mutex from LogAndApply calling pre_cb,
// through installing the new Version until the end of this block, after
// installing the new SuperVersion.
auto pre_cb = [&]() -> Status {
Status cb_s = cfd->SetOptions(db_options, options_map);
if (cb_s.ok()) {
new_options_copy = cfd->GetLatestMutableCFOptions();
}
return cb_s;
};
VersionEdit dummy_edit;
dummy_edit.MarkNoManifestWriteDummy();
TEST_SYNC_POINT_CALLBACK("DBImpl::SetOptions:dummy_edit", &dummy_edit);
s = versions_->LogAndApply(
cfd, read_options, write_options, &dummy_edit, &mutex_,
directories_.GetDbDir(), false /*new_descriptor_log=*/,
nullptr /*new_opts*/, {} /*manifest_wcb*/, pre_cb);
if (!versions_->io_status().ok()) {
assert(!s.ok());
error_handler_.SetBGError(versions_->io_status(),
BackgroundErrorReason::kManifestWrite);
for (const auto& cfd_opts : column_family_datas) {
auto* cfd = cfd_opts.first;
const auto* options_map_ptr = cfd_opts.second;
auto pre_cb = [&]() -> Status {
Status cb_s = cfd->SetOptions(db_options, *options_map_ptr);
if (cb_s.ok()) {
new_options_copy.emplace_back(cfd->GetLatestMutableCFOptions());
}
return cb_s;
};
s = versions_->LogAndApply(
cfd, read_options, write_options, &dummy_edit, &mutex_,
directories_.GetDbDir(), false /*new_descriptor_log=*/,
nullptr /*new_opts*/, {} /*manifest_wcb*/, pre_cb);
if (!versions_->io_status().ok()) {
assert(!s.ok());
error_handler_.SetBGError(versions_->io_status(),
BackgroundErrorReason::kManifestWrite);
}
if (!s.ok()) {
break;
}
}
if (s.ok()) {
// Trigger possible flush/compactions. This has to be before we persist
// options to file, otherwise there will be a deadlock with writer
// thread.
InstallSuperVersionForConfigChange(cfd, &sv_context);
for (const auto& cfd_opts : column_family_datas) {
InstallSuperVersionForConfigChange(cfd_opts.first, &sv_context);
}
persist_options_status =
WriteOptionsFile(write_options, true /*db_mutex_already_held*/);
bg_cv_.SignalAll();
assert(new_options_copy == cfd->GetLatestMutableCFOptions());
assert(cfd->GetLatestMutableCFOptions() ==
cfd->GetCurrentMutableCFOptions());
assert(cfd->GetCurrentMutableCFOptions() ==
cfd->current()->GetMutableCFOptions());
#ifndef NDEBUG
for (size_t i = 0; i < column_family_datas.size(); ++i) {
auto* cfd = column_family_datas[i].first;
assert(new_options_copy[i] == cfd->GetLatestMutableCFOptions());
assert(cfd->GetLatestMutableCFOptions() ==
cfd->GetCurrentMutableCFOptions());
assert(cfd->GetCurrentMutableCFOptions() ==
cfd->current()->GetMutableCFOptions());
}
#endif
}
}
sv_context.Clean();
if (s.ok() && (options_map.count("preserve_internal_time_seconds") > 0 ||
options_map.count("preclude_last_level_data_seconds") > 0)) {
s = RegisterRecordSeqnoTimeWorker();
if (s.ok()) {
bool needs_seqno_worker = false;
for (const auto& cf_opts : column_families_opts_map) {
if (cf_opts.second.count("preserve_internal_time_seconds") > 0 ||
cf_opts.second.count("preclude_last_level_data_seconds") > 0) {
needs_seqno_worker = true;
break;
}
}
if (needs_seqno_worker) {
s = RegisterRecordSeqnoTimeWorker();
}
}
ROCKS_LOG_INFO(
immutable_db_options_.info_log,
"SetOptions() on column family [%s], inputs:", cfd->GetName().c_str());
for (const auto& o : options_map) {
ROCKS_LOG_INFO(immutable_db_options_.info_log, "%s: %s\n", o.first.c_str(),
o.second.c_str());
ROCKS_LOG_INFO(immutable_db_options_.info_log,
"SetOptions() on [%zu] column families, inputs:",
column_family_datas.size());
for (size_t i = 0; i < column_family_datas.size(); ++i) {
const auto* cfd = column_family_datas[i].first;
const auto* options_map_ptr = column_family_datas[i].second;
ROCKS_LOG_INFO(immutable_db_options_.info_log,
"Set options on column family [%s] (%zu/%zu), inputs:",
cfd->GetName().c_str(), i, column_family_datas.size());
for (const auto& o : *options_map_ptr) {
ROCKS_LOG_INFO(immutable_db_options_.info_log, "%s: %s\n",
o.first.c_str(), o.second.c_str());
}
}
if (s.ok()) {
ROCKS_LOG_INFO(immutable_db_options_.info_log,
"[%s] SetOptions() succeeded", cfd->GetName().c_str());
new_options_copy.Dump(immutable_db_options_.info_log.get());
for (size_t i = 0; i < column_family_datas.size(); ++i) {
const auto* cfd = column_family_datas[i].first;
ROCKS_LOG_INFO(immutable_db_options_.info_log,
"Set options on column family [%s] (%zu/%zu) succeeded, "
"updated CF options:",
cfd->GetName().c_str(), i, column_family_datas.size());
new_options_copy[i].Dump(immutable_db_options_.info_log.get());
}
if (!persist_options_status.ok()) {
// NOTE: WriteOptionsFile already logs on failure
s = persist_options_status;
}
} else {
persist_options_status.PermitUncheckedError(); // less important
ROCKS_LOG_WARN(immutable_db_options_.info_log, "[%s] SetOptions() failed",
cfd->GetName().c_str());
ROCKS_LOG_WARN(immutable_db_options_.info_log, "SetOptions() failed: %s",
s.ToString().c_str());
}
LogFlush(immutable_db_options_.info_log);
return s;
@@ -1404,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 !=
@@ -3858,10 +4010,6 @@ Iterator* DBImpl::NewIterator(const ReadOptions& _read_options,
read_options.io_activity = Env::IOActivity::kDBIterator;
}
if (read_options.managed) {
return NewErrorIterator(
Status::NotSupported("Managed iterator is not supported anymore."));
}
Iterator* result = nullptr;
if (read_options.read_tier == kPersistedTier) {
return NewErrorIterator(Status::NotSupported(
@@ -4061,9 +4209,6 @@ Status DBImpl::NewIterators(
if (read_options.io_activity == Env::IOActivity::kUnknown) {
read_options.io_activity = Env::IOActivity::kDBIterator;
}
if (read_options.managed) {
return Status::NotSupported("Managed iterator is not supported anymore.");
}
if (read_options.read_tier == kPersistedTier) {
return Status::NotSupported(
"ReadTier::kPersistedData is not yet supported in iterators.");
@@ -4351,7 +4496,8 @@ void DBImpl::ReleaseSnapshot(const Snapshot* s) {
for (auto* cfd : *versions_->GetColumnFamilySet()) {
if (!cfd->AllowIngestBehind()) {
cfd->current()->storage_info()->UpdateOldestSnapshot(
oldest_snapshot, /*allow_ingest_behind=*/false);
oldest_snapshot, /*allow_ingest_behind=*/false,
cfd->ioptions().user_comparator, cfd->GetFullHistoryTsLow());
if (!cfd->current()
->storage_info()
->BottommostFilesMarkedForCompaction()
@@ -4987,7 +5133,8 @@ Status DBImpl::DeleteFilesInRanges(ColumnFamilyHandle* column_family,
}
if (!deleted_files.empty()) {
vstorage->ComputeCompactionScore(cfd->ioptions(),
cfd->GetLatestMutableCFOptions());
cfd->GetLatestMutableCFOptions(),
cfd->GetFullHistoryTsLow());
}
if (edit.GetDeletedFiles().empty()) {
job_context.Clean();
@@ -6418,8 +6565,11 @@ Status DBImpl::VerifyChecksumInternal(const ReadOptions& read_options,
fmeta->file_checksum_func_name, fname,
read_options);
} else {
FileOptions fopts = file_options_;
fopts.file_checksum = fmeta->file_checksum;
fopts.file_checksum_func_name = fmeta->file_checksum_func_name;
s = ROCKSDB_NAMESPACE::VerifySstFileChecksumInternal(
opts, file_options_, read_options, fname, fd.largest_seqno);
opts, fopts, read_options, fname, fd.largest_seqno);
}
RecordTick(stats_, VERIFY_CHECKSUM_READ_BYTES,
IOSTATS(bytes_read) - prev_bytes_read);
@@ -6487,12 +6637,15 @@ Status DBImpl::VerifyFullFileChecksum(const std::string& file_checksum_expected,
}
std::string file_checksum;
std::string func_name;
FileOptions fopts;
fopts.file_checksum = file_checksum_expected;
fopts.file_checksum_func_name = func_name_expected;
s = ROCKSDB_NAMESPACE::GenerateOneFileChecksum(
fs_.get(), fname, immutable_db_options_.file_checksum_gen_factory.get(),
func_name_expected, &file_checksum, &func_name,
read_options.readahead_size, immutable_db_options_.allow_mmap_reads,
io_tracer_, immutable_db_options_.rate_limiter.get(), read_options,
immutable_db_options_.stats, immutable_db_options_.clock);
immutable_db_options_.stats, immutable_db_options_.clock, fopts);
if (s.ok()) {
assert(func_name_expected == func_name);
if (file_checksum != file_checksum_expected) {
@@ -6848,10 +7001,20 @@ 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->ioptions(), cfd->GetLatestMutableCFOptions(),
cfd->GetFullHistoryTsLow());
EnqueuePendingCompaction(cfd);
if (cfd->queued_for_compaction()) {
ROCKS_LOG_INFO(immutable_db_options_.info_log,
+58 -9
View File
@@ -455,19 +455,20 @@ class DBImpl : public DB {
void EnableManualCompaction() override;
void DisableManualCompaction() override;
void AbortAllCompactions() override;
void ResumeAllCompactions() override;
using DB::SetOptions;
Status SetOptions(
ColumnFamilyHandle* column_family,
const std::unordered_map<std::string, std::string>& options_map) override;
const std::unordered_map<ColumnFamilyHandle*,
std::unordered_map<std::string, std::string>>&
column_families_opts_map) override;
Status SetDBOptions(
const std::unordered_map<std::string, std::string>& options_map) override;
using DB::NumberLevels;
int NumberLevels(ColumnFamilyHandle* column_family) override;
using DB::MaxMemCompactionLevel;
int MaxMemCompactionLevel(ColumnFamilyHandle* column_family) override;
using DB::Level0StopWriteTrigger;
int Level0StopWriteTrigger(ColumnFamilyHandle* column_family) override;
const std::string& GetName() const override;
@@ -1247,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;
@@ -1365,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
@@ -1717,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
@@ -1727,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;
@@ -2149,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,
@@ -2415,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
@@ -2505,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();
@@ -2788,6 +2816,14 @@ class DBImpl : public DB {
// compaction code paths.
std::atomic<int> manual_compaction_paused_ = false;
// If non-zero, all compaction jobs (background automatic compactions,
// manual compactions via CompactRange, and foreground CompactFiles calls)
// are being aborted. Compactions will be signaled to stop. Any new
// compaction job would fail immediately. The value indicates how many threads
// have called AbortAllCompactions(). It is accessed in read mode outside the
// DB mutex in compaction code paths.
std::atomic<int> compaction_aborted_ = 0;
// This condition variable is signaled on these conditions:
// * whenever bg_compaction_scheduled_ goes down to 0
// * if AnyManualCompaction, whenever a compaction finishes, even if it hasn't
@@ -3042,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;
@@ -3051,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
@@ -3106,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;
@@ -3176,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_;
+225 -32
View File
@@ -955,6 +955,10 @@ Status DBImpl::CompactRange(const CompactRangeOptions& options,
return Status::Incomplete(Status::SubCode::kManualCompactionPaused);
}
if (compaction_aborted_.load(std::memory_order_acquire) > 0) {
return Status::Incomplete(Status::SubCode::kCompactionAborted);
}
if (options.canceled && options.canceled->load(std::memory_order_acquire)) {
return Status::Incomplete(Status::SubCode::kManualCompactionPaused);
}
@@ -1449,7 +1453,8 @@ Status DBImpl::PerformTrivialMove(Compaction& c, LogBuffer* log_buffer,
f->file_creation_time, f->epoch_number,
f->file_checksum, f->file_checksum_func_name,
f->unique_id, f->compensated_range_deletion_size,
f->tail_size, f->user_defined_timestamps_persisted);
f->tail_size, f->user_defined_timestamps_persisted,
f->min_timestamp, f->max_timestamp);
moved_bytes += static_cast<size_t>(c.input(l, i)->fd.GetFileSize());
ROCKS_LOG_BUFFER(
log_buffer, "[%s] Moved #%" PRIu64 " to level-%d %" PRIu64 " bytes\n",
@@ -1486,6 +1491,11 @@ Status DBImpl::CompactFilesImpl(
return Status::ShutdownInProgress();
}
// triggered by AbortAllCompactions
if (compaction_aborted_.load(std::memory_order_acquire) > 0) {
return Status::Incomplete(Status::SubCode::kCompactionAborted);
}
// triggered by DisableManualCompactions or by user-set canceled flag in
// CompactionOptions
if (manual_compaction_paused_.load(std::memory_order_acquire) > 0 ||
@@ -1636,17 +1646,17 @@ Status DBImpl::CompactFilesImpl(
c->mutable_cf_options().paranoid_file_checks,
c->mutable_cf_options().report_bg_io_stats, dbname_,
&compaction_job_stats, Env::Priority::USER, io_tracer_,
kManualCompactionCanceledFalse_, db_id_, db_session_id_,
c->column_family_data()->GetFullHistoryTsLow(), c->trim_ts(),
&blob_callback_, &bg_compaction_scheduled_,
kManualCompactionCanceledFalse_, compaction_aborted_, db_id_,
db_session_id_, c->column_family_data()->GetFullHistoryTsLow(),
c->trim_ts(), &blob_callback_, &bg_compaction_scheduled_,
&bg_bottom_compaction_scheduled_);
// Creating a compaction influences the compaction score because the score
// takes running compactions into account (by skipping files that are already
// being compacted). Since we just changed compaction score, we recalculate it
// here.
version->storage_info()->ComputeCompactionScore(cfd->ioptions(),
c->mutable_cf_options());
version->storage_info()->ComputeCompactionScore(
cfd->ioptions(), c->mutable_cf_options(), cfd->GetFullHistoryTsLow());
compaction_job.Prepare(std::nullopt /*subcompact to be computed*/);
@@ -1709,6 +1719,11 @@ Status DBImpl::CompactFilesImpl(
"[%s] [JOB %d] Stopping manual compaction",
c->column_family_data()->GetName().c_str(),
job_context->job_id);
} else if (status.IsCompactionAborted()) {
// Don't report aborted compaction as error
ROCKS_LOG_INFO(
immutable_db_options_.info_log, "[%s] [JOB %d] Compaction aborted",
c->column_family_data()->GetName().c_str(), job_context->job_id);
} else {
ROCKS_LOG_WARN(immutable_db_options_.info_log,
"[%s] [JOB %d] Compaction error: %s",
@@ -1971,7 +1986,8 @@ Status DBImpl::ReFitLevel(ColumnFamilyData* cfd, int level, int target_level) {
f->oldest_ancester_time, f->file_creation_time, f->epoch_number,
f->file_checksum, f->file_checksum_func_name, f->unique_id,
f->compensated_range_deletion_size, f->tail_size,
f->user_defined_timestamps_persisted);
f->user_defined_timestamps_persisted, f->min_timestamp,
f->max_timestamp);
}
ROCKS_LOG_DEBUG(immutable_db_options_.info_log,
"[%s] Apply version edit:\n%s", cfd->GetName().c_str(),
@@ -2009,10 +2025,6 @@ int DBImpl::NumberLevels(ColumnFamilyHandle* column_family) {
return cfh->cfd()->NumberLevels();
}
int DBImpl::MaxMemCompactionLevel(ColumnFamilyHandle* /*column_family*/) {
return 0;
}
int DBImpl::Level0StopWriteTrigger(ColumnFamilyHandle* column_family) {
auto cfh = static_cast_with_check<ColumnFamilyHandleImpl>(column_family);
InstrumentedMutexLock l(&mutex_);
@@ -2168,6 +2180,17 @@ Status DBImpl::RunManualCompaction(
return manual.status;
}
if (compaction_aborted_.load(std::memory_order_acquire) > 0) {
// All compactions are being aborted. Return immediately.
int counter = compaction_aborted_.load(std::memory_order_acquire);
ROCKS_LOG_INFO(
immutable_db_options_.info_log,
"RunManualCompaction: Aborting due to compaction_aborted_=%d", counter);
manual.status = Status::Incomplete(Status::SubCode::kCompactionAborted);
manual.done = true;
return manual.status;
}
// When a manual compaction arrives, temporarily disable scheduling of
// non-manual compactions and wait until the number of scheduled compaction
// jobs drops to zero. This used to be needed to ensure that this manual
@@ -2192,6 +2215,13 @@ Status DBImpl::RunManualCompaction(
// and `CompactRangeOptions::canceled` might not work well together.
while (bg_bottom_compaction_scheduled_ > 0 ||
bg_compaction_scheduled_ > 0) {
if (compaction_aborted_.load(std::memory_order_acquire) > 0) {
// Pretend the error came from compaction so the below cleanup/error
// handling code can process it.
manual.done = true;
manual.status = Status::Incomplete(Status::SubCode::kCompactionAborted);
break;
}
if (manual_compaction_paused_ > 0 || manual.canceled == true) {
// Pretend the error came from compaction so the below cleanup/error
// handling code can process it.
@@ -2310,7 +2340,12 @@ Status DBImpl::RunManualCompaction(
if (!scheduled) {
// There is nothing scheduled to wait on, so any cancellation can end the
// manual now.
if (manual_compaction_paused_ > 0 || manual.canceled == true) {
if (compaction_aborted_.load(std::memory_order_acquire) > 0) {
// Stop waiting since it was canceled. Pretend the error came from
// compaction so the below cleanup/error handling code can process it.
manual.done = true;
manual.status = Status::Incomplete(Status::SubCode::kCompactionAborted);
} else if (manual_compaction_paused_ > 0 || manual.canceled == true) {
// Stop waiting since it was canceled. Pretend the error came from
// compaction so the below cleanup/error handling code can process it.
manual.done = true;
@@ -2889,16 +2924,8 @@ Status DBImpl::WaitForFlushMemTables(
Status DBImpl::EnableAutoCompaction(
const std::vector<ColumnFamilyHandle*>& column_family_handles) {
Status s;
for (auto cf_ptr : column_family_handles) {
Status status =
this->SetOptions(cf_ptr, {{"disable_auto_compactions", "false"}});
if (!status.ok()) {
s = status;
}
}
return s;
return SetOptions(column_family_handles,
{{"disable_auto_compactions", "false"}});
}
// NOTE: Calling DisableManualCompaction() may overwrite the
@@ -2936,10 +2963,65 @@ void DBImpl::EnableManualCompaction() {
manual_compaction_paused_.fetch_sub(1, std::memory_order_release);
}
void DBImpl::AbortAllCompactions() {
InstrumentedMutexLock l(&mutex_);
// Increment the abort counter to signal all compactions to abort
compaction_aborted_.fetch_add(1, std::memory_order_release);
TEST_SYNC_POINT("DBImpl::AbortAllCompactions:FlagSet");
// Mark all manual compactions as canceled
for (const auto& manual_compaction : manual_compaction_dequeue_) {
manual_compaction->canceled = true;
}
// Wake up any waiting compaction threads to check the abort signal
bg_cv_.SignalAll();
// Wait for all running compactions (both manual and automatic) to finish
// or abort before returning.
// Note: bg_cv_.Wait() releases the mutex while waiting, so other threads
// can make progress and signal when compactions complete.
while (bg_bottom_compaction_scheduled_ > 0 || bg_compaction_scheduled_ > 0 ||
HasPendingManualCompaction()) {
bg_cv_.Wait();
}
}
void DBImpl::ResumeAllCompactions() {
InstrumentedMutexLock l(&mutex_);
int before = compaction_aborted_.load(std::memory_order_acquire);
// Guard against calling Resume without prior Abort
if (before <= 0) {
ROCKS_LOG_WARN(immutable_db_options_.info_log,
"ResumeAllCompactions called without prior "
"AbortAllCompactions (counter=%d)",
before);
return;
}
// Decrement the abort counter
compaction_aborted_.fetch_sub(1, std::memory_order_release);
// As the operation is executed under db mutex, we could just use before value
// to calculate the current value.
int current = before - 1;
ROCKS_LOG_INFO(immutable_db_options_.info_log,
"ResumeAllCompactions: counter %d -> %d", before, current);
// If this is the last resume call (abort counter back to 0), schedule
// compactions that may have been waiting
if (current == 0) {
MaybeScheduleFlushOrCompaction();
}
}
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;
}
@@ -3000,6 +3082,9 @@ void DBImpl::MaybeScheduleFlushOrCompaction() {
if (bg_compaction_paused_ > 0) {
// we paused the background compaction
return;
} else if (compaction_aborted_.load(std::memory_order_acquire) > 0) {
// we are aborting all compactions
return;
} else if (error_handler_.IsBGWorkStopped()) {
// Compaction is not part of the recovery sequence from a hard error. We
// might get here because recovery might do a flush and install a new
@@ -3060,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();
@@ -3495,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
@@ -3519,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(
@@ -3537,7 +3715,8 @@ void DBImpl::BackgroundCallCompaction(PrepickedCompaction* prepicked_compaction,
10000); // prevent hot loop
mutex_.Lock();
} else if (!s.ok() && !s.IsShutdownInProgress() &&
!s.IsManualCompactionPaused() && !s.IsColumnFamilyDropped()) {
!s.IsManualCompactionPaused() && !s.IsColumnFamilyDropped() &&
!s.IsCompactionAborted()) {
// Wait a little bit before retrying background compaction in
// case this is an environmental problem and we do not want to
// chew up resources for failed compactions for the duration of
@@ -3569,6 +3748,7 @@ void DBImpl::BackgroundCallCompaction(PrepickedCompaction* prepicked_compaction,
// case of a failure). Thus, we force full scan in FindObsoleteFiles()
FindObsoleteFiles(&job_context, !s.ok() && !s.IsShutdownInProgress() &&
!s.IsManualCompactionPaused() &&
!s.IsCompactionAborted() &&
!s.IsColumnFamilyDropped() &&
!s.IsBusy());
TEST_SYNC_POINT("DBImpl::BackgroundCallCompaction:FoundObsoleteFiles");
@@ -3599,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
@@ -3673,6 +3857,8 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
if (!error_handler_.IsBGWorkStopped()) {
if (shutting_down_.load(std::memory_order_acquire)) {
status = Status::ShutdownInProgress();
} else if (compaction_aborted_.load(std::memory_order_acquire) > 0) {
status = Status::Incomplete(Status::SubCode::kCompactionAborted);
} else if (is_manual &&
manual_compaction->canceled.load(std::memory_order_acquire)) {
status = Status::Incomplete(Status::SubCode::kManualCompactionPaused);
@@ -3839,7 +4025,8 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
->current()
->storage_info()
->ComputeCompactionScore(c->immutable_options(),
c->mutable_cf_options());
c->mutable_cf_options(),
cfd->GetFullHistoryTsLow());
EnqueuePendingCompaction(cfd);
c.reset();
@@ -4096,7 +4283,9 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
in_file->unique_id,
in_file->compensated_range_deletion_size,
in_file->tail_size,
in_file->user_defined_timestamps_persisted};
in_file->user_defined_timestamps_persisted,
in_file->min_timestamp,
in_file->max_timestamp};
out_files.push_back(std::move(out_file_metadata));
}
@@ -4286,8 +4475,9 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
&compaction_job_stats, thread_pri, io_tracer_,
is_manual ? manual_compaction->canceled
: kManualCompactionCanceledFalse_,
db_id_, db_session_id_, c->column_family_data()->GetFullHistoryTsLow(),
c->trim_ts(), &blob_callback_, &bg_compaction_scheduled_,
compaction_aborted_, db_id_, db_session_id_,
c->column_family_data()->GetFullHistoryTsLow(), c->trim_ts(),
&blob_callback_, &bg_compaction_scheduled_,
&bg_bottom_compaction_scheduled_);
compaction_job.Prepare(std::nullopt /*subcompact to be computed*/);
@@ -4370,7 +4560,7 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
}
if (status.ok() || status.IsCompactionTooLarge() ||
status.IsManualCompactionPaused()) {
status.IsManualCompactionPaused() || status.IsCompactionAborted()) {
// Done
} else if (status.IsColumnFamilyDropped() || status.IsShutdownInProgress()) {
// Ignore compaction errors found during shutting down
@@ -4401,7 +4591,8 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
->current()
->storage_info()
->ComputeCompactionScore(c->immutable_options(),
c->mutable_cf_options());
c->mutable_cf_options(),
cfd->GetFullHistoryTsLow());
EnqueuePendingCompaction(cfd);
}
}
@@ -4507,7 +4698,7 @@ Compaction* DBImpl::CreateIntendedCompactionForwardedToBottomPriorityPool(
nullptr /* snapshot_checker */, c->compaction_reason());
cfd->compaction_picker()->RegisterCompaction(intended_compaction);
vstorage->ComputeCompactionScore(io, mo);
vstorage->ComputeCompactionScore(io, mo, cfd->GetFullHistoryTsLow());
intended_compaction->FinalizeInputInfo(cfd->current());
return intended_compaction;
@@ -4632,6 +4823,7 @@ void DBImpl::BuildCompactionJobInfo(
compaction_job_info->cf_id = cfd->GetID();
compaction_job_info->cf_name = cfd->GetName();
compaction_job_info->status = st;
compaction_job_info->aborted = st.IsCompactionAborted();
compaction_job_info->thread_id = env_->GetThreadID();
compaction_job_info->job_id = job_id;
compaction_job_info->base_input_level = c->start_level();
@@ -4879,7 +5071,8 @@ void DBImpl::ResetBottomPriCompactionIntent(ColumnFamilyData* cfd,
std::unique_ptr<Compaction>& c) {
c->ReleaseCompactionFiles(Status::OK());
cfd->current()->storage_info()->ComputeCompactionScore(
c->immutable_options(), c->mutable_cf_options());
c->immutable_options(), c->mutable_cf_options(),
cfd->GetFullHistoryTsLow());
c.reset();
}
+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();
+4 -2
View File
@@ -46,7 +46,8 @@ Status DBImpl::SuggestCompactRange(ColumnFamilyHandle* column_family,
// Since we have some more files to compact, we should also recompute
// compaction score
vstorage->ComputeCompactionScore(cfd->ioptions(),
cfd->GetLatestMutableCFOptions());
cfd->GetLatestMutableCFOptions(),
cfd->GetFullHistoryTsLow());
EnqueuePendingCompaction(cfd);
MaybeScheduleFlushOrCompaction();
}
@@ -143,7 +144,8 @@ Status DBImpl::PromoteL0(ColumnFamilyHandle* column_family, int target_level) {
f->file_creation_time, f->epoch_number, f->file_checksum,
f->file_checksum_func_name, f->unique_id,
f->compensated_range_deletion_size, f->tail_size,
f->user_defined_timestamps_persisted);
f->user_defined_timestamps_persisted, f->min_timestamp,
f->max_timestamp);
}
status = versions_->LogAndApply(cfd, read_options, write_options, &edit,
+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();
Loaded 100 of 488 files, more files were not shown because too many files have changed in this diff. Show more