Compare commits

..
Author SHA1 Message Date
Maciej Szeszko b0fe00d3db Fix Git Codex Review invocation 2026-05-12 17:30:15 -07:00
Xingbo Wang cbd61a3165 Add parser for raw table iterator keys (#14726)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14726

Add SstFileReader::ParseTableIteratorKey() so callers of NewTableIterator() have a public way to decode raw table keys without duplicating RocksDB internal-key layout. The implementation delegates to the existing public ParsedEntryInfo parser using the reader comparator.

Reviewed By: pdillinger

Differential Revision: D104584393

fbshipit-source-id: 98e21c4d6676fbba69e533376b3da67539dd8fad
2026-05-12 16:26:18 -07:00
Peter Dillinger 59633e36ed Bug fix: Reject empty string as a column family name (#14732)
Summary:
Previously, calling `DB::CreateColumnFamily(opts, "", &handle)` returned `Status::OK()` with a usable handle, but the column family was not persisted in the manifest. Any data written to the empty-named CF was silently lost on DB reopen, and `ListColumnFamilies` would not show it.

The empty string is also reserved as a sentinel meaning "no/unknown column family" in various RocksDB APIs and serialization formats (e.g. `TablePropertiesCollectorFactory::Context::kUnknownColumnFamily` and table properties), so allowing it as a real CF name is ambiguous in addition to being broken.

This change rejects an empty CF name with `Status::InvalidArgument` at the top of `DBImpl::CreateColumnFamilyImpl`, which covers the single-CF `CreateColumnFamily` API as well as both `CreateColumnFamilies` overloads (by-names and by-descriptors).

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

Test Plan: * Added `ColumnFamilyTest.EmptyNameRejected` covering all three Create entry points; verifies `IsInvalidArgument()` and that no spurious handles are returned.

Reviewed By: hx235

Differential Revision: D104753911

Pulled By: pdillinger

fbshipit-source-id: e52d792830b965484a618f4e55981eee4eb6f515
2026-05-12 16:06:58 -07:00
Xingbo Wang 4707775ae9 Fix GetContext status propagation and blob-backed wide-column merge operands (#14640)
Summary:
- propagate lower-level read and merge failures through `GetContext` via `read_status`, so `Get` and `GetEntity` preserve the original error instead of synthesizing `Corruption` when blob-backed reads or merge resolution fail
- teach `GetMergeOperands` to resolve blob-backed default columns from wide-column entities, covering both the direct base-value path and the merge-plus-base path
- add regression coverage for blob-read IO errors during `Get`/`GetEntity` merge resolution and for `GetMergeOperands` on blob-backed wide-column entities
- fix the `DBFlushTest.MemPurgeCorrectLogNumberAndSSTFileCreation` test race by waiting for flush callbacks and cleaning up sync points

## Testing

- `make db_blob_basic_test -j14`
- `/usr/bin/perl -e 'alarm shift; exec ARGV' 60 ./db_blob_basic_test --gtest_filter='DBBlobBasicTest/DBBlobBasicIOErrorTest.GetBlob_IOError/*:DBBlobBasicTest/DBBlobBasicIOErrorTest.GetEntityMergeWithBlobBaseIOError/*'`

## Task
T265824017, T265415808

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

Reviewed By: anand1976

Differential Revision: D101690700

Pulled By: xingbowang

fbshipit-source-id: 2b6fc357b37a01efa72a2d54dcff55be8992f42a
2026-05-12 15:29:27 -07:00
Maciej Szeszko 46ce1a03a9 Cap Claude thinking budget (#14731)
Summary:
### Motivation
Claude's auto review workflow classifies the PR as complex and sets `MAX_THINKING_TOKENS`=**32000** in `.github/workflows/ai-review-analysis.yml:534`. The Claude action then sends a request where `thinking.budget_tokens` is **32000**, but the request `max_tokens` is not greater than that, so Anthropic rejects it before the review starts ([example](https://github.com/facebook/rocksdb/actions/runs/25691112276/job/75427435133)).

### Fix
Reduce the "complex" thinking budget from **32000** to **24000** tokens and clamps manual overrides to the same ceiling, preventing the thinking budget from exceeding or equalling the action's effective `max_tokens`. 24k is still generous — enough for deep reasoning on complex PRs while guaranteeing the model can emit the formatted review.

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

Reviewed By: xingbowang

Differential Revision: D104749426

Pulled By: mszeszko-meta

fbshipit-source-id: bf94d0f50e7c2c9bc9e4d4cdffd61087d739018a
2026-05-11 17:45:43 -07:00
Peter Dillinger 795f3bd61f Fix check-sources.sh non-ASCII check and remove non-ASCII from sources (#14729)
Summary:
The non-ASCII character check in check-sources.sh used git grep -P (Perl regex), which requires git compiled with PCRE support. On systems without it, the command fails with exit code 128, which is != 1 (no match), so the check always reported a violation -- effectively dead.

Even in CI where git has PCRE2 support, the check was silently broken: git grep -P uses PCRE2 in UTF mode by default, which interprets [\x80-\xFF] as a Unicode codepoint range (U+0080 to U+00FF). Characters like em-dash (U+2014), arrows (U+2192), and math symbols (U+2248, etc.) fall outside that range and were not detected. Only Latin-1 Supplement characters (U+0080-U+00FF) would have been caught.

Replace with LC_ALL=C git grep using bash $'[\x80-\xff]' literal byte range, which works with basic regex in the C locale, and replace all non-ASCII characters in non-excluded source files:
- em-dash to --
- arrow to ->
- math symbols to ASCII equivalents (~=, <=, >=)
- box-drawing characters to ASCII art

Also exclude .github/ from the check, as scripts there can use non-ascii without disrupting RocksDB builds on non-UTF-8 systems.

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

Test Plan: manual / CI (make check-sources passes clean)

Reviewed By: hx235

Differential Revision: D104692574

Pulled By: pdillinger

fbshipit-source-id: 1d884c21056dcd83558b825a04b867f1c08e3f45
2026-05-11 17:02:22 -07:00
Maciej Szeszko 330962bff6 CI: Make Codex complexity classification non-fatal (#14721)
Summary:
Previously, `Classify PR complexity (Codex)` ran under `bash -e`, so any `codex exec` failure aborted the entire Codex review before the real review step could run. The classifier only selects the review budget, so on failure we now log the classifier output tail, default to the `complex` review budget, and continue. This keeps actual Codex review failures visible through the existing review exit-code/log handling while preventing the auxiliary classifier from blocking review generation.

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

Reviewed By: xingbowang

Differential Revision: D104326081

Pulled By: mszeszko-meta

fbshipit-source-id: c17388bbf576f71ff85ded320e0740f89072f8c1
2026-05-11 10:59:26 -07:00
Josh Kang e07ccc3528 block GetCreationTimeOfOldestFile on async file open completion (#14723)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14723

### Context

`GetCreationTimeOfOldestFile()` assumed `max_open_files` = -1 meant every live SST had a pinned table reader. That is not true with `open_files_async`: recovery intentionally skips loading table files, `DB::Open()` returns, and `BGWorkAsyncFileOpen()` pins readers later. Any caller — e.g. fb_rocksdb's daily report at `FbRocksDb.cpp:1025` — invoking the API in the window between `DB::Open` returning and the background opener completing trips a debug assert. Reported in https://fb.workplace.com/groups/rocksdb/permalink/31668956482726231/.

Removing the assert alone is insufficient. For legacy DBs whose manifest does not carry `file_creation_time`, `FileMetaData::TryGetFileCreationTime()` falls back to the pinned reader; with no reader, it returns `kUnknownFileCreationTime` and the function silently returns 0 (the "info unavailable" sentinel). The caller cannot distinguish "no info" from "raced with async open."

### Changes

- Remove the invalid debug assert in `Version::GetCreationTimeOfOldestFile`.
- Add a private helper `DBImpl::WaitForAsyncFileOpen()` that blocks on `bg_cv_` while `bg_async_file_open_state_ == kScheduled`. The synchronization machinery (`bg_async_file_open_state_` + `bg_cv_`) already exists — the destructor wait loop in `db_impl.cc:674-687` uses the same pattern. The helper is a no-op when `open_files_async = false`, and bails on `shutting_down_` so `DB::Close()` is not blocked by an in-flight caller.
- Call `WaitForAsyncFileOpen()` at the top of `DBImpl::GetCreationTimeOfOldestFile()` (inside the `max_open_files == -1` branch).
- Document the blocking behavior in `include/rocksdb/db.h`.
- Replace the regression test with one that uses a `DBImpl::WaitForAsyncFileOpen::BeforeWait` sync point: spawn a thread that calls `GetCreationTimeOfOldestFile`, deterministically confirm it blocks inside the wait, release async open, confirm the caller wakes with the real value.

### Potential Followups (not included here)

- Apply the same wait to `GetLiveFilesMetaData` and `GetColumnFamilyMetaData` — both zero out `oldest_ancester_time` / `file_creation_time` in the SST metadata they return during the async-open window (`db/version_set.cc:7877-7878`, `2090-2091`, `2172-2173`).
- Address compaction-picker effects: TTL/periodic file selection (`db/version_set.cc:4039`, `4092-4094`), bottommost over-marking (`db/version_set.cc:4700-4701`), FIFO TTL/temperature pickers (`db/compaction/compaction_picker_fifo.cc:105-107`, `167-170`, `401-411`), and tiered-compaction output time inheritance (`db/compaction/compaction.cc:981`, `1000`).
- Harden `FbRocksDb.cpp:1025` to check `status.ok()` instead of `status.code() != kNotSupported`.

Reviewed By: mszeszko-meta

Differential Revision: D104285992

fbshipit-source-id: ea46375ea1b3ba77fe6b548071aee1101ac0da77
2026-05-08 19:13:20 -07:00
xingbowang 224e849e8d env: suppress liburing TSAN false positives (#14710)
Summary:
- Use the liburing TSAN suppressions in `tools/tsan_suppressions.txt` instead of defining the process-wide `__tsan_default_suppressions()` hook, avoiding conflicts with downstream applications.
- Wire RocksDB TSAN make and crash-test flows to use that suppressions file by default without overriding caller-provided `TSAN_OPTIONS`.
- Cover direct `db_crashtest.py` launches by passing the default suppressions to `db_stress` subprocesses.
- Fix the GCC 16 unity-build warning in `CacheItemHelper` by directly initializing the no-secondary-cache helper fields instead of delegating with `this`.

Imported from D101303486.

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

Test Plan:
- `COMPILE_WITH_TSAN=1 make -j128 env_test`
- `timeout 60s ./env_test --gtest_filter=EnvPosixTest.IOUringAddressReuseNoTsanFalsePositive`
- `python3 tools/db_crashtest_test.py`
- `python3 -m py_compile tools/db_crashtest.py tools/db_crashtest_test.py`
- CI: `build-linux-unity-and-headers` passed after the `CacheItemHelper` fix

Reviewed By: hx235

Differential Revision: D104103800

Pulled By: xingbowang

fbshipit-source-id: 6066d9abe02a3c44d75f9ce449889468c927ce56
2026-05-07 16:40:56 -07:00
Anand Ananthabhotla c734b7cc60 Add reuse_manifest_on_open DBOption (#14704)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14704

Add an immutable DBOption `reuse_manifest_on_open` (default false). When enabled, `DB::Open` can keep using the recovered MANIFEST for the first post-open metadata update instead of rebuilding a fresh MANIFEST, which can reduce warm-open latency for DBs whose MANIFEST is expensive to regenerate.

Reuse is still best-effort. If RocksDB cannot safely resume appending to the recovered MANIFEST, it falls back to the existing fresh-MANIFEST path. The option is also disabled under `best_efforts_recovery`.

This diff also teaches the reopened MANIFEST writer to adopt the existing file size before appending, documents the small-`max_manifest_file_size` caveat for the reused path, and keeps the full warm-reopen composition working with `optimize_manifest_for_recovery`.

Reviewed By: hx235, pdillinger

Differential Revision: D103568447

fbshipit-source-id: f4f5c35ea3ef0b80a0d52d94be40c6bd11505999
2026-05-07 13:10:59 -07:00
Anand Ananthabhotla 60b34f30c4 Extend optimize_manifest_for_recovery through DB::Close (#14703)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14703

Extend `optimize_manifest_for_recovery` so a clean `DB::Close` can persist up-to-date WAL recovery markers when that can be done safely. Combined with the recovery-side optimization in the previous diff, a clean close/reopen can avoid recovery-time MANIFEST appends.

This remains best-effort: if the close-time write is disabled, skipped, or fails, RocksDB falls back to the standard recovery path on the next open. The option stays mutable so it can be turned off before close to suppress the optimization without restarting the DB.

The close-time path respects the existing recovery constraints for 2PC, non-empty column families, dropped column families, and WAL tracking, and preserves the existing file-number invariants.

Reviewed By: pdillinger, hx235

Differential Revision: D103568449

fbshipit-source-id: ae62867507a8a87640a2c140bea852b7c608cb66
2026-05-07 12:30:54 -07:00
Anand Ananthabhotla 02a2b3501d Add optimize_manifest_for_recovery DBOption (#14702)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14702

Add a mutable DBOption `optimize_manifest_for_recovery` (default false) as a temporary rollout / kill switch for warm-reopen MANIFEST optimizations.

In this diff, enabling the option lets recovery skip MANIFEST updates during `DB::Open` when the recovered state is already reflected on disk, which reduces metadata appends after a clean shutdown and can lower warm-reopen latency on storage where MANIFEST appends are expensive.

If the option is disabled, RocksDB follows the existing recovery path unchanged. The optimization is disabled under `best_efforts_recovery`, where recovery intentionally rewrites metadata as part of salvage, and the option is mutable so later diffs in this stack can share the same rollout knob.

Reviewed By: pdillinger, hx235

Differential Revision: D103568448

fbshipit-source-id: 9ec930343e434f1bee6130bcdbd7738dddd92b6d
2026-05-07 11:44:37 -07:00
generatedunixname3846135475516776 1dc4813d97 Rocksdb Crash Test failed: assertion failed - cached_file_is_live_or_quar (#14717)
Summary: Pull Request resolved: https://github.com/facebook/rocksdb/pull/14717

Reviewed By: pdillinger

Differential Revision: D103953026

fbshipit-source-id: 4b81853dad792d77fe6dbc1ab7255a9f04834078
2026-05-07 11:21:18 -07:00
Josh Kang 4af61efaf4 Add blog post for interpolation search (#14701)
Summary:
title

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

Reviewed By: pdillinger

Differential Revision: D103709672

Pulled By: joshkang97

fbshipit-source-id: 09cd8644cfa139ed59287ca1e508df6ce470f9e9
2026-05-07 10:39:42 -07:00
xingbowang e0d549fbba Keep remote compaction stats serialization compatible with 11.1 (#14712)
Summary:
- Reapply c4941760c9 so remote compaction serializes InternalStats::CompactionStats::counts with the pre-11.2 compaction-reason count.
- Keep remote compaction result metadata compatible with 11.1 readers and writers until the stats serialization path has version-aware array handling.

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

Test Plan:
- make format-auto
- make -j$(sysctl -n hw.ncpu) compaction_job_test compaction_service_test
- /usr/local/bin/timeout 60 ./compaction_job_test --gtest_filter=CompactionJobTest.ResultSerialization
- /usr/local/bin/timeout 60 ./compaction_service_test --gtest_filter=CompactionServiceTest.VerifyStats

Reviewed By: joshkang97, jaykorean

Differential Revision: D104130456

Pulled By: xingbowang

fbshipit-source-id: e9e92cb044253edbec0fc79b92c3efff53f01d8b
2026-05-07 01:06:56 -07:00
Maciej Szeszko c25dcbccba Fix GCC 16 warning in CacheItemHelper constructor (#14713)
Summary:
This fixes a GCC 16 unity-build failure in `Cache::CacheItemHelper`. The no-secondary-cache constructor delegated to the full constructor while passing this as `without_secondary_compat`. GCC 16 reports that pattern as `-Wmaybe-uninitialized` because the delegated constructor receives a pointer to the object under construction and its debug assertions dereference it. Since RocksDB builds with `-Werror`, the warning is promoted to a build error. We see it now because the unity job uses `gcc:latest`, which now has `GCC 16.1.0`. The code pattern itself dates back to PR https://github.com/facebook/rocksdb/issues/11299 / ccaa3225b from 2023.

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

Reviewed By: xingbowang

Differential Revision: D104162382

Pulled By: mszeszko-meta

fbshipit-source-id: d6cb579e37bed0d833c6427c3b0541c857425d42
2026-05-06 20:11:57 -07:00
Peter Dillinger c99d5ec06e Add prefix varint codec and tests (#14692)
Summary:
Add MLIR-compatible PrefixVarint32/64 helpers in util/prefix_varint.h. Document the format, split decode API, and hot paths, and cover the codec in coding_test with round-trip, disk-read, overflow, and truncation cases.

I'm intending to use this in the new blog file format because you only need to read the first byte to know how many varint bytes to read total, and it might be more CPU efficient in other uses as well (to be determined in follow-up work).

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

Test Plan: Unit tests included

Reviewed By: joshkang97

Differential Revision: D103245079

Pulled By: pdillinger

fbshipit-source-id: dca435beccb6e666d864a31d8683ad19e2121c34
2026-05-06 19:27:14 -07:00
Josh Kang a4045b0a5f ExternalTable Iterator Prefer IterKey over InternalKey (#14695)
Summary:
Reduces per-step heap allocations in the external table iterator's hot path
by replacing the `InternalKey` member of `ExternalTableIteratorAdapter` with
`IterKey`. `InternalKey` stores its bytes in a `std::string`, which
heap-allocates whenever a key exceeds the small-string threshold. `IterKey`
keeps a 39-byte inline buffer and only spills to the heap for larger keys.
`UpdateKey` runs on every `Next` / `Prev` / `Seek*`, so this swap removes a
recurring per-step allocation for typical key sizes.

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

Test Plan: CI

Reviewed By: anand1976

Differential Revision: D103440780

Pulled By: joshkang97

fbshipit-source-id: deb0a8ea04110e3dff0a1bcad767e7136192fdc6
2026-05-06 10:48:44 -07:00
Josh Kang b874d2546e Preserve published-boundary visibility during flush/compaction (#14681) (#14681)
Summary:
See SC crash test failure 2729181374202803470.

Flush/compaction retention was protecting only explicit snapshots, while readers without an explicit snapshot were still bounded by GetLastPublishedSequence(). That mismatch becomes observable in WRITE_COMMITTED when commit_bypass_memtable runs: a newer version can already be installed into WBWI-backed immutable memtables before SetLastSequence() publishes it. This can happen with two-write-queue enabled OR disabled.

Example setup:
- K@P = "old_value" is the current published version of key K.
- K@U = "new_value" is written later with U > P.
- commit_bypass_memtable ingests K@U into immutable memtables, but publication is still at P.
- flush starts in that window and builds its snapshot context.

Before this change, if there was no explicit snapshot at P, flush/compaction was free to collapse K@P under K@U. A reader at the published boundary P then saw neither version: K@P had been discarded, while K@U was still too new, so Get(K) returned NotFound.

A simple solution is to add a "fake" snapshot boundary for pessimistic write committed txns.

NOTE: A behavioral side effect is that this will prevent a merge operand at snapshot seqno from being filtered via compaction filter because of the new managed snapshot. But this keeps it aligned with the behavior that write prepared and write unprepared have.

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

Test Plan: Regression test that fails without fix, specifically K@P gets deleted and a snapshot read returns NotFound, when it should exist.

Reviewed By: hx235

Differential Revision: D102856943

Pulled By: joshkang97

fbshipit-source-id: 46cb3936e9d4e771966d39fd0d15436335bf6ccf
2026-05-05 14:58:01 -07:00
anand76 c1dbe0a6ac Disable MultiScanAsyncIOTest.MultiScanPrepare test (#14707)
Summary:
This test is failing in CI. Disable until root caused.

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

Reviewed By: mszeszko-meta

Differential Revision: D103888323

Pulled By: anand1976

fbshipit-source-id: d2639ddda10a8caa2f36ae2ba529d65cb8bd2ea9
2026-05-05 11:33:13 -07:00
Peter Dillinger 52d8574629 Fix AI review workflows broken by cc8d9ea04 (#14696)
Summary:
This commit fixes several potential issues introduced by cc8d9ea04 (CI: AI review workflow improvements) that broke the agent review jobs in GitHub CI.

1. Codex workflows fail when OPENAI_API_KEY is not configured:
   - Added Codex review workflows fail with exit code 1 if the API key secret is missing
   - Added script-level checks to skip Codex steps gracefully when OPENAI_API_KEY is unset, avoiding CI noise

2. Missing pull-requests: read permission in manual-review job:
   - The manual-review job calls github.rest.pulls.get() but lacked the required permission
   - Added 'pull-requests: read' to the manual-review job permissions

3. Potential crash when headSha is undefined:
   - parse-claude-review.js and parse-codex-review.js accessed meta.headSha.substring() without null check
   - Added defensive checks to handle undefined headSha gracefully

4. Claude model claude-opus-4-7 incompatible with thinking API:
   - The commit upgraded default model to claude-opus-4-7, but this model doesn't support the 'thinking.type.enabled' API used by the Claude Code action
   - Error: 'thinking.type.enabled' is not supported for this model. Use 'thinking.type.adaptive' and 'output_config.effort' to control thinking behavior
   - Reverted default model to claude-opus-4-6 and removed claude-opus-4-7 from options

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

Test Plan: Created a draft PR with this change and draft production code changes, including a branch on facebook/rocksdb. Go to https://github.com/facebook/rocksdb/actions/workflows/claude-review.yml and choose "Run workflow" with that branch and PR. (Changes in the PR do are not picked up for the agent code review workflows; the only way to test what is outside of main is with a branch on facebook/rocksdb)

Reviewed By: joshkang97

Differential Revision: D103453631

Pulled By: pdillinger

fbshipit-source-id: 15a82257b5762e9e0b1b393dc45c4b343c866f7a
2026-05-01 16:35:45 -07:00
Josh Kang a12bd7d641 ExternalTable support FS (#14629)
Summary:
Extends the `ExternalTable` plug-in interface so external table implementations have access to the same `FileSystem` RocksDB itself uses. Previously only `ExternalTableOptions` (read path) had `fs`.

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

Test Plan: - Existing tests pass

Reviewed By: anand1976

Differential Revision: D101384103

Pulled By: joshkang97

fbshipit-source-id: cade6f4699337e07bdd4875d070f629058fd6795
2026-05-01 12:23:54 -07:00
Josh Kang a69e9fdb7e Optimize iterator with tracking dirty state (#14684)
Summary:
Two CPU-side optimizations to `DBIter`'s hot iteration path.

1. `PrepareValueInternal` always re-parses the internal key after `iter_.PrepareValue()`, even though most inner iterators report their value was already prepared and cannot have moved `iter_.key()`.
2. `ResetValueAndColumns` and `ResetBlobData` unconditionally clear wide-column / blob state at the top of every `Next()`, even on plain-value scans where neither has been populated since the last reset.

Both wins are pure CPU on hot, fully-cached workloads. The dirty-flag bookkeeping introduced for (2) is extracted into a small reusable `DirtyTracked<T>` utility so the same pattern can be reused for any T whose `Reset()` is non-trivial.

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

Test Plan:
**Benchmark — `db_bench seekrandom`.** Each op = 1 Seek + 100 Next on a fully cached 10M-key DB (16 B key, 100 B value, no compression).

| | ops/sec |
|---|---:|
| BEFORE | 42,131 |
| AFTER  | 45,269 |
| **Δ** | **+3,138 (+7.4%)** |

Reviewed By: xingbowang

Differential Revision: D103070805

Pulled By: joshkang97

fbshipit-source-id: 6b184a1aa559649c575d95be28ab43bbee9ffe64
2026-04-30 14:30:25 -07:00
Peter Dillinger fa8f4f1ef0 Update buckifier to use folly/coro instead of folly/experimental/coro (#14685)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14685

D100235293 and D102823090 migrated the generated RocksDB BUCK file from
`//folly/experimental/coro:*` to `//folly/coro:*`, but did not update
the buckifier script that generates it in internal_repo_rocksdb. The next release would revert the change.

Update buckify_rocksdb.py to match, so the generated BUCK file stays consistent with the folly coro migration.

Reviewed By: nmk70

Differential Revision: D103096688

fbshipit-source-id: 9055769ed5e9893397c7504ada22e21980f59dd2
2026-04-30 13:04:51 -07:00
Xingbo Wang d534974711 Fix BUCK file format check failure (#14691)
Summary:
BUCK file is out of sync on folly dependency.

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

Test Plan: Build pass

Reviewed By: archang19

Differential Revision: D103233002

Pulled By: xingbowang

fbshipit-source-id: 252fa27229126995a0492bc426a90ead1d10d958
2026-04-30 10:55:26 -07:00
xingbowang cc8d9ea04d CI: AI review workflow improvements (#14659)
Summary:
- Upgrade the Claude review workflow models and add complexity-based thinking budget selection for the main review run.
- Keep AI review comments tied to the reviewed commit and restructure long reviews so high-severity findings stay visible while details live in a collapsible section.
- Add early auto-trigger gating plus Codex review/comment workflows, including shared comment-building and parsing helpers.

## Testing

- Not run. The branch refresh was a no-op rebase onto current `upstream/main`, and no new code changes were made during this task.

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

Reviewed By: pdillinger

Differential Revision: D102224743

Pulled By: xingbowang

fbshipit-source-id: 39c25494062cbcd52d3e85f8a859b5a3907c758e
2026-04-30 09:37:58 -07:00
Davis Rollman 1fd2c202c7 Migrate folly coro references from folly/experimental to folly/coro 9
Summary: Migrate coro references across the repo to the non shim version.

Differential Revision: D102823090

fbshipit-source-id: 3bec59aed1f2645139a1ad9c18363e6af462e61f
2026-04-29 13:13:12 -07:00
Peter Dillinger c28b4f0e12 Fixes and enhancements to pre-push hook (#14680)
Summary:
The previous pre-push hook auto-formatted, committed, and re-pushed on behalf of the user. This was fragile: it's not clear whether the format fix should amend the current commit or create a new one. Adding a commit breaks populating the summary for creating a new PR. Also, the hook's internal re-push would drop flags like --set-upstream from the original command. Replace with a simple check-only approach that blocks the push and tells the user what to fix.

Also add a new check for untracked source files (.cc, .h, .py, etc.) in tracked directories (excluding third-party/). These typically indicate files that were forgotten in the commit, which would cause the pushed code to fail to build.

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

Test Plan: manual

Reviewed By: xingbowang

Differential Revision: D102855227

Pulled By: pdillinger

fbshipit-source-id: 7dc2c4e7a2b2c392bf8da74d7ea43883c8c075a9
2026-04-28 15:56:32 -07:00
Anand Ananthabhotla 4e2fe35bbb Gate file_open_metadata consumption on fast_sst_open option (#14676)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14676

When fast_sst_open is disabled, RocksDB was still passing previously-persisted
file_open_metadata from the MANIFEST to NewRandomAccessFile. This could cause
failures when the metadata becomes stale (e.g. expired filesystem credentials).

This change gates the consumption of file_open_metadata in
TableCache::GetTableReader on the fast_sst_open option. When fast_sst_open is
false, previously persisted metadata is ignored and not passed to the filesystem
via FileOptions::file_metadata.

The fast_sst_open flag is threaded from MutableDBOptions through VersionSet ->
ColumnFamilySet -> ColumnFamilyData -> TableCache at construction time, ensuring
the gate is active before any table readers are opened during recovery. Dynamic
changes via SetDBOptions are also propagated to all existing TableCache instances.

Reviewed By: mszeszko-meta, xingbowang

Differential Revision: D102735581

fbshipit-source-id: 9a2c4dc0644a2f65c36b2468605df57779e127cd
2026-04-28 14:30:52 -07:00
Peter Dillinger b3b7668bf5 Convert some ROCKSDB_GTEST_SKIP to BYPASS (#14679)
Summary:
Fix false internal warnings about skipped tests (seen on D102718613).
 ROCKSDB_GTEST_SKIP signals a gap in test coverage due to the build/execution
 environment, while ROCKSDB_GTEST_BYPASS is for intentionally and permanently
 omitting certain parameterizations. These six tests skip based on the test
 parameterization (forward vs reverse, primary vs secondary mode), not due to
 any environment limitation, so BYPASS is the correct macro.

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

Test Plan: these are tests, look at internal signals

Reviewed By: joshkang97

Differential Revision: D102833346

Pulled By: pdillinger

fbshipit-source-id: e57bd1e217ca63aaf418b68c788563e67bbb07e3
2026-04-28 14:11:33 -07:00
Josh Kang b12d3a6da6 External Table Get Optimizations (#14673)
Summary:
- External Table is only PUTs with no seqno, so we can route it to the simpler more efficient `GetContext::SaveValue`.
- `SstFileReader::Get` was allocating a string to append key footer, instead use LookUpKey to allocate on stack for short keys.

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

Test Plan: CI passes

Reviewed By: anand1976

Differential Revision: D102659533

Pulled By: joshkang97

fbshipit-source-id: e55127548c9e4b7bdeb63c46acfdb8eb5883db14
2026-04-28 11:43:18 -07:00
Josh Kang 8fb8fdd349 Use saved_key_ instead of iter_.key() in forward range conversion (#14672)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14672

This fixes the forward path introduced in the original pull request: https://www.internalfb.com/diff/D102409405

That change tried to avoid using user-owned `iterate_upper_bound_` memory by reading `iter_.key()` when the internal iterator was still valid and using it as the exclusive end key for the synthetic range tombstone. That is not safe: after the upper-bound break, `iter_.key()` is only the current merged child position, not a key the bounded scan proved safe to read or use as a range endpoint.

Example: run a prefix-bounded scan over prefix `b` with tombstones `ba`, `bb`, `bc`, `iterate_upper_bound = "bd"`, live key `be` in an SST, and later same-prefix key `bz` in the memtable. The SST child can drop out when it notices `be >= bd`, while the memtable child is not upper-bound aware and can still surface `bz`. If cleanup uses `iter_.key() == "bz"`, it inserts `[ba, bz)` and covers the live `be`, even though that key was never safe for the bounded scan to reason about.

The fix is to always end the forward range at `saved_key_`, which is the last tombstone user key the iterator actually proved deleted. That is conservative but safe. The regression test builds the mixed SST/memtable example above and asserts that the live `be` remains visible. The diff also re-enables randomized `min_tombstones_for_range_conversion` in `db_crashtest.py` and forces it back to `0` when `use_multiscan == 1`, since multiscan still does not support read-path range conversion.

Reviewed By: xingbowang

Differential Revision: D102493855

fbshipit-source-id: 45bdb86464268e2c4fa2ea735dbb059a5e054f28
2026-04-27 14:35:40 -07:00
Peter Dillinger 959fa315b1 Use unique_ptr for StreamingCompress and StreamingUncompress (#14665)
Summary:
Replace raw owning pointers with std::unique_ptr for StreamingCompress/Uncompress in Create functions and log::Writer/Reader, eliminating manual delete calls in destructors. Also update the StreamingCompressionTest to use unique_ptr for its local StreamingCompress, StreamingUncompress, and MemoryAllocator objects.

Bonus: improve error message for missing/broken ruby (new build requirement)

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

Test Plan: ./log_test - all 211 tests pass.

Reviewed By: xingbowang

Differential Revision: D102381893

Pulled By: pdillinger

fbshipit-source-id: 02806b46c8371474a969d6fa3012fff5eb7886c3
2026-04-27 12:17:09 -07:00
Anand Ananthabhotla f4fa2d2386 Fix obsolete file cache entries not being erased with concurrent readers (#14662)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14662

When a file becomes obsolete, `ReleaseObsolete()` calls `ReleaseAndEraseIfLastRef()` to remove it from the table cache. However, if concurrent readers hold references to the cache entry, this is not the last ref, so the entry stays. When those readers later call plain `Release()`, the entry becomes unreferenced but remains in the cache — there is no mechanism to trigger cleanup.

The fix is to call `cache->Erase()` in `ReleaseObsolete()` instead of relying solely on `ReleaseAndEraseIfLastRef()`. `Erase()` marks the entry as Invisible in the cache. The cache's `Release()` implementation already checks `IsInvisible()` and erases Invisible entries when the last reference is released. This guarantees that obsolete file entries are cleaned up regardless of concurrent reader timing.

Also reverts the D102158618 workaround that reordered `EraseUnRefEntries()` before `TEST_VerifyNoObsoleteFilesCached` in `CloseHelper`. With the root cause fixed, the assertion correctly passes in its original position.

Reviewed By: pdillinger

Differential Revision: D102158618

fbshipit-source-id: 5796abee916dfbd99554a36d3bbf579842d2fb8b
2026-04-27 12:13:20 -07:00
Josh Kang ebaa925006 Disable range tombstone conversions again (#14670)
Summary:
Seems like crashtests are failing more often now, disabling again to investigate.

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

Reviewed By: archang19

Differential Revision: D102479287

Pulled By: joshkang97

fbshipit-source-id: 3f147f58b0cf2901ffe3b99e8681ed5a6085e7c8
2026-04-25 12:37:32 -07:00
Xingbo Wang 3ae314446b Fix integration gap between blob direct write and blob_compression_opts (#14669)
Summary:
`blob_compression_opts` is dynamically changeable through `SetOptions()`, but
blob direct write cached its per-thread compressor by `CompressionType` only.
After updating `blob_compression_opts`, new blob records on an existing writer
thread could keep using stale compression settings.

This change threads `CompressionOptions` through the direct-write settings and
rebuilds the cached per-thread compressor whenever the published options for
that compression type change. When the options stay the same, the steady-state
cache behavior is unchanged.

It also adds a regression test that toggles the ZSTD checksum flag through
`SetOptions()` and inspects the raw blob records to confirm each direct-write
record used the latest compression options.

## Testing

- `timeout 60s ./db_blob_direct_write_test --gtest_filter='DBBlobDirectWriteTest.DirectWriteCompressionOptionsUpdateRebuildsCachedCompressor' --gtest_repeat=5`

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

Reviewed By: jainraj91

Differential Revision: D102423376

Pulled By: xingbowang

fbshipit-source-id: 504243d72cf22e62097f7e88ac1cc36c5bf29eee
2026-04-25 05:19:55 -07:00
Peter Dillinger 6842ba8455 Add kNoChecksum to crash test checksum_type coverage (#14667)
Summary:
kNoChecksum was not being tested in db_crashtest.py despite being a supported checksum type. Add it to the random selection to ensure crash test coverage of the no-checksum code path.

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

Test Plan: Ran multiple blackbox and whitebox crash test validation runs with kNoChecksum forced and with random selection including kNoChecksum. All passed crash-recovery verification.

Reviewed By: hx235

Differential Revision: D102404837

Pulled By: pdillinger

fbshipit-source-id: 0120f1269c917dd6e347764f2bdc0095d46ff71a
2026-04-24 17:27:38 -07:00
Josh Kang a2abe3cfd6 Avoid setting read option upper bound for range tombstone conversion (#14664)
Summary:
`DBIter::FindNextUserEntryInternal` previously used `iterate_upper_bound_` (user-controlled memory) as the exclusive end-key when flushing an accumulated tombstone run. That pointer is dangerous as it points to user memory space, which can be modified at any moment.

This PR removes that dependency: the end-key is always derived from RocksDB-owned data — either the live `iter_` key that broke the run, or `saved_key_` when `iter_` is exhausted.

The tradeoff here is that there is potentially 1 less tombstone to get covered in this path, which is reasonable.

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

Test Plan: Updated unit tests, CI passes

Reviewed By: xingbowang

Differential Revision: D102409405

Pulled By: joshkang97

fbshipit-source-id: b2beed73e5fe5b955b2d782fc467eda637c4fcf2
2026-04-24 15:33:10 -07:00
Danny Chen 2e7cf42cda Add MANIFEST_VALIDATION_FAILURE_COUNT statistic (#14657)
Summary:
CONTEXT: The manifest validation on close feature (verify_manifest_content_on_close) detects corruption but does not increment any statistics counter, making it harder to monitor in production.

WHAT: Add a new ticker MANIFEST_VALIDATION_FAILURE_COUNT that is incremented each time content validation detects manifest corruption during DB::Close(). The counter fires per corruption detection, so it can increment up to 2 times per close (once on initial check, once after rewrite attempt). Updated all existing manifest validation tests to verify the counter value.

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

Test Plan:
- All 7 manifest validation tests pass with new stat assertions
- 5x repeat with COERCE_CONTEXT_SWITCH=1 shows no flakiness
- Full version_set_test suite (212 tests) passes

Reviewed By: anand1976

Differential Revision: D102404260

Pulled By: dannyhchen

fbshipit-source-id: 21a0aa1ad8de12a935caf5642e41ccf2a47b46d9
2026-04-24 15:22:15 -07:00
Yoshinori Matsunobu 3cdf942192 Make ParseCompressionNameForDisplay manager-aware (#14658)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14658

Follow up to D101463511. Add a  hook and a manager-aware overload of  so custom CompressionManagers can provide human-readable names for custom compression types while preserving the existing generic fallback when no compatible manager is available.

Reviewed By: pdillinger

Differential Revision: D102201365

fbshipit-source-id: 0c7456bb9db2e54927a4349d12c035fc8b5ad562
2026-04-24 13:51:20 -07:00
Xingbo Wang d8fc727592 Revert temporary 11.1 remote compaction stats serialization workaround (#14663)
Summary:
There is a separate way to handle incompatibility. Revert this.

- revert the temporary workaround from https://github.com/facebook/rocksdb/issues/14656 that serialized `InternalStats::CompactionStats::counts` with the pre-11.2 compaction-reason count
- restore remote compaction stats serialization to use the full `CompactionReason::kNumOfReasons` array size again

## Context
https://github.com/facebook/rocksdb/issues/14656 temporarily shrank the serialized `counts` array to keep remote compaction metadata readable across the 11.1/11.2 boundary. This follow-up removes that special case because the compatibility issue is being handled separately.

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

Test Plan: - Not run in this metadata-prep task

Reviewed By: jaykorean

Differential Revision: D102376258

Pulled By: xingbowang

fbshipit-source-id: 55693ea25a97b70de9d53b0d6eebcbd88a325b1c
2026-04-24 12:08:25 -07:00
anand76 ff16ff23ed Update version to 11.3.0 for 11.2 release (#14638)
Summary:
- Bump version.h from 11.2.0 to 11.3.0
- Add `11.2.fb` to `check_format_compatible.sh`
- Cherry-pick HISTORY.md from `11.2.fb`
- Update folly hash in `folly.mk`
- Delete `unreleased_history/` note files

Part of 11.2 release workflow.

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

Reviewed By: xingbowang

Differential Revision: D101893027

Pulled By: anand1976

fbshipit-source-id: a4d0d355a9dca6199228f91ba08665524e6c8f5b
2026-04-24 09:53:51 -07:00
Josh Kang 03e5a0b9a8 Fix range tombstone conversion + ingest sst and re-enable crash tests (#14654)
Summary:
Fixes a correctness bug in read-path range-tombstone synthesis when it races with `IngestExternalFile`. The synthesis path could insert a tombstone into the active memtable at a snapshot's sequence number, while ingestion installed an L0 SST at `LastSequence + 1` — a higher seqno than the synthesized tombstone. This breaks the main assumption of range tombstone reads that all lower levels have lower seqno.

The fix introduces a per-CF `port::RWMutex` (`ColumnFamilyData::ingest_sst_lock_`) plus a per-memtable `ingest_seqno_barrier_`. Ingestion takes the read lock and range tombstone synthesis **tries** to take a write lock.

If iterator lock is successful, then we have a new updated barrier seqno that we can validate the iterator seqno against. An added benefit is we no longer need to gate against empty memtable. This was originally added as an easy fix to prevent memtables from being inserted into while ingestion was happening.

## The bug, by example

`ReadPathRangeTombstoneTest.NewerPointInOlderFileStillVisible` (`db/db_iterator_test.cc:6929`):

1. L0 file `b@1, c@2, d@3`, then L0 file `Delete(b)4, Delete(c)5`.
2. Active memtable: `Put(z)6`. Snapshot taken at seq 6.
3. `IngestExternalFile({c → "vc_live"})` → installed at L0 with seq 7.
4. Iterator at snap 6 walks the deletion run and synthesizes `[b, d) @ seq 6` into the active memtable via `MemTable::AddLogicallyRedundantRangeTombstone`.
5. `Get("c")` at the latest snapshot: memtable returns covering tombstone (seq 6), `Version::Get` short-circuits, **never reads `c@7`** — returns `NotFound` instead of `"vc_live"`.

The invariant `Version::Get` relies on (memtable seqs ≥ any L0 seq for the same key) is broken because synthesis writes at the *snapshot's* seq while ingestion writes at `LastSequence + 1`.

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

Test Plan:
- Updated regression test
- Re-enable crashtests and manually run

| Flavor | Jobs |
|---|---:|
| `fbcode_blackbox_crash_test` | 200 |
| `fbcode_whitebox_crash_test` | 30 |
| `fbcode_asan_blackbox_crash_test` | 30 |
| `fbcode_tsan_blackbox_crash_test` | 30 |
| `fbcode_crash_test_with_atomic_flush` | 30 |
| `fbcode_crash_test_with_wc_txn` | 30 |
| `fbcode_crash_test_with_ts` | 30 |
| **Total** | **380** |

Reviewed By: xingbowang

Differential Revision: D102044512

Pulled By: joshkang97

fbshipit-source-id: 0c69187595edc5a5fa80be24bffdba710a92e56e
2026-04-23 18:59:14 -07:00
Yoshinori Matsunobu 763401b595 Add ParseCompressionNameForDisplay API (#14637)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14637

Add public API ParseCompressionNameForDisplay() to render TableProperties::compression_name in human-readable form for both legacy and format_version >= 7 SST metadata, including BuiltinV2/compression-manager encodings and filtered no-compression markers for display.

Reviewed By: xingbowang

Differential Revision: D101463511

fbshipit-source-id: 51c44850183cb9757644a323376844adffc7a8c2
2026-04-22 20:39:26 -07:00
Xingbo Wang c4941760c9 Keep remote compaction stats serialization compatible with 11.1 (#14656)
Summary:
- Keep remote compaction stats serialization compatible with RocksDB 11.1.
- Serialize `InternalStats::CompactionStats::counts` using the pre-11.2 compaction-reason count so remote compaction metadata remains readable across the version boundary.
- Preserve the existing formatting-only follow-up commit on the branch.

## Notes

- Needs a better mechanism to make OptionTypeInfo::Array ser/des friendly. Will follow up after 11.2 release.

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

Reviewed By: pdillinger

Differential Revision: D102071507

Pulled By: xingbowang

fbshipit-source-id: 6eeb87d86494f528f0d1ce46ce4c3e8e7dd2da53
2026-04-22 17:34:30 -07:00
xingbowang 809ed26be5 Revert "Migrate fbcode coro references from folly/experimental to folly/coro (5/6 - fbcode a-m)" (#14652)
Summary:
This reverts commit `bad2d5b0af7e160de5ca58869f40941af0f9da95`.

The reverted change switched fbcode Buck dependencies from `//folly/experimental/coro:*` to `//folly/coro:*` in:

- `BUCK`
- `buckifier/buckify_rocksdb.py`

That change broke the internal build, so this PR restores the previous dependency paths.

## Testing

- Not run locally; this task only fetched, rebased, and verified the existing revert branch.

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

Reviewed By: archang19

Differential Revision: D101980062

Pulled By: xingbowang

fbshipit-source-id: 9a6edf14c226caf87e2c63c7f2806d409dc809ef
2026-04-22 08:53:09 -07:00
Hui Xiao 13ea89f66b Handle DB::Open failure in fault_injection_test better (#14646)
Summary:
**Summary:**
Assert the DB::Open status before asserting on the DB pointer in FaultInjectionTest::OpenDB(). This avoids aborting the test process when reopen fails and preserves the underlying RocksDB error for ASSERT_OK(OpenDB()). This is to deal with recent failures on a host with nearby tests failing with space issue already.
```
[ RUN      ] ExternalSSTFileBasicTest.LargeSizeSstFileWriter
db/external_sst_file_basic_test.cc:3303: Failure
sst_file_writer.Finish()
IO error: No space left on device: While appending to file: /dev/shm/rocksdb_testt/run-external_sst_file_basic_test-shard-3/external_sst_file_basic_test_578578_8746831521190564136/large_key.sst: No space left on device
[  FAILED  ] ExternalSSTFileBasicTest.LargeSizeSstFileWriter (8551 ms)

[ RUN      ] FaultTest/FaultInjectionTestSplitted.FaultTest/2
fault_injection_test: db/fault_injection_test.cc:269: rocksdb::Status rocksdb::FaultInjectionTest::OpenDB(): Assertion `db_ != nullptr' failed.
Received signal 6 (Aborted)
```

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

Test Plan: Test changes only

Reviewed By: anand1976

Differential Revision: D101742874

Pulled By: hx235

fbshipit-source-id: de3b9349d9d067dd8f538b44ac08d108924faed3
2026-04-21 14:59:40 -07:00
Hui Xiao 4c1ffedb1e Disable skip_stats_update_on_db_open with remote compaction in stress test (#14647)
Summary:
**Summary:**
When remote compaction is enabled with skip_stats_update_on_db_open=true, the remote worker's secondary DB skips UpdateAccumulatedStats(), leaving FileMetaData fields (num_entries, num_range_deletions) at 0. This breaks standalone range deletion file filtering in
FilterInputsForCompactionIterator() (https://github.com/facebook/rocksdb/blob/b374ba5968bdd6b16ff58918c561781c8bdab442/db/compaction/compaction.cc#L1085 + https://github.com/facebook/rocksdb/blob/b374ba5968bdd6b16ff58918c561781c8bdab442/db/version_edit.h#L462) , causing the primary and remote worker to disagree on input key count and triggering a "Compaction number of input keys does not match number of keys processed" corruption error.

Repro test (should be landed with the fix in the future):  https://github.com/hx235/rocksdb/commit/aa1225c69cd6b6e2d2af735e1d1d9b147edfd6ae
```
[==========] Running 1 test from 1 test case.
[----------] Global test environment set-up.
[----------] 1 test from CompactionServiceTest
[ RUN      ] CompactionServiceTest.StandaloneRangeDeletionWithSkipStatsUpdateOnOpen
db/compaction/compaction_service_test.cc:2970: Failure
s
Corruption: Compaction number of input keys does not match number of keys processed. Expected 0 but processed 4. Compaction summary: Base version 15 Base level 5, inputs: [20(1280B)], [18(1270B filtered:true) 19(1270B filtered:true)]
[  FAILED  ] CompactionServiceTest.StandaloneRangeDeletionWithSkipStatsUpdateOnOpen (1257 ms)
[----------] 1 test from CompactionServiceTest (1257 ms total)

[----------] Global test environment tear-down
[==========] 1 test from 1 test case ran. (1257 ms total)
[  PASSED  ] 0 tests.
[  FAILED  ] 1 test, listed below:
[  FAILED  ] CompactionServiceTest.StandaloneRangeDeletionWithSkipStatsUpdateOnOpen

 1 FAILED TEST
```
This can happen even when standalone range deletion ingestion is disabled in the current run, because such files may have been ingested in a previous crash test run with different randomized options. So we disable skip_stats_update_on_db_open unconditionally when remote compaction is active until the real fix lands .

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

Test Plan:
1. Verify skip_stats_update_on_db_open is forced to 0 with remote compaction
2. Verify skip_stats_update_on_db_open can still be 1 without remote compaction:

Reviewed By: anand1976

Differential Revision: D101744943

Pulled By: hx235

fbshipit-source-id: c1dcd8943433e8a9dfb7385c998be2c339e3b859
2026-04-21 14:57:05 -07:00
Hui Xiao 7f89e5fb09 Disable inplace_update_support in stress test (#14645)
Summary:
**Summary:**
Temporarily disables inplace_update_support in db_crashtest.py. See the db_crashtest.py new comments for mow.

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

Test Plan: - Run db_crashtest.py without passing inplace_update_support option, with short interval, and verify each run shows inplace_update_support is false in the db_stress command line.

Reviewed By: joshkang97

Differential Revision: D101739997

Pulled By: hx235

fbshipit-source-id: 39936b8282e392e688afea5e9bd074b60138f210
2026-04-21 13:37:18 -07:00
Anand Ananthabhotla b374ba5968 Rocksdb Unit Test failed: Unit test failed (#14643)
Summary: Pull Request resolved: https://github.com/facebook/rocksdb/pull/14643

Reviewed By: xingbowang

Differential Revision: D101559659

fbshipit-source-id: 56d405f36bfe83c009053796c0d065f4032522b1
2026-04-20 17:25:18 -07:00
Xingbo Wang 6ff96384dd db_iter: eagerly resolve blob-backed iterator columns (#14632)
Summary:
Restore the iterator contract for blob-backed wide-column entities by eagerly materializing all blob-backed columns before a `DBIter` entry is exposed as valid. Also capture similar error inside compaction filter V4, so that the error is captured a surfaced up like older compaction filter V3.

## Problem

Lazy wide-column blob resolution let `DBIter` report `Valid() == true` before all columns were actually prepared. Callers could read `value()` successfully, then hit a blob-read failure in `columns()`, which flipped the iterator to invalid after the entry had already been observed as valid.

## Solution

- Restore back to old behavior.
- Resolve and materialize all blob-backed wide columns during iterator positioning.
- Keep `columns()` as a pure accessor once `Valid()` is true.
- Add coverage for eager blob resolution on iterator scans.
- Add coverage for blob resolution failures invalidating the iterator before the entry is exposed.
- Capture the same error in compaction filter and set db to bg error status if the error is not retriable.

## Next step
If we want lazy iterator resolution in the future, we will need an API that can surface lazy resolution failures explicitly instead of hiding I/O and status transitions inside value() or columns().

## Testing

- `make -j14 db_wide_blob_direct_write_test`
- `timeout 60s ./db_wide_blob_direct_write_test --gtest_filter='*DirectWriteIteratorValueScanEagerlyResolvesBlobColumns:*DirectWriteIteratorBlobResolutionErrorInvalidatesEntry'`

## Task
T265294130

# Bonus fix

  ## Non-compaction blob filtering fixes

  ### 1. Flush-time wide-entity lazy blob resolution now works correctly

  Blob direct-write can leave wide-entity blob references in memtables before flush. Flush already runs through `CompactionIterator`, but outside a real compaction it did not have blob read support. As a result, `FilterV4` lazy resolution on flush could fail immediately with
  `NotSupported("Blob fetcher not available")` instead of either:
  - resolving the blob successfully, or
  - surfacing the real blob read error and failing the flush

  This stack fixes that by plumbing blob read support into non-compaction table-file creation. `BuildTable()` now gives `CompactionIterator` enough context to construct a `BlobFetcher` for flush/recovery table building, including write-path fallback for direct-write blob files
  that are not yet manifest-visible.

  With that in place:
  - flush-time `FilterV4` can lazily resolve blob-backed wide columns
  - if resolution fails, the failure is latched and flush fails after `FilterV4()` returns
  - `bg_error` is set instead of silently preserving the entry

  ### 2. Plain blob fallback outside compaction now uses the resolved user value

  For plain blob-backed values, `FilterBlobByKey()` is allowed to return `kUndetermined`, which is supposed to fall back to the normal value-based filter path. That fallback worked in compaction, but the non-compaction `BuildTable()` path still treated plain blob indexes as
  unsupported/corrupt because it could not read the blob.

  This stack fixes that behavior for flush/recovery table-file creation:
  - when `FilterBlobByKey()` returns `kUndetermined`, RocksDB now eagerly resolves the plain blob value
  - `FilterV2`/`FilterV3`/`FilterV4` then see the actual user value as `ValueType::kValue`, not the `BlobIndex` encoding
  - legacy filters therefore behave the same way outside compaction as they already do inside compaction

  Wide-column entities still use the `FilterV4` lazy resolver path. The plain-blob fix is specifically about restoring the documented fallback behavior for blob-backed `kValue` records outside compaction.

  ## Why this matters

  Without these last two commits, flush-time filtering still had two inconsistent behaviors:
  - wide-entity lazy resolution could not actually read blob-backed columns in the direct-write case
  - plain blob-backed values could not fall back to normal value-based filtering outside compaction

  So even after fixing iterator validity and compaction error propagation, non-compaction table-file creation still had remaining contract holes. These commits make flush/recovery behave consistently with compaction.

  ## Coverage added

  The tests added in these commits cover:
  - flush-time plain-blob `FilterV3` fallback using the resolved user value
  - flush-time plain-blob `FilterV4` fallback using the resolved user value
  - flush-time direct-write wide-entity lazy resolver success
  - flush-time direct-write wide-entity lazy resolver failure on missing blob file, including flush failure and `bg_error` latching

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

Reviewed By: anand1976

Differential Revision: D101425233

Pulled By: xingbowang

fbshipit-source-id: 695b2df5189033d30309b35849815e31fd965664
2026-04-18 00:38:50 -07:00
Anand Ananthabhotla a4139ef9e9 IODispatcher: fall back to synchronous coalesced reads when async IO is unavailable (#14633)
Summary: Pull Request resolved: https://github.com/facebook/rocksdb/pull/14633

Reviewed By: xingbowang

Differential Revision: D101299989

fbshipit-source-id: ce30f91da23031b57b2f440f3cdc56f67cc673a0
2026-04-17 17:57:35 -07:00
Andrew Chang 3ef8b1b743 Guard auto-readahead against exhausted index iter (#14631)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14631

Observed crash from ZippyDB iterate scans:

```
onFatalError: unexpected error : Invariant SignalFatal:
fbcode/zippydb/server/Main.cpp:handleSigToFatalCommon:281:FATAL false
failed: Signal 11 (SIGSEGV) at address 0x0 ... method: MultiIterate ...
RocksDB Activity: iterate
```

Key stack frames:
  rocksdb::BlockBasedTableIterator::IsNextBlockOutOfReadaheadBound()
    fbcode/rocksdb/src/table/block_based/block_based_table_iterator.h:471
  rocksdb::BlockBasedTableIterator::BlockCacheLookupForReadAheadSize()
    fbcode/rocksdb/src/table/block_based/block_based_table_iterator.cc:878
  rocksdb::FinalizeAsyncRead()
  rocksdb::FilePrefetchBuffer::PollIfNeeded()
  rocksdb::BlockFetcher::ReadBlockContents()
  rocksdb::BlockBasedTableIterator::InitDataBlock()
  rocksdb::DBIter::Next()

Reviewed By: xingbowang

Differential Revision: D101217429

fbshipit-source-id: e7657909a0f07526d070b214cb783f581b8e98ca
2026-04-17 12:36:29 -07:00
anand76 4bc0bff0e4 Mark IODispatcher as experimental (#14630)
Summary:
As titled

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

Reviewed By: xingbowang

Differential Revision: D101383049

Pulled By: anand1976

fbshipit-source-id: 19fb1c27bee465e691412a30681f9a21c70672f7
2026-04-17 12:12:13 -07:00
xingbowang 49ec219955 cmake: fix folly nightly build regressions (#14609)
Summary:
Fix nightly build regressions introduced by recent CI/toolchain changes.

This PR includes two parts:
- CMake/link fixes for the Folly and Folly Lite nightly jobs
- a targeted nightly workflow fix for the clang-21 ASAN/UBSAN + Folly job

Changes:
- link gflags explicitly for USE_FOLLY CMake tool and benchmark targets
- resolve USE_FOLLY_LITE glog via its installed library path instead of bare -lglog
- in the clang-21 nightly job, build Folly/getdeps with gcc/g++ instead of inheriting clang-21 for third-party dependency builds
- disable ccache only for that standalone Folly build step so getdeps/CMake does not inject the broken ccache compiler launcher for ASM

Root cause:
- recent CI/container changes exposed missing explicit link dependencies in the CMake Folly paths
- the clang-21 nightly job exported CC/CXX at job scope, so Folly getdeps inherited clang-21 into libiberty/binutils build logic that expects GCC-style driver behavior such as -print-multi-os-directory
- the same standalone Folly build path also misbehaved when ccache was auto-detected and used as a compiler launcher for assembler-with-cpp inputs

Verification:
- make format-auto
- git diff --check
- local CMake configure sanity check for default config
- upstream nightly reruns used to confirm failure signatures before and after the first-round fixes

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

Reviewed By: anand1976

Differential Revision: D100695693

Pulled By: xingbowang

fbshipit-source-id: 703546ad3ddc518ab80c936442709d65fe2d22af
2026-04-17 11:22:35 -07:00
Josh Kang 790294f3ad Disallow table_filters entirely when range tombstone conversion is enabled (#14618)
Summary:
The prior fix (https://github.com/facebook/rocksdb/issues/14586) disabled read-path range tombstone synthesis at the `DBIter` level when `table_filter` was set. However, synthesized tombstones persist in the memtable beyond the lifetime of the iterator that created them. A prior unfiltered iterator can synthesize a range tombstone that then silently affects a subsequent filtered iterator's results — the filtered scan sees the tombstone but not the SSTs it was derived from, allowing hidden SST state to corrupt the filtered view. See failed crash test in T264151327.

This PR takes the stronger approach of rejecting iterator creation outright (`InvalidArgument`) when `ReadOptions::table_filter` is used on a column family with `min_tombstones_for_range_conversion > 0`. This eliminates the entire class of interaction bugs between the two features rather than trying to suppress conversion in individual code paths.

## Example

- `min_tombstones_for_range_conversion = 2`
- L1 SST_keep: `Put(a), Put(b), Put(c)`   (`c` is the live boundary)
- L0 SST_dels: `Delete(a), Delete(b)`     (2 contiguous tombstones, newer)

### Step 1 — Iterator A (no filter)
A walks `Del(a) Del(b)` (2 contig) then `c` (live).
Synthesizes range tombstone `[a, c)` into the active memtable.

### Step 2 — Iterator B (`table_filter` skips SST_dels)
B's filter excludes SST_dels, so `a, b` from SST_keep should appear live.
But the memtable now holds range tombstone `[a, c)` from step 1, which
applies to B regardless of `table_filter`. B sees only `c` —
**a, b are silently hidden**.

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

Test Plan:
- `TableFilterNotAllowed` test validates the rejection path end-to-end: unfiltered scan synthesizes the tombstone, filtered iterator is rejected with `InvalidArgument`, and the full-DB view remains correct via `VerifyIteration`.
- Crash test sanitization ensures `db_stress` won't hit the incompatible configuration.
- Run crash test sanitization unit test: `python3 tools/db_crashtest_test.py`
- Run iterator tests: `make db_iterator_test && ./db_iterator_test --gtest_filter="*TableFilterNotAllowed*"`

Reviewed By: xingbowang

Differential Revision: D100873156

Pulled By: joshkang97

fbshipit-source-id: dd0d123d94b1bfda2a4a5040681fcc7cf14f760e
2026-04-17 01:33:16 -07:00
Josh Kang 9886d52fc4 Disable range tomb conversion when using legacy prefix iterator (#14625)
Summary:
There is an assumption in the code that within a given prefix all keys can be seen after a seek to that prefix. The problem occurs when there is a Next() followed by a Prev() back into the same prefix. The merging iterator actually uses SeekForPrev during Prev for non-child iterators, and as a result changes the prefix being used. Now that we are back to our original prefix, the view is incomplete.

The fix is to simply disable range tombstone conversion in this legacy prefix mode.

### Example

Use a 1-byte prefix extractor, so the prefix is just the first character.

Think of the merged iterator as combining two children:

- Child A: `b8`
- Child B: `b7, c1`

Global sorted order is:

```text
b7, b8, c1
```

So:

- predecessor of `c1` should be `b8`
- predecessor of `b8` should be `b7`

## Sequence

Start in legacy prefix mode:

- `total_order_seek = false`
- `prefix_same_as_start = false`

Now do:

1. `Seek("b8")`
2. `Next()`
3. `Prev()`

## What should happen

```text
Seek("b8") -> b8
Next()     -> c1
Prev()     -> b8
```

Because in the global order, `b8` is immediately before `c1`.

## What the buggy iterator did

```text
Seek("b8") -> b8
Next()     -> c1
Prev()     -> b7
```

So it skipped `b8`.

## Why it happens

When `Prev()` switches direction, the merging iterator tells all non-current
 children to `SeekForPrev(current_key)`.

At that moment:

- current key is `c1`
- so the other children get `SeekForPrev("c1")`

But in legacy prefix mode, those child seeks are allowed to use prefix
filtering. So a child that only contains `b*` keys can effectively say:

```text
"I was asked for prefix c; I do not have prefix c."
```

That child drops out instead of returning its real global predecessor `b8`.

Then the current child simply moves back from `c1` to `b7`, and the merged
result becomes `b7`.

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

Test Plan: Update unit tests to disable feature.

Reviewed By: xingbowang

Differential Revision: D101259748

Pulled By: joshkang97

fbshipit-source-id: 23e19c470f931406f96a34a7baebca89e91c7e39
2026-04-16 18:20:59 -07:00
xingbowang 36e50a61fc db_stress: exclude info logs from metadata read faults (#14616)
Summary:
- fix a false positive in `db_stress` PrefixScan fault accounting caused by info-log `FileExists()` probes going through metadata-read fault injection
- exclude `kInfoLogFile` from metadata-read injection and extend `FaultInjectionTestFS` filename parsing so LOG/LOG.old.* and `db_log_dir` info-log paths are recognized consistently
- add regression coverage showing info-log `FileExists()` bypasses metadata-read injection while manifest lookups still inject

## Testing
- make format-auto
- make -j48 fault_injection_fs_test db_stress
- timeout 60 ./fault_injection_fs_test
- timeout 60 ./fault_injection_fs_test --gtest_filter='*MetadataReadFaultExcludesInfoLogFiles*' --gtest_repeat=5

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

Reviewed By: hx235

Differential Revision: D100838168

Pulled By: xingbowang

fbshipit-source-id: 9ef996908b393aea2a8221fcdda67ed609643dbe
2026-04-16 18:20:44 -07:00
xingbowang 7b06a9ae7e Track blob garbage during flush of direct-write blob references (#14623)
Summary:
- meter blob garbage during `BuildTable()` when flush input can contain direct-write blob references, so overwrite elision and flush-time compaction filters register garbage in the manifest
- plumb `BlobFileGarbage` through flush and recovery manifest edits even when the flush produces no SST output
- add wide-column direct-write tests covering TTL-filtered flushes with both mixed live/expired records and all-expired input

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

Reviewed By: anand1976

Differential Revision: D101212841

Pulled By: xingbowang

fbshipit-source-id: e5803ddaf17b5dfd92312e42191b76e311257f3b
2026-04-16 16:45:13 -07:00
xingbowang df5695cf24 Fix workflow YAML parsing and add validation (#14624)
Summary:
This fixes the Claude review workflow YAML parse error and adds a CI validation step for GitHub Actions workflow YAML via `make check-
  workflow-yaml`.

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

Reviewed By: archang19

Differential Revision: D101225058

Pulled By: xingbowang

fbshipit-source-id: 0f371dcbd70ece2c25220c96e43305d9665554b1
2026-04-16 16:06:32 -07:00
Xingbo Wang 820c4a77c1 dump disk usage on no space stress test failure (#14619)
Summary:
Detect out-of-space failures from combined db_stress stdout/stderr in the Python wrapper so both OpenAndCompact stdout failures and verification stderr failures trigger the same diagnostics.

When a match is found, print filesystem usage for /dev/shm and the db roots, then summarize per-directory and per-extension usage to make it clear which files consumed space. Add unit coverage for the failure matcher and suffix accounting.

This help triage any regression in additional file usage in stress test.

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

Reviewed By: archang19

Differential Revision: D100984310

Pulled By: xingbowang

fbshipit-source-id: 23765ff405f0e64382e601b0da173ab6b37dba6d
2026-04-15 09:52:43 -07:00
Laurynas Biveinis 94b6566505 Fix CompareDbtEndpoints crash with timestamp-aware comparators (#14601) (#14611)
Summary:
`RangeTreeLockManager::CompareDbtEndpoints()` called `Comparator::Compare()` on range lock endpoint keys that never contain user-defined timestamps. With a timestamp-aware comparator (`timestamp_size() > 0`), this caused assertion failures in debug builds and silent endpoint misordering in release builds.

Replace `Compare()` with the 4-argument `CompareWithoutTimestamp()` using `a_has_ts=false, b_has_ts=false`, which is the correct contract for serialized range lock endpoints (format: `[1-byte suffix][key bytes]`, no timestamp).

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

Test Plan:
- New test `RangeLockWithTimestampComparator` reopens with `BytewiseComparatorWithU64Ts` and exercises range lock acquisition, conflict detection, and non-overlapping success with short keys.
- Verified RED (assertion failure) before fix, GREEN after.
- Full `range_locking_test` suite passes (16/16).
- Stress tested with `COERCE_CONTEXT_SWITCH=1 --gtest_repeat=5`.

Reviewed By: xingbowang

Differential Revision: D100775201

Pulled By: laurynas-biveinis

fbshipit-source-id: 58e3846e62e9b3cc5bdc69557458b245f90b3967
2026-04-15 09:38:59 -07:00
Anand Ananthabhotla 1cc216a45b Fast file open: retrieve, persist, and pass file open metadata (#14596)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14596

When DBOptions::fast_sst_open is enabled, RocksDB retrieves opaque file system
metadata for SST files after flush, compaction, and external file ingestion via
FSRandomAccessFile::GetFileOpenMetadata(). This metadata is persisted in the
MANIFEST using a new forward-compatible NewFileCustomTag (kFileOpenMetadata = 17),
and passed back to the file system via FileOptions::file_metadata on subsequent
file opens. This accelerates DB open time on remote storage systems by allowing
the file system to skip expensive metadata RPCs.

The feature is gated by DBOptions::fast_sst_open (default false). Everything
works seamlessly regardless of the option setting, file metadata support, or
presence/absence of the metadata in the MANIFEST. The MANIFEST change is backward
compatible - older RocksDB versions safely ignore the new tag.

Reviewed By: xingbowang

Differential Revision: D100220973

fbshipit-source-id: f52de9dd853a50653b3297ab4a37a868fe41cc04
2026-04-14 19:07:29 -07:00
Josh Kang 978bf67426 Disable min_tombstones_for_range_conversion in crash tests (#14617)
Summary:
Temporarily disable the feature until all fixes land and crash tests are stabilized.

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

Reviewed By: xingbowang

Differential Revision: D100853151

Pulled By: joshkang97

fbshipit-source-id: f2c9018f0b55891f6c0623902c3b961bc7f623aa
2026-04-14 13:52:59 -07:00
Xingbo Wang 6b8ca89f45 Ignore post-SIGTERM io_uring stderr in stress test (#14613)
Summary:
Blackbox crash tests intentionally terminate `db_stress` with `SIGTERM` on timeout. When `io_uring` is enabled, that shutdown can emit the expected
`PosixRandomAccessFile::MultiRead: io_uring_submit_and_wait returned terminal error: -9.`
message on `stderr`, which currently makes the timeout path look like a real failure.

This change filters only that known post-`SIGTERM` stderr after a timeout when the process output confirms the `SIGTERM` handler ran. Any other stderr is still surfaced and fails the test, while the ignored lines are appended to `stdout` so the signal remains visible in logs.

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

Test Plan:
- Added unit tests covering fully ignored post-`SIGTERM` stderr
- Added unit tests covering mixed stderr where unrelated lines must still fail
- Added unit tests covering guard conditions when the run did not time out or did not print the `SIGTERM` marker
- Not run (not requested)

Reviewed By: archang19

Differential Revision: D100806204

Pulled By: xingbowang

fbshipit-source-id: 01248a371afae91bb43df55f88400baf155373f5
2026-04-14 10:02:10 -07:00
Xingbo Wang 50852b5c8d db_stress: document expected-state trace/replay contract (#14612)
Summary:
- add a `docs/components/` landing page and a stress-test docs index
- document the `db_stress` expected-state trace/replay lifecycle, file invariants, and prefix-recovery contract in `expected_state_trace.md`
- align `db_stress` comments with the restore semantics: missing trace entries are fatal, while extra tail trace entries are tolerated
- keep the new docs tree trackable and point repo instructions at the new component-docs entrypoint

## Testing
- Not run (documentation and comment updates only)

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

Reviewed By: joshkang97

Differential Revision: D100797173

Pulled By: xingbowang

fbshipit-source-id: 25be8c6239b9fdd84580818efe7520c371f9a46b
2026-04-14 09:58:20 -07:00
Peter Dillinger 9f474a1034 Enforce < 4GB sizes for keys, values, and some BlockBuilder inputs (#14461)
Summary:
BlockBuilder was somewhat inconsistent in its treatment of Slices whose size exceeds 4GB, which in a random corruption case could lead to (for example) serializing only the bottom 32 bits of a value size (uncorrupted) but appending the full multi-GB (corrupted) size. Because this is inner loop code, we don't want to pay CPU for extra conversions, data movements, or Status plumbing (already ruled out by Xingbo).

In this change we make BlockBuilder more internally consistent and lift the 32-bit size requirement to callers (of a few functions specifically, for now). To ensure that's satisfied, I've added additional checks near the perimeter of RocksDB to ensure keys and values do not exceed 4GB, plus an extra random corruption or backstop check in BlockBasedTableBuilder. In detail,

BlockBuilder (block_builder.cc/h):
* Add API comments documenting the < 4GB assumption on Add/AddWithLastKey
* Add debug assertions verifying input slice sizes < 4GB
* Simplify AddWithLastKeyImpl to use uint32_t locals, reducing static_cast
* Use only bottom 32 bits when appending derivative slices for consistency
* Update MaybeStripTimestampFromKey to also truncate to 32-bit size
* Add FIXME comments where buffer_/values_buffer_ sizes are truncated

BlockBasedTableBuilder (block_based_table_builder.cc):
* Add value size check (> uint32_t max) as a safety net against random corruption, since we have seen such corruptions in production

WriteBatch (write_batch.cc):
* Tighten existing key size checks to account for kNumInternalBytes (8-byte internal key suffix), using new kMaxWriteBatchKeySize constant
* Add missing size checks to Delete, SingleDelete, and DeleteRange (both Slice and SliceParts variants)

SstFileWriter (sst_file_writer.cc):
* Add key and value size checks in AddImpl and DeleteRangeImpl, which bypass WriteBatch and go directly to the table builder

MergeHelper (merge_helper.cc):
* Add merge result size check (> uint32_t max) in both overloads of TimedFullMergeImpl, returning Corruption if exceeded
* Add PartialMergeMulti result size check in MergeUntil

CompactionIterator (compaction_iterator.cc):
* Add size check on compaction filter output values (kChangeValue and kChangeWideColumnEntity), returning Corruption if > 4GB

meta_blocks.cc:
* Use Slice with uint32_t-truncated sizes in PropertyBlockBuilder::Finish to handle potentially oversized user property collector output

Needed follow-up:
* Check for blocks that are mis-encoded due to overflowing 32 bits for restart point offsets (and similar). See FIXME comments in block_builder.cc for why this is tricky.

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

Test Plan:
New extreme-size unit tests, along with some testing infrastructure improvements:

Added test::HasBigMem() (in test_util/testharness.h) which returns true when the system has ≥128GB RAM (via sysconf(_SC_PHYS_PAGES) on Linux/macOS) or when ROCKSDB_BIGMEM_TESTS is set. All extreme-size tests use HasBigMem() to skip gracefully on smaller machines rather than being permanently disabled.

Where possible, tests use MemMapping::AllocateLazyZeroed() to supply large keys/values as Slices backed by anonymous mmap (cleaner with new MemMapping::AsSlice()). On Linux, read-only access to these pages maps to the shared kernel zero page, so the source data consumes no physical RAM — only the destination copy (e.g., WriteBatch::rep_) materializes, cutting peak memory roughly in half vs. std::string.

Tests (all enabled, skip via HasBigMem()):

./write_batch_test --gtest_filter='*LargeKeyValueSizeLimit*'
./external_sst_file_basic_test --gtest_filter='*LargeSizeSstFileWriter*'
./merge_test --gtest_filter='*LargeMergeResultRejected*'
./merge_helper_test --gtest_filter='*LargePartialMergeResultRejected*'
./db_compaction_test --gtest_filter='*CompactionFilterLargeValueRejected*'

All 5 pass (verified on a 128GB+ machine). On smaller machines, all 5 bypass cleanly with "insufficient memory for reliable continuous testing".

Reviewed By: xingbowang

Differential Revision: D96521899

Pulled By: pdillinger

fbshipit-source-id: 70f4b5e6a23ab074d60e653fbb7ddc5edbe162ab
2026-04-10 17:04:47 -07:00
Josh Kang f3a20854e0 Revert trace changes (#14600)
Summary:
Reverts the two commits https://github.com/facebook/rocksdb/pull/14578 https://github.com/facebook/rocksdb/pull/14575 that changed tracer behavior. The correct behavior is that trace should be written to before WAL.

Although a trace file may not be fully consumed, it is recycles after each crash, so there is no need to worry about applying an uncomited write. The original crash test was a false positive for a different error.

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

Reviewed By: xingbowang

Differential Revision: D100397989

Pulled By: joshkang97

fbshipit-source-id: 3da6fb80f682ac6f9529c1a76eaf169e38cf2477
2026-04-10 16:04:37 -07:00
Xingbo Wang c47e35395b db: disable read-path tombstone conversion for partial UDT reads (#14595)
Summary:
T263957043 reproduces in whitebox crash testing when DBIter converts a run of point deletes into a range tombstone while reading with an older user-defined timestamp. The scan can observe an older delete for the boundary keys but miss newer live versions of the same keys and interior keys, so the synthesized tombstone is based on partial visibility and later hides valid max-timestamp reads.

## Problem

Read-path range tombstone conversion (`min_tombstones_for_range_conversion`) assumes the scan observes all interior live keys between the start and end of a contiguous delete run. With user-defined timestamps, a read at an older timestamp can see deletes but miss newer Puts for the same keys. The synthesized range tombstone then incorrectly covers those newer versions, causing data loss on subsequent max-timestamp reads.

## Fix

Gate read-path range conversion on full timestamp visibility. The optimization is now only enabled when:
1. There is no `table_filter` (existing guard — SSTs hidden by filter can break the contiguity assumption)
2. There is no `iter_start_ts` (time-travel scans see a subset of versions)
3. Either no read timestamp is set, or the read timestamp is the max timestamp (all `0xff` bytes)

This is done via a new helper `HasFullTimestampVisibility()` checked at `DBIter` construction time.

## Test Changes

- Updated all existing UDT test variants in `ReadPathRangeTombstoneTest` to read at max timestamp so the optimization remains covered where it is valid.
- Added `UDTOlderTimestampDisablesInsertion`: a regression test that writes at ts=1, deletes at ts=2, writes again at ts=3, then reads at ts=2. Verifies no range tombstone is synthesized and a subsequent max-timestamp read still sees all live values.

## Validation

- `make -j192 db_iterator_test`
- `./db_iterator_test --gtest_filter='*ReadPathRangeTombstoneTest*'`
- `./db_iterator_test --gtest_filter='*UDTOlderTimestampDisablesInsertion*' --gtest_repeat=5`
- Reran the original whitebox crash-test seed from T263957043 against the patched tree; crash-recovery verification passed

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

Reviewed By: joshkang97

Differential Revision: D100235999

Pulled By: xingbowang

fbshipit-source-id: 7a287f3c7fdc47428fda0ab89eedd5d43f663985
2026-04-10 10:47:35 -07:00
Xingbo Wang a9ac4df630 env: avoid unused info in TSAN mapping helper (#14597)
Summary:
TsanAnnotateMappedMemory() always builds a TsanMappedMemoryInfo payload
for TEST_SYNC_POINT_CALLBACK(), but in builds where sync points compile to a
no-op the local variable becomes unused and Clang fails with
-Werror,-Wunused-variable.

Move the explicit (void)info cast so it applies regardless of whether
__SANITIZE_THREAD__ is enabled. This keeps the helper warning-free in:
- non-TSAN builds
- builds where TEST_SYNC_POINT_CALLBACK expands to nothing
- TSAN builds that still want the SyncPoint payload for tests

This was missed by CI because the warning only appears in the
COMPILE_WITH_TSAN=1 + NDEBUG/release configuration. Our CI matrix covers
release builds without TSAN and TSAN builds in debug mode, but not the
TSAN+NDEBUG combination.

No behavior change is intended. The helper still:
- exposes the mapping metadata to SyncPoint-based tests
- calls AnnotateNewMemory() in TSAN builds
- remains a no-op with respect to runtime behavior in non-TSAN builds

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

Differential Revision: D100333855

Pulled By: xingbowang

fbshipit-source-id: 3ff0b0c8a51b4959b7a36984e40ac31e49da82eb
2026-04-10 08:43:03 -07:00
Xingbo Wang 4254c705a2 env: reset TSAN state for recycled mappings (#14594)
Summary:
TSAN was reporting false data-race warnings after the kernel reused a virtual address for a new mapping while TSAN still associated that address range with the old mapping.

This showed up in two RocksDB paths:
- `io_uring` setup on helper threads vs later `io_uring` `MultiRead` access
- `io_uring` setup vs file mmap reads when `use_mmap_reads` is enabled

## Changes

Introduce `TsanAnnotateMappedMemory()`, which calls `AnnotateNewMemory()` as soon as a fresh mapping exists so TSAN drops any stale shadow state for the recycled address range.

Apply the helper to:
- io_uring SQ/CQ/SQE mappings created by `CreateIOUring()`
- `PosixFileSystem` mmap-read mappings
- `PosixFileSystem` raw memory-mapped file buffers
- `PosixMmapFile` writable mmap region growth

Also add deterministic regression tests that force virtual-address reuse with `MAP_FIXED` for both the io_uring and mmap-read cases. The tests pass mapping metadata over a pipe so teardown/remap ordering is deterministic without introducing TSAN-visible synchronization that would mask the problem.

## Testing

- `COMPILE_WITH_TSAN=1 make -j192 env_test`
- `env_test --gtest_filter='EnvPosixTest.SupportedOpsNoAsyncIOOnIOUringInitFailure:EnvPosixTest.IOUringAddressReuseNoTsanFalsePositive:EnvPosixTest.MmapReadAddressReuseNoTsanFalsePositive'`

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

Reviewed By: archang19

Differential Revision: D100235746

Pulled By: xingbowang

fbshipit-source-id: 8ef8d85c08a7646540b9c11c3741978898a5af3d
2026-04-09 17:19:11 -07:00
Davis Rollman bad2d5b0af Migrate fbcode coro references from folly/experimental to folly/coro (5/6 - fbcode a-m)
Summary: Migrate coro references in remaining fbcode services (a-m).

Reviewed By: iahs

Differential Revision: D98841546

fbshipit-source-id: 83bb5a2ef6a29a1ff464efa0e4e0f76b48662ab0
2026-04-09 11:11:05 -07:00
xingbowang 1fe1b1bd46 Reduce claude review failure CI noise (#14591)
Summary:
### Summary

  Fix flaky Claude Code Review workflow failures caused by stale workflow_run SHAs.

  Problem:

  - The auto-review workflow is triggered from pr-jobs via workflow_run.
  - In some cases, by the time the review job starts, the PR branch head has already advanced.
  - Then Get PR info cannot find an open PR whose head SHA matches the older workflow_run.head_sha, so it fails with:
      - Could not find PR for SHA ... after retries
  - This creates unnecessary CI noise even though a newer review job will be triggered for the updated commit.

  Solution:

  - Treat “PR not found for this head SHA” as a stale run, not an error.
  - In Get PR info, replace workflow failure with:
      - a warning
      - skip=true
      - skip_reason=stale_sha:<sha>
  - Gate all later auto-review steps on steps.pr_info.outputs.skip != 'true'
  - Add a small logging step to report the skip reason

  Result:

  - stale auto-review runs exit successfully instead of failing
  - CI becomes less noisy
  - the newest commit still gets reviewed by the later workflow run

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

Reviewed By: mszeszko-meta

Differential Revision: D100190375

Pulled By: xingbowang

fbshipit-source-id: fa4a0479a10e953e52b3d99c2b483dae01d3e7fb
2026-04-09 11:01:12 -07:00
Peter Dillinger f3f7feba92 Document Flush(), Sync(), and Fsync() contracts; remove dead enum (#14590)
Summary:
Flush(), Sync(), and Fsync() on WritableFile and FSWritableFile were essentially undocumented. Added comprehensive API contracts covering durability guarantees (process crash vs. power failure), data readability after Flush(), the implication relationship (Sync implies Flush), and thread-safety (referencing IsSyncThreadSafe()). The contracts are written to be implementation-agnostic, avoiding assumptions about OS-level mechanisms, so they apply equally to local, remote, and virtual/wrapper filesystem implementations. Several such implementations have been audited for adherence.

Note that WritableFileWriter currently calls FSWritableFile::Flush() UNNECESSARILY for cases like writing SST files. This should be optimized away in follow-up assuming this interpretation of the contract is agreed upon.

Also removed the dead FileSystem::WriteLifeTimeHint enum, which was never referenced anywhere. The actively used enum is Env::WriteLifeTimeHint.

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

Test Plan: Documentation-only change to public headers, plus removal of an unused enum with no references. No functional changes. Verified no compilation errors with existing tests.

Reviewed By: xingbowang

Differential Revision: D100183288

Pulled By: pdillinger

fbshipit-source-id: 7f1982c41e09fe39896dbc6cc328316be559ec4a
2026-04-09 10:45:50 -07:00
Peter Dillinger b7395b3fa4 Allow Compressor to override parallel_threads for SST building (#14580)
Summary:
Add Compressor::GetRecommendedParallelThreads() virtual method so that the compression subsystem can influence the parallel compression thread count used when building SST files. The base Compressor returns 0 (no opinion), while built-in compressors (via CompressorBase) return the parallel_threads value from their CompressionOptions. This gives CompressionManager a clean mechanism to override parallel_threads by customizing the CompressionOptions passed to GetCompressor() in its GetCompressorForSST() implementation.

The table builder now reads the thread count from the compressor after creation, rather than only from CompressionOptions directly. Hard structural constraints (partition_filters without decoupled mode, user_defined_index_factory) still force single-threaded compression regardless of the compressor's recommendation.

Also adds a sync point in MaybeStartParallelCompression for test observability, and compression_test coverage for the new functionality.

Bonus: extends CompressionManagerCustomCompression test to cover re-opening a DB at format_version=7 with a non-default compression manager that uses the built-in CompatibilityName ("BuiltinV2"). Verifies that data is readable after re-opening without the original manager, and that neither GetId() nor CompatibilityName() resolves to the custom manager via CreateFromString.

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

Test Plan:
New unit tests in compression_test:
- GetRecommendedParallelThreads: verifies built-in compressors return the parallel_threads from their CompressionOptions for all supported compression types.
- CompressionManagerOverridesParallelThreads: end-to-end test with a custom CompressionManager that overrides parallel_threads from 1 to 4 in GetCompressorForSST, verified via sync point that parallel compression actually activates with the overridden thread count, plus data readback verification.

Existing parallel compression tests (DBCompressionTestMaybeParallel) continue to pass.

Reviewed By: xingbowang

Differential Revision: D99896343

Pulled By: pdillinger

fbshipit-source-id: 6b3a30856a78641714d33ec7ba2099f33533d3af
2026-04-08 21:22:52 -07:00
zaidoon 3b38fde293 Support UDI as primary index (#14547)
Summary:
Add `use_udi_as_primary_index` option to `BlockBasedTableOptions`. When
enabled, the UDI becomes the primary index — all reads (including
internal operations like compaction and VerifyChecksum) automatically
route through the UDI without needing `ReadOptions::table_index_factory`.

Both the standard binary search index and the UDI are always fully
built. The standard index serves as a safety fallback (e.g., for
backup/restore or rollback to a non-UDI configuration). A future
refactor will extract the index abstraction to allow skipping the
standard index build when the UDI is primary (see discussion below).

## Write path

- `UserDefinedIndexBuilderWrapper` always forwards `AddIndexEntry` and
  `OnKeyAdded` to both the internal standard builder and the UDI builder
- New `udi_is_primary_index` table property marks primary-mode SSTs
- Validates incompatible options at `DB::Open` and builder creation:
  partitioned index, partitioned filters, missing
  `user_defined_index_factory`

## Read path

- `UserDefinedIndexReaderWrapper` defaults to UDI when `udi_is_primary_`,
  even when `ReadOptions::table_index_factory` is null — this handles
  the 15+ internal call sites that don't set `table_index_factory`
- `use_udi_as_primary_index` automatically enforces `fail_if_no_udi_on_open`
  to prevent silent data loss if SSTs are opened without UDI support

## Rollback

Since the standard index is always fully populated, rollback from
primary mode is straightforward: set `use_udi_as_primary_index=false`.
No compaction required — SSTs written in primary mode are immediately
readable through the standard index.

## Public API

- `BlockBasedTableOptions::use_udi_as_primary_index` (default: false)
- `UserDefinedIndexBuilder::EstimatedSize()` — pure virtual, O(1) via
  running counter in the trie implementation

## Bug fixes (issues https://github.com/facebook/rocksdb/issues/14560, https://github.com/facebook/rocksdb/issues/14561, https://github.com/facebook/rocksdb/issues/14562)

Fixed trie index correctness bugs that caused crash test failures:

- **Always-on seqno encoding**: `must_use_separator_with_seq_` is now
  unconditionally true. Non-boundary separators store tag=0 (sentinel),
  same-user-key boundaries store the real tag, and the last block stores
  its real last-key tag. This fixes the `NonBoundaryTag` bug where
  non-boundary separators with `kMaxSequenceNumber` caused the post-seek
  correction to incorrectly advance past the correct block.
- **Standard index always built in primary mode**: An empty (stub)
  standard index block caused behavioral divergence in
  `BlockBasedTableIterator` under concurrent flush/compaction, leading to
  `test_batches_snapshots` prefix scan inconsistencies.

## Stress test

- `use_udi_as_primary_index` flag randomized by `db_crashtest.py`
- Both primary and secondary UDI modes exercised in crash tests
- Trie index probability halved (~6%) per reviewer request
- Re-enables trie crash tests (disabled by https://github.com/facebook/rocksdb/issues/14559)

## Tests

- Parameterized `TrieIndexDBTest` on UDI mode (secondary vs primary)
- New factory-level tests: non-boundary separator seek, Prev within
  overflow, SeekToLast with overflow, empty trie, overflow exhaustion,
  all-scans-exhausted
- New DB-level tests: multi-CF coalescing iterator, GetEntity with
  explicit snapshot, reverse iteration across same-user-key blocks,
  non-boundary separator seek correctness, rollback from primary

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

Reviewed By: anand1976

Differential Revision: D99494181

Pulled By: xingbowang

fbshipit-source-id: ca52a0d5c0e523770c80e1fe2b9b5d50406b67bc
2026-04-08 21:05:03 -07:00
Xingbo Wang 3070f73e97 Wide-column blob separation: lazy resolution through read, compaction, and write paths (#14386)
Summary:
Wide-column blob separation: lazy resolution through read, compaction, and write paths

Extend blob direct write to support wide-column entities (PutEntity), and add
lazy blob resolution for wide-column values across all read and compaction paths.

**Write path -- PutEntity blob separation:**
- BlobWriteBatchTransformer::PutEntityCF now extracts large column values
  (>= min_blob_size) to blob files and serializes V2 entities with BlobIndex
  references, matching the existing Put behavior.
- Add MaybePreprocessWideColumns() static helper to share blob extraction
  logic between the WriteBatch transformer and the new PutEntity fast path.
- Add PutEntityFastPath() in DBImpl that preprocesses columns (sort, blob
  extract, serialize) before calling WriteImpl, skipping the redundant
  WriteBatch transformation pass. Trace batch preserves the original columns.

**Read path -- blob resolution for Get/MultiGet/Iterator:**
- GetContext::SaveValue resolves V2 entity blob columns eagerly: for
  value (Get), resolves the default column's blob reference; for columns
  (GetEntity), resolves all blob columns and re-serializes as V1.
- DBIter::SetValueAndColumnsFromEntity detects V2 entities, deserializes
  with DeserializeV2, and eagerly resolves all blob columns via a new
  ReadPathBlobResolver. Resolved values are cached in the resolver and
  wide_columns_ Slices point into the cache, avoiding copies.
- Add ReadPathBlobResolver (new file) -- on-demand blob fetcher for the
  read path with per-column caching, used by both DBIter and GetContext.
- BlobFetcher gains allow_write_path_fallback to read from in-flight
  direct-write blob files not yet visible through Version (pre-flush reads).
- Memtable lookups for Get(key) on V2 entities with a blob default column
  now return the blob index with is_blob_index=true, triggering the
  existing BDW resolution in MaybeResolveWritePathValue.
- MaybeResolveWritePathValue (renamed from MaybeResolveDirectWriteBlobIndex)
  now also resolves V2 entity blob columns for GetEntity/MultiGetEntity,
  re-serializing as V1 after resolution.

**Compaction path -- filter, GC, and extraction:**
- CompactionIterator::InvokeFilterIfNeeded handles V2 entities: FilterV3
  gets eagerly-resolved column values for backward compatibility; FilterV4
  gets a CompactionBlobResolver for lazy on-demand resolution.
- Add CompactionFilter::FilterV4 with WideColumnBlobResolver* parameter
  and SupportsFilterV4() opt-in. Default delegates to FilterV3.
- CompactionBlobResolver (new class) implements WideColumnBlobResolver
  for the compaction path with stats tracking.
- ExtractLargeColumnValuesIfNeeded extracts inline columns to blob files
  during compaction (entities without existing blob columns only).
- GarbageCollectEntityBlobsIfNeeded relocates blob values from old blob
  files to new ones during compaction GC, with helpers FetchBlobsNeedingGC,
  RelocateBlobValues, and SerializeEntityAfterGC.
- PrepareOutput unified entity deserialization: single DeserializeV2 call
  reused by both filter and GC/extraction paths via entity_deserialized_
  flag, avoiding redundant parsing.

**Merge path -- V2 entity base value resolution:**
- MergeHelper::MergeUntil, GetContext::MergeWithWideColumnBaseValue, and
  DBIter::MergeWithWideColumnBaseValue resolve V2 blob columns before
  calling TimedFullMerge, using ResolveEntityForMerge.

**Blob garbage accounting:**
- BlobGarbageMeter tracks blob file in/out flow for V2 entity blob
  columns via ForEachBlobFileNumber, used for accurate GC decisions.
- FileMetaData::UpdateBoundaries tracks oldest_blob_file_number for
  V2 entities, ensuring blob files referenced by entities are not
  prematurely deleted.

**Serialization improvements:**
- WideColumnSerialization::SerializeV2Impl allocates serialized_blob_indices
  only for actual blob columns (not all columns) and uses autovector for
  name/value sizes.
- Add ForEachBlobFileNumber for lightweight blob file number extraction
  without full deserialization.
- Add ResolveEntityForMerge helper for merge-path resolution.
- Add section-size validation in DeserializeV2Impl.
- Add empty blob index and column type validation.
- blob_column_resolver_util.h -- shared helpers (FindBlobColumn, FindInCache,
  CacheInlinedBlob) used by both ReadPathBlobResolver and CompactionBlobResolver.

**Testing:**
- db_blob_direct_write_test: end-to-end PutEntity with BDW before/after flush,
  verifying Get, GetEntity, MultiGetEntity, and Iterator.
- db_blob_index_test: ~1550 lines covering V2 entity blob resolution through
  Get, GetEntity, MultiGet, Iterator, compaction filter (V3 compat and V4 lazy),
  merge with blob base, and compaction GC/extraction.
- compaction_iterator_test: ~950 lines testing entity blob GC, extraction,
  filter interaction, and combined GC+filter scenarios.
- db_wide_basic_test: ~1200 lines for wide-column lazy blob resolution through
  all read paths plus compaction round-trips.
- db_open_with_config_test: ~450 lines for BDW entity config validation.

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

Reviewed By: anand1976

Differential Revision: D99739701

Pulled By: xingbowang

fbshipit-source-id: 6badd89b577f3054802eaaa654738468efb9dbdb
2026-04-08 18:58:40 -07:00
Anand Ananthabhotla 9d609dd6fe 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-08 15:25:02 -07:00
Xingbo Wang 4d8a0acd76 Disable read-path range tombstone synthesis when ReadOptions.table_filter is set (#14586)
Summary:
**Root cause:** DBIter's read-path range tombstone conversion assumes the iterator can observe *every* interior live key between point tombstones. When `ReadOptions.table_filter` is active, whole SSTs can be hidden from the scan. DBIter may then mistake non-contiguous deletes for a contiguous run and synthesize a range tombstone that covers real, still-live keys — silently corrupting the read path.

This is not UDT-specific; UDT just changes the failure surface. The stress test failures that triggered investigation were from `db_stress_tool/no_batched_ops_stress.cc` applying an SQFC-backed `table_filter` for range queries even when `total_order_seek` is forced.

## Fix

`db/db_iter.cc`: When initializing `min_tombstones_for_range_conversion_`, set it to 0 (disabling range conversion) whenever `read_options.table_filter` is non-null. This is the conservative and correct behavior — filtered scans don't provide the full key visibility the optimization requires.

## Test

`db/db_iterator_test.cc` — new test `ReadPathRangeTombstoneTest.TableFilterHiddenInteriorKey`:

- Constructs five flushed SSTs so that the live interior key `"b"` lives in a 2-entry SST that the `table_filter` hides.
- Keeps the active memtable non-empty (key `"zz"`) so range conversion has a valid insertion point — without this the test is a false negative.
- Asserts `inserted_ranges_.size() == 0` (no synthesis occurred).
- Verifies `"b"` is still readable via a normal `Get`.
- Runs both with and without UDT (user-defined timestamps) via `SCOPED_TRACE`.

On unpatched HEAD this test fails with `inserted_ranges_.size() == 1`; with the fix it passes.

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

Reviewed By: joshkang97

Differential Revision: D100023487

Pulled By: xingbowang

fbshipit-source-id: 5ef885c89a6cfd7a97b814e38bdcc48d3a1ab349
2026-04-08 10:28:02 -07:00
Josh Kang 3efe9460ba Fix Range Tombstone Entry Accounting Bug (#14579)
Summary:
Crash test T263619547 exposed a flush input accounting bug in
`FragmentedRangeTombstoneList`.

When the constructor sees that the incoming range tombstones are not sorted, it
falls back to rereading all tombstones into `keys`/`values` and sorting them via
`VectorIterator` before fragmentation. However, `num_unfragmented_tombstones_`
was left with the partial count from the aborted first pass. In timestamp
stripping flushes, this stale count could make flush verification report
`Expected X entries in memtables, but read Y` even though all tombstones were
still processed.

Fix the slow path by resetting `num_unfragmented_tombstones_` from
`keys.size()` after the second pass. Add a regression test that feeds unsorted
range tombstones into the fragmenter and verifies the full tombstone count is
preserved.

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

Test Plan: - New unit test that fails without the fix.

Reviewed By: xingbowang

Differential Revision: D99872374

Pulled By: joshkang97

fbshipit-source-id: 38e2bb73c68dc1c677870b8973c93b933ded9038
2026-04-07 16:34:24 -07:00
Xingbo Wang 4dd05023c0 Rocksdb Crash Test failed: unknown (#14584)
Summary: Pull Request resolved: https://github.com/facebook/rocksdb/pull/14584

Reviewed By: mszeszko-meta

Differential Revision: D98721280

fbshipit-source-id: 0a0f0da678312424b43dfab449e636cb1782d9cf
2026-04-07 16:32:33 -07:00
Maciej Szeszko 6a618d0686 Rocksdb Warm Storage Test failed: assertion failed - iters[i]->status().ok() (#14583)
Summary: Pull Request resolved: https://github.com/facebook/rocksdb/pull/14583

Reviewed By: joshkang97, xingbowang

Differential Revision: D99576714

fbshipit-source-id: 71f6cccfc2c5f9043e656e6a81e794d555919fe6
2026-04-07 16:28:02 -07:00
Josh Kang ec832ff42c Minimize WAL and and tracer gap (#14578)
Summary:
https://github.com/facebook/rocksdb/pull/14575 addresses an existing issue where trace record before WAL lead to inconsistent expected state.

However, placing the trace record after the WAL is still problematic because. The trace may not have all the records in the WAL, resulting in `Error restoring historical expected values -> Trace ended before replaying all expected write ops` on recovery.

This change minimizes the gap to reduce stress test failures, but it is still not a full solution. It is however still better than previously writing the trace before the WAL because the error is much more obvious when writing the trace after WAL.

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

Test Plan: Existing CI passes.

Reviewed By: xingbowang

Differential Revision: D99870545

Pulled By: joshkang97

fbshipit-source-id: 024ee3b15e258afd2dd8ff98db7cbe6a01906be7
2026-04-07 14:05:21 -07:00
Josh Kang 91fa765f3a Rocksdb Crash Test failed: assertion failed - corruption: Sequence number is being set backwards dur (#14567) (#14567)
Summary:
Handle the case where a transaction commit with `commit_bypass_memtable` succeeds in writing the commit marker to WAL, but then `IngestWBWIAsMemtable` fails (e.g., WAL creation failure during `SwitchMemtable` or invalid column family). Previously this failure path was not distinguished from a direct WBWI ingest, so the DB could continue operating with committed data durable in WAL but not published to memtables.

This resulted in a higher seqno in the WAL that what the DB's latest seqno was.

Now `IngestWBWIAsMemtable` takes an `ingest_wbwi_for_commit` parameter. When true and the ingest fails (without a prior memtable update), the DB sets a fatal background error, stopping all writes until the DB is closed and reopened for WAL recovery. A close and reopen is required because the auto-recovery mechanism works by flushing memtables to disk, but here the committed data was never published to memtables in the first place — it only exists as prepare + commit records in the WAL. Only a full WAL replay during `DB::Open` can reconstruct the committed transaction data and insert it into memtables.

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

Test Plan:
- Added `CommitBypassMemtableTest.SwitchMemtableFailureStopsDBUntilReopen`:
  - Injects an IO error during `SwitchMemtable` WAL creation via `TEST_SYNC_POINT_CALLBACK`
  - Verifies commit returns `Corruption` status
  - Verifies DB enters fatal background error state and rejects further writes
  - Verifies that after close and reopen, committed data is recovered from WAL and the DB resumes normal operation

Reviewed By: xingbowang

Differential Revision: D98638705

Pulled By: joshkang97

fbshipit-source-id: f116b1f257b19984b0413f6aeba0ba4a7c0a5b25
2026-04-06 19:47:36 -07:00
Peter Dillinger 92cedd58af Fix table cache leak when flush install fails (#14577)
Summary:
When BuildTable succeeds during flush, it caches the output SST file in the table cache for subsequent user reads (builder.cc:460). If the flush then fails to install — e.g., LogAndApply MANIFEST I/O error, CF dropped, or shutdown — the cache entry was never evicted. BuildTable only cleans up its own failures (builder.cc:511), not failures in the install path.

The FindObsoleteFiles full-scan backstop would normally catch this by finding the orphan file on disk and evicting the cache entry. However, under crash test metadata read fault injection
(open_metadata_read_fault_one_in), GetChildren fails and the orphan is never found, causing the TEST_VerifyNoObsoleteFilesCached assertion to fire during Close().

This is the same class of bug previously fixed in the compaction path by https://github.com/facebook/rocksdb/issues/14469 (Run failure) and https://github.com/facebook/rocksdb/issues/14549 (Install failure), but in the flush path which had no analogous cleanup.

Fix: call TableCache::ReleaseObsolete in FlushJob::Run() when the flush fails after BuildTable succeeded (meta_.fd.GetFileSize() > 0).

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

Test Plan:
New unit test DBFlushTest.LeakedTableCacheEntryOnFlushInstallFailure:
- Injects failure after BuildTable via sync point, deactivates filesystem to prevent backstop cleanup
- Without fix: assertion fires ("Leaked table cache entry")
- With fix: passes
```
COMPILE_WITH_ASAN=1 make -j db_flush_test
./db_flush_test --gtest_filter="DBFlushTest.LeakedTableCacheEntry*"
[  PASSED  ] 1 test.
```

Existing compaction leak tests still pass:
```
COMPILE_WITH_ASAN=1 make -j db_compaction_test
./db_compaction_test --gtest_filter="*LeakedTableCacheEntry*"
[  PASSED  ] 2 tests.
```

Reviewed By: xingbowang

Differential Revision: D99692601

Pulled By: pdillinger

fbshipit-source-id: ff5aa1ad165b3abec915844f97f6af9e59d85774
2026-04-06 15:21:58 -07:00
Xingbo Wang 3dd6d060e5 Upgrade CI Docker images: clang-18→clang-21, ubuntu24 24.0→24.1 (#14576)
Summary:
Upgrade the Ubuntu 24 CI Docker image to include clang-21 (from clang-18), and bump all workflow references to the new image tags. Also adds ccache to both images and renames all clang-18 job references to clang-21.

This is mainly for upgrading the clang version later used for generating C API automatically. See PR https://github.com/facebook/rocksdb/issues/14572

### Changes

**`build_tools/ubuntu24_image/Dockerfile`**
- Add clang-21 installation from LLVM snapshot repo (`apt.llvm.org/noble/llvm-toolchain-noble-21`)
- Add ccache
- Add comment pointing Meta employees to internal devvm build guide

**`build_tools/ubuntu22_image/Dockerfile`**
- Add ccache
- Add comment pointing Meta employees to internal devvm build guide

**`.github/workflows/pr-jobs.yml`**
- Rename `build-linux-clang-18-no_test_run` → `build-linux-clang-21-no_test_run`
- Rename `build-linux-clang18-asan-ubsan` → `build-linux-clang21-asan-ubsan`
- Rename `build-linux-clang18-mini-tsan` → `build-linux-clang21-mini-tsan`
- Update all `clang-18`/`clang++-18` references to `clang-21`/`clang++-21`
- Update ccache key prefixes: `clang18-asan-ubsan` → `clang21-asan-ubsan`, `clang18-tsan` → `clang21-tsan`
- Bump `rocksdb_ubuntu:24.0` → `rocksdb_ubuntu:24.1`

**`.github/workflows/nightly.yml`**
- Rename `build-linux-clang-18-asan-ubsan-with-folly` → `build-linux-clang-21-asan-ubsan-with-folly`
- Update clang-18 → clang-21 compiler references
- Bump `rocksdb_ubuntu:24.0` → `rocksdb_ubuntu:24.1`

**`.github/workflows/clang-tidy.yml`**
- Update clang-18 → clang-21
- Bump `rocksdb_ubuntu:24.0` → `rocksdb_ubuntu:24.1`

### New Docker Images

Both images have been built and tested locally. They will be pushed to `ghcr.io/facebook/rocksdb_ubuntu` before this PR is merged.

- `ghcr.io/facebook/rocksdb_ubuntu:22.2` — adds ccache, adds devvm build note
- `ghcr.io/facebook/rocksdb_ubuntu:24.1` — adds clang-21 from LLVM snapshot repo, adds ccache

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

Test Plan: Built both images locally and verified CI job names/compiler flags are consistent.

Reviewed By: joshkang97

Differential Revision: D99694293

Pulled By: xingbowang

fbshipit-source-id: c23c27f5cf870fb2c8b4e3d1cba281d0ce63f9d6
2026-04-06 15:00:53 -07:00
Josh Kang 1b817cff45 Trace writes only after successful write (#14575)
Summary:
Fix trace ordering for `preserve_write_order` mode to only record writes that actually succeeded.

Previously, when `preserve_write_order = true`, trace records were emitted **before** confirming the write succeeded — at a point where the write could still fail (e.g., WAL I/O error). This meant the trace could contain records for writes that never actually committed to the DB. When `db_stress` crash testing replays the trace to reconstruct expected state, these phantom records cause the expected state to diverge from reality, leading to false verification failures.

This fix moves the `preserve_write_order` tracing to happen **after** the write is confirmed successful (after memtable insert, right before `SetLastSequence`), in all three write paths: `WriteImpl`, `PipelinedWriteImpl`, and `WriteImplWALOnly`.
default and pipelined write modes. Replays the trace into a fresh DB and confirms only the successful write is present.

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

Test Plan:
- New unit test: `TracePreserveWriteOrderSkipsFailedWrite` in `db/db_test2.cc`
  - Tests both `enable_pipelined_write = false` and `true`
  - Uses `FaultInjectionTestEnv` to force a write failure
  - Verifies the failed write is absent from both the original DB and the replayed trace

Reviewed By: xingbowang

Differential Revision: D99624672

Pulled By: joshkang97

fbshipit-source-id: 5793727c91cb01a12fbb4b2cd59615802ae3d394
2026-04-06 14:50:51 -07:00
Anand Ananthabhotla fc85a700cf 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-06 12:35:18 -07:00
Anand Ananthabhotla 06699cd0fb Add IODispatcher memory limiting and multiscanrandom benchmark to db_bench (#14570)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14570

Add support for benchmarking MultiScan with IODispatcher memory limiting in db_bench. This includes:

1. **New flags for existing `multiscan` benchmark:**
   - `--io_dispatcher_max_prefetch_memory_bytes`: Sets the global memory budget for IODispatcher prefetching across all ReadSets. When this limit is reached, `SubmitJob()` blocks until memory is released. 0 means unlimited (no IODispatcher created).
   - `--multiscan_max_prefetch_size`: Sets the per-file prefetch size limit (`MultiScanArgs::max_prefetch_size`).

2. **New `multiscanrandom` benchmark with randomized workload:**
   - Random batch sizes (1-64 ranges per MultiScan call)
   - Random range sizes (up to ~1MB worth of keys per range)
   - Generates sorted, non-overlapping ranges each iteration
   - Supports all IODispatcher flags above
   - `--use_multiscan` flag: when true (default), uses MultiScan API; when false, uses normal iterators (Seek + Next) for A/B comparison

## Benchmark Results

Setup: 5M keys, key=16B, value=256B, 5-level LSM (267 SSTs), direct reads, 8MB block cache, 30s duration.

### Normal Iterators vs MultiScan (`multiscanrandom`)

```
| Mode                                    | ops/sec |
|-----------------------------------------|---------|
| Normal iterators (use_multiscan=false)  |       6 |
| MultiScan sync (async=off)              |      15 |
| MultiScan async (async=on)              |      25 |
```

MultiScan sync is **2.5x** faster than normal iterators. MultiScan async is **4.2x** faster.

### MultiScan async=off with Memory Limits (P=4MB peak concurrent memory)

```
| Config          | Limit  | ops/sec | blocked |
|-----------------|--------|---------|---------|
| Baseline (10GB) | unlim  |      15 |       0 |
| Fits budget     | 4MB    |      15 |     246 |
| Exceeds 10%     | 3.6MB  |      14 |     698 |
| Exceeds 50%     | 2.67MB |      14 |   1,314 |
| Exceeds 100%    | 2MB    |      15 |  10,724 |
```

Sync IO is disk-bound so memory limiting has negligible throughput impact, even at 2x oversubscription.

### MultiScan async=on with Memory Limits (P=35MB peak concurrent memory)

```
| Config          | Limit  | ops/sec | blocked |
|-----------------|--------|---------|---------|
| Baseline (10GB) | unlim  |      25 |       0 |
| Fits budget     | 35MB   |      24 | 204,525 |
| Exceeds 10%     | 31.8MB |      19 | 361,444 |
| Exceeds 50%     | 23.3MB |      14 | 289,495 |
| Exceeds 100%    | 17.5MB |      19 | 464,878 |
```

Async IO uses ~9x more peak concurrent memory (35MB vs 4MB) due to multiple in-flight reads. At ~50% oversubscription, async throughput degrades to sync-level, negating the async benefit.

Reviewed By: xingbowang

Differential Revision: D99489650

fbshipit-source-id: e88d3a980f592e4e86628604e6389a1f2f71cfef
2026-04-06 11:59:33 -07:00
Peter Dillinger 0186937710 Skip AllocateTest block-count checks on btrfs (#14553)
Summary:
btrfs accepts fallocate without error but uses copy-on-write, so preallocated extents are not reflected in st_blocks. This caused AllocateTest to fail spuriously on btrfs filesystems. Detect btrfs via statfs and skip only the block-count assertions, keeping the rest of the test (write, flush, close, size checks) intact.

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

Test Plan: env_test AllocateTest passes on btrfs (skips with message) and would still enforce preallocation checks on ext4.

Reviewed By: joshkang97

Differential Revision: D99307962

Pulled By: pdillinger

fbshipit-source-id: 8f40c0109aa397e9ca7d9ee4496fa0614e971970
2026-04-06 11:14:19 -07:00
Peter Dillinger 8ec2177bd1 Fix missing sync point in DeleteScheduler MarkAsTrash failure path (#14556)
Summary:
When MarkAsTrash fails (e.g., rename error due to filesystem conditions), AddFileToDeletionQueue falls back to deleting the file directly via fs_->DeleteFile(). This bypasses DeleteFileImmediately() and its TEST_SYNC_POINT("DeleteScheduler::DeleteFile") callback, causing tests that count deletions via sync points to undercount. This explains flaky failures in DBSSTTest.DeleteSchedulerMultipleDBPaths where bg_delete_file is 4 instead of the expected 8.

Fix by calling DeleteFileImmediately() in the error path instead of fs_->DeleteFile() directly. DeleteFileImmediately already handles the sync point, OnDeleteFile tracking, and FILES_DELETED_IMMEDIATELY stat, so the manual bookkeeping is also removed to avoid duplication.

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

Test Plan:
- Existing tests: db_sst_test (DeleteSchedulerMultipleDBPaths, DestroyDBWithRateLimitedDelete) and delete_scheduler_test all pass.
- Ran DeleteSchedulerMultipleDBPaths 100x with COERCE_CONTEXT_SWITCH=1.

Reviewed By: joshkang97

Differential Revision: D99308177

Pulled By: pdillinger

fbshipit-source-id: 3918c4800c2420ff1ae667beb321da3f27406420
2026-04-06 11:13:20 -07:00
Josh Kang 156328107d Fix range deletion crash test errors (#14573)
Summary:
Fix two bugs in read-path range tombstone conversion (`min_tombstones_for_range_conversion`):

1. **SeekToLast stale saved_key_**: `SeekToLast()` did not clear `saved_key_` before calling `PrevInternal()`. A stale key from a prior `Seek()` would be swapped into `range_tomb_end_key_`, corrupting the tombstone tracking bounds and triggering an assertion failure when `range_tomb_first_key_ > end_key`.

2. **Tombstone inserted at wrong sequence**: The current behavior uses the "max" seqno of the tombstones as the insertion seqno. This is not correct because it does not take into account the seqno range deletions seen throughout the iteration. Unfortunately, range deletions are not visible to the db_iter as the are filtered at the merging iterator level. A future refactor may try to expose this, but currently the simplest solution is to just use the iterator seqno.

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

Test Plan: Unit tests for both bugs that fail without the fix.

Reviewed By: xingbowang

Differential Revision: D99569668

Pulled By: joshkang97

fbshipit-source-id: bb1b154beccced7438831cd5cf2780ad1e11a4cc
2026-04-06 09:05:42 -07:00
Josh Kang 3f51c0a185 Convert sequential single deletes into range tombstones (#14448)
Summary:
Add a read-path optimization that converts contiguous point tombstones into range tombstones during forward/reverse iteration. When a configurable threshold of consecutive point deletions (kTypeDeletion, kTypeDeletionWithTimestamp, kTypeSingleDeletion — with no live keys between them) is detected, a range tombstone covering `[first_tombstone_key, next_live_key)` is inserted into the active mutable memtable. This benefits future iterators by enabling efficient skipping via range tombstone fragmentation.

If there is a memtable switch during the read iteration, then the range deletion entry is discarded.

The inserted range tombstones are logically redundant (they don't delete anything that isn't already deleted by point tombstones), skip WAL (they're a derived optimization regenerated by future reads on crash), and use the max tombstone sequence number so they don't interfere with newer writes.

## Key changes
- **New option `min_tombstones_for_range_conversion`** (`AdvancedColumnFamilyOptions`): Threshold of contiguous point tombstones before converting to a range tombstone. Default 0 (disabled). Dynamically changeable via `SetOptions()`.
- **`DBIter` tracking logic** (`db/db_iter.cc`): Tracks contiguous tombstones during `FindNextUserEntryInternal()` (forward) and `PrevInternal()` (reverse). When a live key terminates a run that meets the threshold, `MaybeInsertRangeTombstone()` inserts `[first_tombstone, live_key)` into the active memtable.
- **`FindValueForCurrentKey` `found_visible` output** (`db/db_iter.cc`): Distinguishes "key deleted at this snapshot" from "no visible entries" so reverse tracking doesn't treat post-snapshot keys as tombstones.
- **`IterKey::Swap()`** (`db/dbformat.h`): Efficiently tracks reverse tombstone run end keys without extra allocations.
- **`MemTable::AddLogicallyRedundantRangeTombstone()`** (`db/memtable.cc`): Concurrent-safe range tombstone insertion into the active memtable. Range tombstone skiplist always uses concurrent inserts.
- **`ConstructFragmentedRangeTombstones` race fix** (`db/db_impl/db_impl_write.cc`): Moved after `MarkImmutable()` to prevent lost entries.
- **`MarkImmutable` ordering fix** (`db/memtable_list.cc`): Called before `current_->Add()` to close a race window.
- **Prefix filter awareness** (`db/db_iter.cc`): Tombstone tracking scoped to the seek prefix when prefix filtering is active. See dedicated section below.
- **Transaction awareness** (`db/db_iter.cc`): Tombstones with `seq > snapshot` excluded from tracking. `min_uncommitted` guard uses `insert_seq` (which may be bumped to `earliest_seq`) instead of `range_tomb_max_seq_`. See dedicated section below.
- **Duplicate range check**: Skips insertion if the memtable already covers `[start, end)`.
- **New statistics**: `READ_PATH_RANGE_TOMBSTONES_INSERTED` and `READ_PATH_RANGE_TOMBSTONES_DISCARDED`.
- **Memtable MultiGet batch lookup** (`memtable/inlineskiplist.h`, `db/memtable.cc`): `InlineSkipList::MultiGet()` with cached search path ("finger") for sorted key lookups.
- **New option `memtable_batch_lookup_optimization`** (`AdvancedColumnFamilyOptions`): Enables batch lookup for memtable MultiGet. Default false. Immutable.

## Deciding Range Tombstone Seqno
- The range tombstone is inserted with `insert_seq = max(range_tomb_max_seq_, earliest_seq)` where `range_tomb_max_seq_` is the maximum sequence number across all point tombstones in the contiguous run, and `earliest_seq` is the memtable's earliest sequence number. This preserves the memtable's `earliest_seqno_` invariant.
- If the iterator's snapshot sequence (`sequence_`) predates the memtable's `earliest_seq`, insertion is skipped entirely to avoid unintentionally covering entries between `sequence_` and `earliest_seq`.

## ConstructFragmentedRangeTombstones Race Fix
- `MarkImmutable()` and `ConstructFragmentedRangeTombstones()` are now called before `mutex_.Lock()` in `SwitchMemtable`, keeping this work outside the DB mutex. `MarkImmutable()` blocks concurrent `AddLogicallyRedundantRangeTombstone()` calls via `immutable_mutex_`, ensuring no range tombstones are inserted after the fragmented list is built. `MarkImmutable()` is idempotent, so `MemTableList::Add()` calling it again inside the mutex is harmless.

## Prefix Filter Safety
- When prefix filtering is active, the BBTI bloom filter may reject SST files outside the seek prefix, but the memtable (no bloom filter) returns keys across prefix boundaries. Tombstone tracking is scoped to the seek prefix so that converted range tombstones cannot cover live keys hidden in filtered files.
- `total_order_seek=true` disables prefix filtering — all files are visible, so tombstones safely span prefix boundaries.

- **Behavior change**: Seeking to an out-of-domain key with `total_order_seek=false` now treats it as total-order (prefix_ not set). When `prefix_same_as_start=true`, iterating past an out-of-domain key cleanly invalidates the iterator instead of calling `Transform()` on it (which was UB in release builds with `FixedPrefixTransform`). This is a requirement because an incorrect iterator scan could lead to a range tombstone covering a live key.

## Transaction Support
- Tombstones written by the transaction's own uncommitted writes (sequence > snapshot) are now excluded from contiguous tombstone tracking entirely at the tracking site in `FindNextUserEntryInternal()` and `PrevInternal()`. Previously, tracking relied on a `min_uncommitted` check at insertion time, but this was insufficient — a transaction's own Delete with `seq > snapshot` could extend a run of committed tombstones, and the resulting range tombstone would cover data visible to other snapshots.
- The fix skips any tombstone with `ikey_.sequence > sequence_` during tracking. If a transaction-owned tombstone appears mid-run, it flushes the accumulated committed run first, then resets tracking. This ensures only tombstones visible to the current snapshot are ever converted.
- Both WritePrepared and WriteUnprepared transactions are supported with dedicated test coverage:
  - **WritePrepared**: When tombstones are committed before `Prepare()`, their seqnos are below `min_uncommitted` and insertion proceeds safely. When `Prepare()` happens first, tombstone seqnos exceed `min_uncommitted` and insertion is blocked.
  - **WriteUnprepared**: Multiple unprepared batches with different seqno ranges are handled correctly. Own transaction Deletes that extend a committed tombstone run block insertion of the entire run. After rollback, data correctness is verified.

## UDT support
- When user-defined timestamps (UDT) are enabled, keys include an 8-byte timestamp suffix. The comparator, Put/Delete APIs, and ReadOptions all require timestamps.
- Forward exhaustion with UDT: `iterate_upper_bound_` is a plain user key without a timestamp suffix. It is padded with min timestamp via `AppendKeyWithMinTimestamp()` so it sorts after all entries with this user key, preserving the exclusive bound semantics.
- Reverse exhaustion with UDT: The end key comes from either the previous live key (which already has a proper timestamp suffix) or the seek target set by `SetSavedKeyToSeekForPrevTarget()` (which appends a timestamp via `SetInternalKey(..., timestamp_ub_)` and `UpdateInternalKey(..., ts)`). In both cases, the end key is already properly timestamped, so no additional padding is needed unlike forward exhaustion.
- Contiguous tombstone detection works correctly with UDT because the underlying `kTypeDeletionWithTimestamp` entries are tracked the same way as `kTypeDeletion`.

## Concurrent Iterators
- Should concurrent iterators happen to read the range at the same time, both will produce the same range and seqno entry. Only one will be accepted by the skip list and the others will be rejected. Future iterators will read the range and not even attempt to insert the range.
- There is nothing preventing similar ranges from being inserted however. Two iterators can produce overlapping ranges, but this protection would be complicated to implement and there is no evidence that it is a likely scenario yet.

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

Test Plan:
- **Unit tests** (`db/db_iterator_test.cc`): `ReadPathRangeTombstoneTest` parameterized by forward/reverse with cases for basic insertion, non-contiguous (below threshold), memtable switch, exhausted iterator with/without bounds, direction change, mixed Delete/SingleDelete, single-delete-only runs, snapshot predating memtable, block cache tier incomplete, skip when covered by existing range, UDT basic scan, UDT exhaustion, prefix filter cross-prefix scan (`PrefixFilterCrossPrefixScanCoversLiveKey` with default/total_order_seek/prefix_same_as_start variants), stale ikey from forward-then-reverse scan (`StaleIkeyFromForwardThenReverse`), and reseek stale ikey (`ReseekStaleIkey`).
- **Concurrency test** (`db/db_test2.cc`): `DBTestConcurrentRangeTombstoneConversions` parameterized by `(allow_concurrent_memtable_write, min_tombstones_for_range_conversion)` with mixed writers, deleters, range deleters, and concurrent forward/reverse readers.
- **Transaction tests** (`utilities/transactions/write_prepared_transaction_test.cc`, `write_unprepared_transaction_test.cc`): Tests for WritePrepared (insertion allowed when tombstones committed before prepare, blocked when after; seqno bump shadowing prepared writes `RangeTombstoneSeqnoBumpShadowsPreparedWrite`) and WriteUnprepared (multiple batches, extended visibility with CalcMaxVisibleSeq, own deletions with rollback).
- **IterKey::Swap tests** (`db/dbformat_test.cc`): `IterKeySwapTest` parameterized over `(key_len, copy, use_secondary)` × 2 covering all inline/heap/pinned/secondary combinations.
- **InlineSkipList MultiGet tests** (`memtable/inlineskiplist_test.cc`): Basic, exact matches, empty, single key, randomized validation against `std::set::lower_bound`, duplicate keys with callback walk, and concurrent MultiGet with read-after-write consistency.
- **Memtable MultiGet tests** (`db/db_basic_test.cc`): Batch lookup, overwrite, flush, merge, disabled by default, paranoid checks, and snapshot tests.
- **Stress test coverage**: `min_tombstones_for_range_conversion` and `memtable_batch_lookup_optimization` options added to `db_crashtest.py` and `db_stress` flags.
- `make check` passes all tests.

### Benchmark results

Tombstones scattered randomly in clusters via `seek_nexts_to_delete` for realistic workloads.

**DB Setup A (scattered deletes, 100 per seek)**:
```
# Step 1: Create and compact
./db_bench --benchmarks=fillseq,compact --seed=1 --compression_type=none --num=1000000 --db=<DB>
# Step 2: Scatter tombstones (5000 seeks × 100 deletes ≈ 500k tombstones)
./db_bench --benchmarks=seekrandom,flush --seed=1 --compression_type=none --num=2000 \
  --seek_nexts=0 --seek_nexts_to_delete=100 --use_existing_db=1 --threads=1 --db=<DB>
```

**DB Setup A2 (scattered deletes, 8 per seek)**:
```
# Step 1: Create and compact
./db_bench --benchmarks=fillseq,compact --seed=1 --compression_type=none --num=1000000 --db=<DB>
# Step 2: Scatter tombstones (5000 seeks × 8 deletes ≈ 40k tombstones)
./db_bench --benchmarks=seekrandom,flush --seed=1 --compression_type=none --num=2000 \
  --seek_nexts=0 --seek_nexts_to_delete=8 --use_existing_db=1 --threads=1 --db=<DB>
```

**DB Setup B (no deletes)**:
```
./db_bench --benchmarks=fillseq,compact --seed=1 --compression_type=none --num=1000000 \
  [--key_size=100] --db=<DB>
```

**Read workload** (same for all):
```
./db_bench --benchmarks=seekrandom --seek_nexts=100 --threads=8 \
  --reverse_iterator={true,false} --seed=1 --use_existing_db=1 \
  --compression_type=none --num=1000000 --duration=10 \
  --disable_auto_compactions \
  [--key_size=100] [--min_tombstones_for_range_conversion=X] --db=<DB_COPY>
```

Each workload averaged over 3 runs.

**Table 1: seekrandom forward, scattered deletes (2000 seeks × 100 deletes/seek)**

| Variant | avg ops/s | % vs main |
|---------|-----------|-----------|
| main | 2,895 | - |
| threshold=0 | 2,869 | -0.9% |
| threshold=8 | 287,334 | +9,824% |

**Table 2: seekrandom reverse, scattered deletes (2000 seeks × 100 deletes/seek)**

| Variant | avg ops/s | % vs main |
|---------|-----------|-----------|
| main | 544 | - |
| threshold=0 | 548 | +0.7% |
| threshold=8 | 206,491 | +37,860% |

**Table 3: seekrandom forward, scattered deletes (2000 seeks × 8 deletes/seek)**

| Variant | avg ops/s | % vs main |
|---------|-----------|-----------|
| main | 194,049 | - |
| threshold=0 | 195,703 | +0.9% |
| threshold=8 | 310,740 | +60.1% |

**Table 4: seekrandom reverse, scattered deletes (2000 seeks × 8 deletes/seek)**

| Variant | avg ops/s | % vs main |
|---------|-----------|-----------|
| main | 63,854 | - |
| threshold=0 | 69,266 | +8.5% |
| threshold=8 | 218,101 | +241.6% |

**Table 5: seekrandom forward, no deletes (regression check)**

| Variant | key=16B avg ops/s | % vs main | key=100B avg ops/s | % vs main |
|---------|-------------------|-----------|---------------------|-----------|
| main | 330,901 | - | 236,048 | - |
| threshold=0 | 328,398 | -0.8% | 238,055 | +0.9% |
| threshold=8 | 332,539 | +0.5% | 233,776 | -1.0% |

**Table 6: seekrandom reverse, no deletes (regression check)**

| Variant | key=16B avg ops/s | % vs main | key=100B avg ops/s | % vs main |
|---------|-------------------|-----------|---------------------|-----------|
| main | 261,445 | - | 192,177 | - |
| threshold=0 | 265,020 | +1.4% | 191,616 | -0.3% |
| threshold=8 | 250,881 | -4.0% | 189,239 | -1.5% |

Reviewed By: xingbowang

Differential Revision: D96203950

Pulled By: joshkang97

fbshipit-source-id: 06ba66ebde3c355f04671d1e681f1b1586e8751d
2026-04-03 22:47:51 -07:00
Jialiang Tan 65e77282ce 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-03 16:48:09 -07:00
Jialiang Tan a0ff5b143f Add num_data_blocks_compression_rejected/bypassed to TableProperties (#14551)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14551

Add two new per-SST TableProperties fields that track how many data blocks
were stored uncompressed:

- `num_data_blocks_compression_rejected`: blocks where compression was
  attempted but the compressed output exceeded the ratio limit set by
  `CompressionOptions::max_compressed_bytes_per_kb`.
- `num_data_blocks_compression_bypassed`: blocks where compression was
  never attempted (e.g., kNoCompression type, no compressor available).

Together with `num_data_blocks`, these give a full decomposition:
  num_data_blocks = compressed + rejected + bypassed

Previously this information was only available via global Statistics tickers
(NUMBER_BLOCK_COMPRESSION_REJECTED / BYPASSED) which aggregate across all
SSTs. Now it is persisted per-SST in the properties block and visible via
sst_dump --show_properties.

Reviewed By: anand1976

Differential Revision: D99229339

fbshipit-source-id: 8333f51acff30a759d725e5e94bd80f4fcb18b57
2026-04-03 16:48:09 -07:00
Rajat Jain 64f8d5765b Add blob_compression_opts to ColumnFamilyOptions (#14568)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14568

Port of D98326754 to internal_repo_rocksdb (GitHub-synced repo).

BlobDB blob file compression currently uses hardcoded default
CompressionOptions{} in BlobFileBuilder, ignoring any user-specified
compression options like level, window_bits, or strategy. This means
ZSTD always uses level 3 (doubleFast strategy with two hash tables),
even when a lower level would significantly reduce CPU usage.

This diff adds a new `blob_compression_opts` field to
AdvancedColumnFamilyOptions (and MutableCFOptions) and plumbs it into
BlobFileBuilder::GetCompressor(). The option is registered as mutable
and dynamically changeable via SetOptions().

For example, setting level=1 with ZSTD switches from "doubleFast"
(two hash tables per thread) to "fast" (single hash table), reducing
L3 cache pressure on flush-heavy workloads.

There was an existing TODO in blob_file_builder.cc requesting exactly
this feature — this diff fulfills it.

This change is safe as the default value is always dFast. So this will not impact the current code.

Reviewed By: xingbowang

Differential Revision: D99478562

fbshipit-source-id: e79b847a854f60dea552442b18fd3f1384efac70
2026-04-03 16:42:16 -07:00
Xingbo Wang 4cf28450a5 Add customizable blob partition strategy for blob direct write (#14565)
Summary:
This PR adds a `BlobFilePartitionStrategy` interface that allows users to plug in custom partition selection logic for blob direct writes. The default behavior (round-robin across partitions) is preserved as the built-in `RoundRobinBlobFilePartitionStrategy`.

### Motivation

The existing blob direct write implementation distributes writes across partitions using a fixed round-robin strategy (atomic counter mod num_partitions). While this works well for uniform workloads, some use cases benefit from custom routing:

- **Key-based routing**: co-locate related keys in the same blob partition for locality-aware reads.
- **CF-based routing**: direct writes for different column families to dedicated partitions.
- **Value-size routing**: send large vs. small values to different partitions.
- **Application-defined affinity**: any domain-specific grouping that the default round-robin cannot express.

### Design

A new pure-virtual interface `BlobFilePartitionStrategy` is added to `include/rocksdb/advanced_options.h`:

```cpp
class BlobFilePartitionStrategy {
 public:
  virtual ~BlobFilePartitionStrategy() = default;

  // Select a partition for the given blob direct write.
  // The return value can be any uint32_t; the caller applies
  // modulo num_partitions internally.
  // Implementations must be thread-safe.
  virtual uint32_t SelectPartition(uint32_t num_partitions,
                                   uint32_t column_family_id,
                                   const Slice& key,
                                   const Slice& value) = 0;
};
```

The strategy receives:
- `num_partitions`: the configured partition count (for implementations that want to be partition-count-aware).
- `column_family_id`: useful for CF-based routing.
- `key` / `value`: the key and value being written.

The return value is taken modulo `num_partitions` internally, so implementations can return any `uint32_t` without worrying about bounds.

A new column family option `blob_direct_write_partition_strategy` (type `std::shared_ptr<BlobFilePartitionStrategy>`, default `nullptr`) wires the strategy into `BlobFilePartitionManager`. When `nullptr`, the built-in `RoundRobinBlobFilePartitionStrategy` is used, preserving existing behavior exactly.

### Changes

| File | Change |
|---|---|
| `include/rocksdb/advanced_options.h` | New `BlobFilePartitionStrategy` interface; new `blob_direct_write_partition_strategy` option |
| `db/blob/blob_file_partition_manager.h/.cc` | Accept strategy in constructor; replace atomic counter with strategy call; move `RoundRobinBlobFilePartitionStrategy` here as default |
| `options/cf_options.h/.cc` | Thread strategy through `ImmutableCFOptions` |
| `options/options_helper.cc` | Propagate strategy in `UpdateColumnFamilyOptions` |
| `options/options_settable_test.cc` | Register new option field in options settability test |
| `db/db_impl/db_impl.cc` | Pass strategy when constructing `BlobFilePartitionManager` |
| `db/blob/db_blob_direct_write_test.cc` | New test with `FixedBlobDirectWritePartitionStrategy` |

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

Test Plan:
New test `DirectWriteCustomPartitionStrategyRoutesWritesToOneBlobFile` verifies that a `FixedBlobDirectWritePartitionStrategy` that always returns partition 3 correctly routes all writes to a single blob file across 4 configured partitions.

```
make -j128 db_blob_direct_write_test && ./db_blob_direct_write_test --gtest_filter='*CustomPartition*'
make -j128 db_blob_direct_write_test && ./db_blob_direct_write_test
```

Reviewed By: joshkang97

Differential Revision: D99458813

Pulled By: xingbowang

fbshipit-source-id: 54cfbb0f75e24b58a8db61ad8755204d0db6ece7
2026-04-03 15:27:57 -07:00
Peter Dillinger ae0c9faaef Fix transaction lock timeout in crash test (#14540)
Summary:
When BatchedOpsStressTest runs on a TransactionDB, db_->Write() creates internal transactions using default_lock_timeout (1 second), which is too short under heavy contention with many threads. This causes sporadic "Timeout waiting to lock key" errors printed to stderr, which the crash test framework treats as fatal failures.

Increase default_lock_timeout to 600000ms (10 minutes) to match the lock_timeout already used for explicit transactions in NewTxn().

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

Test Plan: Manual `make crash_test_with_wc_txn` as a sanity check

Reviewed By: mszeszko-meta

Differential Revision: D98944474

Pulled By: pdillinger

fbshipit-source-id: 13b9e13fddc6332da010b01f7c41a51748f75624
2026-04-03 12:10:18 -07:00
Omkar Gawde 51561fc2cd Add explicit Close() call in WriteStringToFile (#14566)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14566

**What**: Adds an explicit `file->Close()` call in `WriteStringToFile()` before
the function returns, instead of relying on the `unique_ptr` destructor to close
the file.

**How**: In `file_system.cc`, after the optional `Sync()` and before the
error-cleanup path, calls `file->Close()`. If `Close()` fails, the file is
deleted just like any other write failure. Adds two unit tests in `env_test.cc`:
- `WriteStringToFileClosesFile`: uses `CountedFileSystem` to verify `Close()` is
  called and content is written correctly.
- `WriteStringToFileCloseFailureDeletesFile`: uses a custom `CloseFailFS`
  wrapper to inject a `Close()` failure and verify the file is deleted on error.

**Why**: The previous implementation never called `Close()` explicitly — it
relied on the `unique_ptr` destructor which may silently swallow write errors
that occur during close (e.g., flushing buffered data). Explicit `Close()`
ensures such errors are detected and propagated to the caller.

Reviewed By: archang19

Differential Revision: D99462173

fbshipit-source-id: 525b5489bd0dbfc3159b007a13f9474f84d3c84e
2026-04-03 12:10:15 -07:00
Xingbo Wang ebd118711c Temporarily disable trie UDI index in stress test (#14559)
Summary:
**Summary:**
Disable `use_trie_index` in `db_crashtest.py` to avoid trie UDI stress test failures.

The default param was previously set to randomly enable trie index ~12.5% of the time (`random.choice([0, 0, 0, 0, 0, 0, 0, 1])`). Setting it to `0` until the underlying issues are resolved.

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

Test Plan: Stress test runs without trie UDI failures.

Reviewed By: hx235

Differential Revision: D99343747

Pulled By: xingbowang

fbshipit-source-id: 6f86b5dd5ddb2869d797e93ddbda521981614bae
2026-04-02 15:01:36 -07:00
Hui Xiao ffe58fa976 Fix NULL persisted_seqno_ in AnonExpectedState (#14523) (#14523)
Summary:
AnonExpectedState::Open() never initialized the base class
persisted_seqno_ pointer, leaving it as nullptr. Any call to
SetPersistedSeqno() or GetPersistedSeqno() on an AnonExpectedState
(used when expected_values_dir is empty) would dereference a null
pointer. Fix by allocating and assigning it in Open(), matching
FileExpectedState's pattern.

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

Reviewed By: mszeszko-meta

Differential Revision: D98556119

Pulled By: hx235

fbshipit-source-id: c4c3103272735d36c1d05b96f7d009b6b750bf53
2026-04-02 13:30:10 -07:00
Xingbo Wang d6b03d9c96 Add pre-push git hook to auto-format code (#14558)
Summary:
Add a git pre-push hook that automatically runs `make format-auto` before every push. If formatting issues are found, the hook fixes them, creates a new commit, and retries the push — all in one shot.

The hook is auto-configured on the first `make` build via `core.hooksPath`, so there is no manual install step needed.

## Changes

**`githooks/pre-push`** — pre-push hook that:
1. Runs `make check-format` to detect formatting issues
2. If issues found, runs `make format-auto` to fix them
3. Creates a new "format" commit with the fixes
4. Retries the push automatically with the format commit included
5. Stashes/restores any uncommitted work so it is not affected
6. Skips in non-interactive environments (CI) unless `ROCKSDB_FORMAT_HOOK` is set
7. Can be skipped with `git push --no-verify`

**`Makefile`**:
- `setup-hooks` target: auto-sets `core.hooksPath=githooks` in the local repo config. Runs as a dependency of `all`, so any `make` build activates the hook with zero manual setup.
- `install-hooks` / `uninstall-hooks` targets: manual copy-based alternative for those who prefer it

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

Reviewed By: archang19

Differential Revision: D99324330

Pulled By: xingbowang

fbshipit-source-id: b824e56d572ad423fab7de7e15ead5fa8fd8e847
2026-04-02 12:34:19 -07:00
Peter Dillinger abc1f7ced5 Fix table cache leak when InstallCompactionResults fails (#14549)
Summary:
When CompactionJob::Run() succeeds but Install() fails (e.g., LogAndApply MANIFEST I/O error), compact_->status was never updated with the install failure. CleanupCompaction() passed the stale OK status to SubcompactionState::Cleanup(), which skipped ReleaseObsolete -- leaking table cache entries for output files that were cached by VerifyOutputFiles but never installed into any Version.

This is the same class of bug fixed in https://github.com/facebook/rocksdb/issues/14469 (where Run() failed after VerifyOutputFiles), but in the Install() failure path. The FindObsoleteFiles full-scan backstop would normally catch this, but fails under crash test metadata read fault injection
(--open_metadata_read_fault_one_in), causing the
TEST_VerifyNoObsoleteFilesCached assertion to fire during Close().

Fix: propagate Install()'s local status back to compact_->status before CleanupCompaction(), so Cleanup() sees the failure and calls ReleaseObsolete on the output files.

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

Test Plan:
New unit test DBCompactionTest.LeakedTableCacheEntryOnInstallFailure:
- Without fix (ASAN): assertion fires -- "File 12 is not live nor quarantined"
- With fix (ASAN): passes -- ReleaseObsolete properly cleans up the entry
```
COMPILE_WITH_ASAN=1 make -j db_compaction_test
./db_compaction_test --gtest_filter="DBCompactionTest.LeakedTableCacheEntry*"
[  PASSED  ] 2 tests.
```

Reviewed By: joshkang97

Differential Revision: D99155908

Pulled By: pdillinger

fbshipit-source-id: ed5374a38d7903866a38a0fe0f5539e12321bc84
2026-04-02 10:39:22 -07:00
Xingbo Wang 01ec15f328 Remove accidentally committed .claude/settings.local.json (#14554)
Summary:
This file was accidentally included in https://github.com/facebook/rocksdb/issues/14535. It contains local Claude Code permission settings that are user-specific and should not be in the repo.

Also adds it to `.gitignore` to prevent future accidental commits.

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

Reviewed By: pdillinger

Differential Revision: D99296922

Pulled By: xingbowang

fbshipit-source-id: e229dd85179ae25bb15df1633bba7197dd2fa1f9
2026-04-02 09:49:27 -07:00
Xingbo Wang af4e32945b Blob direct write v1: write-path blob separation with partitioned files (reduced scope) (#14535)
Summary:
This PR introduces **blob direct write v1**, a reduced-scope write-path optimization where large values (>= `min_blob_size`) are written directly to blob files during `Put()` and replaced in the memtable with compact `BlobIndex` references. This avoids holding full values in memory until flush time.

### Motivation

In the existing BlobDB architecture, values are written to the WAL and memtable in their full form and separated into blob files only at flush time. This means:
- Large values are held in memory twice (raw in memtable + blob file at flush)
- Blob I/O is serialized through a single flush thread per column family

Blob direct write addresses both: values leave the write path as small `BlobIndex` references, and multiple **partitions** (configurable via `blob_direct_write_partitions`) allow concurrent blob writes with independent locks.

### Design (v1 — single-writer, WAL-disabled, reduced scope)

The v1 design intentionally keeps scope narrow for correctness and reviewability:

- **Single writer thread assumption**: no concurrent writes to the same partition file. One logical writer serializes the batch.
- **WAL-disabled**: direct-write blob files are only registered in MANIFEST at flush time. WAL replay cannot recover unregistered blob references, so WAL is disabled for this v1.
- **Flush-on-write**: each `AddRecord` call flushes to the OS immediately.
- **FIFO generation batching**: each memtable switch creates one generation batch. Direct-write files for that memtable are sealed and registered atomically when the batch is flushed to MANIFEST.
- **Round-robin partitions**: blob writes are distributed across `blob_direct_write_partitions` files using an atomic counter.

### New components

| Component | Description |
|---|---|
| `BlobFilePartitionManager` | Owns N partition files per CF. Manages open/seal/register lifecycle tied to memtable generations. |
| `BlobWriteBatchTransformer` | A `WriteBatch::Handler` that rewrites qualifying `Put` values as `BlobIndex` entries before the batch enters the write group. |

### Write path integration

1. `DBImpl::WriteImpl` calls `BlobWriteBatchTransformer::TransformBatch` before entering the writer group (for default write path), or before joining the batch group (for pipelined/unordered write).
2. Values >= `min_blob_size` are written to a partition file; the key is stored with a `BlobIndex` in the transformed batch. A rollback guard marks blob bytes as initial garbage if the write fails.
3. On `SwitchMemtable`, `RotateCurrentGeneration` moves active partitions into the next immutable batch.
4. `FlushMemTableToOutputFile` / `AtomicFlushMemTablesToOutputFiles` call `PrepareFlushAdditions` to seal partition files and collect `BlobFileAddition` + `BlobFileGarbage` entries registered to MANIFEST alongside the flush.
5. Shutdown paths (`CancelAllBackgroundWork`, `WaitForCompact` with `close_db=true`) force-flush all CFs with active direct-write managers to ensure blob files are registered before close.

### Read path

- **Get/MultiGet**: `MaybeResolveBlobForWritePath` resolves `BlobIndex` references found in memtable or immutable memtable via `BlobFilePartitionManager::ResolveBlobDirectWriteIndex`, which first checks manifest-visible state and falls back to direct blob-file reads via `BlobFileCache`.
- **Iterator**: `DBIter::BlobReader` is extended with a `BlobFilePartitionManager*` to resolve direct-write blob indexes during iteration. The unified `ResolveBlobDirectWriteIndex` path handles both manifest-visible and not-yet-flushed files.

### New options

| Option | Default | Description |
|---|---|---|
| `enable_blob_direct_write` | `false` | Enable write-path blob separation for this CF. Requires `enable_blob_files = true`. Not dynamically changeable. |
| `blob_direct_write_partitions` | `1` | Number of parallel partition files per CF. Not dynamically changeable. |

### Feature incompatibilities (reduced v1 scope)

The following features are *not supported* when `enable_blob_direct_write = true`, and are enforced both in `db_stress_tool` validation and `db_crashtest.py` sanitization:

**Write model constraints:**
- `threads` must be 1 (single writer assumption)
- `allow_concurrent_memtable_write` = 0
- `enable_pipelined_write` = 0 (transformation done before batch group, but pipelined path supported with pre-transform)
- `two_write_queues` = 0
- `unordered_write` = 0 (transformation done before batch group, but unordered path supported with pre-transform)

**WAL and recovery:**
- `disable_wal` = 1 (required — WAL replay of unregistered blob files is out of v1 scope)
- `best_efforts_recovery` = 0
- `reopen` = 0 (no crash-restart with WAL replay)
- All WAL-related stress features disabled: `manual_wal_flush_one_in`, `sync_wal_one_in`, `lock_wal_one_in`, `get_sorted_wal_files_one_in`, `get_current_wal_file_one_in`, `track_and_verify_wals`, `rate_limit_auto_wal_flush`, `recycle_log_file_num`

**Blob GC and dynamic options:**
- `use_blob_db` = 0 (stacked BlobDB not supported)
- `allow_setting_blob_options_dynamically` = 0
- `enable_blob_garbage_collection` = 0
- `blob_compaction_readahead_size` = 0
- `blob_file_starting_level` = 0

**Unsupported value types and APIs:**
- Merge (`use_merge`, `use_full_merge_v1`) — merge values pass through untransformed
- Entity APIs (`use_put_entity_one_in`, `use_get_entity`, `use_multi_get_entity`, `use_attribute_group`)
- `use_timed_put_one_in`
- User-defined timestamps (`user_timestamp_size`, `persist_user_defined_timestamps`, `create_timestamped_snapshot_one_in`)
- Transactions (`use_txn`, `use_optimistic_txn`, `test_multi_ops_txns`, `commit_bypass_memtable_one_in`) — though `WriteCommittedTxn::CommitInternal` falls back from bypass-memtable to normal path when BDW is active
- `IngestWriteBatchWithIndex` returns `NotSupported`
- `inplace_update_support` = 0

**Fault injection:**
- All write/read/metadata fault injection disabled (`sync_fault_injection`, `write_fault_one_in`, `metadata_write_fault_one_in`, `read_fault_one_in`, `metadata_read_fault_one_in`, `open_*_fault_one_in`)

**Infrastructure/snapshot APIs:**
- `remote_compaction_worker_threads` = 0
- `test_secondary` = 0
- `backup_one_in` = 0
- `checkpoint_one_in` = 0
- `get_live_files_apis_one_in` = 0
- `ingest_external_file_one_in` = 0
- `ingest_wbwi_one_in` = 0

### Tests

- `db/blob/db_blob_basic_test.cc`: ~660 lines of new direct-write unit tests covering basic put/get, multi-partition, flush/compaction, recovery, and error injection.
- `db/blob/blob_file_cache_test.cc`: ~96 lines of new tests for direct-write blob file cache behavior.
- `db/write_batch_test.cc`: ~96 lines of tests for WriteBatch with blob index entries.
- `utilities/transactions/transaction_test.cc`: verifies transaction commit path falls back correctly with direct write enabled.
- `db_stress_tool/`: full stress test support with `--enable_blob_direct_write` and `--blob_direct_write_partitions` flags, integrated into `db_crashtest.py` with 10% random selection alongside regular blob params.

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

Test Plan:
```
make -j128 db_blob_basic_test && ./db_blob_basic_test
make -j128 blob_file_cache_test && ./blob_file_cache_test
make -j128 write_batch_test && ./write_batch_test
make -j128 transaction_test && ./transaction_test
make -j128 check
```

Stress test:
```
python3 tools/db_crashtest.py blackbox --enable_blob_direct_write=1 \
  --enable_blob_files=1 --blob_direct_write_partitions=4 \
  --disable_wal=1 --threads=1
```

Reviewed By: pdillinger

Differential Revision: D98766843

Pulled By: xingbowang

fbshipit-source-id: 1577653826913a59d05680a87bce5534ac5a5e69
2026-04-02 07:31:56 -07:00
Hemal Shah b611aa7309 Export header to add log data on a transaction (#14543)
Summary:
Transactions in rockdb supported adding log data using the `put_log_data` api, but this was not exported for other language bindings. Exported this binding allows other languages like rust, go, etc add log data on an transaction

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

Reviewed By: joshkang97

Differential Revision: D99171663

Pulled By: xingbowang

fbshipit-source-id: 24c94d71668a41cc7b2913972427255ab67d0863
2026-04-01 16:19:11 -07:00
Andrew Chang 830347c765 Fix null sqe crash in ReadAsync when io_uring submission queue is full (#14521)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14521

ReadAsync calls io_uring_get_sqe() without checking for nullptr. When the
io_uring submission queue is full (outstanding completions not yet reaped),
io_uring_get_sqe returns NULL and the subsequent io_uring_prep_readv
dereferences it, causing a segfault.

MultiRead already handles this correctly by using io_uring_sq_space_left()
to cap submissions. ReadAsync submits exactly one SQE per call so a simple
null check with error return is sufficient.

On null sqe, clean up the already-allocated Posix_IOHandle and return
IOStatus::Busy so the caller can retry after reaping completions.

The io_uring queue depth is kIoUringDepth (256), and each thread gets its
own io_uring instance via thread-local storage. In practice the SQ rarely
fills because ReadAsync calls io_uring_submit() after each io_uring_get_sqe(),
immediately flushing the SQE to the kernel. The null SQE would only occur
under unusual kernel backpressure where the kernel cannot consume from the
SQ ring fast enough.

IOStatus::Busy was chosen (over IOError) because this is a transient
condition. The caller has two options:
1. Call Poll() to reap outstanding completions from the CQ, then retry
   ReadAsync. This mirrors how MultiRead handles queue pressure internally
   by capping submissions and reaping between batches.
2. Fall back to synchronous Read(). Existing callers (FilePrefetchBuffer,
   IODispatcher) already have synchronous fallback paths for non-OK
   ReadAsync status, so IOStatus::Busy naturally triggers that fallback
   without additional code changes. Given the rarity of this condition,
   the synchronous fallback is pragmatic and avoids adding retry complexity.

Also adds a TEST_SYNC_POINT_CALLBACK on io_uring_get_sqe to enable test
injection, and a new ReadAsyncQueueFull unit test that uses SyncPoint to
force a null SQE and verifies the Busy return, handle cleanup, and no crash.

Reviewed By: xingbowang

Differential Revision: D98533853

fbshipit-source-id: f6d181e5c0d5154b570ff6da39e2f52e2a6aea84
2026-04-01 15:54:30 -07:00
Andrew Chang de6cdbb62d Fix SupportedOps to verify per-thread io_uring before advertising kAsyncIO (#14514)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14514

SupportedOps previously checked only that the ThreadLocalPtr container existed (non-null), which was set during construction based on a one-time probe on the main thread. However, CreateIOUring() can fail on other threads due to kernel resource limits or flag incompatibilities, causing ReadAsync to hit "failed to init io_uring" at runtime.

Now SupportedOps eagerly initializes the thread-local io_uring instance via Get() + CreateIOUring() and only advertises kAsyncIO if it succeeds. Also logs to stderr (with thread id) when io_uring init fails, and adds a TEST_SYNC_POINT_CALLBACK for testing simulated CreateIOUring failures.

Reviewed By: mszeszko-meta, xingbowang

Differential Revision: D98409140

fbshipit-source-id: efa92d9ac920860e95a46710c4a87e36bacbb466
2026-04-01 14:52:54 -07:00
Maciej Szeszko b8bbee0f3e Log IO uring init failures to stdout (instead of stderr) (#14548)
Summary: Pull Request resolved: https://github.com/facebook/rocksdb/pull/14548

Reviewed By: archang19

Differential Revision: D98667574

fbshipit-source-id: 4f01c9d8f2ae12051d4b3e4138258091b3c9eb3d
2026-04-01 14:06:03 -07:00
Xingbo Wang 24fac74206 docs: add end-to-end flow documentation (read_flow, write_flow) (#14486)
Summary:
Add comprehensive flow documentation tracing data through the full RocksDB system.

## Contents

### read_flow/ (11 chapters)
Get/MultiGet/Iterator paths through SuperVersion, MemTable, block cache, SST files (L0→Ln), bloom filters, range deletions, prefetch, async I/O.

### write_flow/ (10 chapters)
Put/Delete/Merge through WAL, MemTable, flush, compaction, delete cleanup, flow control, crash recovery, performance.

## Documentation Pattern
Each component follows a consistent structure:
- `index.md` — short overview (40-80 lines) with chapter table and key characteristics
- `NN_topic.md` — deep-dive chapters with source file references and workflow descriptions

All content validated against current codebase. No code snippets — refers to header files and struct/function names.

Part 1 of 11 in the RocksDB AI documentation series.

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

Reviewed By: anand1976

Differential Revision: D97795679

Pulled By: xingbowang

fbshipit-source-id: 07c3be1a7f27e0a9cc1d82ccad16e8e4084f4ab0
2026-04-01 13:02:17 -07:00
Hui Xiao f12465cb93 Add COERCE_CONTEXT_SWITCH and ASSERT_STATUS_CHECKED guidance to CLAUDE.md (#14522)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14522

Add two verification steps to RocksDB CLAUDE.md:

1. **Unit Test flakiness testing**: After writing a test, stress-test for
   flakiness with `COERCE_CONTEXT_SWITCH=1 make {test_binary}` followed by
   `--gtest_repeat=5`. This catches race conditions and context-switch-dependent
   failures early.

2. **Status object verification**: Run `ASSERT_STATUS_CHECKED=1 make check`
   to catch missing error handling that can lead to silent data corruption.
   These are compile-time checks enforced via a special build mode.

These are existing RocksDB best practices that both the stress test agent
and human developers should follow when writing or fixing code.

Reviewed By: xingbowang

Differential Revision: D98249994

fbshipit-source-id: 25ca6a628a82ecf968d5ed86aaa5c2f4b1060471
2026-04-01 11:58:43 -07:00
Xingbo Wang 7832ee886f db_stress: fix prefix scan correctness and crashtest stability (#14512)
Summary:
This PR fixes several correctness and stability issues in the stress test and crash test infrastructure, plus adds regression coverage for trie UDI iterators.

### db_stress: constrain batched prefix scans
`BatchedOpsStressTest::TestPrefixScan` opens 10 iterators with different prefixes and compares results in lockstep. Without `prefix_same_as_start`, unconstrained iterators could return keys beyond their seek prefix, producing more entries than bounded iterators and causing spurious assertion failures. Fix: set `prefix_same_as_start = true` on all iterators so every iterator stops at the seek prefix boundary.

### db_stress: skip invalid cross-prefix iterator checks
When prefix iteration is enabled, `ReadOptions` requires that the seek key and `iterate_lower_bound` share the same prefix. Previously, stress test verification could run against configurations where they don't (e.g. when `lower_bound` spans a prefix boundary), producing false positives. Fix: detect this invalid configuration and mark the check as diverged (skip verification) rather than asserting.

### db_crashtest: disable BlobDB in best-efforts recovery
BlobDB is not compatible with best-efforts recovery mode. Fix: explicitly disable blob file options when `best_efforts_recovery=True` in db_crashtest.py to avoid spurious failures.

### db_crashtest: preserve expected-state dirs across restarts
The expected-state directory was being wiped on certain restart paths, causing verification failures on the next run. Fix: preserve expected-state dirs across db_crashtest restarts. Adds a new `db_crashtest_test.py` test to verify the behavior, runnable via `make db_crashtest_test`.

### Add trie UDI iterator regression coverage
Adds regression tests for trie-based UserDefinedIndex (UDI) iterators covering snapshot-based reads, lower/upper bound iteration, `auto_refresh_iterator_with_snapshot`, and multi-version key handling. Also fixes an MSVC C4244 warning (int→char implicit narrowing) in the test.

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

Reviewed By: hx235

Differential Revision: D98302018

Pulled By: xingbowang

fbshipit-source-id: 84f5878665e5aeb61338ec2b6bfb2d2b077bb9f2
2026-03-31 22:48:24 -07:00
Danny Chen ae322e0a73 Add SortedRunBuilder: use RocksDB as an external… (#14499)
Summary:
CONTEXT: MyRocks and other third-party users currently need to understand deep RocksDB internals to sort unsorted data into SST files. The existing pattern requires: opening a full DB instance, configuring VectorRepFactory with universal compaction, disabling auto-compaction, writing unsorted data, manually triggering CompactRange with kForceOptimized, collecting output via GetColumnFamilyMetaData, and finally ingesting via IngestExternalFile with allow_db_generated_files. This is error-prone and requires ~30 lines of RocksDB configuration knowledge.

WHAT: This adds a new utility class, SortedRunBuilder, that wraps all of the above into a simple Create/Add/Finish API. Callers feed in unsorted key-value pairs (in any order, from any number of threads) and receive sorted SST files with seqno=0 ready for ingestion -- or can iterate sorted output directly.

The name SortedRunBuilder was chosen over alternatives like UnsortedSstFileWriter because this utility targets a broad audience: anyone wanting to use RocksDB as an external sort engine, not just users migrating from SstFileWriter. That said, this explicitly removes the sorted-input requirement that SstFileWriter enforces -- callers no longer need to pre-sort their data before writing SST files.

KEY DESIGN DECISIONS:
- Zero changes to core RocksDB internals. This is a pure utility layer that exclusively calls existing public APIs:
  * DB::Open(), DB::Put(), DB::Write(), DB::Flush(), DB::CompactRange(), DB::GetColumnFamilyMetaData(), DB::NewIterator(), DestroyDB()
  * VectorRepFactory (sort-on-flush memtable)
  * BottommostLevelCompaction::kForceOptimized (zero seqnos)
- No modifications to: compaction logic, memtable implementation, SST file format, ingestion logic, or DB open/close paths
- All new code lives in utilities/sorted_run_builder/ and the public header include/rocksdb/utilities/sorted_run_builder.h
- Build file changes are limited to registering the new source/test files

API SURFACE:
  SortedRunBuilderOptions opts; opts.temp_dir = "/tmp/sort_work"; std::unique_ptr<SortedRunBuilder> builder; SortedRunBuilder::Create(opts, &builder);
  builder->Add(key, value);   // any order, thread-safe
  builder->AddBatch(&batch);  // WriteBatch for throughput
  builder->Finish();           // flush + compact + collect
  builder->GetOutputFiles();   // sorted SSTs for ingestion
  builder->NewIterator(ro);    // iterate sorted output

FILES CHANGED:
  New: include/rocksdb/utilities/sorted_run_builder.h (public header) New: utilities/sorted_run_builder/sorted_run_builder.cc (implementation) New: utilities/sorted_run_builder/sorted_run_builder_test.cc (12 tests) New: docs/plans/sorted_run_builder_plan.md (design plan) New: docs/plans/sorted_run_builder_usage_guide.md (usage guide) Modified: src.mk, CMakeLists.txt, Makefile (register new files only)

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

Test Plan:
$ make clean && make -j$(nproc) sorted_run_builder_test $ ./sorted_run_builder_test [==========] Running 12 tests from 1 test case. [  PASSED  ] 12 tests. (688 ms total)

  Tests cover: basic sort correctness, empty builder, WriteBatch path, concurrent multi-threaded writes, ingestion into a target DB, large random dataset (10K keys), entry/size counters, error cases (Add after Finish, iterator before Finish, empty temp_dir), cleanup verification, and duplicate key handling.

Reviewed By: xingbowang

Differential Revision: D98935972

Pulled By: dannyhchen

fbshipit-source-id: e3bb7f1ea5f6004aab697e4da667fa21292ca250
2026-03-31 14:23:35 -07:00
Maciej Szeszko 1e23ee053f skip stats if setup-ccache did not run or failed (#14542)
Summary:
`teardown-ccache` runs with if: always(), but in folly jobs `setup-folly` precedes `setup-ccache`. When `setup-folly` fails, `setup-ccache` is skipped, so `CCACHE_DIR` is unset and ccache is not on `PATH`. This is exactly what happened [here](https://github.com/facebook/rocksdb/actions/runs/23660261947/job/68928337162). Fix guards `teardown-ccache` to exit gracefully when `CCACHE_DIR` is unset, and tolerate missing ccache binary for stats.

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

Reviewed By: joshkang97

Differential Revision: D98964757

Pulled By: mszeszko-meta

fbshipit-source-id: 8e83c1b62ef66130ba479142f002897d0a97f6c0
2026-03-31 13:42:34 -07:00
Maciej Szeszko effac3a918 Update CLANGTIDY to latest stable & secure version (#14541)
Summary:
As advertised.

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

Reviewed By: xingbowang

Differential Revision: D98961628

Pulled By: mszeszko-meta

fbshipit-source-id: f0519089bb53649668e791b18428a65fc666b71c
2026-03-31 13:30:39 -07:00
Peter Dillinger c7f0e1fa93 Store UniqueIdVerifier file in expected_values_dir on local filesystem (#14539)
Summary:
The warm_storage_crash_test was failing because UniqueIdVerifier stored its .unique_ids bookkeeping file in the DB directory, which lives on warm storage. After a crash, warm storage's weaker durability guarantees could cause flushed-but-not-synced data to be lost, making the file appear shorter on a second open within the same constructor. This caused CopyFile to return Corruption and trigger assert(false).

Move the file to expected_values_dir (which is always on local filesystem) and always use Env::Default() for file operations, so that POSIX flush semantics are sufficient for read consistency without needing Sync.

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

Test Plan: Full local run of `make blackbox_crash_test`

Reviewed By: mszeszko-meta

Differential Revision: D98934451

Pulled By: pdillinger

fbshipit-source-id: 5497f23c2382cb5ac2c3d7f238c6c4ca1dd29f5b
2026-03-31 12:30:54 -07:00
Xingbo Wang 141a1f0abf Add regression tests and diagnostic logging for trie UDI post-seek seqno correction (#14524)
Summary:
Adds regression tests and failure-diagnostic tooling for the trie UDI post-seek seqno correction bug (T258590238). The core fix itself already landed in https://github.com/facebook/rocksdb/issues/14466.

## What's in this PR

### Regression tests

- `TrieIndexFactoryTest.ZeroSeqMustNotSkipLeafForSmallerUserKey` — minimal unit test proving the original bug; exercises the seqno-based block advancement with a smaller user key that should NOT trigger advancement
- `TrieIndexDBTest.AutoRefreshSnapshotNextAcrossSameUserKeyBoundaries` — DB-level test for snapshot-refresh iteration across same-user-key block boundaries
- `TrieIndexDBTest.AutoRefreshSnapshotNextAfterCompactionAcrossSameUserKeyBoundaries` — same scenario after compaction reshapes SST layout
- `TrieIndexDBTest.AutoRefreshSnapshotStressLikeSingleCfCoalescingIterator` — stress-like test exercising snapshot-refresh + coalescing-iterator interaction

### Diagnostic logging in db_stress

Extracts the iterator verification failure dump into a `DumpIteratorVerificationFailure()` helper in `NonBatchedOpsStressTest`. On verification failure, logs:
- Expected-state window (pre/post read values, raw state, pending flags)
- Iterator config (UDI, trie, snapshot, multi-CF)
- Replay comparison: creates fresh iterators with standard vs trie index, direct vs coalescing, seek-to-failure-key vs replay-from-mid — making it possible to identify which index/iterator combination diverges

This logging was instrumental in triaging the original bug and will help with future trie UDI stress failures.

## Tests

All existing trie index tests pass. New tests listed above all pass.

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

Reviewed By: hx235

Differential Revision: D98583342

Pulled By: xingbowang

fbshipit-source-id: 57e099055d6beae9b5f03f4e6605f0af6e65b94a
2026-03-31 11:38:13 -07:00
zaidoon 41e5eb1a4d Support reverse iteration in UDI/trie index and optimize hot paths (#14466)
Summary:
The following performance optimizations are included in this PR:
- Inline NextSetBit/PrevSetBit into header (called on every trie level)
- Unroll Rank1 popcount loop (the single hottest function)
- Advance/Retreat: replace path stack top in-place instead of pop+push
- std::swap for prev_key_scratch_ instead of O(n) string copy
- assign() instead of ToString() to reuse buffer capacity
- Cache IndexValue in wrapper to avoid repeated virtual dispatch
- Mark TrieIndexIterator/TrieIndexBuilder as final for devirtualization

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

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

Reviewed By: anand1976

Differential Revision: D97979699

Pulled By: xingbowang

fbshipit-source-id: 7ec56ecc8b6d68548a336dd1aaccfb215189eff0
2026-03-31 07:35:11 -07:00
Hui Xiao 875b8544df Revert D98425822: Rocksdb Warm Storage Test failed: not implemented: SyncWAL() is not supported for this implementatio
Differential Revision:
D98425822

Original commit changeset: ec17a15c837c

Original Phabricator Diff: D98425822

fbshipit-source-id: 52c7c86d260bb7c239962c299841d62408ee5d88
2026-03-30 19:40:55 -07:00
generatedunixname3846135475516776 e96295a288 Rocksdb Warm Storage Test failed: not implemented: SyncWAL() is not supported for this implementatio (#14515)
Summary: Pull Request resolved: https://github.com/facebook/rocksdb/pull/14515

Reviewed By: xingbowang

Differential Revision: D98425822

fbshipit-source-id: ec17a15c837c2b3e2fbe7ae6bc62aee3685e7bcc
2026-03-30 13:00:44 -07:00
Josh Kang f897c1b2ef Add uniform block tracking stats (#14513)
Summary:
Add tracking of uniform index blocks as a table property (`num_uniform_blocks`). When `uniform_cv_threshold` is set, the block builder detects uniformly distributed keys via coefficient of variation of key gaps. This information is now surfaced end-to-end: from block building through index builders to SST table properties.

## Key changes
- **BlockBuilder**: Persist the uniformity result from `ScanForUniformity()` as a member (`is_uniform_`) exposed via `IsUniform()`, rather than a local variable discarded after `Finish()`.
- **Index builders**: All three index builder types (`ShortenedIndexBuilder`, `HashIndexBuilder`, `PartitionedIndexBuilder`) implement `NumUniformIndexBlocks()`. For partitioned indexes, the count accumulates across partition `Finish()` calls.
- **Table property serialization**: Added `TablePropertiesNames::kNumUniformBlocks` (`"rocksdb.num.uniform.blocks"`) with full serialization/deserialization support. Without this, the property was computed in memory but never persisted to SST files.
- **Table property aggregation**: `num_uniform_blocks` included in `Add()` and `GetAggregatablePropertiesAsMap()` for correct cross-SST aggregation.

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

Test Plan:
- **Unit tests**: `block_test` (7504 passed), `table_test` (6917 passed), `table_properties_collector_test` (8 passed).
- **End-to-end verification via db_bench + sst_dump**:
  - Positive: `./db_bench --benchmarks=fillseq,compact --num=10000 --uniform_cv_threshold=0.2 --index_shortening_mode=0 --compression_type=none` produces SST with `# uniform blocks: 1`.
  - Negative: Same with `--uniform_cv_threshold=-1` (disabled) produces `# uniform blocks: 0`.

Reviewed By: xingbowang

Differential Revision: D98343170

Pulled By: joshkang97

fbshipit-source-id: ed08e83b2dcfb70f074bfb0f7bb5c31d75dc6da9
2026-03-30 11:58:45 -07:00
Xingbo Wang dae450b78e Improve Claude review reliability (#14526)
Summary:
## Problems

**(1) Claude review times out on large PRs**
The `claude-code-base-action` defaults to `timeout_minutes=10`. Large PRs (e.g. https://github.com/facebook/rocksdb/issues/14499) get killed with exit code 124 after 600 seconds mid-review.

**(2) Exported PRs never get reviewed**
PRs exported from Meta's internal pipeline (e.g. https://github.com/facebook/rocksdb/issues/14515) trigger CI within milliseconds of PR creation. The `workflow_run` payload has `pull_requests=[]`, and the SHA-based fallback also misses because GitHub hasn't registered the PR yet — so Claude review never fires.

## Fixes

- Set `timeout_minutes: "60"` on both auto-review and manual-review `Run Claude` steps
- Retry the SHA→PR lookup up to 5 times with a 10s delay, giving GitHub up to ~50s to register the PR. Also bumped `per_page` from 30 to 100.

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

Reviewed By: pdillinger

Differential Revision: D98636217

Pulled By: xingbowang

fbshipit-source-id: bba19095ab2ddf468c5c19cf5c77d19536f498b0
2026-03-30 11:12:08 -07:00
Andrew Chang 2af38206ad Log errno and thread ID on CreateIOUring failure (#14520)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14520

CreateIOUring() silently returns nullptr on failure, discarding the errno
from io_uring_queue_init. This makes it impossible to diagnose why
io_uring initialization fails on specific threads (e.g. ENOMEM from
memlock limits, EINVAL from unsupported flags, EMFILE from fd exhaustion).

Add a fprintf(stderr, ...) that logs strerror, errno, and pthread thread
ID when io_uring_queue_init fails, so failures are diagnosable from logs
without needing to reproduce.

Reviewed By: xingbowang

Differential Revision: D98526792

fbshipit-source-id: 1eb5042c8b62663c4d24c09f29ef7c55b90032f0
2026-03-28 15:36:15 -07:00
Josh Kang 5db0603613 Read-triggered compactions (#14426)
Summary:
Add read-triggered compaction, a new feature that reduces read amplification by compacting SST files that receive high read traffic. When an SST file's read frequency (`num_reads_sampled / file_size`) exceeds a configurable threshold, it is marked for compaction to a lower level.

The feature introduces two new options: a CF option `read_triggered_compaction_threshold` (default 0, disabled) and a DB option `max_periodic_compaction_trigger_seconds` (default 43200s) that controls how often the background thread re-evaluates compaction scores on quiet databases. Both options are dynamically changeable.

Lowering `max_periodic_compaction_trigger_seconds` does add some overhead, but generally is minimal, so running this every couple of minutes in a production environment seems fairly reasonable.

## Key changes

- **New CF option `read_triggered_compaction_threshold`** (`advanced_options.h`): When positive, files with `reads_per_byte > threshold` are marked for compaction. Files at the last non-empty level are skipped (bottommost compaction handles those separately). Marked files are sorted by hotness (reads_per_byte descending).
- **New DB option `max_periodic_compaction_trigger_seconds`** (`options.h`): Replaces the hardcoded 12-hour ceiling in `ComputeTriggerCompactionPeriod()`. Essential for read-triggered compaction on quiet DBs since there are no writes to trigger score re-evaluation.
- **Leveled compaction picker** (`compaction_picker_level.cc`): Adds read-triggered as the lowest-priority compaction reason in `SetupInitialFiles()`, using the existing `PickFileToCompact` helper.
- **Universal compaction picker** (`compaction_picker_universal.cc`): Adds `PickReadTriggeredCompaction` as lowest priority. Refactors shared "find output level + compute overlapping inputs + create Compaction" logic from both `PickDeleteTriggeredCompaction` and `PickReadTriggeredCompaction` into `BuildCompactionToNextLevel`, handling both single-level and multi-level universal cases.
- **Periodic trigger integration** (`db_impl.cc`): `TriggerPeriodicCompaction` now also fires for CFs with `read_triggered_compaction_threshold > 0`, even without time-based compaction configured.
- **Stress test & db_bench support**: Both `db_stress` and `db_bench` support the new options. `db_crashtest.py` randomly enables read-triggered compaction and sets a short periodic trigger interval when enabled.

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

Test Plan:
**Unit tests**:
- `compaction_picker_test` — 7 new tests: `ReadTriggeredCompactionDisabled`, `ReadTriggeredCompactionBelowThreshold`, `ReadTriggeredCompactionAboveThreshold`, `NeedsCompactionReadTriggered`, `ReadTriggeredPicksFile`, `UniversalReadTriggeredCompaction`, `ReadTriggeredSkipsLastLevel`, `UniversalReadTriggeredNoPickWhenNotMarked`
- `db_compaction_test` — `ReadTriggeredCompaction` integration test verifying end-to-end behavior with sync points
- Stress test coverage

**Stress test**:
```
make V=1 -j "CRASH_TEST_EXT_ARGS=--duration=600 --max_key=2500000 --max_compaction_trigger_wakeup_seconds=10
  --read_triggered_compaction_threshold=0.0001 --interval=600" blackbox_crash_test
```
- confirmed read triggered compactions from LOGS

**Benchmark** (`db_bench`):

Setup: 5M keys (100B values, 16B keys), leveled compaction, 5 levels, 4MB target file size. DB fully compacted, then 2M overlapping keys written without compaction to create L0/L1 overlap (82 files, ~294MB).

LSM shape change during readrandom with read-triggered compaction:
```
BEFORE: L0=9 files (15MB), L1=4 (16MB), L2=20 (69MB), L3=49 (194MB) — 82 files, 294MB
AFTER:  L3=66 files (223MB)
```

| Benchmark | Config | avg ops/s | % change |
|-----------|--------|-----------|----------|
| readrandom (8 threads, 5M reads) | baseline (threshold=0) | 1,086,965 | — |
| readrandom (8 threads, 5M reads) | threshold=0.000001, trigger=5s | 1,453,697 | **+33.7%** |

Reviewed By: xingbowang

Differential Revision: D97838716

Pulled By: joshkang97

fbshipit-source-id: a21fcb270c7fadd4f78d98b9c821982f220dd3f0
2026-03-27 14:43:52 -07:00
Xingbo Wang a965706e73 blob: skip empty values even with min_blob_size=0 (#14517)
Summary:
When `min_blob_size=0`, the existing guard condition:

```cpp
if (value.size() < min_blob_size_) {
    return Status::OK();  // skip blob creation
}
```

is **false** for empty values (`0 < 0`), so empty values proceed to blob creation. This writes a 0-byte blob to disk and creates a `BlobContents` object with an empty `Slice`.

When that `BlobContents` is later evicted from the primary blob cache to `CompressedSecondaryCache`, the eviction handler calls `SaveTo(value, 0, 0, out)`, which hits:

```cpp
// typed_cache.h:230
assert(from_offset < slice.size());  // 0 < 0 → CRASH
```

This crash was found by the stress test with `--min_blob_size=0` and `--blob_cache` + secondary cache enabled (T261142690).

## Fix

Add an explicit `value.empty()` check before the blob path:

```cpp
if (value.empty() || value.size() < min_blob_size_) {
    return Status::OK();
}
```

Empty values are now always stored inline in the SST, regardless of `min_blob_size`. This is also correct on principle: a `BlobIndex` reference is larger than an empty value, so storing an empty value as a blob is pure overhead with no benefit.

## Root Cause Chain

1. User writes `Put("key", "")` with `min_blob_size=0`
2. `BlobFileBuilder::Add()` — `0 < 0` is false, empty value proceeds to blob creation
3. 0-byte blob written to blob file; `BlobContents` created with 0-size slice
4. `BlobContents` inserted into primary blob cache (LRU)
5. Cache eviction triggers `CacheWithSecondaryAdapter::EvictionHandler()`
6. `CompressedSecondaryCache::InsertInternal()` → `SaveTo(value, 0, 0, out)`
7. `assert(from_offset < slice.size())` → `assert(0 < 0)` → **** assertion failure

## Test

Added `DBBlobBasicTest.EmptyValueNotStoredAsBlob` which:
- Writes an empty value and a non-empty value with `min_blob_size=0`
- Verifies both are readable
- Confirms the empty value is stored **inline** (readable from `kBlockCacheTier` without blob I/O)
- Confirms the non-empty value is stored as a **blob** (returns `IsIncomplete()` from `kBlockCacheTier`)

## Related

- Task: T261142690

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

Reviewed By: hx235

Differential Revision: D98499174

Pulled By: xingbowang

fbshipit-source-id: 5713923daec83db6491d00ce58acdf8231fabeba
2026-03-27 13:55:18 -07:00
Xingbo Wang 1a4b1e42cc Add include_blob_files option to GetApproximateSizes (#14501)
Summary:
**Summary:**
Add a new boolean flag `include_blob_files` (default: `false`) to `SizeApproximationOptions` and a corresponding `INCLUDE_BLOB_FILES` enum value to `SizeApproximationFlags`. When set to `true`, the returned size includes an approximation of blob file data in the queried key range.

**Algorithm:**
The blob file size contribution is prorated using the SST size ratio:
```
blob_size_in_range ≈ total_blob_size * (sst_size_in_range / total_sst_size)
```
The blob-to-SST ratio (`total_blob_size / total_sst_size`) is computed once before the per-range loop, so iterating levels and blob files only happens once per `GetApproximateSizes` call regardless of how many ranges are queried. The per-range SST size (`ApproximateSize`) is computed once and shared between `include_files` and `include_blob_files`.

**Limitations:**
- Assumes blob data is distributed proportionally to SST data across the key space. May be inaccurate if blob value sizes vary significantly across different key ranges (e.g., one range has large blobs while another has small ones).
- If there are no SST files (all data in memtables), the blob size contribution will be 0 even if blob files exist on disk.

**Changes:**
- `include/rocksdb/options.h`: New `include_blob_files` field in `SizeApproximationOptions`; updated doc comments for `include_memtables`/`include_files`
- `include/rocksdb/db.h`: New `INCLUDE_BLOB_FILES` in `SizeApproximationFlags` enum, updated flags-to-options mapping
- `include/rocksdb/c.h`: New `rocksdb_size_approximation_flags_include_blob_files` C API enum value
- `java/`: Added `INCLUDE_BLOB_FILES` to `SizeApproximationFlag.java` and JNI flag mapping in `rocksjni.cc`
- `db/db_impl/db_impl.cc`: Blob-to-SST ratio computed once before loop, SST range size computed once per range and shared
- `db_stress_tool/db_stress_test_base.cc`: Randomized `include_blob_files` in stress test

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

Test Plan:
- New `DBBlobBasicTest.GetApproximateSizesIncludingBlobFiles` — verifies:
  - Size with blobs > without (full range)
  - Non-overlapping range returns 0
  - Partial range returns proportionally less than full range
  - `SizeApproximationFlags` API works
  - Multi-range query: two sub-ranges sum approximately to the full-range result
- Stress test now exercises the new option randomly

Reviewed By: hx235

Differential Revision: D97984211

Pulled By: xingbowang

fbshipit-source-id: e9127eac3308687fd4f0b17a771fd61fba6a8380
2026-03-27 13:53:21 -07:00
Andrew Chang 47de3a3a2c Fix io_uring kernel resource leak in DeleteIOUring() (#14518)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14518

When RocksDB creates an io_uring instance via `CreateIOUring()`, it calls
`io_uring_queue_init()` under the hood. This function asks the Linux kernel
to set up a submission queue and completion queue for async I/O — the kernel
allocates file descriptors and memory-mapped ring buffers to make this work.

To properly release those kernel resources, you must call
`io_uring_queue_exit()` before freeing the `io_uring` struct. This function
tells the kernel "I'm done with this io_uring" — it unmaps the shared
memory regions and closes the kernel file descriptors. Without it, those
resources leak every time an io_uring instance is destroyed.

`DeleteIOUring()` (the ThreadLocalPtr destructor callback) was only doing
`delete iu` — freeing the C++ struct's heap memory but never telling the
kernel to clean up. This meant every thread exit leaked kernel resources.

The same bug also existed in the `PosixFileSystem` constructor, which
creates a temporary test io_uring to check kernel support and then did
`delete new_io_uring` without cleanup.

Fix:
1. Add `io_uring_queue_exit(iu)` before `delete iu` in `DeleteIOUring()`.
2. Replace bare `delete new_io_uring` in fs_posix.cc with
   `DeleteIOUring(new_io_uring)` to reuse the now-correct helper.

Note: the error-recovery path in io_posix.cc (~line 907) already correctly
calls `io_uring_queue_exit()` before `delete`, confirming this was the
intended pattern that was missed in these two spots.

Reviewed By: anand1976

Differential Revision: D98500559

fbshipit-source-id: bd6c10c19d9fd67cf537dd7f100ef9dc49bfe77e
2026-03-27 12:00:06 -07:00
Xingbo Wang 926e2b589f Fix nightly CI regressions, flaky tests, and Windows ccache from #14478 (#14508)
Summary:
Fix build failures, flaky tests, and Windows ccache issues exposed by PR https://github.com/facebook/rocksdb/issues/14478.

### 1. Release build (NDEBUG) compile error
**Job**: `build-linux-release-with-folly`

The SyncPoint cleanup listener in testharness.cc referenced `SyncPoint::GetInstance()` unconditionally, but SyncPoint is only declared in debug builds. Wrapped with `#ifndef NDEBUG`.

### 2. Folly-lite link error
**Job**: `build-linux-cmake-with-folly-lite`

The `USE_FOLLY_LITE` cmake path unconditionally linked `-lglog`, which fails with lld (added in https://github.com/facebook/rocksdb/issues/14478) when glog isn't installed. Use `find_library()` to link only when available.

### 3. Flaky tests — leaked `Env::Default()` thread pool state

Sharded execution runs multiple tests per process. Several tests in `db_compaction_test.cc` modified the global `Env::Default()` BOTTOM thread pool without resetting. With a leaked BOTTOM pool, subsequent tests' last-level compactions get forwarded to the bottom pool, freeing the LOW thread to pick additional compactions unexpectedly.

Fix: add `TearDown()` override to `DBCompactionTest` that captures default thread pool sizes in the constructor and restores them after every test. This is more robust than per-test cleanup because:
- It runs even when a test fails (gtest calls TearDown after assertion failures)
- It catches all current and future leakers without per-test maintenance

### 4. Windows ccache — 0.26% hit rate → should be ~99%

The Windows nightly build takes 57 minutes because ccache has near-zero hit rate. Two issues:
- **Cache key**: The `hendrikmuhs/ccache-action` used a timestamp-based key, so each nightly run created a unique key and never found the previous run's cache (`No cache found.`). Fixed by using a stable key `ccache-windows-<workflow>` with prefix-based restore.
- **Compiler check**: Missing `compiler_check=content` setting, so MSVC path/version changes between runners invalidated all cache entries. Added `compiler_check=content` (same fix as macOS in https://github.com/facebook/rocksdb/issues/14478).

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

Test Plan:
- Release and debug builds compile cleanly
- 70 consecutive shuffled runs of db_compaction_test (367 tests each) pass — 50 on full cores + 20 on 4 cores
- Format check passes
- Windows ccache fix requires CI run to verify (first run populates cache, second run should see high hit rate)

Reviewed By: anand1976

Differential Revision: D98380757

Pulled By: xingbowang

fbshipit-source-id: d74079b75786ba3299e335b145a6c5fdc81fd5c1
2026-03-27 10:54:08 -07:00
Andrew Chang 453ebd6f37 Fix MultiScan not respecting SupportedOps for async IO (#14510)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14510

When a filesystem does not return kAsyncIO in SupportedOps(), MultiScan
still sends ReadAsync/Poll/AbortIO requests. Regular iterators check via
CheckFSFeatureSupport in ArenaWrappedDBIter::Init and ForwardIterator,
but MultiScan blindly trusted the caller-provided
MultiScanArgs::use_async_io flag.

Fix: In the MultiScan constructor, after scan_opts_ is initialized,
check CheckFSFeatureSupport and disable use_async_io if the FS doesn't
support it. Also pass the (potentially modified) scan_opts_ to Prepare()
instead of the original scan_opts parameter.

Reviewed By: mszeszko-meta

Differential Revision: D97995735

fbshipit-source-id: 331639d950fd3cb9f491feed996d5820294beb4e
2026-03-26 09:24:04 -07:00
Andrew Chang 24dc8306f8 Add atomic_flush to CreateBackupOptions (#14511)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14511

Add `atomic_flush` option to `CreateBackupOptions` that plumbs through
`CheckpointImpl::CreateCustomCheckpoint` to `LiveFilesStorageInfoOptions`.

When `flush_before_backup=true` and `atomic_flush=true`, the backup engine
atomically flushes all column families before creating the backup. This
ensures cross-CF consistency without needing WAL files. Combined with
`BackupEngineOptions::backup_log_files=false`, this allows safely skipping
WAL backup for multi-CF databases, reducing backup size and complexity.

Changes:
- `backup_engine.h`: Add `bool atomic_flush = false` to `CreateBackupOptions`
- `checkpoint_impl.h/.cc`: Add `bool atomic_flush` parameter to
  `CreateCustomCheckpoint`, plumb to `LiveFilesStorageInfoOptions::atomic_flush`
- `backup_engine.cc`: Pass `options.atomic_flush` through to
  `CreateCustomCheckpoint`
- `checkpoint_test.cc`: Add `BackupWithAtomicFlushSkipsWAL` test verifying
  backup with atomic flush + no WAL creates a valid restorable multi-CF backup

Reviewed By: xingbowang

Differential Revision: D98208696

fbshipit-source-id: e818ba8669ac52a206e30e9dd56f1d7573cc0175
2026-03-26 08:49:18 -07:00
Xingbo Wang 0921ec1200 Improve Claude Code review: comprehensive prompt, incremental findings, recovery flow (#14507)
Summary:
Improves the Claude Code review CI workflow to produce deeper, more reliable reviews.

**Motivation:** The previous 30-turn limit caused reviews to silently produce "no output" on complex PRs (e.g. https://github.com/facebook/rocksdb/issues/14477). The review prompt was also too shallow — single-pass with no codebase context phase.

**Changes:**

**1. Comprehensive multi-agent review prompt** (`claude_md/ci_review_prompt.md`)
- 9 specialized review agents: design, correctness, cross-component, invariant-adversary, caller-audit, performance, API, serialization, test coverage
- Deep codebase context phase before agents spawn: caller-chain analysis (3-5 levels up), callee side-effect tracing, cross-component data consumer analysis, execution context verification, assumption stress-testing
- Inter-agent debate with round-robin critique assignments
- Final report quality rules: disproven findings removed, no stream-of-consciousness

**2. Incremental findings + recovery flow**
- `Write` tool added so Claude saves findings to `review-findings.md` after each phase
- If the review hits the turn limit, a recovery step launches a Sonnet session to format partial findings into the standard output
- Recovery file existence check prevents crash if recovery step fails
- `getLastAssistantText` fallback truncated to 50KB to avoid enormous PR comments

**3. Prompts extracted to files** (`claude_md/ci_*.md`)
- `ci_review_prompt.md` — full review methodology
- `ci_query_prompt.md` — `/claude-query` system prompt
- `ci_recovery_prompt.md` — recovery formatting prompt
- Secure: checkout is from base branch (main), not PR head

**4. `max_turns` increased from 30 to 300**
- Orchestrator budget for multi-agent workflow; sub-agents get their own turns
- Recovery flow ensures partial results if limit is still hit

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

Reviewed By: archang19

Differential Revision: D98170111

Pulled By: xingbowang

fbshipit-source-id: 390626a53e7a7f91c2d3e91ed4403494532425ed
2026-03-25 22:11:29 -07:00
Josh Kang f620a6d039 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-03-25 15:11:15 -07:00
anand76 c78144e452 Update version to 11.2.0 and add 11.1.fb to format compat (#14509)
Summary:
- Bump version.h from 11.1.0 to 11.2.0
- Add `11.1.fb` to `check_format_compatible.sh`

## Remaining manual steps
- [x] Cherry-pick HISTORY.md update from release branch
- [ ] Update folly hash in Makefile to latest - Follow up in another PR

Part of 11.1 release workflow.

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

Reviewed By: archang19

Differential Revision: D98173862

Pulled By: anand1976

fbshipit-source-id: 020dbb30126d054788ed979f42fabd1be8d97abb
2026-03-25 12:41:45 -07:00
Xingbo Wang 056a0f9664 Add atomic_flush option for checkpoint (#14477)
Summary:
Add `force_atomic_flush` to `FlushOptions` so any flush caller (Checkpoint, `DB::Flush()`, etc.) can force atomic flush of all column families even when `DBOptions::atomic_flush` is disabled.

## Changes

### Public API
- Added `bool force_atomic_flush = false` to `FlushOptions`
- Added `bool atomic_flush = false` to `LiveFilesStorageInfoOptions`

### Flush dispatch
- `FlushAllColumnFamilies()` — uses `immutable_db_options_.atomic_flush || flush_options.force_atomic_flush`
- `DB::Flush()` (single-CF and multi-CF) — respects `force_atomic_flush`
- `FlushForGetLiveFiles()` — constructs `FlushOptions` with `force_atomic_flush` from `LiveFilesStorageInfoOptions::atomic_flush`

### Per-request atomic flush in pipeline
- `FlushRequest.atomic_flush` and `BGFlushArg.atomic_flush_` carry the per-request flag
- `GenerateFlushRequest`, `EnqueuePendingFlush`, `PopFirstFromFlushQueue`, `BackgroundFlush`, `FlushMemTablesToOutputFiles` all use the per-request flag

### Stress test
- Added `--checkpoint_atomic_flush` flag to db_stress, randomly enabled in db_crashtest.py

### Unit tests (6 new)
- End-to-end checkpoint with atomic flush override
- Negative test (default = no atomic flush)
- Single-CF edge case
- Interaction with `DBOptions::atomic_flush=true`
- Mixed non-atomic then atomic flushes in queue
- Mixed atomic then non-atomic flushes in queue

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

Reviewed By: joshkang97

Differential Revision: D97639233

Pulled By: xingbowang

fbshipit-source-id: b924c044d6179e68b38c6604eb46c9140516d42a
2026-03-25 09:54:00 -07:00
Hui Xiao f8ff77db6a Fix FIFO crash test false verification failures by disabling file dropping (#14503)
Summary:
FIFO compaction drops old SST files when total size exceeds `max_table_files_size`
or `max_data_files_size`. The stress test's expected state can't track these drops,
causing false "GetEntity returns NotFound" failures. Confirmed by
`rocksdb.fifo.max.size.compactions COUNT: 7` in an asan_crash_test whitebox run
(`compaction_style=2`, `fifo_compaction_max_data_files_size_mb=100`).

This diff adds a `fifo_compaction_max_table_files_size_mb` flag and sets it to
100GB in db_crashtest.py. Since `max_table_files_size` is always active (default
1GB), it must always be set very high. `fifo_compaction_max_data_files_size_mb`
is randomized between 0 (disabled, defers to `max_table_files_size`) and 100GB
(overrides `max_table_files_size`) to exercise both code paths while preventing
drops in either case. FIFO intra-L0 compaction (`fifo_allow_compaction`) is
still exercised.

Long-term TODO: handle FIFO drops in expected state via `OnCompactionBegin`
listener calling `SetPendingDel()` on affected key ranges, treating drops as
concurrent deletes.

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

Reviewed By: joshkang97

Differential Revision: D97601382

Pulled By: hx235

fbshipit-source-id: 1a3badfa4de47efd73f965807982a966ebe135ef
2026-03-24 19:32:11 -07:00
Josh Kang dcf3db95a6 Make concurrent Memtable stats more robust (#14506)
Summary:
Fix thread-safety issues in memtable stats tracking and `MaybeUpdateNewestUDT` to prepare for PR https://github.com/facebook/rocksdb/issues/14448, which introduces concurrent range tombstone insertion from the read path into the mutable memtable. Currently the non-concurrent write path updates memtable counters (`num_entries_`, `num_deletes_`, `num_range_deletes_`, `data_size_`) using non-atomic load/store pairs. While this is safe today PR https://github.com/facebook/rocksdb/issues/14448 will make range_tombstone memtable concurrent, but the main write path can still be non-concurrent. This fix ensures stats are tracked correctly.

Similarly, `MaybeUpdateNewestUDT` was not thread-safe and was not called in the concurrent write path at all. This PR also fixes the `post_process_info` delete counter which missed `kTypeSingleDeletion` and `kTypeDeletionWithTimestamp`.

## Key changes
- Switch non-concurrent counter updates from `LoadRelaxed`/`StoreRelaxed` pairs to `FetchAddRelaxed` to avoid races with concurrent range tombstone inserts (e.g. `AddLogicallyRedundantRangeTombstone` calling `BatchPostProcess`).
- Make `MaybeUpdateNewestUDT` thread-safe by replacing `Slice newest_udt_` with `RelaxedAtomic<const char*> newest_udt_data_` and using a CAS loop. The pointed-to memory lives in the arena and remains valid for the memtable's lifetime.
- Call `MaybeUpdateNewestUDT` in the concurrent write path (was previously skipped with a TODO).
- Fix `post_process_info->num_deletes++` to also count `kTypeSingleDeletion` and `kTypeDeletionWithTimestamp`, matching the non-concurrent path.
- Change `GetNewestUDT()` return type from `const Slice&` to `Slice` (returning by value) to avoid dangling reference issues with the new atomic pointer storage. Updated across `MemTable`, `ReadOnlyMemTable`, `MemTableListVersion`, and `WBWIMemTable`.
- Remove unused `Slice newest_udt_` member from `WBWIMemTable`.

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

Test Plan:
- Added `ConcurrentWriteMemTableProperties` test in `db_properties_test.cc`: 4 threads writing puts, deletes, and single deletes concurrently, then verifying `rocksdb.num-entries-active-mem-table` and `rocksdb.num-deletes-active-mem-table` properties are correct. Flush with `flush_verify_memtable_count=true` validates integrity.
- Added `ConcurrentGetTableNewestUDT` test in `memtable_list_test.cc`: 4 threads concurrently inserting entries with UDTs via `allow_concurrent=true`, verifying the newest UDT is correctly tracked.
- Extended `DuplicateSeq` and `ConcurrentMergeWrite` tests in `db_memtable_test.cc` to verify stat counters after `BatchPostProcess`.
- Benchmark results show no performance regression (3 runs each, averaged):

**Workload 1: fillrandom** (pure Put workload)
```
./db_bench -benchmarks=fillrandom -seed=1 -compression_type=none -threads=8 -db=<DB>
```

**Workload 2: readrandomwriterandom** (mixed read/write/delete)
```
# Setup: fillrandom,compact to populate DB (single-threaded)
./db_bench -benchmarks=readrandomwriterandom -seed=1 -compression_type=none \
  -use_existing_db=1 -readwritepercent=50 -deletepercent=20 -threads=8 -db=<DB>
```

**Workload 3: fillrandom with range tombstones** (puts interleaved with range deletes)
```
./db_bench -benchmarks=fillrandom -seed=1 -compression_type=none \
  -writes_per_range_tombstone=100 -range_tombstone_width=10 \
  -max_num_range_tombstones=1000 -threads=8 -db=<DB>
```

```
| Benchmark                          | avg ops/s (main) | avg ops/s (feature) | % change |
|------------------------------------|-----------------|--------------------:|----------|
| fillrandom                         |          531,986 |             532,099 |   +0.02% |
| readrandomwriterandom (50r/30w/20d)|          786,400 |             815,143 |   +3.65% |
| fillrandom (range tombstones)      |          541,656 |             531,008 |   -1.97% |
```

Reviewed By: xingbowang

Differential Revision: D97974456

Pulled By: joshkang97

fbshipit-source-id: ea95f23953fe37771a599aed61ece1a504b12c2e
2026-03-24 17:16:14 -07:00
Xingbo Wang ec31d0074f Accelerate build and test infrastructure (#14478)
Summary:
Reduce `make check` time from ~9.4 minutes to ~3.7 minutes (2.6x) on a 192-core machine, speed up CI by 1.8x across all jobs, and fix ccache hit rates (macOS cmake: 0.51% → 99.80%).

## Changes

### 1. Auto-detect lld linker (`build_detect_platform`)
- Probe for `lld` at configure time; fall back to default `ld.bfd` if unavailable
- Linux-only guard (`TARGET_OS = Linux`)
- **Measured: 12x faster linking** (7.4s → 0.6s per test binary)
- Add `-L/usr/local/lib` for lld library resolution on CI
- Install lld in CI pre-steps action
- Opt out with `ROCKSDB_NO_FAST_LINKER=1`

### 2. Sharded test execution (`Makefile`)

**Before:** `make check` enumerated every individual gtest case via `--gtest_list_tests`, then created a separate shell script for each case with `--gtest_filter=TestName`. For example, `block_based_table_reader_test` has 9,788 parameterized test cases — each spawned its own process, loading a ~50MB binary, initializing gtest, registering all ~10K tests, running exactly ONE, then tearing down. The per-process overhead was ~1.5s, so 9,788 × 1.5s ≈ 15,000s wasted on overhead alone. Across all 302 test binaries this produced 39,467 individual processes.

**After:** Use gtest's built-in sharding (`GTEST_TOTAL_SHARDS`/`GTEST_SHARD_INDEX`) to group ~10 test cases per process. Each shard loads the binary once and runs multiple tests sequentially — eliminating the per-process overhead. The 9,788 cases in `block_based_table_reader_test` become ~980 shards. Total process count drops from 39,467 to ~4,036. Same tests, same coverage, just fewer process spawns.

The shard count adapts to machine size: `min(ceil(test_count / GTEST_SHARD_SIZE), NCORES * 8)`. This ensures large machines (192 cores) get many small shards for parallelism, while small CI runners (4 cores) get fewer larger shards to avoid excessive overhead. Both `GTEST_SHARD_SIZE` (default 10) and `NCORES` can be overridden.

CI sharding uses round-robin distribution across 3 shards to balance heavy tests (db_test, db_compaction_test, etc.) instead of contiguous alphabetical ranges.

Note: gtest continues running all tests after a failure (`GTEST_THROW_ON_FAILURE=0`), so grouping tests does not mask failures — all failures within a shard are reported.

**Measured: 3.6x CPU reduction** (76,121s → 21,033s)

### 3. Fix SyncPoint leaks for test isolation (5 files)
Sharded execution exposed 43 test files that set SyncPoint callbacks but never clean them up. Stale callbacks with captured local variables cause segfaults or data corruption when subsequent tests in the same process trigger them.

**Systematic fix:** Global gtest `TestEventListener` in `testharness.cc` that calls `SyncPoint::DisableProcessing()` + `ClearAllCallBacks()` + `ClearTrace()` + `LoadDependency({})` after every test case. This cleans up all SyncPoint state: callbacks, dependency maps, cleared points, and the point filter. Registered via static initialization — no changes needed to individual test `main()` functions.

**SyncPoint infrastructure fixes:**
- `DisableProcessing()` now calls `cv_.notify_all()` to wake threads blocked in `Process()`. Previously, threads waiting for predecessor sync points that would never fire (because the test ended) would hang forever.
- `Process()` now rechecks `enabled_` after waking from `cv_.wait()`, so threads exit promptly when processing is disabled instead of looping forever.

**Specific fixes (defense-in-depth):**
- `SstFileReaderTest::VerifyNumEntriesCorruption` — leaked `PropertyBlockBuilder::AddTableProperty:Start` callback that corrupted SST entry counts
- `WritePreparedTransactionTest::CommitAndSnapshotDuringCompaction` — leaked `CompactionIterator:AfterInit` callback causing segfault via dangling pointers
- `TransactionTestBase` destructor — cleanup for 26 SyncPoint uses across transaction tests
- `RetriableLogTest` destructor — cleanup for log reader SyncPoint callbacks

### 4. Fix ccache for reliable cross-run caching (`setup-ccache`, `CMakeLists.txt`)
- **`CCACHE_COMPILERCHECK=content`**: Default `mtime` compared compiler binary modification time, which differs on every fresh CI runner → 0% hit rate. `content` hashes compiler output instead — stable across runner instances
- **`CCACHE_SLOPPINESS`**: CMake generates varying `-MF` paths and timestamps. Without sloppiness settings, ccache treated these as different compilations
- **`CMAKE_C/CXX_COMPILER_LAUNCHER`** instead of `RULE_LAUNCH_COMPILE`: Avoids double-wrapping ccache when also injected via PATH. Removes `RULE_LAUNCH_LINK` since ccache cannot cache link operations
- **macOS cmake hit rate: 0.51% → 99.80%**

### 5. Align GTEST_THROW_ON_FAILURE (`Makefile`)
- Set `GTEST_THROW_ON_FAILURE=0` in Makefile default to match CI's `pre-steps/action.yml`
- `=1` caused `std::terminate` in multi-threaded stress tests (e.g., `point_lock_manager_stress_test`, `rate_limiter_test`) when assertions fail — the gtest exception propagates across thread boundaries, triggering undefined behavior and heap-use-after-free under ASAN

### 6. Portability fixes
- `[[maybe_unused]]` instead of `__attribute__((unused))` for MSVC compatibility
- Remove blanket `-fPIC` from `COMMON_FLAGS` — only apply via `PLATFORM_SHARED_CFLAGS` for shared builds, preserving optimal codegen for release/static builds
- `make -s list_all_tests` to suppress make noise in test enumeration

## Results

### Local (192-core devvm, clean build)

| Metric | Before | After | Improvement |
|--------|--------|-------|-------------|
| Build time (clean) | 2m43s | ~2m | lld linker |
| Test wall clock | 400s | 200s | 2.0x |
| Test CPU total | 76,121s | 21,033s | 3.6x |
| Test failures | 0 | 0 | — |
| **Total make check** | **~9.4min** | **3m41s** | **2.6x** |

### CI (GitHub Actions)

| Metric | Before | After | Improvement |
|--------|--------|-------|-------------|
| Aggregate CI time | 15,220s | 8,425s | 1.8x |
| build-linux-arm | 763s | 221s | 3.5x |
| build-linux-release | 616s | 244s | 2.5x |
| build-windows-vs2022 | 2,868s | 1,205s | 2.4x |
| build-linux-java-static | 858s | 335s | 2.6x |

### ccache hit rates (all jobs)

| Job | Before | After |
|-----|--------|-------|
| macOS cmake (0-3) | 0.51% | **99.80%** |
| build-linux | 99.68% | **99.84%** |
| build-linux-clang18-asan-ubsan | 98.58% | **99.84%** |
| build-linux-clang18-mini-tsan | 98.74% | **99.84%** |
| build-windows-vs2022 | 99.22% | **99.83%** |
| All 27 ccache jobs | — | **>94%** |

## Remaining bottleneck

The critical path is now dominated by genuinely slow integration tests:
- `external_sst_file_test` shards: 97–113s each
- `db_test` shards: 109–124s each
- `db_bloom_filter_test`: 103s (non-parallel)

Further improvement would require test optimization or a `make check-fast` target.

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

Test Plan:
- `make check -j192 SKIP_FORMAT_BUCK_CHECKS=1` — all 4,045 shards pass (ASAN+UBSAN), zero failures
- Verified SyncPoint fixes: each crash/corruption reproduces before fix, passes after
- Verified SyncPoint::DisableProcessing wakes blocked threads (previously caused indefinite hangs)
- Verified gtest does NOT stop after failure with `GTEST_THROW_ON_FAILURE=0` — all tests in a shard run and all failures are reported
- CI: all 34 checks pass across two consecutive runs
- ccache: validated hit rates >94% on second run after cache seeding

Reviewed By: hx235

Differential Revision: D97623305

Pulled By: xingbowang

fbshipit-source-id: b299e6d43c8713a9ef2b5659e8bbd74958afe155
2026-03-24 15:39:27 -07:00
Josh Kang 304ae7190a 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-03-24 11:10:42 -07:00
Hui Xiao 9de0ea32e1 Fix crash_test_with_ts failure: disable use_trie_index with timestamps (#14502)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14502

The asan_crash_test_with_ts randomly selected use_trie_index=1 alongside
user_timestamp_size=8. TrieIndexFactory requires plain BytewiseComparator,
but user-defined timestamps wrap it as BytewiseComparator.u64ts, causing
Status::NotSupported errors during flush/compaction.

Fix:
1. Add use_trie_index=0 to ts_params in db_crashtest.py to prevent the
   incompatible combination (matches existing pattern for other features).
2. Add early validation in db_stress_tool.cc to reject this combination
   with a clear error message (defense in depth).

Reviewed By: xingbowang

Differential Revision: D97592648

fbshipit-source-id: 2259f82d95ea8410c4278b52f60983a4b5e84ec2
2026-03-24 09:28:35 -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
408 changed files with 65001 additions and 3368 deletions

No files matched your search

+1 -3
View File
@@ -17,9 +17,7 @@ Checks: "-*,\
cert-err58-cpp,\
cert-msc30-c,\
cert-msc50-cpp,\
clang-analyzer-core.NullDereference,\
clang-analyzer-core.StackAddressEscape,\
clang-analyzer-deadcode.DeadStores,\
clang-analyzer-*,\
clang-diagnostic-*,\
-clang-diagnostic-missing-designated-field-initializers,\
concurrency-mt-unsafe,\
+10 -1
View File
@@ -9,7 +9,16 @@ runs:
steps:
- name: Build folly and dependencies
if: ${{ inputs.cache-hit != 'true' }}
run: make build_folly
run: |
clean_path=()
IFS=: read -ra path_entries <<< "$PATH"
for entry in "${path_entries[@]}"; do
if [[ "$entry" != "/usr/lib/ccache" ]]; then
clean_path+=("$entry")
fi
done
export PATH="$(IFS=:; echo "${clean_path[*]}")"
make build_folly
shell: bash
- name: Skip folly build (using cached version)
if: ${{ inputs.cache-hit == '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' }}
+3
View File
@@ -2,6 +2,9 @@ name: pre-steps
runs:
using: composite
steps:
- name: Install lld linker for faster builds
run: apt-get update -y && apt-get install -y lld 2>/dev/null || true
shell: bash
- name: Setup Environment Variables
run: |-
echo "GTEST_THROW_ON_FAILURE=0" >> "$GITHUB_ENV"
+54
View File
@@ -0,0 +1,54 @@
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
echo "CCACHE_COMPILERCHECK=content" >> $GITHUB_ENV
echo "CCACHE_SLOPPINESS=clang_index_store,file_stat_matches,include_file_ctime,include_file_mtime,ivfsoverlay,pch_defines,modules,system_headers,time_macros" >> $GITHUB_ENV
echo "CCACHE_MAXSIZE=4G" >> $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 and set build marker
run: |
ccache -z
touch "$CCACHE_DIR/.build_marker"
shell: bash
@@ -0,0 +1,15 @@
name: teardown-ccache
description: Trim stale ccache entries and print stats
runs:
using: composite
steps:
- name: Trim and print ccache stats
run: |
if [ -z "$CCACHE_DIR" ]; then
echo "teardown-ccache: CCACHE_DIR not set, skipping (setup-ccache may not have run)"
exit 0
fi
.github/scripts/ccache-trim.sh || true
ccache -s || echo "teardown-ccache: ccache not found, skipping stats"
if: always()
shell: bash
+34 -12
View File
@@ -1,19 +1,31 @@
name: windows-build-steps
inputs:
suite-run:
description: Comma-separated list of test suites to run (empty to skip C++ tests)
required: false
default: arena_test,db_basic_test,db_test,db_test2,db_merge_operand_test,bloom_test,c_test,coding_test,crc32c_test,dynamic_bloom_test,env_basic_test,env_test,hash_test,random_test
run-java:
description: Whether to run Java tests
required: false
default: "true"
runs:
using: composite
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"
key: ccache-windows-${{ github.workflow }}
restore-keys: |
ccache-windows-
- name: Configure ccache
shell: pwsh
run: |
ccache --set-config=base_dir=C:\a\rocksdb\rocksdb
ccache --set-config=hash_dir=false
ccache --set-config=compiler_check=content
- name: Custom steps
env:
THIRDPARTY_HOME: ${{ github.workspace }}/thirdparty
@@ -56,12 +68,22 @@ runs:
msbuild build/rocksdb.sln /m:32 /p:LinkIncremental=false -property:Configuration=Debug -property:Platform=x64
if(!$?) { Exit $LASTEXITCODE }
echo ========================= Test RocksDB =========================
build_tools\run_ci_db_test.ps1 -SuiteRun arena_test,db_basic_test,db_test,db_test2,db_merge_operand_test,bloom_test,c_test,coding_test,crc32c_test,dynamic_bloom_test,env_basic_test,env_test,hash_test,random_test -Concurrency 16
if(!$?) { Exit $LASTEXITCODE }
echo ======================== Test RocksJava ========================
cd build\java
& ctest -C Debug -j 16
if(!$?) { Exit $LASTEXITCODE }
$suiteRun = "${{ inputs.suite-run }}"
if ($suiteRun -ne "") {
$suiteArray = $suiteRun -split ','
build_tools\run_ci_db_test.ps1 -SuiteRun $suiteArray -Concurrency 16
if(!$?) { Exit $LASTEXITCODE }
} else {
echo "Skipping C++ tests (suite-run is empty)"
}
if ("${{ inputs.run-java }}" -eq "true") {
echo ======================== Test RocksJava ========================
cd build\java
& ctest -C Debug -j 16
if(!$?) { Exit $LASTEXITCODE }
} else {
echo "Skipping Java tests"
}
shell: pwsh
- name: Show ccache stats
shell: pwsh
@@ -0,0 +1,27 @@
// Shared markdown builder for AI review comment bodies.
//
// Usage:
// const build = require('./build-ai-review-comment.js');
// return build({ icon, headerTitle, triggerLine, responseBody, footerLines
// });
module.exports = function buildAiReviewComment(
{icon, headerTitle, triggerLine, responseBody, footerLines}) {
return [
`## ${icon} ${headerTitle}`,
'',
triggerLine,
'',
'---',
'',
responseBody,
'',
'---',
'',
'<details>',
'<summary>ℹ️ About this response</summary>',
'',
...footerLines,
'</details>',
].join('\n');
};
+39
View File
@@ -0,0 +1,39 @@
#!/bin/bash
# Trim ccache to keep only entries accessed during the current build.
#
# Usage:
# 1. Before build: touch "$CCACHE_DIR/.build_marker"
# 2. Run build (ccache updates mtime on hits, creates new files for misses)
# 3. After build: .github/scripts/ccache-trim.sh
#
# This removes cache files not accessed during the build (stale entries from
# previous commits). Only intended for CI where each run builds one commit.
# Do NOT use on local builds where multiple worktrees may share the cache.
set -e
CCACHE_DIR="${CCACHE_DIR:?CCACHE_DIR must be set}"
MARKER="$CCACHE_DIR/.build_marker"
if [ ! -f "$MARKER" ]; then
echo "ccache-trim: No .build_marker found, skipping (was the marker created before build?)"
exit 0
fi
# Count files before cleanup
before=$(find "$CCACHE_DIR" -type f \( -name '*R' -o -name '*M' \) | wc -l)
# Delete cache files (results and manifests) older than the marker
find "$CCACHE_DIR" -type f \( -name '*R' -o -name '*M' \) ! -newer "$MARKER" -delete
# Clean up empty directories
find "$CCACHE_DIR" -mindepth 2 -type d -empty -delete 2>/dev/null || true
# Recalculate size counters
ccache -c 2>/dev/null || true
after=$(find "$CCACHE_DIR" -type f \( -name '*R' -o -name '*M' \) | wc -l)
echo "ccache-trim: $before -> $after cache files (removed $((before - after)) stale entries)"
# Clean up marker
rm -f "$MARKER"
+32
View File
@@ -0,0 +1,32 @@
#!/bin/bash
# Compute test shard for parallel CI execution.
# Distributes tests round-robin across N shards for balanced load.
# Outputs ROCKSDBTESTS_SUBSET — the list of test binaries for this shard.
# The Makefile uses this to build and run only the assigned tests.
#
# Usage: compute-test-shard.sh <shard_index> <num_shards>
set -euo pipefail
shard=${1:?Usage: compute-test-shard.sh <shard_index> <num_shards>}
nshards=${2:?Usage: compute-test-shard.sh <shard_index> <num_shards>}
# Get sorted test list (db_test first since it's the heaviest, then alpha)
make -s list_all_tests 2>/dev/null | tr ' ' '\n' | grep '_test$' | sort -u > /tmp/sorted.txt
(echo db_test; grep -v '^db_test$' /tmp/sorted.txt) > /tmp/all_tests.txt
total=$(wc -l < /tmp/all_tests.txt)
# Round-robin: assign test i to shard (i % nshards).
# This spreads heavy tests (which are scattered alphabetically) evenly.
awk -v s="$shard" -v n="$nshards" 'NR > 0 && (NR - 1) % n == s' /tmp/all_tests.txt > /tmp/include.txt
included=$(wc -l < /tmp/include.txt)
first=$(head -1 /tmp/include.txt)
last=$(tail -1 /tmp/include.txt)
# Output space-separated list for ROCKSDBTESTS_SUBSET
subset=$(tr '\n' ' ' < /tmp/include.txt)
echo "subset=${subset}" >> "$GITHUB_OUTPUT"
echo "Shard $shard/$nshards: $included tests (round-robin), first=$first last=$last (total $total)"
+118
View File
@@ -0,0 +1,118 @@
// 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, autoMode, headSha, reviewer, isQuery, isPartial
// }
const fs = require('fs');
const buildComment = require('./build-ai-review-comment.js');
module.exports = function parseClaude({executionFile, conclusion, meta}) {
function getTriggerLine() {
if (meta.trigger !== 'auto') {
return `*Requested by @${meta.reviewer}*`;
}
const shortSha = meta.headSha ? meta.headSha.substring(0, 7) : 'unknown';
if (meta.autoMode === 'early') {
return `*Auto-triggered after CI reached the early-review threshold — reviewing commit ${
shortSha}*`;
}
return `*Auto-triggered after CI passed — reviewing commit ${shortSha}*`;
}
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');
// Helper: extract the last substantial assistant text from the log.
// Used as a fallback when Claude ran out of turns and the recovery
// session also failed to produce a result.
function getLastAssistantText(log, minLength = 200) {
for (let i = log.length - 1; i >= 0; i--) {
const m = log[i];
if (m.type !== 'assistant') continue;
const content = m.message && m.message.content;
if (!Array.isArray(content)) continue;
for (let j = content.length - 1; j >= 0; j--) {
if (content[j].type === 'text' && content[j].text &&
content[j].text.trim().length >= minLength) {
const text = content[j].text.trim();
// Truncate to avoid enormous PR comments
return text.length > 50000 ? text.substring(0, 50000) +
'\n\n*[Truncated — full output in execution log artifact]*' :
text;
}
}
}
return null;
}
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.subtype === 'error_max_turns') {
// The workflow runs a recovery session when this happens, so this
// branch is typically only hit if recovery wasn't attempted (e.g.,
// no findings file was written). Extract what we can.
const partial = getLastAssistantText(executionLog);
if (partial) {
responseBody =
`⚠️ **Review incomplete — Claude hit the turn limit.**\n\nBelow is the last partial output. You can request a fresh review with \`/claude-review\`.\n\n---\n\n${
partial}`;
} else {
responseBody =
'⚠️ **Review incomplete — Claude hit the turn limit before producing output.** You can request a fresh review with `/claude-review`.';
}
} 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 isPartial = !!meta.isPartial;
const icon = isPartial ? '🟡' : (conclusion === 'success' ? '✅' : '⚠️');
const headerTitle = meta.isQuery ? 'Claude Response' : 'Claude Code Review';
const triggerLine = getTriggerLine();
return buildComment({
icon,
headerTitle,
triggerLine,
responseBody,
footerLines: [
'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',
],
});
};
+98
View File
@@ -0,0 +1,98 @@
// Parse Codex review artifacts and produce a markdown review comment.
//
// Usage from actions/github-script:
// const parse = require('./.github/scripts/parse-codex-review.js');
// const markdown = parse({ responseFile, recoveryFile, findingsFile, logFile,
// exitCode, meta });
//
// Parameters:
// responseFile - path to Codex final response output
// recoveryFile - path to recovery output formatted from review-findings.md
// findingsFile - path to incremental findings file written during review
// logFile - path to Codex stdout/stderr log
// exitCode - Codex process exit code
// meta - { trigger, autoMode, headSha, reviewer, isQuery, isPartial }
const fs = require('fs');
const buildComment = require('./build-ai-review-comment.js');
module.exports = function parseCodex(
{responseFile, recoveryFile, findingsFile, logFile, exitCode, meta}) {
function getTriggerLine() {
if (meta.trigger !== 'auto') {
return `*Requested by @${meta.reviewer}*`;
}
const shortSha = meta.headSha ? meta.headSha.substring(0, 7) : 'unknown';
if (meta.autoMode === 'early') {
return `*Auto-triggered after CI reached the early-review threshold — reviewing commit ${
shortSha}*`;
}
return `*Auto-triggered after CI passed — reviewing commit ${shortSha}*`;
}
function readIfPresent(path) {
if (!path || !fs.existsSync(path)) {
return '';
}
const text = fs.readFileSync(path, 'utf8').trim();
return text;
}
function tailFile(path, maxChars = 12000) {
const text = readIfPresent(path);
if (!text) {
return '';
}
if (text.length <= maxChars) {
return text;
}
return text.slice(text.length - maxChars);
}
let responseBody = '';
const recovered = readIfPresent(recoveryFile);
const direct = readIfPresent(responseFile);
const findings = readIfPresent(findingsFile);
if (recovered) {
responseBody = recovered;
} else if (direct) {
responseBody = direct;
} else if (findings) {
responseBody =
'⚠️ **Review incomplete — Codex did not produce a final response.**\n\n' +
'Below are the incremental findings recovered from `review-findings.md`.\n\n---\n\n' +
findings;
} else {
const logTail = tailFile(logFile);
responseBody = logTail ?
`❌ **Codex review failed before producing findings.**\n\n\`\`\`\n${
logTail}\n\`\`\`` :
'❌ Codex review failed before producing any output.';
}
const isPartial = !!meta.isPartial;
const code = Number.parseInt(exitCode || '1', 10);
const icon = isPartial ? '🟡' : (code === 0 ? '✅' : '⚠️');
const triggerLine = getTriggerLine();
return buildComment({
icon,
headerTitle: meta.isQuery ? 'Codex Response' : 'Codex Code Review',
triggerLine,
responseBody,
footerLines: [
'Generated by Codex CLI.',
'Review methodology: `claude_md/code_review.md`',
'',
'**Limitations:**',
'- Codex may miss context from files not in the diff',
'- Large PRs may be truncated',
'- Always apply human judgment to AI suggestions',
'',
'**Commands:**',
'- `/codex-review [context]` — Request a code review',
'- `/codex-query <question>` — Ask about the PR or codebase',
],
});
};
+204
View File
@@ -0,0 +1,204 @@
// 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.
// legacyMarkers - optional list of legacy markers that should be migrated by
// being considered part of the same comment family.
// prunePrefix - optional marker prefix whose older comments should be
// superseded. If obsoleteTitle is set, older comments are
// collapsed instead of deleted.
// preserveLatest - optional count of active comments to keep when
// prunePrefix is set.
// obsoleteMarker - optional HTML marker used to detect already-obsolete
// comments.
// obsoleteTitle - optional heading to use when collapsing superseded
// comments into a details block.
module.exports = async function postPrComment({
github,
context,
core,
prNumber,
body,
marker,
legacyMarkers = [],
prunePrefix = '',
preserveLatest = 0,
obsoleteMarker = '',
obsoleteTitle = '',
}) {
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}`;
async function listComments() {
return await github.paginate(github.rest.issues.listComments, {
owner,
repo,
issue_number: prNumber,
per_page: 100,
});
}
function commentActivityTime(comment) {
const timestamp =
Date.parse(comment.updated_at || comment.created_at || '');
return Number.isNaN(timestamp) ? 0 : timestamp;
}
function commentSortDescending(left, right) {
const timeDelta = commentActivityTime(right) - commentActivityTime(left);
if (timeDelta !== 0) {
return timeDelta;
}
return Number(right.id || 0) - Number(left.id || 0);
}
function isObsoleteComment(comment) {
return !!obsoleteMarker && typeof comment.body === 'string' &&
comment.body.includes(obsoleteMarker);
}
function buildObsoleteBody(comment) {
const originalBody =
typeof comment.body === 'string' && comment.body.trim() ?
comment.body :
'*No original review body preserved.*';
const title = obsoleteTitle || 'AI Review - OBSOLETE';
return `${obsoleteMarker}\n## ${
title}\n\n<details>\n<summary>Superseded by a newer AI review. Expand to see the original review.</summary>\n\n${
originalBody}\n\n</details>`;
}
async function deleteCommentIfPresent(comment, reason) {
try {
await github.rest.issues.deleteComment({
owner,
repo,
comment_id: comment.id,
});
core.info(`${reason} ${comment.id}`);
} catch (error) {
if (error.status === 404) {
core.info(`Comment ${comment.id} was already deleted by another run.`);
return;
}
throw error;
}
}
async function supersedeCommentIfPresent(comment, reason) {
if (!obsoleteTitle) {
await deleteCommentIfPresent(comment, reason);
return;
}
if (isObsoleteComment(comment)) {
return;
}
try {
await github.rest.issues.updateComment({
owner,
repo,
comment_id: comment.id,
body: buildObsoleteBody(comment),
});
core.info(`${reason} ${comment.id}`);
} catch (error) {
if (error.status === 404) {
core.info(`Comment ${comment.id} was already deleted by another run.`);
return;
}
throw error;
}
}
let comments = await listComments();
const existing = comments.find(
comment =>
typeof comment.body === 'string' && comment.body.includes(marker));
let currentCommentId = null;
if (existing) {
try {
const response = await github.rest.issues.updateComment({
owner,
repo,
comment_id: existing.id,
body: markedBody,
});
currentCommentId = existing.id;
core.info(`Updated existing comment ${existing.id}`);
} catch (error) {
if (error.status !== 404) {
throw error;
}
core.info(
`Comment ${existing.id} disappeared before update; ` +
'creating a fresh comment instead.');
}
}
if (!currentCommentId) {
const response = await github.rest.issues.createComment({
owner,
repo,
issue_number: prNumber,
body: markedBody,
});
currentCommentId = response.data.id;
core.info('Created new PR comment');
}
if (prunePrefix || legacyMarkers.length > 0) {
comments = await listComments();
const relatedComments = comments.filter(
comment => comment.id !== currentCommentId &&
typeof comment.body === 'string' &&
((prunePrefix && comment.body.includes(prunePrefix)) ||
legacyMarkers.some(
legacyMarker => comment.body.includes(legacyMarker))));
const activeRelatedComments =
comments
.filter(
comment => typeof comment.body === 'string' &&
!isObsoleteComment(comment) &&
(comment.id === currentCommentId ||
relatedComments.some(
relatedComment => relatedComment.id === comment.id)))
.sort(commentSortDescending);
const keep =
new Set(activeRelatedComments.slice(0, Math.max(preserveLatest, 1))
.map(comment => comment.id));
const supersedeCandidates = obsoleteTitle ?
activeRelatedComments.filter(comment => !keep.has(comment.id)) :
relatedComments.filter(comment => !keep.has(comment.id));
for (const comment of supersedeCandidates) {
if (keep.has(comment.id)) {
continue;
}
await supersedeCommentIfPresent(comment, 'Superseded old AI review');
}
}
};
+347
View File
@@ -0,0 +1,347 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const postPrComment = require('./post-pr-comment.js');
const OBSOLETE_MARKER = '<!-- claude-review-obsolete -->';
const OBSOLETE_TITLE = 'Claude Code Review - OBSOLETE';
function makeComment(id, body, createdAt, updatedAt) {
return {
id,
body,
created_at: createdAt,
updated_at: updatedAt || createdAt,
};
}
function createHarness(initialComments, options = {}) {
let comments = initialComments.map(comment => ({...comment}));
let nextCommentId = options.nextCommentId || 1000;
let paginateCount = 0;
const calls = {
update: [],
create: [],
delete: [],
};
const github = {
paginate: async () => {
paginateCount++;
if (options.onPaginate) {
const updated = options.onPaginate({
paginateCount,
comments: comments.map(comment => ({...comment})),
});
if (updated) {
comments = updated.map(comment => ({...comment}));
}
}
return comments.map(comment => ({...comment}));
},
rest: {
issues: {
listComments: () => {
throw new Error('listComments should only be used through paginate');
},
updateComment: async ({comment_id, body}) => {
calls.update.push({comment_id, body});
const error =
options.updateErrors && options.updateErrors[comment_id];
if (error) {
throw error;
}
const index =
comments.findIndex(comment => comment.id === comment_id);
if (index === -1) {
const notFound = new Error(`Comment ${comment_id} not found`);
notFound.status = 404;
throw notFound;
}
const updated = {
...comments[index],
body,
updated_at: options.updateTimestamp || '2026-04-24T00:00:00Z',
};
comments[index] = updated;
return {data: {...updated}};
},
createComment: async ({issue_number, body}) => {
calls.create.push({issue_number, body});
const created = makeComment(
nextCommentId++, body,
options.createTimestamp || '2026-04-24T00:00:00Z');
comments.push(created);
return {data: {id: created.id}};
},
deleteComment: async ({comment_id}) => {
calls.delete.push({comment_id});
const error =
options.deleteErrors && options.deleteErrors[comment_id];
if (error) {
throw error;
}
comments = comments.filter(comment => comment.id !== comment_id);
},
},
},
};
return {
github,
calls,
getComments: () => comments.map(comment => ({...comment})),
};
}
function createCore() {
return {
info: () => {},
warning: () => {},
};
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}
function assertObsoleteComment(comment, originalBody) {
assert.ok(comment);
assert.match(comment.body, new RegExp(escapeRegExp(OBSOLETE_MARKER)));
assert.match(comment.body, new RegExp(`## ${escapeRegExp(OBSOLETE_TITLE)}`));
assert.match(comment.body, /<details>/);
assert.match(comment.body, /Superseded by a newer AI review/);
assert.match(comment.body, new RegExp(escapeRegExp(originalBody)));
}
const context = {
repo: {
owner: 'facebook',
repo: 'rocksdb',
},
};
test(
'creates a fresh review comment and supersedes legacy comments',
async () => {
const harness = createHarness(
[
makeComment(
1, '<!-- claude-review-auto -->\nold legacy', 'not-a-date'),
makeComment(
2, '<!-- claude-review-auto -->\nnew legacy',
'2026-04-21T00:00:00Z'),
],
{
updateTimestamp: '2026-04-24T00:00:00Z',
});
await postPrComment({
github: harness.github,
context,
core: createCore(),
prNumber: 14659,
body: 'review body',
marker: '<!-- claude-review-auto-run-500 -->',
legacyMarkers: ['<!-- claude-review-auto -->'],
prunePrefix: '<!-- claude-review-auto-',
preserveLatest: 1,
obsoleteMarker: OBSOLETE_MARKER,
obsoleteTitle: OBSOLETE_TITLE,
});
assert.equal(harness.calls.create.length, 1);
assert.deepEqual(
harness.calls.update.map(call => call.comment_id), [2, 1]);
assert.equal(harness.calls.delete.length, 0);
const comments = harness.getComments();
assert.equal(comments.length, 3);
assert.match(
comments.find(comment => comment.id === 1000).body,
/<!-- claude-review-auto-run-500 -->/);
assertObsoleteComment(
comments.find(comment => comment.id === 2),
'<!-- claude-review-auto -->\nnew legacy');
assertObsoleteComment(
comments.find(comment => comment.id === 1),
'<!-- claude-review-auto -->\nold legacy');
});
test(
'updates an exact-match comment and supersedes leftover legacy comments',
async () => {
const harness = createHarness(
[
makeComment(
3, '<!-- claude-review-auto-abcdef0 -->\ncurrent body',
'2026-04-24T00:00:00Z'),
makeComment(
4, '<!-- claude-review-auto -->\nlegacy body',
'2026-04-20T00:00:00Z'),
],
{
updateTimestamp: '2026-04-24T01:00:00Z',
});
await postPrComment({
github: harness.github,
context,
core: createCore(),
prNumber: 14659,
body: 'refreshed body',
marker: '<!-- claude-review-auto-abcdef0 -->',
legacyMarkers: ['<!-- claude-review-auto -->'],
prunePrefix: '<!-- claude-review-auto-',
preserveLatest: 1,
obsoleteMarker: OBSOLETE_MARKER,
obsoleteTitle: OBSOLETE_TITLE,
});
assert.deepEqual(
harness.calls.update.map(call => call.comment_id), [3, 4]);
assert.equal(harness.calls.create.length, 0);
assert.equal(harness.calls.delete.length, 0);
const comments = harness.getComments();
assert.match(
comments.find(comment => comment.id === 3).body,
/<!-- claude-review-auto-abcdef0 -->\nrefreshed body/);
assertObsoleteComment(
comments.find(comment => comment.id === 4),
'<!-- claude-review-auto -->\nlegacy body');
});
test(
'creates a new comment if the target disappears before update',
async () => {
const missing = new Error('gone');
missing.status = 404;
const harness = createHarness(
[
makeComment(
10, '<!-- claude-review-auto-abcdef0 -->\nold body',
'2026-04-20T00:00:00Z'),
],
{
updateErrors: {
10: missing,
},
});
await postPrComment({
github: harness.github,
context,
core: createCore(),
prNumber: 14659,
body: 'replacement body',
marker: '<!-- claude-review-auto-abcdef0 -->',
});
assert.equal(harness.calls.update.length, 1);
assert.equal(harness.calls.create.length, 1);
assert.match(
harness.calls.create[0].body, /<!-- claude-review-auto-abcdef0 -->/);
});
test(
'ignores 404 when superseding a comment already deleted by another run',
async () => {
const missing = new Error('gone');
missing.status = 404;
const harness = createHarness(
[
makeComment(
20, '<!-- claude-review-auto-oldest -->\noldest',
'2026-04-20T00:00:00Z'),
makeComment(
21, '<!-- claude-review-auto-newer -->\nnewer',
'2026-04-21T00:00:00Z'),
],
{
nextCommentId: 30,
createTimestamp: '2026-04-24T00:00:00Z',
deleteErrors: {
20: missing,
},
});
await postPrComment({
github: harness.github,
context,
core: createCore(),
prNumber: 14659,
body: 'fresh body',
marker: '<!-- claude-review-auto-latest -->',
prunePrefix: '<!-- claude-review-auto-',
preserveLatest: 1,
obsoleteMarker: OBSOLETE_MARKER,
obsoleteTitle: OBSOLETE_TITLE,
});
assert.equal(harness.calls.create.length, 1);
assert.equal(harness.calls.delete.length, 0);
assert.deepEqual(
harness.calls.update.map(call => call.comment_id), [21, 20]);
const comments = harness.getComments();
assertObsoleteComment(
comments.find(comment => comment.id === 21),
'<!-- claude-review-auto-newer -->\nnewer');
});
test(
'supersedes the current review when a newer concurrent one appears',
async () => {
const harness = createHarness(
[
makeComment(
40, '<!-- claude-review-auto-current -->\ncurrent',
'2026-04-20T00:00:00Z'),
makeComment(
41, '<!-- claude-review-auto-older -->\nolder',
'2026-04-19T00:00:00Z'),
],
{
updateTimestamp: '2026-04-24T00:00:00Z',
onPaginate: ({paginateCount, comments}) => {
if (paginateCount !== 2) {
return comments;
}
return comments.concat([
makeComment(
42, '<!-- claude-review-auto-newer -->\nnewer',
'2026-04-25T00:00:00Z'),
]);
},
});
await postPrComment({
github: harness.github,
context,
core: createCore(),
prNumber: 14659,
body: 'updated current body',
marker: '<!-- claude-review-auto-current -->',
prunePrefix: '<!-- claude-review-auto-',
preserveLatest: 1,
obsoleteMarker: OBSOLETE_MARKER,
obsoleteTitle: OBSOLETE_TITLE,
});
assert.deepEqual(
harness.calls.update.map(call => call.comment_id), [40, 40, 41]);
assert.equal(harness.calls.delete.length, 0);
const comments = harness.getComments();
assert.match(
comments.find(comment => comment.id === 42).body,
/<!-- claude-review-auto-newer -->\nnewer/);
assertObsoleteComment(
comments.find(comment => comment.id === 40),
'<!-- claude-review-auto-current -->\nupdated current body');
assertObsoleteComment(
comments.find(comment => comment.id === 41),
'<!-- claude-review-auto-older -->\nolder');
});
File diff suppressed because it is too large. Load diff
+185
View File
@@ -0,0 +1,185 @@
# Shared comment-posting workflow for AI reviews.
name: AI Review Comment
on:
workflow_call:
inputs:
provider:
description: Provider key, for example "claude" or "codex"
required: true
type: string
result_artifact_name:
description: Artifact name produced by the analysis workflow
required: true
type: string
comment_file:
description: Markdown comment file produced by the analysis workflow
required: true
type: string
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: ${{ inputs.result_artifact_name }}
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'
env:
PROVIDER: ${{ inputs.provider }}
COMMENT_FILE: ${{ inputs.comment_file }}
RUN_ID: ${{ github.event.workflow_run.id }}
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
if (!fs.existsSync(process.env.COMMENT_FILE) ||
!fs.existsSync('pr_number.txt')) {
core.info('No review results found; skipping.');
return;
}
const post = require('./.github/scripts/post-pr-comment.js');
const provider = process.env.PROVIDER;
const providerTitle = provider === 'codex' ? 'Codex' : 'Claude';
const body = fs.readFileSync(process.env.COMMENT_FILE, 'utf8');
const prNumber = parseInt(
fs.readFileSync('pr_number.txt', 'utf8').trim(),
10
);
const trigger = fs.existsSync('trigger_type.txt') ?
fs.readFileSync('trigger_type.txt', 'utf8').trim() :
'auto';
const headSha = fs.existsSync('head_sha.txt') ?
fs.readFileSync('head_sha.txt', 'utf8').trim() :
'';
const shortSha = headSha ? headSha.substring(0, 7) : '';
const runId = process.env.RUN_ID;
let marker = '';
let legacyMarkers = [];
let prunePrefix = '';
let preserveLatest = 0;
let obsoleteMarker = '';
let obsoleteTitle = '';
if (trigger === 'auto') {
if (!shortSha) {
core.warning(
'Missing head_sha.txt for auto review; skipping comment.'
);
return;
}
marker = `<!-- ${provider}-review-auto-run-${runId} -->`;
legacyMarkers = [`<!-- ${provider}-review-auto -->`];
prunePrefix = `<!-- ${provider}-review-auto-`;
preserveLatest = 1;
obsoleteMarker = `<!-- ${provider}-review-obsolete -->`;
obsoleteTitle = `${providerTitle} Code Review - OBSOLETE`;
} else {
marker = `<!-- ${provider}-review-manual-run-${runId} -->`;
}
await post({
github,
context,
core,
prNumber,
body,
marker,
legacyMarkers,
prunePrefix,
preserveLatest,
obsoleteMarker,
obsoleteTitle,
});
- 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(),
10
);
if (!commentId) return;
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: commentId,
content: 'rocket'
});
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: ${{ inputs.result_artifact_name }}
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(),
10
);
if (!commentId) return;
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: commentId,
content: 'confused'
});
unauthorized-notice:
runs-on: ubuntu-latest
if: >-
github.event.workflow_run.event == 'issue_comment' &&
github.event.workflow_run.conclusion == 'skipped'
steps:
- name: Log skipped unauthorized request
uses: actions/github-script@v7
with:
script: |
core.info(
'Analysis workflow was skipped, which usually means the ' +
'issue_comment requester was not in the authorized list.'
);
+17 -24
View File
@@ -12,14 +12,21 @@ jobs:
if: github.event.workflow_run.event == 'pull_request'
runs-on: ubuntu-latest
steps:
- name: Checkout scripts
uses: actions/checkout@v4
with:
sparse-checkout: .github/scripts
sparse-checkout-cone-mode: true
- name: Download clang-tidy results
id: download
uses: actions/download-artifact@v4.0.0
uses: actions/download-artifact@v4.1.3
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
@@ -32,27 +39,13 @@ jobs:
}
const body = fs.readFileSync('clang-tidy-comment.md', 'utf8');
const prNumber = parseInt(fs.readFileSync('pr_number.txt', 'utf8').trim());
const marker = '<!-- clang-tidy-bot -->';
const { data: comments } = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
const post = require('./.github/scripts/post-pr-comment.js');
await post({
github,
context,
core,
prNumber,
body,
marker: '<!-- clang-tidy-bot -->',
});
const existing = comments.find(c => c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: existing.id,
body,
});
core.info(`Updated existing comment ${existing.id}`);
} else {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: prNumber,
body,
});
core.info('Created new PR comment');
}
+4 -4
View File
@@ -11,7 +11,7 @@ jobs:
runs-on:
labels: 4-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
steps:
- uses: actions/checkout@v4.1.0
with:
@@ -36,14 +36,14 @@ jobs:
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
run: apt-get update && apt-get install -y clang-tidy-21 && ln -sf /usr/bin/clang-tidy-21 /usr/local/bin/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 ..
-DCMAKE_C_COMPILER=clang-21 \
-DCMAKE_CXX_COMPILER=clang++-21 ..
cd ..
ln -sf build/compile_commands.json compile_commands.json
- name: Run clang-tidy on changed files
@@ -0,0 +1,25 @@
# Claude Code Review — Comment Posting Workflow
#
# Thin wrapper around .github/workflows/ai-review-comment.yml so the provider
# specific workflow name stays stable while the shared implementation lives in
# one reusable workflow.
name: Post Claude Review Comment
on:
workflow_run:
workflows: ["Claude Code Review"]
types: [completed]
permissions:
pull-requests: write
issues: write
jobs:
review-comment:
uses: ./.github/workflows/ai-review-comment.yml
with:
provider: claude
result_artifact_name: claude-review-result
comment_file: claude-review-comment.md
secrets: inherit
+69
View File
@@ -0,0 +1,69 @@
# Claude Code Review — Analysis Workflow
#
# Thin wrapper around .github/workflows/ai-review-analysis.yml so provider
# specific triggers and workflow_dispatch inputs stay stable while the shared
# implementation lives in one reusable workflow.
name: Claude Code Review
on:
workflow_run:
workflows: ["facebook/rocksdb/pr-jobs"]
types: [completed]
# The early pull_request_target path is limited to same-repo PRs by the job
# condition below. Fork PRs skip this path and rely on workflow_run instead.
pull_request_target:
types: [opened, reopened, synchronize]
issue_comment:
types: [created]
workflow_dispatch:
inputs:
pr_number:
description: PR number to review
required: true
type: number
model:
description: Claude model to use (defaults to latest Opus)
required: false
type: choice
options:
- claude-opus-4-6
- claude-sonnet-4-6
default: claude-opus-4-6
thinking_budget:
description: Override MAX_THINKING_TOKENS (blank = auto-classify, capped at 24000)
required: false
type: string
default: ""
permissions:
contents: read
# The shared analysis workflow polls check state, inspects prior runs, and
# reads PR metadata. Keep those permissions read-only in the caller.
actions: read
checks: read
pull-requests: read
jobs:
review:
if: >-
github.event_name != 'pull_request_target' ||
github.event.pull_request.head.repo.full_name == github.repository
uses: ./.github/workflows/ai-review-analysis.yml
with:
provider: claude
display_name: Claude
review_command: /claude-review
query_command: /claude-query
result_artifact_name: claude-review-result
comment_file: claude-review-comment.md
default_model: claude-opus-4-6
selected_model: ${{ github.event_name == 'workflow_dispatch' && inputs.model || '' }}
classifier_model: claude-sonnet-4-6
recovery_model: claude-sonnet-4-6
thinking_budget: ${{ github.event_name == 'workflow_dispatch' && inputs.thinking_budget || '' }}
dispatch_pr_number: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || '' }}
secrets: inherit
@@ -0,0 +1,25 @@
# Codex Code Review — Comment Posting Workflow
#
# Thin wrapper around .github/workflows/ai-review-comment.yml so the provider
# specific workflow name stays stable while the shared implementation lives in
# one reusable workflow.
name: Post Codex Review Comment
on:
workflow_run:
workflows: ["Codex Code Review"]
types: [completed]
permissions:
pull-requests: write
issues: write
jobs:
review-comment:
uses: ./.github/workflows/ai-review-comment.yml
with:
provider: codex
result_artifact_name: codex-review-result
comment_file: codex-review-comment.md
secrets: inherit
+68
View File
@@ -0,0 +1,68 @@
# Codex Code Review — Analysis Workflow
#
# Thin wrapper around .github/workflows/ai-review-analysis.yml so provider
# specific triggers and workflow_dispatch inputs stay stable while the shared
# implementation lives in one reusable workflow.
name: Codex Code Review
on:
workflow_run:
workflows: ["facebook/rocksdb/pr-jobs"]
types: [completed]
# The early pull_request_target path is limited to same-repo PRs by the job
# condition below. Fork PRs skip this path and rely on workflow_run instead.
pull_request_target:
types: [opened, reopened, synchronize]
issue_comment:
types: [created]
workflow_dispatch:
inputs:
pr_number:
description: PR number to review
required: true
type: number
model:
description: Codex model to use
required: false
type: choice
options:
- gpt-5.5
- gpt-5.3-codex
- gpt-5.2-codex
default: gpt-5.5
thinking_budget:
description: Override MAX_THINKING_TOKENS (blank = auto-classify)
required: false
type: string
default: ""
permissions:
contents: read
# The shared analysis workflow polls check state, inspects prior runs, and
# reads PR metadata. Keep those permissions read-only in the caller.
actions: read
checks: read
pull-requests: read
jobs:
review:
if: >-
github.event_name != 'pull_request_target' ||
github.event.pull_request.head.repo.full_name == github.repository
uses: ./.github/workflows/ai-review-analysis.yml
with:
provider: codex
display_name: Codex
review_command: /codex-review
query_command: /codex-query
result_artifact_name: codex-review-result
comment_file: codex-review-comment.md
default_model: gpt-5.5
selected_model: ${{ github.event_name == 'workflow_dispatch' && inputs.model || '' }}
thinking_budget: ${{ github.event_name == 'workflow_dispatch' && inputs.thinking_budget || '' }}
dispatch_pr_number: ${{ github.event_name == 'workflow_dispatch' && inputs.pr_number || '' }}
secrets: inherit
+26 -7
View File
@@ -41,22 +41,41 @@ jobs:
- uses: "./.github/actions/pre-steps"
- run: make V=1 -j32 check
- uses: "./.github/actions/post-steps"
build-linux-clang-18-asan-ubsan-with-folly:
build-linux-clang-21-asan-ubsan-with-folly:
if: ${{ github.repository_owner == 'facebook' }}
runs-on:
labels: 16-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
options: --shm-size=16gb
env:
CC: clang-18
CXX: clang++-18
CC: clang-21
CXX: clang++-21
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"
- name: Build folly and dependencies
run: |
clean_path=()
IFS=: read -ra path_entries <<< "$PATH"
for entry in "${path_entries[@]}"; do
if [[ "$entry" != "/usr/lib/ccache" ]]; then
clean_path+=("$entry")
fi
done
export PATH="$(IFS=:; echo "${clean_path[*]}")"
ccache_bin="$(command -v ccache || true)"
if [[ -n "$ccache_bin" && -x "$ccache_bin" ]]; then
mv "$ccache_bin" "${ccache_bin}.disabled"
fi
export CC=gcc
export CXX=g++
export USE_CCACHE=0
export CCACHE_DISABLE=1
make build_folly
shell: bash
- run: LIB_MODE=static USE_CLANG=1 USE_FOLLY=1 COMPILE_WITH_UBSAN=1 COMPILE_WITH_ASAN=1 make -j32 check
- uses: "./.github/actions/post-steps"
build-linux-cmake-with-folly:
@@ -145,13 +164,13 @@ jobs:
runs-on:
labels: 4-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
options: --shm-size=16gb
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- name: Build rocksdb lib
run: CC=clang-18 CXX=clang++-18 USE_CLANG=1 make -j4 static_lib
run: CC=clang-21 CXX=clang++-21 USE_CLANG=1 make -j4 static_lib
- name: Build fuzzers
run: cd fuzz && make sst_file_writer_fuzzer db_fuzzer db_map_fuzzer
- uses: "./.github/actions/post-steps"
+201 -72
View File
@@ -72,6 +72,8 @@ jobs:
run: make check-buck-targets
- name: Simple source code checks
run: make check-sources
- name: Validate GitHub Actions YAML
run: make check-workflow-yaml
- name: Sanity check check_format_compatible.sh
run: |-
export TEST_TMPDIR=/dev/shm/rocksdb
@@ -92,7 +94,12 @@ 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
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
build-linux-cmake-mingw:
if: needs.config.outputs.only_job == 'build-linux-cmake-mingw' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
@@ -100,11 +107,14 @@ jobs:
runs-on:
labels: 4-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
options: --shm-size=16gb
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: cmake-mingw
- run: update-alternatives --set x86_64-w64-mingw32-g++ /usr/bin/x86_64-w64-mingw32-g++-posix
- name: Build cmake-mingw
run: |-
@@ -113,12 +123,14 @@ 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
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
build-linux-make-with-folly:
if: needs.config.outputs.only_job == 'build-linux-make-with-folly' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
runs-on:
labels: 16-core-ubuntu
labels: 32-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
options: --shm-size=16gb
@@ -127,12 +139,17 @@ jobs:
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/cache-getdeps-downloads"
- uses: "./.github/actions/setup-folly"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: make-with-folly
- uses: "./.github/actions/cache-folly"
id: cache-folly
- uses: "./.github/actions/build-folly"
with:
cache-hit: ${{ steps.cache-folly.outputs.cache-hit }}
- run: USE_FOLLY=1 LIB_MODE=static V=1 make -j32 check
- run: USE_FOLLY=1 LIB_MODE=static V=1 make -j64 check
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
build-linux-make-with-folly-lite-no-test:
if: needs.config.outputs.only_job == 'build-linux-make-with-folly-lite-no-test' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
@@ -147,13 +164,18 @@ jobs:
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/cache-getdeps-downloads"
- uses: "./.github/actions/setup-folly"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: make-folly-lite
- run: USE_FOLLY_LITE=1 EXTRA_CXXFLAGS=-DGLOG_USE_GLOG_EXPORT V=1 make -j32 all
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
build-linux-cmake-with-folly-coroutines:
if: needs.config.outputs.only_job == 'build-linux-cmake-with-folly-coroutines' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
runs-on:
labels: 16-core-ubuntu
labels: 32-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
options: --shm-size=16gb
@@ -162,26 +184,45 @@ jobs:
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/cache-getdeps-downloads"
- uses: "./.github/actions/setup-folly"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: cmake-folly-coroutines
- uses: "./.github/actions/cache-folly"
id: cache-folly
- uses: "./.github/actions/build-folly"
with:
cache-hit: ${{ steps.cache-folly.outputs.cache-hit }}
- run: "(mkdir build && cd build && cmake -DUSE_COROUTINES=1 -DWITH_GFLAGS=1 -DROCKSDB_BUILD_SHARED=0 .. && make VERBOSE=1 -j20 && ctest -j20)"
- run: "(mkdir build && cd build && cmake -DUSE_COROUTINES=1 -DWITH_GFLAGS=1 -DROCKSDB_BUILD_SHARED=0 -DPORTABLE=ON .. && make VERBOSE=1 -j64 && ctest -j64)"
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
build-linux-cmake-with-benchmark-no-thread-status:
if: needs.config.outputs.only_job == 'build-linux-cmake-with-benchmark-no-thread-status' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
runs-on:
labels: 16-core-ubuntu
strategy:
fail-fast: false
matrix:
shard: [0, 1, 2, 3]
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"
- 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
- name: Build
run: mkdir build && cd build && cmake -DWITH_GFLAGS=1 -DWITH_BENCHMARK=1 -DPORTABLE=ON -DCMAKE_CXX_FLAGS=-DNROCKSDB_THREAD_STATUS .. && make VERBOSE=1 -j20
- name: Test shard ${{ matrix.shard }} of 4
run: cd build && ctest -j20 -I ${{ matrix.shard }},,4
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
with:
artifact-prefix: "${{ github.job }}-shard${{ matrix.shard }}"
build-linux-encrypted_env-no_compression:
if: needs.config.outputs.only_job == 'build-linux-encrypted_env-no_compression' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
@@ -193,8 +234,13 @@ 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"
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
# ======================== Linux No Test Runs ======================= #
build-linux-release:
@@ -207,6 +253,9 @@ jobs:
options: --shm-size=16gb
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: release
- run: make V=1 -j32 LIB_MODE=shared release
- run: ls librocksdb.so
- run: "./trace_analyzer --version" # A tool dependent on gflags that can run in release build
@@ -223,6 +272,8 @@ jobs:
- run: USE_RTTI=1 make V=1 -j32 release
- run: ls librocksdb.a
- run: if ./trace_analyzer --version; then false; else true; fi
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
build-linux-clang-13-no_test_run:
if: needs.config.outputs.only_job == 'build-linux-clang-13-no_test_run' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
@@ -235,26 +286,36 @@ 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
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
build-linux-clang-18-no_test_run:
if: needs.config.outputs.only_job == 'build-linux-clang-18-no_test_run' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
build-linux-clang-21-no_test_run:
if: needs.config.outputs.only_job == 'build-linux-clang-21-no_test_run' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
runs-on:
labels: 16-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
options: --shm-size=16gb
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- run: CC=clang-18 CXX=clang++-18 USE_CLANG=1 make -j32 all microbench
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: clang-21
- run: CC=clang-21 CXX=clang++-21 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
- run: CC=clang-21 CXX=clang++-21 USE_CLANG=1 DEBUG_LEVEL=0 make -j32 release
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
build-linux-gcc-14-no_test_run:
if: needs.config.outputs.only_job == 'build-linux-gcc-14-no_test_run' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
@@ -262,35 +323,20 @@ jobs:
runs-on:
labels: 16-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
options: --shm-size=16gb
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
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
# ======================== Linux Other Checks ======================= #
build-linux-clang18-clang-analyze:
if: needs.config.outputs.only_job == 'build-linux-clang18-clang-analyze' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
runs-on:
labels: 16-core-ubuntu
container:
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
options: --shm-size=16gb
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- run: CC=clang-18 CXX=clang++-18 ROCKSDB_DISABLE_ALIGNED_NEW=1 CLANG_ANALYZER="/usr/bin/clang++-18" CLANG_SCAN_BUILD=scan-build-18 USE_CLANG=1 make V=1 -j32 analyze
- uses: "./.github/actions/post-steps"
- name: compress test report
run: tar -cvzf scan_build_report.tar.gz scan_build_report
if: failure()
- uses: actions/upload-artifact@v4.0.0
with:
name: scan-build-report
path: scan_build_report.tar.gz
build-linux-unity-and-headers:
if: needs.config.outputs.only_job == 'build-linux-unity-and-headers' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
@@ -303,50 +349,98 @@ 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
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
build-linux-mini-crashtest:
if: needs.config.outputs.only_job == 'build-linux-mini-crashtest' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
runs-on:
labels: 4-core-ubuntu
strategy:
fail-fast: false
matrix:
include:
- crash_test_target: blackbox_crash_test_with_atomic_flush
crash_duration: 480
- crash_test_target: blackbox_crash_test
crash_duration: 240
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"
- 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
- 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=${{ matrix.crash_duration }} --max_key=2500000' ${{ matrix.crash_test_target }}
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
with:
artifact-prefix: "${{ github.job }}-${{ matrix.crash_test_target }}"
# ======================= Linux with Sanitizers ===================== #
build-linux-clang18-asan-ubsan:
if: needs.config.outputs.only_job == 'build-linux-clang18-asan-ubsan' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
build-linux-clang21-asan-ubsan:
if: needs.config.outputs.only_job == 'build-linux-clang21-asan-ubsan' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
runs-on:
labels: 32-core-ubuntu
strategy:
fail-fast: false
matrix:
shard: [0, 1, 2]
container:
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
options: --shm-size=16gb
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- 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
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: clang21-asan-ubsan
- run: COMPILE_WITH_ASAN=1 COMPILE_WITH_UBSAN=1 CC=clang-21 CXX=clang++-21 ROCKSDB_DISABLE_ALIGNED_NEW=1 USE_CLANG=1 make V=1 -j40 check
env:
CI_SHARD_INDEX: ${{ matrix.shard }}
CI_TOTAL_SHARDS: 3
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
build-linux-clang18-mini-tsan:
if: needs.config.outputs.only_job == 'build-linux-clang18-mini-tsan' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
with:
artifact-prefix: "${{ github.job }}-shard${{ matrix.shard }}"
build-linux-clang21-mini-tsan:
if: needs.config.outputs.only_job == 'build-linux-clang21-mini-tsan' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
runs-on:
labels: 32-core-ubuntu
strategy:
fail-fast: false
matrix:
shard: [0, 1, 2]
container:
image: ghcr.io/facebook/rocksdb_ubuntu:24.0
image: ghcr.io/facebook/rocksdb_ubuntu:24.1
options: --shm-size=16gb
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- run: COMPILE_WITH_TSAN=1 CC=clang-18 CXX=clang++-18 ROCKSDB_DISABLE_ALIGNED_NEW=1 USE_CLANG=1 make V=1 -j32 check
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: clang21-tsan
- run: COMPILE_WITH_TSAN=1 CC=clang-21 CXX=clang++-21 ROCKSDB_DISABLE_ALIGNED_NEW=1 USE_CLANG=1 make V=1 -j32 check
env:
CI_SHARD_INDEX: ${{ matrix.shard }}
CI_TOTAL_SHARDS: 3
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
with:
artifact-prefix: "${{ github.job }}-shard${{ matrix.shard }}"
build-linux-static_lib-alt_namespace-status_checked:
if: needs.config.outputs.only_job == 'build-linux-static_lib-alt_namespace-status_checked' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
@@ -358,7 +452,12 @@ 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
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
# ========================= MacOS build only ======================== #
build-macos:
@@ -372,11 +471,16 @@ 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
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
# ========================= MacOS with Tests ======================== #
build-macos-cmake:
@@ -391,6 +495,9 @@ jobs:
- uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: 16.4.0
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: macos-cmake
- uses: "./.github/actions/increase-max-open-files-on-macos"
- uses: "./.github/actions/install-gflags-on-macos"
- uses: "./.github/actions/pre-steps-macos"
@@ -410,6 +517,8 @@ 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 }}
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
# ======================== Windows with Tests ======================= #
# NOTE: some windows jobs are in "nightly" to save resources
@@ -417,12 +526,28 @@ jobs:
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
strategy:
fail-fast: false
matrix:
include:
- test_shard: db_test
suite_run: db_test
run_java: "false"
- test_shard: other
suite_run: arena_test,db_basic_test,db_test2,db_merge_operand_test,bloom_test,c_test,coding_test,crc32c_test,dynamic_bloom_test,env_basic_test,env_test,hash_test,random_test
run_java: "false"
- test_shard: java
suite_run: ""
run_java: "true"
env:
CMAKE_GENERATOR: Visual Studio 17 2022
CMAKE_PORTABLE: 1
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/windows-build-steps"
with:
suite-run: ${{ matrix.suite_run }}
run-java: ${{ matrix.run_java }}
# ============================ Java Jobs ============================ #
build-linux-java:
if: needs.config.outputs.only_job == 'build-linux-java' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
@@ -433,29 +558,20 @@ jobs:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
options: --shm-size=16gb
steps:
# The docker image is intentionally based on an OS that has an older GLIBC version.
# That GLIBC is incompatibile with GitHub's actions/checkout. Thus we implement a manual checkout step.
# NOTE: replaced evolvedbinary/rocksjava:centos7_x64-be with ghcr.io/facebook/rocksdb_ubuntu:22.1
# until a more appropriate docker image with C++20 support is made.
- name: Checkout
env:
GH_TOKEN: ${{ github.token }}
run: |
chown `whoami` . || true
git clone --no-checkout https://oath2:$GH_TOKEN@github.com/${{ github.repository }}.git .
git -c protocol.version=2 fetch --update-head-ok --no-tags --prune --no-recurse-submodules --depth=1 origin +${{ github.sha }}:${{ github.ref }}
git checkout --progress --force ${{ github.ref }}
git log -1 --format='%H'
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: java
- name: Set Java Environment
run: |-
echo "JAVA_HOME=${JAVA_HOME}"
which java && java -version
which javac && javac -version
- name: Test RocksDBJava
# NOTE: replaced scl enable devtoolset-7 'make V=1 J=8 -j8 jtest'
run: make V=1 J=8 -j8 jtest
# post-steps skipped because of compatibility issues with docker image
- uses: "./.github/actions/teardown-ccache"
if: always()
build-linux-java-static:
if: needs.config.outputs.only_job == 'build-linux-java-static' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
@@ -465,29 +581,20 @@ jobs:
image: ghcr.io/facebook/rocksdb_ubuntu:22.1
options: --shm-size=16gb
steps:
# The docker image is intentionally based on an OS that has an older GLIBC version.
# That GLIBC is incompatibile with GitHub's actions/checkout. Thus we implement a manual checkout step.
# NOTE: replaced evolvedbinary/rocksjava:centos7_x64-be with ghcr.io/facebook/rocksdb_ubuntu:22.1
# until a more appropriate docker image with C++20 support is made.
- name: Checkout
env:
GH_TOKEN: ${{ github.token }}
run: |
chown `whoami` . || true
git clone --no-checkout https://oath2:$GH_TOKEN@github.com/${{ github.repository }}.git .
git -c protocol.version=2 fetch --update-head-ok --no-tags --prune --no-recurse-submodules --depth=1 origin +${{ github.sha }}:${{ github.ref }}
git checkout --progress --force ${{ github.ref }}
git log -1 --format='%H'
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: java-static
- name: Set Java Environment
run: |-
echo "JAVA_HOME=${JAVA_HOME}"
which java && java -version
which javac && javac -version
- name: Build RocksDBJava Static Library
# NOTE: replaced scl enable devtoolset-7 'make V=1 J=8 -j8 rocksdbjavastatic'
run: make V=1 J=8 -j8 rocksdbjavastatic
# post-steps skipped because of compatibility issues with docker image
- uses: "./.github/actions/teardown-ccache"
if: always()
build-macos-java:
if: needs.config.outputs.only_job == 'build-macos-java' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
needs: config
@@ -500,6 +607,9 @@ jobs:
- uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: 16.4.0
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: macos-java
- uses: "./.github/actions/increase-max-open-files-on-macos"
- uses: "./.github/actions/install-gflags-on-macos"
- uses: "./.github/actions/install-jdk8-on-macos"
@@ -511,6 +621,8 @@ jobs:
which javac && javac -version
- name: Test RocksDBJava
run: make V=1 J=16 -j16 jtest
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
build-macos-java-static:
if: needs.config.outputs.only_job == 'build-macos-java-static' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
@@ -523,6 +635,9 @@ jobs:
- uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: 16.4.0
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: macos-java-static
- uses: "./.github/actions/increase-max-open-files-on-macos"
- uses: "./.github/actions/install-gflags-on-macos"
- uses: "./.github/actions/install-jdk8-on-macos"
@@ -534,6 +649,8 @@ jobs:
which javac && javac -version
- name: Build RocksDBJava x86 and ARM Static Libraries
run: make V=1 J=16 -j16 rocksdbjavastaticosx
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
build-macos-java-static-universal:
if: needs.config.outputs.only_job == 'build-macos-java-static-universal' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
@@ -546,6 +663,9 @@ jobs:
- uses: maxim-lobanov/setup-xcode@v1.6.0
with:
xcode-version: 16.4.0
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: macos-java-static-universal
- uses: "./.github/actions/increase-max-open-files-on-macos"
- uses: "./.github/actions/install-gflags-on-macos"
- uses: "./.github/actions/install-jdk8-on-macos"
@@ -557,6 +677,8 @@ jobs:
which javac && javac -version
- name: Build RocksDBJava Universal Binary Static Library
run: make V=1 J=16 -j16 rocksdbjavastaticosx_ub
- uses: "./.github/actions/teardown-ccache"
if: always()
- uses: "./.github/actions/post-steps"
build-linux-java-pmd:
if: needs.config.outputs.only_job == 'build-linux-java-pmd' || (needs.config.outputs.only_job == '' && github.repository_owner == 'facebook')
@@ -593,6 +715,13 @@ jobs:
steps:
- uses: actions/checkout@v4.1.0
- uses: "./.github/actions/pre-steps"
- run: sudo apt-get update && sudo apt-get install -y build-essential
- run: sudo apt-get update && sudo apt-get install -y build-essential ccache
- uses: "./.github/actions/setup-ccache"
with:
cache-key-prefix: arm
- run: ROCKSDBTESTS_PLATFORM_DEPENDENT=only make V=1 J=4 -j4 all_but_some_tests check_some
- name: Print ccache stats
run: ccache -s
if: always()
shell: bash
- uses: "./.github/actions/post-steps"
+8
View File
@@ -102,3 +102,11 @@ cmake-build-*
third-party/folly/
.cache
*.sublime-*
# Claude Code local settings
.claude/settings.local.json
tools/__pycache__/
# Keep documentation trackable even if broader ignores match names like "*_test".
!docs/
!docs/**
+9
View File
@@ -0,0 +1,9 @@
# Agent Instructions
This repository's authoritative agent instructions live in `CLAUDE.md`.
Read and follow [`CLAUDE.md`](./CLAUDE.md) in full before making changes or
reviewing code in this checkout.
If there is any ambiguity between this file and `CLAUDE.md`, `CLAUDE.md` takes
precedence.
+59 -8
View File
@@ -32,12 +32,14 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
"db/blob/blob_file_cache.cc",
"db/blob/blob_file_garbage.cc",
"db/blob/blob_file_meta.cc",
"db/blob/blob_file_partition_manager.cc",
"db/blob/blob_file_reader.cc",
"db/blob/blob_garbage_meter.cc",
"db/blob/blob_log_format.cc",
"db/blob/blob_log_sequential_reader.cc",
"db/blob/blob_log_writer.cc",
"db/blob/blob_source.cc",
"db/blob/blob_write_batch_transformer.cc",
"db/blob/prefetch_buffer_collection.cc",
"db/builder.cc",
"db/c.cc",
@@ -106,8 +108,10 @@ 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/read_path_blob_resolver.cc",
"db/wide/wide_column_serialization.cc",
"db/wide/wide_columns.cc",
"db/wide/wide_columns_helper.cc",
@@ -207,6 +211,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",
@@ -329,6 +334,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
"utilities/secondary_index/simple_secondary_index.cc",
"utilities/simulator_cache/cache_simulator.cc",
"utilities/simulator_cache/sim_cache.cc",
"utilities/sorted_run_builder/sorted_run_builder.cc",
"utilities/table_properties_collectors/compact_for_tiering_collector.cc",
"utilities/table_properties_collectors/compact_on_deletion_collector.cc",
"utilities/trace/file_trace_reader_writer.cc",
@@ -362,6 +368,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",
@@ -369,10 +378,10 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
"utilities/write_batch_with_index/write_batch_with_index_internal.cc",
], deps=[
"//folly/container:f14_hash",
"//folly/experimental/coro:blocking_wait",
"//folly/experimental/coro:collect",
"//folly/experimental/coro:coroutine",
"//folly/experimental/coro:task",
"//folly/coro:blocking_wait",
"//folly/coro:collect",
"//folly/coro:coroutine",
"//folly/coro:task",
"//folly/synchronization:distributed_mutex",
], headers=glob(["**/*.h"]), link_whole=False, extra_test_libs=False)
@@ -4799,6 +4808,12 @@ cpp_unittest_wrapper(name="db_blob_corruption_test",
extra_compiler_flags=[])
cpp_unittest_wrapper(name="db_blob_direct_write_test",
srcs=["db/blob/db_blob_direct_write_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="db_blob_index_test",
srcs=["db/blob/db_blob_index_test.cc"],
deps=[":rocksdb_test_lib"],
@@ -4937,6 +4952,12 @@ cpp_unittest_wrapper(name="db_merge_operator_test",
extra_compiler_flags=[])
cpp_unittest_wrapper(name="db_open_with_config_test",
srcs=["db/db_open_with_config_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="db_options_test",
srcs=["db/db_options_test.cc"],
deps=[":rocksdb_test_lib"],
@@ -5027,6 +5048,12 @@ cpp_unittest_wrapper(name="db_wide_basic_test",
extra_compiler_flags=[])
cpp_unittest_wrapper(name="db_wide_blob_direct_write_test",
srcs=["db/wide/db_wide_blob_direct_write_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="db_with_timestamp_basic_test",
srcs=["db/db_with_timestamp_basic_test.cc"],
deps=[":rocksdb_test_lib"],
@@ -5137,6 +5164,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"],
@@ -5521,6 +5554,12 @@ cpp_unittest_wrapper(name="slice_transform_test",
extra_compiler_flags=[])
cpp_unittest_wrapper(name="sorted_run_builder_test",
srcs=["utilities/sorted_run_builder/sorted_run_builder_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="sst_dump_test",
srcs=["tools/sst_dump_test.cc"],
deps=[":rocksdb_test_lib"],
@@ -5623,6 +5662,18 @@ cpp_unittest_wrapper(name="transaction_test",
extra_compiler_flags=[])
cpp_unittest_wrapper(name="trie_index_db_test",
srcs=["utilities/trie_index/trie_index_db_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="trie_index_test",
srcs=["utilities/trie_index/trie_index_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="ttl_test",
srcs=["utilities/ttl/ttl_test.cc"],
deps=[":rocksdb_test_lib"],
@@ -5725,14 +5776,14 @@ cpp_unittest_wrapper(name="write_controller_test",
extra_compiler_flags=[])
cpp_unittest_wrapper(name="write_prepared_transaction_test",
srcs=["utilities/transactions/write_prepared_transaction_test.cc"],
cpp_unittest_wrapper(name="write_prepared_transaction_seqno_test",
srcs=["utilities/transactions/write_prepared_transaction_seqno_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="write_prepared_transaction_test_seqno",
srcs=["utilities/transactions/write_prepared_transaction_test_seqno.cc"],
cpp_unittest_wrapper(name="write_prepared_transaction_test",
srcs=["utilities/transactions/write_prepared_transaction_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
+19
View File
@@ -227,6 +227,11 @@ The following patterns emerged as frequent sources of review feedback:
* 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
* After writing a test, stress-test for flakiness:
```bash
COERCE_CONTEXT_SWITCH=1 make {test_binary}
./{test_binary} --gtest_filter="*YourTestName*" --gtest_repeat=5
```
### Unit test dedup guidelines
* Extract helper functions for repeated patterns such as object
@@ -250,6 +255,9 @@ The following patterns emerged as frequent sources of review feedback:
### Adding new option
Refer to claude_md/add_option.md
### Removing deprecated option
Refer to claude_md/remove_option.md
### Metrics
* When adding a new feature, evaluate whether there is opportunity to add
metrics. Try to avoid causing performance regression on hot path when adding
@@ -258,6 +266,14 @@ The following patterns emerged as frequent sources of review feedback:
### Stress test
* When adding a new feature, make sure stress test covers the new option.
### Component docs
* For component-level design notes and implementation walkthroughs, start with
`docs/components/index.md`.
* Documentation under `docs/components/` is organized by subsystem in
`docs/components/<area>/`.
* Each subsystem directory should have an `index.md` entry point plus focused
chapter files for deeper topics.
### DB bench update
* When adding a performance related feature, support it in db_bench
@@ -271,6 +287,9 @@ The following patterns emerged as frequent sources of review feedback:
* 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.
* Run `ASSERT_STATUS_CHECKED=1 make check` to verify all Status objects are
properly checked. This catches missing error handling that can lead to
silent data corruption.
### Monitoring make check progress
* Use `make check-progress` to get machine-parseable JSON progress while
+78 -5
View File
@@ -82,10 +82,15 @@ if(NOT CMAKE_BUILD_TYPE)
endif()
message(STATUS "CMAKE_BUILD_TYPE is set to ${CMAKE_BUILD_TYPE}")
# Use ccache for compilation if available. Use CMAKE_C/CXX_COMPILER_LAUNCHER
# instead of RULE_LAUNCH_COMPILE to avoid double-wrapping when ccache is also
# injected via PATH (e.g., /usr/lib/ccache or brew --prefix ccache/libexec).
# Note: we intentionally do NOT set RULE_LAUNCH_LINK because ccache cannot
# cache link operations -- it only adds overhead.
find_program(CCACHE_FOUND ccache)
if(CCACHE_FOUND)
set_property(GLOBAL PROPERTY RULE_LAUNCH_COMPILE ccache)
set_property(GLOBAL PROPERTY RULE_LAUNCH_LINK ccache)
set(CMAKE_C_COMPILER_LAUNCHER ccache)
set(CMAKE_CXX_COMPILER_LAUNCHER ccache)
endif(CCACHE_FOUND)
option(WITH_JEMALLOC "build with JeMalloc" OFF)
@@ -515,6 +520,15 @@ elseif(CMAKE_SYSTEM_NAME MATCHES "iOS")
add_definitions(-DOS_MACOSX -DIOS_CROSS_COMPILE)
elseif(CMAKE_SYSTEM_NAME MATCHES "Linux")
add_definitions(-DOS_LINUX)
# Use lld linker if available for faster linking (12x faster than ld.bfd)
if(NOT ROCKSDB_NO_FAST_LINKER)
execute_process(COMMAND ${CMAKE_CXX_COMPILER} -fuse-ld=lld -Wl,--version
OUTPUT_VARIABLE LLD_VERSION_OUTPUT ERROR_QUIET RESULT_VARIABLE LLD_RESULT)
if(LLD_RESULT EQUAL 0 AND LLD_VERSION_OUTPUT MATCHES "LLD")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fuse-ld=lld")
set(CMAKE_SHARED_LINKER_FLAGS "${CMAKE_SHARED_LINKER_FLAGS} -fuse-ld=lld")
endif()
endif()
elseif(CMAKE_SYSTEM_NAME MATCHES "SunOS")
add_definitions(-DOS_SOLARIS)
elseif(CMAKE_SYSTEM_NAME MATCHES "kFreeBSD")
@@ -660,10 +674,38 @@ if(USE_FOLLY)
endif()
endif()
# Folly itself uses gflags transitively, but RocksDB tools and benchmarks
# also call gflags APIs directly, so they need an explicit link dependency.
if(WITH_GFLAGS)
if(NOT TARGET gflags::gflags AND NOT TARGET gflags_shared AND
NOT gflags_LIBRARIES)
find_package(gflags CONFIG QUIET)
if(NOT gflags_FOUND)
find_package(gflags REQUIRED)
endif()
endif()
if(TARGET gflags::gflags)
set(GFLAGS_LIB gflags::gflags)
elseif(TARGET gflags_shared)
set(GFLAGS_LIB gflags_shared)
elseif(DEFINED GFLAGS_TARGET AND TARGET ${GFLAGS_TARGET})
set(GFLAGS_LIB ${GFLAGS_TARGET})
elseif(gflags_LIBRARIES)
set(GFLAGS_LIB ${gflags_LIBRARIES})
else()
message(FATAL_ERROR
"WITH_GFLAGS is enabled, but no gflags library could be resolved")
endif()
list(APPEND THIRDPARTY_LIBS ${GFLAGS_LIB})
endif()
add_compile_definitions(USE_FOLLY FOLLY_NO_CONFIG HAVE_CXX11_ATOMIC)
list(APPEND THIRDPARTY_LIBS Folly::folly)
set(FOLLY_LIBS Folly::folly)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--copy-dt-needed-entries")
# --copy-dt-needed-entries is ld.bfd-specific; lld handles DT_NEEDED transitively
if(NOT CMAKE_EXE_LINKER_FLAGS MATCHES "fuse-ld=lld")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--copy-dt-needed-entries")
endif()
endif()
find_package(Threads REQUIRED)
@@ -690,6 +732,7 @@ set(SOURCES
db/blob/blob_file_addition.cc
db/blob/blob_file_builder.cc
db/blob/blob_file_cache.cc
db/blob/blob_file_partition_manager.cc
db/blob/blob_file_garbage.cc
db/blob/blob_file_meta.cc
db/blob/blob_file_reader.cc
@@ -698,6 +741,7 @@ set(SOURCES
db/blob/blob_log_sequential_reader.cc
db/blob/blob_log_writer.cc
db/blob/blob_source.cc
db/blob/blob_write_batch_transformer.cc
db/blob/prefetch_buffer_collection.cc
db/builder.cc
db/c.cc
@@ -766,8 +810,10 @@ 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/read_path_blob_resolver.cc
db/wide/wide_column_serialization.cc
db/wide/wide_columns.cc
db/wide/wide_columns_helper.cc
@@ -842,6 +888,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
@@ -948,6 +995,7 @@ set(SOURCES
utilities/cassandra/merge_operator.cc
utilities/checkpoint/checkpoint_impl.cc
utilities/compaction_filters.cc
utilities/sorted_run_builder/sorted_run_builder.cc
utilities/compaction_filters/remove_emptyvalue_compactionfilter.cc
utilities/counted_fs.cc
utilities/debug.cc
@@ -999,6 +1047,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
@@ -1114,8 +1165,23 @@ if(USE_FOLLY_LITE)
FMT_INCLUDE_DIR)
include_directories(${FMT_INCLUDE_DIR})
exec_program(python3 ${PROJECT_SOURCE_DIR}/third-party/folly ARGS
build/fbcode_builder/getdeps.py show-inst-dir glog OUTPUT_VARIABLE
GLOG_INST_PATH)
if(EXISTS ${GLOG_INST_PATH}/lib64)
set(GLOG_LIB_DIR ${GLOG_INST_PATH}/lib64)
else()
set(GLOG_LIB_DIR ${GLOG_INST_PATH}/lib)
endif()
add_definitions(-DUSE_FOLLY -DFOLLY_NO_CONFIG)
list(APPEND THIRDPARTY_LIBS glog)
find_library(GLOG_LIBRARY NAMES glog PATHS ${GLOG_LIB_DIR} NO_DEFAULT_PATH)
if(NOT GLOG_LIBRARY)
find_library(GLOG_LIBRARY NAMES glog)
endif()
if(GLOG_LIBRARY)
list(APPEND THIRDPARTY_LIBS ${GLOG_LIBRARY})
endif()
endif()
set(ROCKSDB_STATIC_LIB rocksdb${ARTIFACT_SUFFIX})
@@ -1361,6 +1427,7 @@ if(WITH_TESTS)
db/blob/blob_garbage_meter_test.cc
db/blob/blob_source_test.cc
db/blob/db_blob_basic_test.cc
db/blob/db_blob_direct_write_test.cc
db/blob/db_blob_compaction_test.cc
db/blob/db_blob_corruption_test.cc
db/blob/db_blob_index_test.cc
@@ -1398,6 +1465,7 @@ if(WITH_TESTS)
db/db_memtable_test.cc
db/db_merge_operator_test.cc
db/db_merge_operand_test.cc
db/db_open_with_config_test.cc
db/db_options_test.cc
db/db_properties_test.cc
db/db_range_del_test.cc
@@ -1450,6 +1518,7 @@ if(WITH_TESTS)
db/wal_manager_test.cc
db/wal_edit_test.cc
db/wide/db_wide_basic_test.cc
db/wide/db_wide_blob_direct_write_test.cc
db/wide/wide_column_serialization_test.cc
db/wide/wide_columns_helper_test.cc
db/write_batch_test.cc
@@ -1530,7 +1599,9 @@ if(WITH_TESTS)
utilities/cassandra/cassandra_row_merge_test.cc
utilities/cassandra/cassandra_serialize_test.cc
utilities/checkpoint/checkpoint_test.cc
utilities/sorted_run_builder/sorted_run_builder_test.cc
utilities/env_timed_test.cc
utilities/fault_injection_fs_test.cc
utilities/memory/memory_test.cc
utilities/merge_operators/string_append/stringappend_test.cc
utilities/object_registry_test.cc
@@ -1548,10 +1619,12 @@ if(WITH_TESTS)
utilities/transactions/lock/point/point_lock_manager_stress_test.cc
utilities/transactions/write_committed_transaction_ts_test.cc
utilities/transactions/write_prepared_transaction_test.cc
utilities/transactions/write_prepared_transaction_test_seqno.cc
utilities/transactions/write_prepared_transaction_seqno_test.cc
utilities/transactions/write_unprepared_transaction_test.cc
utilities/transactions/lock/range/range_locking_test.cc
utilities/transactions/timestamped_snapshot_test.cc
utilities/trie_index/trie_index_db_test.cc
utilities/trie_index/trie_index_test.cc
utilities/ttl/ttl_test.cc
utilities/types_util_test.cc
utilities/util_merge_operators_test.cc
+33 -8
View File
@@ -1,17 +1,42 @@
# Rocksdb Change Log
> NOTE: Entries for next release do not go here. Follow instructions in `unreleased_history/README.txt`
## 11.0.4 (03/11/2026)
### Public API Changes
* Re-remove deprecated DB::Open raw pointer variants in db.h
## 11.2.0 (04/18/2026)
### New Features
* Added experimental `DBOptions::fast_sst_open` option. When enabled, RocksDB retrieves opaque file system metadata for SST files after flush, compaction, and external file ingestion, persists it in the MANIFEST, and passes it back to the file system on subsequent file opens to accelerate DB open time.
* Added new option `min_tombstones_for_range_conversion` in `AdvancedColumnFamilyOptions`. When set to a non-zero value N, forward or reverse iteration will convert N or more contiguous point tombstones into a range tombstone in the mutable memtable. Future read operations will then be able to benefit from range tombstone optimizations. There are some limitations when it comes to table_filters, prefix_filters, and UDTs. See header comments for more details.
* Added read-triggered compaction: a new column family option `read_triggered_compaction_threshold` (default 0, disabled) that marks SST files for compaction when their read frequency (`num_collapsible_entry_reads_sampled / file_size`) exceeds the threshold. This helps reduce read amplification for frequently-read ("hot") keys. A new DB option `max_compaction_trigger_wakeup_seconds` (default 43200s / 12 hours) controls the maximum interval for periodic compaction score re-evaluation, which is necessary for this feature to work on quiet (no-write) databases.
## 11.0.3 (03/03/2026)
### Public API Changes
* Relax option sanitization for kv ratio compaction
* Added new `WideColumnBlobResolver` interface and `CompactionFilter::FilterV4()` method, including `ResolveColumn()` / `ResolveColumns()` helpers for lazy loading blob column values in wide-column entities during compaction. This allows compaction filters to resolve blob values on-demand, avoiding unnecessary I/O for blob columns they don't need to access.
* 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.
## 11.0.2 (02/26/2026)
### Public API Changes
* Revert removing deprecated DB::Open raw pointer variants in db.h
### Behavior Changes
* Prefix filter changes - when seeking to a key that is out of domain, and total_order_seek is false, total_order_seek is treated as if it were true. When prefix_same_as_start = true, now iterating past a key that is out of domain invalidates the iterator. The existing behavior does not check for InDomain in the DBIter, so Transform() can produce undefined behavior (e.g. key of size 3 on FixedPrefixTransform(4)).
* Wide-column entities with blob-backed columns now use a new V2 on-disk encoding; older RocksDB versions that do not support wide-column blob separation will reject DBs or SSTs containing those entities.
### Bug Fixes
* Fix blob garbage accounting for blob direct-write flushes so flush-time filtering and overwrite elision correctly register obsolete blob bytes in blob metadata.
* 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.
* Fix MultiScan to fall back to synchronous coalesced reads when async I/O is unsupported at runtime.
## 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
+130 -23
View File
@@ -384,6 +384,10 @@ endif
# TSAN doesn't work well with jemalloc. If we're compiling with TSAN, we should use regular malloc.
ifdef COMPILE_WITH_TSAN
DISABLE_JEMALLOC=1
# Use a suppressions file instead of the process-wide TSAN default
# suppressions hook, which belongs to the final application.
TSAN_OPTIONS?=suppressions=$(CURDIR)/tools/tsan_suppressions.txt
export TSAN_OPTIONS
EXEC_LDFLAGS += -fsanitize=thread
PLATFORM_CCFLAGS += -fsanitize=thread -fPIC -DFOLLY_SANITIZE_THREAD
PLATFORM_CXXFLAGS += -fsanitize=thread -fPIC -DFOLLY_SANITIZE_THREAD
@@ -450,7 +454,7 @@ ifndef USE_FOLLY
endif
ifndef GTEST_THROW_ON_FAILURE
export GTEST_THROW_ON_FAILURE=1
export GTEST_THROW_ON_FAILURE=0
endif
ifndef GTEST_HAS_EXCEPTIONS
export GTEST_HAS_EXCEPTIONS=1
@@ -621,25 +625,24 @@ endif
ROCKSDBTESTS_SUBSET ?= $(TESTS)
# c_test - doesn't use gtest
# env_test - suspicious use of test::TmpDir
# deletefile_test - serial because it generates giant temporary files in
# 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
# c_test - doesn't use gtest, can't be sharded
# Other tests previously listed here (backup_engine_test, db_bloom_filter_test,
# perf_context_test, etc.) were NON_PARALLEL due to /dev/shm memory concerns
# when the old per-test-case sharding spawned thousands of processes. With the
# new gtest-based sharding (GTEST_SHARD_SIZE=10, max NCORES*8 shards), at most
# ~4 shards run concurrently on CI (4 cores), so peak memory is manageable
# (4 * 1GB = 4GB << 16GB shm).
NON_PARALLEL_TEST = \
c_test \
env_test \
deletefile_test \
db_bloom_filter_test \
$(PLUGIN_TESTS) \
PARALLEL_TEST = $(filter-out $(NON_PARALLEL_TEST), $(TESTS))
PARALLEL_TEST = $(filter-out $(NON_PARALLEL_TEST), $(ROCKSDBTESTS_SUBSET))
# Not necessarily well thought out or up-to-date, but matches old list
TESTS_PLATFORM_DEPENDENT := \
db_basic_test \
db_blob_basic_test \
db_blob_direct_write_test \
db_encryption_test \
external_sst_file_basic_test \
auto_roll_logger_test \
@@ -647,6 +650,7 @@ TESTS_PLATFORM_DEPENDENT := \
dynamic_bloom_test \
c_test \
checkpoint_test \
sorted_run_builder_test \
crc32c_test \
coding_test \
inlineskiplist_test \
@@ -807,7 +811,18 @@ endif # PLATFORM_SHARED_EXT
rocksdbjavastatic rocksdbjava install install-static install-shared \
uninstall analyze tools tools_lib check-headers checkout_folly clang-tidy
all: $(LIBRARY) $(BENCHMARKS) tools tools_lib test_libs $(TESTS)
# Auto-configure git hooks on first build so developers do not need to run
# "make install-hooks" manually. This is a no-op if already set.
setup-hooks:
@if [ -d .git ] && [ -d githooks ]; then \
cur=$$(git config core.hooksPath 2>/dev/null); \
if [ "$$cur" != "githooks" ]; then \
git config core.hooksPath githooks; \
echo "git hooks: configured core.hooksPath = githooks"; \
fi; \
fi
all: setup-hooks $(LIBRARY) $(BENCHMARKS) tools tools_lib test_libs $(TESTS)
all_but_some_tests: $(LIBRARY) $(BENCHMARKS) tools tools_lib test_libs $(ROCKSDBTESTS_SUBSET)
@@ -871,21 +886,42 @@ coverage: clean
parallel_tests = $(patsubst %,parallel_%,$(PARALLEL_TEST))
.PHONY: gen_parallel_tests $(parallel_tests)
# Shard size controls how many test cases run per process. The actual number
# of shards per binary is: min(ceil(test_count / GTEST_SHARD_SIZE), NCORES * 8)
# This adapts to machine size: many small shards on beefy machines, fewer
# larger shards on CI (typically 2-4 cores).
GTEST_SHARD_SIZE ?= 10
NCORES ?= $(shell nproc 2>/dev/null || sysctl -n hw.ncpu 2>/dev/null || echo 4)
$(parallel_tests):
$(AM_V_at)TEST_BINARY=$(patsubst parallel_%,%,$@); \
TEST_NAMES=` \
(./$$TEST_BINARY --gtest_list_tests || echo " $${TEST_BINARY}__list_tests_failure") \
| awk '/^[^ ]/ { prefix = $$1 } /^[ ]/ { print prefix $$1 }'`; \
echo " Generating parallel test scripts for $$TEST_BINARY"; \
for TEST_NAME in $$TEST_NAMES; do \
TEST_SCRIPT=t/run-$$TEST_BINARY-$${TEST_NAME//\//-}; \
TEST_COUNT=` \
(./$$TEST_BINARY --gtest_list_tests 2>/dev/null || echo " list_failure") \
| grep -c '^ '`; \
if [ "$$TEST_COUNT" -le 0 ]; then TEST_COUNT=1; fi; \
MAX_SHARDS=$$(( $(NCORES) * 8 )); \
NUM_SHARDS=$$(( (TEST_COUNT + $(GTEST_SHARD_SIZE) - 1) / $(GTEST_SHARD_SIZE) )); \
if [ "$$NUM_SHARDS" -gt "$$MAX_SHARDS" ]; then NUM_SHARDS=$$MAX_SHARDS; fi; \
if [ "$$NUM_SHARDS" -le 0 ]; then NUM_SHARDS=1; fi; \
echo " Generating $$NUM_SHARDS shards for $$TEST_BINARY ($$TEST_COUNT tests)"; \
SHARD_IDX=0; \
while [ "$$SHARD_IDX" -lt "$$NUM_SHARDS" ]; do \
if [ -n "$(CI_TOTAL_SHARDS)" ] && [ $$(( $$SHARD_IDX % $(CI_TOTAL_SHARDS) )) -ne $(CI_SHARD_INDEX) ]; then \
SHARD_IDX=$$((SHARD_IDX + 1)); \
continue; \
fi; \
TEST_SCRIPT=t/run-$$TEST_BINARY-shard-$$SHARD_IDX; \
printf '%s\n' \
'#!/bin/sh' \
"d=\$(TEST_TMPDIR)$$TEST_SCRIPT" \
'mkdir -p $$d' \
"TEST_TMPDIR=\$$d $(DRIVER) ./$$TEST_BINARY --gtest_filter=$$TEST_NAME" \
"TEST_TMPDIR=\$$d GTEST_TOTAL_SHARDS=$$NUM_SHARDS GTEST_SHARD_INDEX=$$SHARD_IDX $(DRIVER) ./$$TEST_BINARY" \
'test_retcode=$$?' \
'[ $$test_retcode -eq 0 ] && rm -rf $$d' \
'exit $$test_retcode' \
> $$TEST_SCRIPT; \
chmod a=rx $$TEST_SCRIPT; \
SHARD_IDX=$$((SHARD_IDX + 1)); \
done
gen_parallel_tests:
@@ -909,8 +945,10 @@ gen_parallel_tests:
# 152.120 PASS t/DBTest.FileCreationRandomFailure
# 107.816 PASS t/DBTest.EncodeDecompressedBlockSizeTest
#
# With sharded test execution, prioritize binaries known to be slow.
# These generate many shards and should start early for good load balancing.
slow_test_regexp = \
^.*MySQLStyleTransactionTest.*$$|^.*SnapshotConcurrentAccessTest.*$$|^.*SeqAdvanceConcurrentTest.*$$|^t/run-table_test-HarnessTest.Randomized$$|^t/run-db_test-.*(?:FileCreationRandomFailure|EncodeDecompressedBlockSizeTest)$$|^.*RecoverFromCorruptedWALWithoutFlush$$
^.*block_based_table_reader_test.*$$|^.*table_test.*$$|^.*block_test.*$$|^.*write_prepared_transaction_test.*$$|^.*transaction_test.*$$|^.*external_sst_file_test.*$$|^.*db_wal_test.*$$|^.*db_with_timestamp_basic_test.*$$|^.*db_test-.*$$
prioritize_long_running_tests = \
perl -pe 's,($(slow_test_regexp)),100 $$1,' \
| sort -k1,1gr \
@@ -945,7 +983,13 @@ check_0:
'To monitor subtest <duration,pass/fail,name>,' \
' run "make watch-log" in a separate window' ''; \
{ \
printf './%s\n' $(filter-out $(PARALLEL_TEST),$(TESTS)); \
NON_PARALLEL_LIST="$(filter-out $(PARALLEL_TEST),$(ROCKSDBTESTS_SUBSET))"; \
if [ -n "$$NON_PARALLEL_LIST" ]; then \
printf './%s\n' $$NON_PARALLEL_LIST \
| if [ -n "$(CI_TOTAL_SHARDS)" ]; then \
awk -v s=$(CI_SHARD_INDEX) -v n=$(CI_TOTAL_SHARDS) '(NR-1)%n==s'; \
else cat; fi; \
fi; \
find t -name 'run-*' -print; \
} \
| $(prioritize_long_running_tests) \
@@ -1001,6 +1045,11 @@ check-progress:
# If J != 1 and GNU parallel is installed, run the tests in parallel,
# via the check_0 rule above. Otherwise, run them sequentially.
check: all
$(AM_V_at)echo "Cleaning up stale test directories older than 3 hours..."; \
test_tmpdir_parent=$$(dirname $(TEST_TMPDIR)); \
find $$test_tmpdir_parent -maxdepth 1 -name 'rocksdb.*' -type d \
-mmin +180 -exec rm -rf {} + 2>/dev/null; \
true
$(MAKE) gen_parallel_tests
$(AM_V_GEN)if test "$(J)" != 1 \
&& (build_tools/gnu_parallel --gnu --help 2>/dev/null) | \
@@ -1016,6 +1065,7 @@ ifneq ($(PLATFORM), OS_AIX)
$(PYTHON) tools/check_all_python.py
ifndef ASSERT_STATUS_CHECKED # not yet working with these tests
$(PYTHON) tools/ldb_test.py
$(PYTHON) tools/db_crashtest_test.py
sh tools/rocksdb_dump_test.sh
endif
endif
@@ -1023,6 +1073,7 @@ ifndef SKIP_FORMAT_BUCK_CHECKS
$(MAKE) check-format
$(MAKE) check-buck-targets
$(MAKE) check-sources
$(MAKE) check-workflow-yaml
endif
# TODO add ldb_tests
@@ -1033,6 +1084,10 @@ check_some: $(ROCKSDBTESTS_SUBSET)
ldb_tests: ldb
$(PYTHON) tools/ldb_test.py
.PHONY: db_crashtest_tests
db_crashtest_tests:
$(PYTHON) tools/db_crashtest_test.py
include crash_test.mk
asan_check: clean
@@ -1216,12 +1271,43 @@ format-auto:
check-format:
build_tools/format-diff.sh -c
# Crude alternative to setup-hooks: copies hooks into .git/hooks/ instead of
# using core.hooksPath. The copies won't track changes to githooks/.
install-hooks:
@echo "Installing git hooks from githooks/..."
@if [ -d githooks ]; then \
for hook in githooks/*; do \
hook_name=$$(basename "$$hook"); \
cp "$$hook" .git/hooks/"$$hook_name"; \
chmod +x .git/hooks/"$$hook_name"; \
echo " Installed $$hook_name"; \
done; \
echo "Done. Hooks installed to .git/hooks/"; \
else \
echo "Error: githooks/ directory not found"; \
exit 1; \
fi
# Reverse of install-hooks (not needed if using setup-hooks / core.hooksPath).
uninstall-hooks:
@echo "Removing installed git hooks..."
@for hook in githooks/*; do \
hook_name=$$(basename "$$hook"); \
rm -f .git/hooks/"$$hook_name"; \
echo " Removed $$hook_name"; \
done
@echo "Done."
check-buck-targets:
buckifier/check_buck_targets.sh
check-sources:
build_tools/check-sources.sh
check-workflow-yaml:
build_tools/check-workflow-yaml.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:
@@ -1409,6 +1495,9 @@ db_basic_test: $(OBJ_DIR)/db/db_basic_test.o $(TEST_LIBRARY) $(LIBRARY)
db_blob_basic_test: $(OBJ_DIR)/db/blob/db_blob_basic_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
db_blob_direct_write_test: $(OBJ_DIR)/db/blob/db_blob_direct_write_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
db_blob_compaction_test: $(OBJ_DIR)/db/blob/db_blob_compaction_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
@@ -1418,6 +1507,9 @@ db_readonly_with_timestamp_test: $(OBJ_DIR)/db/db_readonly_with_timestamp_test.o
db_wide_basic_test: $(OBJ_DIR)/db/wide/db_wide_basic_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
db_wide_blob_direct_write_test: $(OBJ_DIR)/db/wide/db_wide_blob_direct_write_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
db_with_timestamp_basic_test: $(OBJ_DIR)/db/db_with_timestamp_basic_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
@@ -1427,6 +1519,9 @@ db_with_timestamp_compaction_test: db/db_with_timestamp_compaction_test.o $(TEST
db_encryption_test: $(OBJ_DIR)/db/db_encryption_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
db_open_with_config_test: $(OBJ_DIR)/db/db_open_with_config_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
db_test: $(OBJ_DIR)/db/db_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
@@ -1571,6 +1666,9 @@ backup_engine_test: $(OBJ_DIR)/utilities/backup/backup_engine_test.o $(TEST_LIBR
checkpoint_test: $(OBJ_DIR)/utilities/checkpoint/checkpoint_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
sorted_run_builder_test: $(OBJ_DIR)/utilities/sorted_run_builder/sorted_run_builder_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
cache_simulator_test: $(OBJ_DIR)/utilities/simulator_cache/cache_simulator_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
@@ -1589,6 +1687,12 @@ object_registry_test: $(OBJ_DIR)/utilities/object_registry_test.o $(TEST_LIBRARY
ttl_test: $(OBJ_DIR)/utilities/ttl/ttl_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
trie_index_db_test: $(OBJ_DIR)/utilities/trie_index/trie_index_db_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
trie_index_test: $(OBJ_DIR)/utilities/trie_index/trie_index_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
types_util_test: $(OBJ_DIR)/utilities/types_util_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
@@ -1640,6 +1744,9 @@ io_posix_test: $(OBJ_DIR)/env/io_posix_test.o $(TEST_LIBRARY) $(LIBRARY)
fault_injection_test: $(OBJ_DIR)/db/fault_injection_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
fault_injection_fs_test: $(OBJ_DIR)/utilities/fault_injection_fs_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
rate_limiter_test: $(OBJ_DIR)/util/rate_limiter_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
@@ -1838,7 +1945,7 @@ write_committed_transaction_ts_test: $(OBJ_DIR)/utilities/transactions/write_com
write_prepared_transaction_test: $(OBJ_DIR)/utilities/transactions/write_prepared_transaction_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
write_prepared_transaction_test_seqno: $(OBJ_DIR)/utilities/transactions/write_prepared_transaction_test_seqno.o $(TEST_LIBRARY) $(LIBRARY)
write_prepared_transaction_seqno_test: $(OBJ_DIR)/utilities/transactions/write_prepared_transaction_seqno_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
write_unprepared_transaction_test: $(OBJ_DIR)/utilities/transactions/write_unprepared_transaction_test.o $(TEST_LIBRARY) $(LIBRARY)
@@ -2561,7 +2668,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 clang-tidy 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 check-workflow-yaml clang-tidy jclean jtest package analyze tags rocksdbjavastatic% unity.% unity_test checkout_folly, $(MAKECMDGOALS))
ifneq ("$(ROCKS_DEP_RULES)", "")
-include $(DEPFILES)
endif
+4 -4
View File
@@ -146,10 +146,10 @@ def generate_buck(repo_path, deps_map):
src_mk["RANGE_TREE_SOURCES"] + src_mk["TOOL_LIB_SOURCES"],
deps=[
"//folly/container:f14_hash",
"//folly/experimental/coro:blocking_wait",
"//folly/experimental/coro:collect",
"//folly/experimental/coro:coroutine",
"//folly/experimental/coro:task",
"//folly/coro:blocking_wait",
"//folly/coro:collect",
"//folly/coro:coroutine",
"//folly/coro:task",
"//folly/synchronization:distributed_mutex",
],
headers=LiteralValue("glob([\"**/*.h\"])")
+18
View File
@@ -148,6 +148,24 @@ PLATFORM_SHARED_LDFLAGS="-Wl,--no-as-needed -shared -Wl,-soname -Wl,"
PLATFORM_SHARED_CFLAGS="-fPIC"
PLATFORM_SHARED_VERSIONED=true
# Prefer lld linker when available on Linux. lld is typically 5-10x faster
# than the default ld.bfd for large C++ projects. macOS uses ld64 (or
# ld-prime) which is already fast, so we skip lld detection there.
# Set ROCKSDB_NO_FAST_LINKER=1 to disable this auto-detection.
if [ -z "$ROCKSDB_NO_FAST_LINKER" ] && [ "$TARGET_OS" = "Linux" ]; then
if $CXX -fuse-ld=lld -L/usr/local/lib -x c++ - -o /dev/null 2>/dev/null <<EOF
int main() { return 0; }
EOF
then
PLATFORM_LDFLAGS="$PLATFORM_LDFLAGS -fuse-ld=lld"
# Ensure lld can find libraries in /usr/local/lib (lld does not
# search there by default, unlike ld.bfd)
if [ -d /usr/local/lib ]; then
PLATFORM_LDFLAGS="$PLATFORM_LDFLAGS -L/usr/local/lib"
fi
fi
fi
# generic port files (working on all platform by #ifdef) go directly in /port
GENERIC_PORT_FILES=`cd "$ROCKSDB_ROOT"; find port -name '*.cc' | tr "\n" " "`
+1 -1
View File
@@ -37,7 +37,7 @@ if [ "$?" != "1" ]; then
BAD=1
fi
git grep -n -P "[\x80-\xFF]" -- ':!docs' ':!*.md'
LC_ALL=C git grep -n $'[\x80-\xff]' -- ':!docs' ':!*.md' ':!.github'
if [ "$?" != "1" ]; then
echo '^^^^ Use only ASCII characters in source files'
BAD=1
+42
View File
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
#
# Validate GitHub Actions workflow YAML before it reaches CI runtime.
set -euo pipefail
if ! command -v ruby >/dev/null 2>&1; then
echo "ruby is required to validate GitHub Actions workflow YAML"
echo "On CentOS Stream: sudo dnf install ruby rubygems rubygem-psych"
exit 1
fi
if ! ruby -e 'require "psych"' 2>/dev/null; then
echo "ruby is installed but cannot load required library 'psych'"
echo "On CentOS Stream: sudo dnf install rubygems rubygem-psych"
exit 1
fi
ruby <<'RUBY'
require "psych"
bad = false
workflow_files = Dir[".github/workflows/*.{yml,yaml}"].sort
if workflow_files.empty?
warn "No workflow YAML files found under .github/workflows"
exit 1
end
workflow_files.each do |path|
begin
Psych.parse_file(path)
puts "OK #{path}"
rescue Psych::Exception => e
warn "Invalid YAML in #{path}: #{e.message}"
bad = true
end
end
exit(bad ? 1 : 0)
RUBY
+1 -1
View File
@@ -1913,7 +1913,7 @@ sub drain_job_queue {
}
$last_progress_time = time();
$ps_reported = 0;
} elsif (not $ps_reported and (time() - $last_progress_time) >= 60) {
} elsif (not $ps_reported and (time() - $last_progress_time) >= 300) {
# No progress in at least 60 seconds: run ps
print $Global::original_stderr "\n";
my $script_dir = ::dirname($0);
+5 -3
View File
@@ -5,6 +5,8 @@
# the docker and docker-registry groups, and logging out and back in to pick
# those up.)
#
# Meta employees: see https://fburl.com/rocksdb-docker to build this on a devvm.
#
# Follow https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-with-a-personal-access-token-classic
# to login with your GitHub credentials, as in
#
@@ -15,8 +17,8 @@
# Then in the build_tools/ubuntu22_image directory, (bump minor version for
# random docker file updates, major version tracks Ubuntu release)
#
# $ docker build -t ghcr.io/facebook/rocksdb_ubuntu:22.0
# $ docker push ghcr.io/facebook/rocksdb_ubuntu:22.0
# $ docker build -t ghcr.io/facebook/rocksdb_ubuntu:22.2
# $ docker push ghcr.io/facebook/rocksdb_ubuntu:22.2
#
# Might need to change visibility to public through
# https://github.com/orgs/facebook/packages/container/rocksdb_ubuntu/settings
@@ -28,7 +30,7 @@ FROM ubuntu:22.04
RUN apt-get update
RUN apt-get upgrade -y
# install basic tools
RUN apt-get install -y vim wget curl
RUN apt-get install -y vim wget curl ccache
# install tzdata noninteractive
RUN DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt-get -y install tzdata
# install git and default compilers
+9 -3
View File
@@ -5,6 +5,8 @@
# the docker and docker-registry groups, and logging out and back in to pick
# those up.)
#
# Meta employees: see https://fburl.com/rocksdb-docker to build this on a devvm.
#
# Follow https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry#authenticating-with-a-personal-access-token-classic
# to login with your GitHub credentials, as in
#
@@ -15,8 +17,8 @@
# Then in the build_tools/ubuntu24_image directory, (bump minor version for
# random docker file updates, major version tracks Ubuntu release)
#
# $ docker build -t ghcr.io/facebook/rocksdb_ubuntu:24.0
# $ docker push ghcr.io/facebook/rocksdb_ubuntu:24.0
# $ docker build -t ghcr.io/facebook/rocksdb_ubuntu:24.1
# $ docker push ghcr.io/facebook/rocksdb_ubuntu:24.1
#
# Might need to change visibility to public through
# https://github.com/orgs/facebook/packages/container/rocksdb_ubuntu/settings
@@ -28,11 +30,15 @@ FROM ubuntu:24.04
RUN apt-get update
RUN apt-get upgrade -y
# install basic tools
RUN apt-get install -y vim wget curl
RUN apt-get install -y vim wget curl ccache
# install tzdata noninteractive
RUN DEBIAN_FRONTEND=noninteractive TZ=Etc/UTC apt-get -y install tzdata
# install git and default compilers
RUN apt-get install -y git gcc g++ clang clang-tools
# install clang-21 from LLVM snapshot repo
RUN curl -fsSL https://apt.llvm.org/llvm-snapshot.gpg.key | gpg --dearmor -o /etc/apt/trusted.gpg.d/llvm.gpg && \
echo "deb https://apt.llvm.org/noble/ llvm-toolchain-noble-21 main" > /etc/apt/sources.list.d/llvm-21.list && \
apt-get update && apt-get install -y clang-21
# install basic package
RUN apt-get install -y lsb-release software-properties-common gnupg
# install gflags, tbb
+61
View File
@@ -0,0 +1,61 @@
# PR Complexity Classifier — CI Triage Prompt
## Purpose
Quickly classify a RocksDB pull request as **simple** or **complex** so the
downstream reviewer can pick an appropriate thinking budget.
## Output Contract (STRICT)
Your final response MUST be exactly one of these two lowercase tokens, on a
line by itself, with no other text:
```
simple
```
or
```
complex
```
If you are unsure, output `complex`. The downstream review will use this token
to set its thinking budget — when in doubt, prefer the more thorough budget.
## Classification Heuristics
Treat a PR as **simple** when ALL of the following hold:
- Diff touches < ~200 lines of non-test code
- No changes to public headers under `include/rocksdb/`
- No changes to on-disk format (SST block layout, WAL record layout,
manifest, options file format, version edit encoding)
- No changes to compaction picker, write-path locking, recovery, or
WAL/memtable lifecycle
- No new public option, no new statistics counter, no new RPC/serialized
message
- Tests-only or docs-only changes
- Trivial refactors (rename, comment, dead-code removal, lint fixes)
- One-line bug fix with obvious root cause and a focused regression test
Treat a PR as **complex** when ANY of the following hold:
- Touches files in `db/`, `db/compaction/`, `db/blob/`, `cache/`,
`table/block_based/`, `file/`, `env/io_*` with > ~50 lines of logic change
- Modifies thread-safety/synchronization (mutexes, atomics, memory ordering,
refcounting, condvars)
- Changes serialization or on-disk format
- Changes public API surface in `include/rocksdb/`
- Adds a new feature flag, option, or compaction style
- Touches transactions (`utilities/transactions/`), backup/checkpoint,
user-defined timestamps, or remote compaction
- Cross-cutting refactor spanning > 5 directories
## How To Decide
1. Read the diff summary and changed-file list provided below.
2. Skim the diff hunks — focus on which subsystems are touched and whether
the change is mechanical vs. semantic.
3. Apply the heuristics above. Default to `complex` if the rules conflict.
4. Output the single token. No preamble. No explanation.
## Tool Use
You may use `View`, `GlobTool`, and `GrepTool` to peek at one or two files
if the diff snippet alone is ambiguous, but keep this fast — this is a
triage step, not a review. Do NOT spawn sub-agents.
+3
View File
@@ -0,0 +1,3 @@
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.
+54
View File
@@ -0,0 +1,54 @@
Read review-findings.md. This file contains partial code review findings
from a session that ran out of turns before finishing. Reformat them into
a clean final review comment.
## Required Output Structure
Use this exact structure so the PR page stays scrollable:
```markdown
## Summary
> **Partial review** — the analysis was interrupted before all perspectives
> were completed. Findings below cover only the perspectives that finished.
> You can request a fresh full review with `/claude-review`.
<!-- One or two sentences of overall assessment. -->
**High-severity findings (N):**
- **[file.cc:123]** One-line description. <!-- repeat per HIGH finding -->
<!-- If no HIGH findings were salvaged, write: -->
<!-- _No high-severity findings recovered._ -->
<details>
<summary>Recovered findings (click to expand)</summary>
### Findings
#### :red_circle: HIGH
... (H1, H2, ...)
#### :yellow_circle: MEDIUM
... (M1, M2, ...)
#### :green_circle: LOW / NIT
... (L1, L2, ...)
### Cross-Component Analysis
<!-- Whatever cross-component results were captured. -->
### Positive Observations
<!-- Optional. -->
</details>
```
## Rules
- The `## Summary`, the partial-review notice, and the HIGH bullet list MUST
stay outside the `<details>` block.
- All per-finding detail and analysis MUST live inside the `<details>` block.
- Do NOT nest `<details>` blocks.
- Do NOT add any text after the closing `</details>` — the comment-builder
appends its own footer.
- Output ONLY the formatted review. No commentary, no explanation.
+661
View File
@@ -0,0 +1,661 @@
# Code Review Workflow — CI Prompt
## Purpose
Thorough multi-agent code review for RocksDB commits following CLAUDE.md guidelines,
with structured codebase exploration, inter-agent debate, and verification.
IMPORTANT — Incremental output: After completing EACH major phase (Setup,
Codebase Context, Initial Review, Debate, Synthesis), append your findings
to review-findings.md using the Write tool. This ensures partial results are
saved even if the review is interrupted by the turn limit. After the final
synthesis, write the complete review to review-findings.md, replacing
previous content. Also output the final review as your response text.
## Workflow Steps
### 1. Setup Phase
- Read CLAUDE.md to understand review guidelines
- Identify the commit/diff/PR to review
- The PR diff is provided below in this prompt. The repository is already
checked out — use local file search tools (Grep, Glob, Read) to explore.
- Parse the changed files and categorize them (API, core logic, tests, etc.)
- Read all changed files to build full context
### 2. Codebase Context Phase (CRITICAL — Do Not Skip)
**Why this matters**: Review agents that only see the diff miss systemic bugs.
For example, a change to an iterator's return value might look correct in
isolation but break multi-SST iteration 5 layers up the call stack. A change
to error handling might silently drop errors that a caller 3 levels up relies
on for recovery. This phase builds the context that prevents those blind spots.
Go deep. Surface-level reading leads to reviews that miss caching layers,
existing helper functions, or concurrency requirements. The most expensive
failure mode is findings that look correct in isolation but miss how the
change breaks the surrounding system.
The team lead spawns one or more research agents to perform the following
analyses using local file search tools (Grep, Glob, Read). The research
must be written to `context.md` BEFORE any review agent is spawned.
#### 2a. Subsystem Deep-Read
Read the changed files AND their surrounding subsystem **in depth** — not just
signatures, but actual logic, edge cases, data flows, locking protocols,
concurrency patterns, and interactions between components.
For each changed file, also read:
- The header that declares its interface (if the change is in a .cc file)
- The sibling implementation that does the same thing the "standard" way
(e.g., for a UDI iterator change, read how `IndexBlockIter` handles the
same scenario — it's the reference implementation)
- The wrapper/adapter layer that sits between this code and its callers
- Any existing tests to understand what behaviors are currently guaranteed
#### 2b. Caller-Chain Analysis (upstream impact)
For each changed function/method, trace the call chain UPWARD 3-5 levels to
understand who consumes the changed behavior and what invariants they rely on.
**Focus on control flow decisions** — if/else branches, while loop conditions,
early returns — that depend on the changed return values or state.
**How to do this**:
- Search for callers of each changed function (search for the function name, the
class method, virtual overrides)
- For each caller, search for ITS callers, repeating 3-5 levels up
- At each level, read the actual code to understand the control flow decision
- Stop when you reach a "terminal" caller (e.g., user-facing API, top-level
iterator) or when the propagation is clearly safe
#### 2c. Callee-Chain Analysis (downstream dependencies AND side effects)
For each changed function, trace what it calls and whether those calls have
new preconditions or changed semantics. **Critically, trace SIDE EFFECTS —
not just return values.** Many bugs hide in side effects on shared state
(counters, sequence numbers, flags) that are invisible if you only check
return values.
**For each callee, ask:**
1. What does it RETURN? (the obvious check)
2. What shared state does it MUTATE? (the non-obvious check)
3. Under what PRECONDITIONS does it operate correctly?
4. Are those preconditions met in the new calling context?
#### 2d. Sibling Implementation Comparison
Identify the "reference" or "standard" implementation that does the same thing
the changed code does, and compare their behavior. This is critical for plugin/
extension APIs where the implementation must match an implicit contract.
#### 2e. Invariant Documentation
Document the key invariants that the changed code must maintain. Read existing
comments, assertions, and test expectations to discover these invariants.
#### 2f. Related Functionality and Existing Conventions
Identify related functionality that already exists in the codebase:
- Are there existing helper functions the change should use (or is duplicating)?
- Are there existing patterns for the same kind of change (e.g., how other
Customizable subclasses handle deprecation)?
- Are there existing tests that test similar scenarios (e.g., multi-SST
iteration tests that could be extended)?
#### 2g. Cross-Component Data Consumer Analysis (CRITICAL — common blind spot)
**Why this is separate from caller-chain analysis:** Sections 2b-2c trace
who CALLS the changed functions. This section traces who CONSUMES the data
that the changed code PRODUCES. These are often completely different
subsystems.
When the change writes data to a shared structure (memtable, SST block, cache
entry, statistics counter), ask: **"Who reads this data, under what rules,
and do those rules match the writer's assumptions?"**
**How to do this:**
1. Identify every piece of data the change WRITES (e.g., a range tombstone
entry, a counter update, a metadata field)
2. For each, search the codebase for ALL readers of that data structure
3. For each reader, check:
- Does the reader apply visibility rules (seqno, read_callback, snapshot)?
- Are those rules compatible with the writer's assumptions?
- Does the reader assume invariants about the data (e.g., seqno ordering,
monotonicity) that the writer might violate?
#### 2h. Alternative Execution Contexts
The same code path may run in different contexts with different assumptions.
**Enumerate all contexts and check each one.** This is where "works in the
common case but breaks in edge configurations" bugs hide.
For RocksDB, always check whether the changed code interacts differently with:
| Context | Key difference | Common failure mode |
|---------|---------------|-------------------|
| WritePreparedTxnDB / WriteUnpreparedTxnDB | `read_callback_` controls visibility, not just seqno | Visibility bypass |
| ReadOnly DB / SecondaryInstance | No mutable memtable, writes not allowed | Null pointer or no-op needed |
| CompactionService / Remote compaction | Different process, serialized state | Serialization mismatch |
| User-defined timestamps | Extra dimension in key comparison and visibility | Wrong ordering |
| MemPurge | Memtable-to-memtable, not memtable-to-SST | Missing data |
| Column family with BlobDB | Values may be in blob files, not inline | Missing dereference |
| Snapshots (old, held long-term) | Snapshot seqno may be far behind current state | Metadata corruption |
| Concurrent writers (allow_concurrent_memtable_write) | Lock-free paths vs locked paths | Lost updates |
| FIFO / Universal compaction | Different compaction invariants than Level | Wrong assumptions |
| Prefix seek / total order seek | Different iterator behavior | Wrong iteration results |
**For each context, ask:**
1. Does the code execute in this context? (Check constructor, feature flags)
2. If yes, do the assumptions still hold?
3. If not, should the feature be disabled in this context?
#### 2i. Assumption Stress-Testing (the "logically X" audit)
When the change claims a property (e.g., "logically redundant," "no-op in
this case," "safe because X"), **systematically break it:**
1. **State the claim precisely.** E.g., "The inserted range tombstone is
logically redundant — it covers only keys already deleted by point
tombstones."
2. **List the preconditions** that must be true for the claim to hold.
3. **For each precondition, construct a counterexample** — a concrete scenario
where the precondition is violated.
4. **Verify each counterexample against the code.** Does the code guard
against it? If not, it's a bug.
**Anti-pattern: treating asserts as proofs.** When you see an assert, do NOT
conclude "the invariant holds." Instead ask: "Can I construct an input where
this assert fires?" If yes, it's a bug (the assert catches it in debug but
release builds silently corrupt). If you cannot construct a counterexample
after trying, document WHY it's impossible.
#### 2j. Write Context Document
Write the complete analysis to `context.md` in the review folder. This document
is provided to ALL review agents as part of their prompt.
**Context document template**:
```markdown
# Codebase Context for Review
## How the Relevant Subsystem Works Today
[Detailed description of the subsystem architecture, data flows,
and component interactions. Not just "what" but "how" and "why."]
## Changed Functions and Their Call Chains
### function_name() (file:line)
**What changed**: [brief description of the behavioral change]
**Upstream callers** (who depends on this):
1. CallerA::method() (file:line) — uses return value to decide X
2. CallerB::method() (file:line) — passes result to CallerC
3. CallerC::method() (file:line) — THE CRITICAL DECISION POINT: ...
**Downstream callees** (what this depends on):
1. calleeA() — behavior unchanged
2. calleeB() — NEW dependency, requires X
**Sibling implementation** (how the "standard" version handles this):
- StandardImpl::method() does Y at file:line, which ensures invariant Z
- The changed code must match or document any deviation
**Key invariants**:
- Must never return X when Y is true because CallerC will...
- Must always set Z before returning because CallerB assumes...
## System Architecture Context
[How the changed components fit into the overall system]
## Known Invariants That Must Be Preserved
1. ...
2. ...
## Existing Conventions and Related Code
- [helper functions, patterns, existing tests that are relevant]
## Cross-Component Data Consumers
For each piece of data the change WRITES to a shared structure:
### [data item] written to [structure]
**Writer assumptions**: [what the writer assumes about how this data is consumed]
**All readers**:
1. ReaderA::method() (file:line) — reads under [visibility rules]
- Compatible with writer? YES/NO. If NO, explain the mismatch.
2. ReaderB::method() (file:line) — reads under [different rules]
- Compatible with writer? YES/NO.
## Alternative Execution Contexts
| Context | Does code execute? | Assumptions hold? | Action needed? |
|---------|-------------------|-------------------|----------------|
| WritePreparedTxnDB | YES/NO | YES/NO | [disable/guard/safe] |
| Old snapshots | YES/NO | YES/NO | ... |
| User-defined timestamps | YES/NO | YES/NO | ... |
| ReadOnly DB | YES/NO | YES/NO | ... |
| ... | ... | ... | ... |
## Assumption Stress Test
### Claim: "[the design claim, e.g., logically redundant]"
**Preconditions for claim to hold:**
1. [precondition] — counterexample: [scenario]. Guarded? YES/NO.
2. [precondition] — counterexample: [scenario]. Guarded? YES/NO.
## Potential Pitfalls
- [specific scenarios where the change could interact badly with
the surrounding system, identified from the caller-chain analysis]
```
### 3. Initial Review Phase (Parallel)
Create a team and spawn 5 review agents in parallel. **Include the context
document from Phase 2 in each agent's prompt.** Each agent writes findings
to its own file and sends a summary message to the team lead.
Each agent's prompt should include:
```
## Codebase Context (READ THIS FIRST)
[paste or reference the context.md file]
You MUST consider how your findings interact with the upstream callers
and system invariants documented above. A finding that looks correct
in isolation may be a critical bug when you consider the full call chain.
```
#### Agent: Design & Approach Reviewer
- Read the commit message deeply to understand the problem being solved
- Analyze whether this is the right approach to the problem
- Consider alternative designs and trade-offs:
- Are there simpler solutions that achieve the same goal?
- Does the approach introduce unnecessary complexity?
- Would a different data structure or algorithm be more appropriate?
- Evaluate whether the change follows existing architectural patterns or
introduces new patterns that may be inconsistent
- Assess the scope: is the change too large? Should it be split into
smaller, independently reviewable pieces?
- Check if the approach has precedent in the codebase or in the academic
literature it references (e.g., SuRF paper)
#### Agent: Correctness Reviewer
- Thread safety and concurrency analysis
- Error handling and propagation via Status type
- Edge cases (empty inputs, overflow, boundary conditions)
- Data corruption scenarios
- Logic correctness of new algorithms
- **Behavioral contract changes**: Do return value semantics match what
upstream callers expect? (Use the caller-chain analysis from context.md)
- **Callee side effects**: Does the change call functions that mutate shared
state (counters, seqnos, metadata)? Are the mutations correct for the new
calling context? (Use the callee-chain analysis from context.md)
#### Agent: Cross-Component & Adversarial Reviewer
This agent exists because the most critical bugs hide at component boundaries,
not within a single component. It uses a fundamentally different methodology
than the correctness reviewer: instead of verifying "does the code do what it
intends?", it asks "what breaks when we change the assumptions?"
**Data-flow analysis:** Perform the cross-component data consumer analysis
described in section 2g. For every piece of data the change WRITES to a shared
structure, trace ALL READERS and verify their visibility rules are compatible
with the writer's assumptions. This is a DATA-FLOW question, not a
CONTROL-FLOW question — readers may be in completely different subsystems.
**Alternative execution contexts:** Use the canonical table from section 2h.
Enumerate all contexts where the changed code executes and verify assumptions
hold in each.
**Assumption stress-testing and assert-breaking:** Follow the methodology from
section 2i. Identify every design claim, enumerate preconditions, construct
counterexamples. Treat asserts as hypotheses to break, not proofs.
**Red flag words**: "logically redundant", "safe because", "no-op",
"always true", "cannot happen", "invariant holds"
**Callee side-effect audit:** Perform the callee side-effect audit described
in section 2c. For every callee, ask what it RETURNS and what it MUTATES.
Write findings to `findings-cross-component.md`.
#### Agent: Invariant Adversary
This agent does NOT read the diff in detail. It takes the feature summary and
systematically tries to break it. While other agents verify "does the code do
what it says?", this agent asks "what existing system invariants does this
feature violate?" and "under what inputs do the design claims fail?"
**Steps 1-4: Assumption stress-testing.** Follow the methodology from section
2i: extract design claims, enumerate preconditions, construct counterexamples,
and attempt to break every assert. Be exhaustive about input parameter ranges,
shared state configurations, and concurrent interleaving.
**Step 5: Callee side-effect audit.** Perform the callee side-effect audit
described in section 2c. For every function the changed code calls, list ALL
mutations to shared state (atomic CAS loops, counter increments, flag/metadata
updates, cache invalidation). A function returning `Status::OK()` does NOT
mean its side effects are correct.
Write findings to `findings-invariant-adversary.md`.
#### Agent: Caller-Context Auditor
This agent traces BACKWARD from every entry point the changed code hooks into.
While other agents read the changed code forward ("what does it do?"), this
agent enumerates WHO invokes it and WITH WHAT parameter ranges.
The key insight: a function that is correct for all *typical* inputs may be
catastrophically wrong for *atypical-but-reachable* inputs. This agent's job
is to find the atypical inputs.
**Step 1: Identify entry points.** List every function/constructor/method in
the changed code that receives external input (parameters, config values,
pointers to shared state). Include:
- Constructor parameters (e.g., `active_mem`, `read_callback`, `sequence`)
- Virtual method overrides that callers invoke polymorphically
- Functions called from multiple subsystems
**Step 2: Enumerate all callers (3-5 levels up).** For each entry point,
search the codebase for ALL callers. Do NOT stop at the first caller — trace
the full call chain. Use `Grep` and `Glob` to find:
- Direct callers of the function
- Callers of wrapper/adapter functions that forward to it
- Factory functions that construct the object
- Virtual dispatch sites (search for the base class method name)
**Step 3: Parameter range analysis.** For each caller, determine:
- What values does it pass for each parameter?
- Under what conditions does it call this function?
- Are there callers that pass unusual values? (e.g., `kMaxSequenceNumber`,
`nullptr`, old snapshot seqnos, non-null `read_callback_`)
Build a parameter range table for each entry point, listing all callers
and the values they pass for each parameter.
**Step 4: Configuration matrix.** Enumerate option/config combinations that
affect the changed code path:
- Which options enable/disable the feature?
- Which options change the execution context? (e.g.,
`allow_concurrent_memtable_write`, `use_trie_index`)
- Are there option combinations that create contradictions?
**Step 5: Cross-reference with context.md.** Compare your caller analysis
with the invariants and execution contexts documented in context.md. Flag
any caller that violates an assumed precondition.
Write findings to `findings-caller-audit.md`.
#### Agent: Performance Reviewer
- Memory allocation on hot paths
- Unnecessary copies (string, buffer)
- Cache efficiency and data locality
- Loop optimization opportunities
- Branch prediction (LIKELY/UNLIKELY)
- Zero-overhead design verification
#### Agent: API & Compatibility Reviewer
- Public API backwards compatibility
- API consistency with existing patterns
- Documentation completeness
- Deprecation policy compliance
- Forward compatibility (struct extensibility)
- **Behavioral compatibility**: Do changed semantics (e.g., kOutOfBound vs
kUnknown) break implicit contracts with callers?
#### Agent: Serialization & Deserialization Reviewer
- Format correctness and versioning
- Alignment requirements
- Integer overflow in size computations
- Bounds checking on untrusted data
- Crafted input attack vectors
#### Agent: Test Coverage Reviewer
- Edge case coverage
- Failure mode testing (corruption, truncation)
- Round-trip testing (build → serialize → deserialize → verify)
- Integration test coverage
- **System-level test coverage**: Are the caller-chain interactions tested?
(e.g., multi-SST iteration with UDI, level compaction with new behavior)
- Test quality (no flaky patterns, good assertions)
### 4. Round-Robin Debate Phase
After all 5 agents complete their initial review, the team lead orchestrates
a structured debate:
#### Step 4a: Share findings
- Team lead sends each agent a summary of ALL other agents' findings
(or instructs each agent to read the other agents' findings files)
#### Step 4b: Critique round (2 rounds)
Each agent reviews the findings from the other agents and sends messages to
challenge, support, or refine them:
- **Challenge**: "I disagree with [agent] [finding] because [evidence]."
- **Support**: "I independently found the same issue — this confirms it."
- **Refine**: "[Finding A] is actually a consequence of [Finding B].
The root cause is the same."
The debate assignment follows a round-robin pattern:
```
correctness-reviewer → critiques → invariant-adversary, serialization
cross-component-reviewer → critiques → correctness, caller-audit
invariant-adversary → critiques → correctness, cross-component
caller-audit → critiques → invariant-adversary, cross-component
performance-reviewer → critiques → api, cross-component
api-reviewer → critiques → correctness, performance
serialization-reviewer → critiques → correctness, invariant-adversary
test-reviewer → critiques → serialization, caller-audit
design-reviewer → critiques → cross-component, invariant-adversary
```
**Note:** The invariant-adversary and caller-audit agents are deliberately
cross-linked with correctness and cross-component because their findings
often reveal the same underlying bug from different angles (invariant
violation vs reachable bad input vs data-flow mismatch).
Each agent should:
1. Read the assigned agents' findings files
2. Send a critique message to each assigned agent
3. Respond to critiques received from other agents
4. Update their own findings file with any revisions (upgraded/downgraded severity,
withdrawn findings, new findings inspired by others)
#### Step 4c: Team lead collects debate results
- Read all updated findings files
- Review the debate messages
- Build consensus: findings supported by 2+ agents → HIGH confidence
- Write consensus document with final severity classifications
### 5. Consensus Phase
Team lead synthesizes the debate into a consensus document:
- Cross-reference overlapping findings across agents
- Count agreement (majority = 2+ agents independently flagging or supporting)
- Note disagreements and how they were resolved
- Classify findings as HIGH/MEDIUM/LOW severity
- Write consensus document
### 6. Summary Phase
- Compile all validated findings ordered by severity
- Include detailed bug analysis (root cause, vulnerable code paths)
- Include suggested fixes
- Note which findings were debated and the outcome
- Write the complete review to review-findings.md and output as response
**Final report quality rules:**
- The final report must be CLEAN and POLISHED. No stream-of-consciousness,
no "Wait, actually...", no "on closer re-examination". Those belong in
the working notes, not the output.
- If a finding was raised during review but later disproven during debate
or deeper analysis, REMOVE IT entirely from the final report. Do not
include it with a retraction — just drop it.
- If a finding was downgraded (e.g., Critical → Suggestion), present it
at the final severity only. Do not narrate the severity change.
- Each finding in the final report should read as a confident, verified
conclusion — not a record of the analysis process.
- The reader should never see the reviewer arguing with itself.
**REQUIRED output structure (so the PR page stays scrollable):**
The final response (and contents of `review-findings.md`) MUST follow this
exact structure. The summary appears first so reviewers can see HIGH findings
at a glance; everything else is hidden behind a `<details>` block.
```markdown
## Summary
<!-- One or two sentences of overall assessment. -->
**High-severity findings (N):**
- **[file.cc:123]** One-line description of the issue. <!-- repeat per HIGH finding -->
<!-- If there are NO high-severity findings, write exactly: -->
<!-- _No high-severity findings._ -->
<details>
<summary>Full review (click to expand)</summary>
### Findings
#### :red_circle: HIGH
##### H1. <Title> — `file.cc:123`
- **Issue:** ...
- **Root cause:** ...
- **Suggested fix:** ...
#### :yellow_circle: MEDIUM
... (same structure: M1, M2, ...)
#### :green_circle: LOW / NIT
... (same structure: L1, L2, ...)
### Cross-Component Analysis
<!-- Execution-context table and assumption stress-test results. -->
### Positive Observations
<!-- Optional: good patterns, clever optimizations. -->
</details>
```
Rules for this structure:
- The top-level `## Summary` and the bullet list of HIGH findings MUST stay
outside the `<details>` block — they are always visible.
- Every detail (per-finding root cause, fix, debate outcomes, cross-component
analysis, positive observations) MUST live inside the `<details>` block.
- Do NOT nest a `<details>` inside another `<details>`.
- Do NOT add any text after the closing `</details>` — the comment-builder
appends its own footer.
- If the review is partial (recovery path), still produce the summary block
first, and put whatever was salvaged inside the `<details>` block.
## Review Checklist
### Context Phase (must be completed before agents spawn)
- [ ] Subsystem deep-read — read changed files AND surrounding subsystem
in depth (logic, edge cases, data flows, locking, concurrency)
- [ ] Caller chain for every changed public/virtual method (3-5 levels up)
- [ ] Critical decision points where callers branch on changed behavior
- [ ] Callee chain with SIDE EFFECTS — downstream dependencies, new
preconditions, AND mutations to shared state (section 2c)
- [ ] Sibling implementations — how does the standard version handle same scenarios?
- [ ] Invariants that callers rely on
- [ ] Related functionality — existing helpers, patterns, conventions
- [ ] Cross-component data consumers — for every data WRITTEN, find ALL readers
and verify visibility rules match (section 2g)
- [ ] Alternative execution contexts verified (section 2h table)
- [ ] Assumption stress-test — for every design claim, list preconditions
and construct counterexamples (section 2i)
- [ ] Multi-component interactions (compaction, recovery, snapshots, iterators)
- [ ] Configuration dependencies and unexpected option combinations
- [ ] Potential pitfalls from caller-chain analysis
### Review Phase (verified by agents)
- [ ] Database semantics preserved (snapshot isolation, key ordering)
- [ ] All error cases handled
- [ ] Thread-safe with correct synchronization
- [ ] No data races or deadlocks
- [ ] Appropriate test coverage (edge cases, failure modes, system-level)
- [ ] No unnecessary allocations or copies in hot paths
- [ ] Backwards compatible
- [ ] New APIs consistent with existing patterns and documented
- [ ] Code follows RocksDB style conventions
- [ ] Behavioral contracts with upstream callers preserved
- [ ] All callers enumerated with parameter range table
## Output Structure
Write all review artifacts to the working directory root:
- `review-findings.md` — Incremental findings (appended after each phase),
then replaced with the final synthesized review at the end
- `context.md` — Codebase context (call chains, invariants)
- `findings-*.md` — Per-agent findings (design, correctness, cross-component,
invariant-adversary, caller-audit, performance, api, serialization, tests)
- `consensus.md` — Cross-review consensus (post-debate)
## Team Structure
```
Team Lead (you)
│
├── Phase 2: Codebase Context (team lead or dedicated research agent)
│ └── context-researcher (general-purpose agent)
│ ├── Trace caller chains (3-5 levels up)
│ ├── Trace callee chains (dependencies AND side effects)
│ ├── Trace data consumers (who reads what the change writes?)
│ └── Document invariants
│
├── Phase 3: Initial Review (parallel, run_in_background)
│ │ (all agents receive context.md in their prompt)
│ ├── design-reviewer (general-purpose agent)
│ ├── correctness-reviewer (general-purpose agent)
│ ├── cross-component-reviewer (general-purpose agent)
│ ├── invariant-adversary (general-purpose agent) ← NEW
│ ├── caller-audit (general-purpose agent) ← NEW
│ ├── performance-reviewer (general-purpose agent)
│ ├── api-reviewer (general-purpose agent)
│ ├── serialization-reviewer (general-purpose agent)
│ └── test-reviewer (general-purpose agent)
│
├── Phase 4: Debate (agents message each other)
│ ├── correctness ↔ invariant-adversary, serialization
│ ├── cross-component ↔ correctness, caller-audit
│ ├── invariant-adversary ↔ correctness, cross-component
│ ├── caller-audit ↔ invariant-adversary, cross-component
│ ├── performance ↔ api, cross-component
│ ├── api ↔ correctness, performance
│ ├── serialization ↔ correctness, invariant-adversary
│ ├── test-coverage ↔ serialization, caller-audit
│ └── design ↔ cross-component, invariant-adversary
│
```
## Agent Communication Protocol
### During Initial Review (Phase 3)
- Each agent writes findings to its own file
- Each agent sends a summary message to team lead when done
- Agents do NOT communicate with each other yet
### During Debate (Phase 4)
- Team lead sends each agent a message with instructions to:
1. Read the assigned agents' findings files
2. Send critique messages to those agents
3. Respond to incoming critiques
4. Update their own findings file with revisions
- Each critique message should include:
- Which finding they're addressing (e.g., "Correctness F1")
- Whether they AGREE, DISAGREE, or want to REFINE
- Their reasoning with code evidence
- Suggested severity adjustment (if any)
### Message format example
```
To: correctness-reviewer
Re: Your Finding F1
AGREE/DISAGREE/REFINE - [reasoning with code evidence].
[Suggested severity adjustment if any.]
```
## Review Anti-Patterns
These recurring failure modes lead to missed bugs. Each is detailed in the
referenced section; this table is a quick-reference checklist.
| Anti-Pattern | Fix | Reference |
|---|---|---|
| Return-Value Tunnel Vision | Trace callee MUTATIONS, not just returns | Section 2c |
| Default-Configuration Bias | Enumerate all execution contexts | Section 2h |
| Assert-as-Proof | Try to BREAK every assert | Section 2i |
| Write-Path-Only Analysis | Trace data readers, not just writers | Section 2g |
| Confirmation-Seeking Research | Use adversarial prompts ("find where X fails") | Invariant Adversary agent |
| Data-Flow vs Control-Flow Confusion | Separate who CALLS from who READS the data | Section 2g |
+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/`
+7
View File
@@ -7,6 +7,13 @@ DB_STRESS_CMD?=./db_stress
include common.mk
ifdef COMPILE_WITH_TSAN
# Keep direct `make -f crash_test.mk COMPILE_WITH_TSAN=1 ...` runs
# aligned with the main Makefile's TSAN runtime options.
TSAN_OPTIONS?=suppressions=$(CURDIR)/tools/tsan_suppressions.txt
export TSAN_OPTIONS
endif
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)' --destroy_db_initially=1
+18 -7
View File
@@ -5,6 +5,8 @@
#include "db/blob/blob_fetcher.h"
#include "db/blob/blob_file_partition_manager.h"
#include "db/blob/blob_index.h"
#include "db/version_set.h"
namespace ROCKSDB_NAMESPACE {
@@ -14,10 +16,13 @@ Status BlobFetcher::FetchBlob(const Slice& user_key,
FilePrefetchBuffer* prefetch_buffer,
PinnableSlice* blob_value,
uint64_t* bytes_read) const {
assert(version_);
return version_->GetBlob(read_options_, user_key, blob_index_slice,
prefetch_buffer, blob_value, bytes_read);
BlobIndex blob_index;
Status status = blob_index.DecodeFrom(blob_index_slice);
if (status.ok()) {
status = FetchBlob(user_key, blob_index, prefetch_buffer, blob_value,
bytes_read);
}
return status;
}
Status BlobFetcher::FetchBlob(const Slice& user_key,
@@ -25,10 +30,16 @@ Status BlobFetcher::FetchBlob(const Slice& user_key,
FilePrefetchBuffer* prefetch_buffer,
PinnableSlice* blob_value,
uint64_t* bytes_read) const {
assert(version_);
if (!allow_write_path_fallback_) {
assert(version_);
return version_->GetBlob(read_options_, user_key, blob_index, prefetch_buffer,
blob_value, bytes_read);
return version_->GetBlob(read_options_, user_key, blob_index,
prefetch_buffer, blob_value, bytes_read);
}
return BlobFilePartitionManager::ResolveBlobDirectWriteIndex(
read_options_, user_key, blob_index, version_, blob_file_cache_,
prefetch_buffer, blob_value, bytes_read);
}
} // namespace ROCKSDB_NAMESPACE
+13 -3
View File
@@ -10,17 +10,25 @@
namespace ROCKSDB_NAMESPACE {
class BlobFileCache;
class Version;
class Slice;
class FilePrefetchBuffer;
class PinnableSlice;
class BlobIndex;
// A thin wrapper around the blob retrieval functionality of Version.
// A thin wrapper around blob retrieval. By default it reads through Version,
// and it can optionally fall back to direct-write blob files that are not yet
// manifest-visible.
class BlobFetcher {
public:
BlobFetcher(const Version* version, const ReadOptions& read_options)
: version_(version), read_options_(read_options) {}
BlobFetcher(const Version* version, const ReadOptions& read_options,
BlobFileCache* blob_file_cache = nullptr,
bool allow_write_path_fallback = false)
: version_(version),
read_options_(read_options),
blob_file_cache_(blob_file_cache),
allow_write_path_fallback_(allow_write_path_fallback) {}
Status FetchBlob(const Slice& user_key, const Slice& blob_index_slice,
FilePrefetchBuffer* prefetch_buffer,
@@ -33,5 +41,7 @@ class BlobFetcher {
private:
const Version* version_;
ReadOptions read_options_;
BlobFileCache* blob_file_cache_;
bool allow_write_path_fallback_;
};
} // namespace ROCKSDB_NAMESPACE
+2 -4
View File
@@ -67,13 +67,11 @@ 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_)),
mutable_cf_options->blob_compression_opts, blob_compression_type_)),
blob_compressor_wa_(blob_compressor_
? blob_compressor_->ObtainWorkingArea()
: Compressor::ManagedWorkingArea{}),
@@ -111,7 +109,7 @@ Status BlobFileBuilder::Add(const Slice& key, const Slice& value,
assert(blob_index);
assert(blob_index->empty());
if (value.size() < min_blob_size_) {
if (value.empty() || value.size() < min_blob_size_) {
return Status::OK();
}
+125 -3
View File
@@ -9,6 +9,7 @@
#include <memory>
#include "db/blob/blob_file_reader.h"
#include "logging/logging.h"
#include "options/cf_options.h"
#include "rocksdb/cache.h"
#include "rocksdb/slice.h"
@@ -38,7 +39,8 @@ BlobFileCache::BlobFileCache(Cache* cache,
Status BlobFileCache::GetBlobFileReader(
const ReadOptions& read_options, uint64_t blob_file_number,
CacheHandleGuard<BlobFileReader>* blob_file_reader) {
CacheHandleGuard<BlobFileReader>* blob_file_reader,
bool allow_footer_skip_retry) {
assert(blob_file_reader);
assert(blob_file_reader->IsEmpty());
@@ -73,10 +75,22 @@ Status BlobFileCache::GetBlobFileReader(
{
assert(file_options_);
const Status s = BlobFileReader::Create(
Status s = BlobFileReader::Create(
*immutable_options_, read_options, *file_options_, column_family_id_,
blob_file_read_hist_, blob_file_number, io_tracer_, &reader);
blob_file_read_hist_, blob_file_number, io_tracer_,
/*skip_footer_validation=*/false, &reader);
if (!s.ok() && s.IsCorruption() && allow_footer_skip_retry) {
reader.reset();
s = BlobFileReader::Create(
*immutable_options_, read_options, *file_options_, column_family_id_,
blob_file_read_hist_, blob_file_number, io_tracer_,
/*skip_footer_validation=*/true, &reader);
}
if (!s.ok()) {
ROCKS_LOG_WARN(immutable_options_->logger,
"BlobFileCache open failed for blob file %" PRIu64
" in CF %u: %s",
blob_file_number, column_family_id_, s.ToString().c_str());
RecordTick(statistics, NO_FILE_ERRORS);
return s;
}
@@ -99,12 +113,120 @@ Status BlobFileCache::GetBlobFileReader(
return Status::OK();
}
Status BlobFileCache::OpenBlobFileReaderUncached(
const ReadOptions& read_options, uint64_t blob_file_number,
std::unique_ptr<BlobFileReader>* blob_file_reader,
bool allow_footer_skip_retry) {
assert(blob_file_reader);
assert(!*blob_file_reader);
Statistics* const statistics = immutable_options_->stats;
RecordTick(statistics, NO_FILE_OPENS);
Status s = BlobFileReader::Create(
*immutable_options_, read_options, *file_options_, column_family_id_,
blob_file_read_hist_, blob_file_number, io_tracer_,
/*skip_footer_validation=*/false, blob_file_reader);
if (!s.ok() && s.IsCorruption() && allow_footer_skip_retry) {
blob_file_reader->reset();
s = BlobFileReader::Create(
*immutable_options_, read_options, *file_options_, column_family_id_,
blob_file_read_hist_, blob_file_number, io_tracer_,
/*skip_footer_validation=*/true, blob_file_reader);
}
if (!s.ok()) {
RecordTick(statistics, NO_FILE_ERRORS);
}
return s;
}
Status BlobFileCache::InsertBlobFileReader(
uint64_t blob_file_number,
std::unique_ptr<BlobFileReader>* blob_file_reader,
CacheHandleGuard<BlobFileReader>* cached_blob_file_reader) {
assert(blob_file_reader);
assert(*blob_file_reader);
assert(cached_blob_file_reader);
assert(cached_blob_file_reader->IsEmpty());
const Slice key = GetSliceForKey(&blob_file_number);
// Serialize refreshes for the same blob file. The cache API does not expose
// a conditional replace, so refresh is intentionally modeled as
// Lookup/optional Erase/Insert under this per-key mutex.
MutexLock lock(&mutex_.Get(key));
TypedHandle* handle = cache_.Lookup(key);
if (handle) {
*cached_blob_file_reader = cache_.Guard(handle);
blob_file_reader->reset();
return Status::OK();
}
constexpr size_t charge = 1;
Status s = cache_.Insert(key, blob_file_reader->get(), charge, &handle);
if (!s.ok()) {
RecordTick(immutable_options_->stats, NO_FILE_ERRORS);
return s;
}
// Ownership transferred to the cache.
[[maybe_unused]] BlobFileReader* released_reader =
blob_file_reader->release();
*cached_blob_file_reader = cache_.Guard(handle);
return Status::OK();
}
Status BlobFileCache::RefreshBlobFileReader(
uint64_t blob_file_number,
std::unique_ptr<BlobFileReader>* blob_file_reader,
CacheHandleGuard<BlobFileReader>* cached_blob_file_reader) {
assert(blob_file_reader);
assert(*blob_file_reader);
assert(cached_blob_file_reader);
assert(cached_blob_file_reader->IsEmpty());
const Slice key = GetSliceForKey(&blob_file_number);
MutexLock lock(&mutex_.Get(key));
TypedHandle* handle = cache_.Lookup(key);
if (handle) {
BlobFileReader* const cached_reader = cache_.Value(handle);
assert(cached_reader != nullptr);
// Active direct-write blob files can grow between refresh attempts. Keep
// whichever reader observed the larger on-disk size so an older refresh
// cannot overwrite a newer one that another thread already installed.
if (cached_reader->GetFileSize() >= (*blob_file_reader)->GetFileSize()) {
*cached_blob_file_reader = cache_.Guard(handle);
blob_file_reader->reset();
return Status::OK();
}
cache_.Release(handle);
cache_.get()->Erase(key);
}
constexpr size_t charge = 1;
Status s = cache_.Insert(key, blob_file_reader->get(), charge, &handle);
if (!s.ok()) {
RecordTick(immutable_options_->stats, NO_FILE_ERRORS);
return s;
}
// Ownership transferred to the cache.
[[maybe_unused]] BlobFileReader* released_reader =
blob_file_reader->release();
*cached_blob_file_reader = cache_.Guard(handle);
return Status::OK();
}
void BlobFileCache::Evict(uint64_t blob_file_number) {
// NOTE: sharing same Cache with table_cache
const Slice key = GetSliceForKey(&blob_file_number);
assert(cache_);
MutexLock lock(&mutex_.Get(key));
cache_.get()->Erase(key);
}
+27 -1
View File
@@ -32,9 +32,35 @@ class BlobFileCache {
BlobFileCache(const BlobFileCache&) = delete;
BlobFileCache& operator=(const BlobFileCache&) = delete;
// Returns a cached reader for `blob_file_number`, opening and caching it on
// miss. If `allow_footer_skip_retry` is true, a footer-validation corruption
// retries once without requiring a footer.
Status GetBlobFileReader(const ReadOptions& read_options,
uint64_t blob_file_number,
CacheHandleGuard<BlobFileReader>* blob_file_reader);
CacheHandleGuard<BlobFileReader>* blob_file_reader,
bool allow_footer_skip_retry = false);
// Opens a blob file reader without inserting it into the cache.
Status OpenBlobFileReaderUncached(
const ReadOptions& read_options, uint64_t blob_file_number,
std::unique_ptr<BlobFileReader>* blob_file_reader,
bool allow_footer_skip_retry = false);
// Inserts a freshly opened uncached reader unless another thread already
// cached the same blob file.
Status InsertBlobFileReader(
uint64_t blob_file_number,
std::unique_ptr<BlobFileReader>* blob_file_reader,
CacheHandleGuard<BlobFileReader>* cached_blob_file_reader);
// Installs a refresh-opened reader into the cache. If another thread has
// already cached a reader opened on at least as large a file size, keep that
// reader instead so a racing refresh cannot reintroduce an older active-file
// view after the blob file grows.
Status RefreshBlobFileReader(
uint64_t blob_file_number,
std::unique_ptr<BlobFileReader>* blob_file_reader,
CacheHandleGuard<BlobFileReader>* cached_blob_file_reader);
// Called when a blob file is obsolete to ensure it is removed from the cache
// to avoid effectively leaking the open file and assicated memory
+96
View File
@@ -192,6 +192,102 @@ TEST_F(BlobFileCacheTest, GetBlobFileReader_Race) {
SyncPoint::GetInstance()->ClearAllCallBacks();
}
TEST_F(BlobFileCacheTest, RefreshBlobFileReaderPrefersLargestObservedFileSize) {
Options options;
options.env = mock_env_.get();
options.statistics = CreateDBStatistics();
options.cf_paths.emplace_back(
test::PerThreadDBPath(
mock_env_.get(),
"BlobFileCacheTest_"
"RefreshBlobFileReaderPrefersLargestObservedFileSize"),
0);
options.enable_blob_files = true;
constexpr uint32_t column_family_id = 1;
ImmutableOptions immutable_options(options);
constexpr uint64_t blob_file_number = 123;
const std::string blob_file_path =
BlobFileName(immutable_options.cf_paths.front().path, blob_file_number);
std::unique_ptr<FSWritableFile> file;
ASSERT_OK(NewWritableFile(immutable_options.fs.get(), blob_file_path, &file,
FileOptions()));
std::unique_ptr<WritableFileWriter> file_writer(new WritableFileWriter(
std::move(file), blob_file_path, FileOptions(), immutable_options.clock));
constexpr Statistics* statistics = nullptr;
constexpr bool use_fsync = false;
constexpr bool do_flush = false;
BlobLogWriter blob_log_writer(std::move(file_writer), immutable_options.clock,
statistics, blob_file_number, use_fsync,
do_flush);
constexpr bool has_ttl = false;
constexpr ExpirationRange expiration_range;
BlobLogHeader header(column_family_id, kNoCompression, has_ttl,
expiration_range);
ASSERT_OK(blob_log_writer.WriteHeader(WriteOptions(), header));
uint64_t key_offset = 0;
uint64_t blob_offset = 0;
ASSERT_OK(blob_log_writer.AddRecord(WriteOptions(), "key0", "blob0",
&key_offset, &blob_offset));
ASSERT_OK(blob_log_writer.Sync(WriteOptions()));
constexpr size_t capacity = 10;
std::shared_ptr<Cache> backing_cache = NewLRUCache(capacity);
FileOptions file_options;
constexpr HistogramImpl* blob_file_read_hist = nullptr;
BlobFileCache blob_file_cache(backing_cache.get(), &immutable_options,
&file_options, column_family_id,
blob_file_read_hist, nullptr /*IOTracer*/);
const ReadOptions read_options;
std::unique_ptr<BlobFileReader> stale_reader;
ASSERT_OK(blob_file_cache.OpenBlobFileReaderUncached(
read_options, blob_file_number, &stale_reader,
/*allow_footer_skip_retry=*/true));
std::unique_ptr<BlobFileReader> initial_cached_reader;
ASSERT_OK(blob_file_cache.OpenBlobFileReaderUncached(
read_options, blob_file_number, &initial_cached_reader,
/*allow_footer_skip_retry=*/true));
CacheHandleGuard<BlobFileReader> cached_reader;
ASSERT_OK(blob_file_cache.RefreshBlobFileReader(
blob_file_number, &initial_cached_reader, &cached_reader));
ASSERT_NE(cached_reader.GetValue(), nullptr);
const uint64_t initial_file_size = cached_reader.GetValue()->GetFileSize();
ASSERT_OK(blob_log_writer.AddRecord(WriteOptions(), "key1", "blob1",
&key_offset, &blob_offset));
ASSERT_OK(blob_log_writer.Sync(WriteOptions()));
std::unique_ptr<BlobFileReader> fresh_reader;
ASSERT_OK(blob_file_cache.OpenBlobFileReaderUncached(
read_options, blob_file_number, &fresh_reader,
/*allow_footer_skip_retry=*/true));
ASSERT_GT(fresh_reader->GetFileSize(), initial_file_size);
CacheHandleGuard<BlobFileReader> refreshed_reader;
ASSERT_OK(blob_file_cache.RefreshBlobFileReader(
blob_file_number, &fresh_reader, &refreshed_reader));
ASSERT_NE(refreshed_reader.GetValue(), nullptr);
ASSERT_GT(refreshed_reader.GetValue()->GetFileSize(), initial_file_size);
BlobFileReader* const largest_reader = refreshed_reader.GetValue();
CacheHandleGuard<BlobFileReader> preserved_reader;
ASSERT_OK(blob_file_cache.RefreshBlobFileReader(
blob_file_number, &stale_reader, &preserved_reader));
ASSERT_NE(preserved_reader.GetValue(), nullptr);
ASSERT_EQ(preserved_reader.GetValue(), largest_reader);
ASSERT_EQ(stale_reader.get(), nullptr);
}
TEST_F(BlobFileCacheTest, GetBlobFileReader_IOError) {
Options options;
options.env = mock_env_.get();
+851
View File
@@ -0,0 +1,851 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#include "db/blob/blob_file_partition_manager.h"
#include <array>
#include <atomic>
#include <cinttypes>
#include <memory>
#include <utility>
#include "cache/cache_key.h"
#include "cache/typed_cache.h"
#include "db/blob/blob_contents.h"
#include "db/blob/blob_file_cache.h"
#include "db/blob/blob_file_completion_callback.h"
#include "db/blob/blob_file_reader.h"
#include "db/blob/blob_index.h"
#include "db/blob/blob_log_writer.h"
#include "db/version_set.h"
#include "file/filename.h"
#include "file/read_write_util.h"
#include "file/writable_file_writer.h"
#include "logging/logging.h"
#include "util/aligned_buffer.h"
#include "util/compression.h"
#include "util/mutexlock.h"
namespace ROCKSDB_NAMESPACE {
namespace {
class RoundRobinBlobFilePartitionStrategy : public BlobFilePartitionStrategy {
public:
using BlobFilePartitionStrategy::SelectPartition;
const char* Name() const override {
return "RoundRobinBlobFilePartitionStrategy";
}
uint32_t SelectPartition(uint32_t num_partitions,
uint32_t /*column_family_id*/, const Slice& /*key*/,
const Slice& /*value*/) override {
assert(num_partitions > 0);
return static_cast<uint32_t>(
next_partition_.fetch_add(1, std::memory_order_relaxed) %
static_cast<uint64_t>(num_partitions));
}
private:
std::atomic<uint64_t> next_partition_{0};
};
struct DirectWriteCompressionState {
CompressionOptions compression_opts;
// `working_area` must be released before its owning compressor.
std::unique_ptr<Compressor> compressor;
Compressor::ManagedWorkingArea working_area;
};
DirectWriteCompressionState& GetDirectWriteCompressionState(
CompressionType compression, const CompressionOptions& compression_opts) {
assert(compression <= kLastBuiltinCompression);
static thread_local std::array<DirectWriteCompressionState,
static_cast<size_t>(kLastBuiltinCompression) +
1>
compression_states;
auto& compression_state =
compression_states[static_cast<size_t>(compression)];
if (compression != kNoCompression &&
(compression_state.compressor == nullptr ||
compression_state.compression_opts != compression_opts)) {
// BDW compression settings are mutable, so rebuild the per-thread cached
// compressor when the latest published opts for this type change.
compression_state.working_area = Compressor::ManagedWorkingArea{};
compression_state.compressor.reset();
compression_state.compression_opts = compression_opts;
compression_state.compressor =
GetBuiltinV2CompressionManager()->GetCompressor(compression_opts,
compression);
if (compression_state.compressor != nullptr) {
compression_state.working_area =
compression_state.compressor->ObtainWorkingArea();
}
}
return compression_state;
}
} // namespace
BlobFilePartitionManager::BlobFilePartitionManager(
uint32_t num_partitions,
std::shared_ptr<BlobFilePartitionStrategy> strategy,
FileNumberAllocator file_number_allocator, FileSystem* fs,
SystemClock* clock, Statistics* statistics, const FileOptions& file_options,
std::string db_path, std::string column_family_name,
uint64_t blob_file_size, bool use_fsync, BlobFileCache* blob_file_cache,
BlobFileCompletionCallback* blob_callback,
const std::vector<std::shared_ptr<EventListener>>& listeners,
FileChecksumGenFactory* file_checksum_gen_factory,
const FileTypeSet& checksum_handoff_file_types,
const std::shared_ptr<IOTracer>& io_tracer, std::string db_id,
std::string db_session_id, Logger* info_log)
: num_partitions_(num_partitions == 0 ? 1 : num_partitions),
strategy_(strategy != nullptr
? std::move(strategy)
: std::make_shared<RoundRobinBlobFilePartitionStrategy>()),
file_number_allocator_(std::move(file_number_allocator)),
fs_(fs),
clock_(clock),
statistics_(statistics),
file_options_(file_options),
db_path_(std::move(db_path)),
column_family_name_(std::move(column_family_name)),
blob_file_size_(blob_file_size),
use_fsync_(use_fsync),
blob_file_cache_(blob_file_cache),
blob_callback_(blob_callback),
listeners_(listeners),
file_checksum_gen_factory_(file_checksum_gen_factory),
checksum_handoff_file_types_(checksum_handoff_file_types),
io_tracer_(io_tracer),
db_id_(std::move(db_id)),
db_session_id_(std::move(db_session_id)),
info_log_(info_log) {
partitions_.reserve(num_partitions_);
for (uint32_t i = 0; i < num_partitions_; ++i) {
partitions_.emplace_back(new Partition());
}
}
BlobFilePartitionManager::~BlobFilePartitionManager() {
if (blob_file_cache_ != nullptr) {
std::vector<uint64_t> tracked_file_numbers;
{
ReadLock lock(&file_partition_mutex_);
tracked_file_numbers.reserve(file_to_partition_.size());
for (const auto& entry : file_to_partition_) {
tracked_file_numbers.push_back(entry.first);
}
}
for (uint64_t file_number : tracked_file_numbers) {
// Dropping the CF or closing the DB can destroy the manager while active
// direct-write readers are still cached. Evict them before the CF-owned
// blob cache goes away so Close() does not retain obsolete footer-less
// readers for files that are no longer live.
blob_file_cache_->Evict(file_number);
}
}
}
void BlobFilePartitionManager::ResetPartitionState(Partition* partition) {
partition->writer.reset();
partition->file_number = 0;
partition->file_size = 0;
partition->blob_count = 0;
partition->total_blob_bytes = 0;
partition->garbage_blob_count = 0;
partition->garbage_blob_bytes = 0;
partition->column_family_id = 0;
partition->compression = kNoCompression;
partition->sync_required = false;
}
void BlobFilePartitionManager::AddFilePartitionMapping(uint64_t file_number,
uint32_t partition_idx) {
WriteLock lock(&file_partition_mutex_);
file_to_partition_[file_number] = partition_idx;
}
void BlobFilePartitionManager::RemoveFilePartitionMapping(
uint64_t file_number) {
WriteLock lock(&file_partition_mutex_);
file_to_partition_.erase(file_number);
}
Status BlobFilePartitionManager::OpenNewBlobFile(Partition* partition,
uint32_t column_family_id,
CompressionType compression,
uint32_t partition_idx) {
assert(partition != nullptr);
assert(!partition->writer);
const uint64_t blob_file_number = file_number_allocator_();
const std::string blob_file_path = BlobFileName(db_path_, blob_file_number);
AddFilePartitionMapping(blob_file_number, partition_idx);
if (blob_callback_ != nullptr) {
blob_callback_->OnBlobFileCreationStarted(
blob_file_path, column_family_name_,
/*job_id=*/0, BlobFileCreationReason::kFlush);
}
std::unique_ptr<FSWritableFile> file;
Status s = NewWritableFile(fs_, blob_file_path, &file, file_options_);
if (!s.ok()) {
RemoveFilePartitionMapping(blob_file_number);
return s;
}
const bool perform_data_verification =
checksum_handoff_file_types_.Contains(FileType::kBlobFile);
auto file_writer = std::make_unique<WritableFileWriter>(
std::move(file), blob_file_path, file_options_, clock_, io_tracer_,
statistics_, Histograms::BLOB_DB_BLOB_FILE_WRITE_MICROS, listeners_,
file_checksum_gen_factory_, perform_data_verification);
// This only drains WritableFileWriter's buffered bytes so readers can see
// each appended record promptly. Durability still comes from SyncAllOpenFiles
// or AppendFooter(), both of which call Sync().
constexpr bool kDoFlushEachRecord = true;
auto blob_log_writer = std::make_unique<BlobLogWriter>(
std::move(file_writer), clock_, statistics_, blob_file_number, use_fsync_,
kDoFlushEachRecord);
constexpr bool has_ttl = false;
constexpr ExpirationRange expiration_range{};
BlobLogHeader header(column_family_id, compression, has_ttl,
expiration_range);
s = blob_log_writer->WriteHeader(WriteOptions(), header);
if (!s.ok()) {
RemoveFilePartitionMapping(blob_file_number);
return s;
}
partition->writer = std::move(blob_log_writer);
partition->file_number = blob_file_number;
partition->file_size = BlobLogHeader::kSize;
partition->blob_count = 0;
partition->total_blob_bytes = 0;
partition->garbage_blob_count = 0;
partition->garbage_blob_bytes = 0;
partition->column_family_id = column_family_id;
partition->compression = compression;
partition->sync_required = false;
return Status::OK();
}
Status BlobFilePartitionManager::SealActiveBlobFile(
const WriteOptions& write_options, Partition* partition,
SealedFile* sealed_file) {
assert(partition != nullptr);
assert(partition->writer);
assert(sealed_file != nullptr);
const uint64_t file_number = partition->file_number;
Status s =
FinalizeBlobFile(write_options, partition->writer.get(), file_number,
partition->blob_count, partition->total_blob_bytes,
&sealed_file->addition);
if (!s.ok()) {
RemoveFilePartitionMapping(file_number);
ResetPartitionState(partition);
return s;
}
sealed_file->garbage_blob_count = partition->garbage_blob_count;
sealed_file->garbage_blob_bytes = partition->garbage_blob_bytes;
ResetPartitionState(partition);
return Status::OK();
}
Status BlobFilePartitionManager::SealDeferredFile(
const WriteOptions& write_options, DeferredFile* deferred,
SealedFile* sealed_file) {
assert(deferred != nullptr);
assert(deferred->writer);
assert(sealed_file != nullptr);
Status s = FinalizeBlobFile(
write_options, deferred->writer.get(), deferred->file_number,
deferred->blob_count, deferred->total_blob_bytes, &sealed_file->addition);
if (!s.ok()) {
RemoveFilePartitionMapping(deferred->file_number);
return s;
}
sealed_file->garbage_blob_count = deferred->garbage_blob_count;
sealed_file->garbage_blob_bytes = deferred->garbage_blob_bytes;
deferred->writer.reset();
return Status::OK();
}
Status BlobFilePartitionManager::FinalizeBlobFile(
const WriteOptions& write_options, BlobLogWriter* writer,
uint64_t file_number, uint64_t blob_count, uint64_t total_blob_bytes,
BlobFileAddition* addition) {
assert(writer != nullptr);
assert(addition != nullptr);
BlobLogFooter footer;
footer.blob_count = blob_count;
std::string checksum_method;
std::string checksum_value;
Status s = writer->AppendFooter(write_options, footer, &checksum_method,
&checksum_value);
if (!s.ok()) {
return s;
}
if (blob_callback_ != nullptr) {
const std::string blob_file_path = BlobFileName(db_path_, file_number);
s = blob_callback_->OnBlobFileCompleted(
blob_file_path, column_family_name_, /*job_id=*/0, file_number,
BlobFileCreationReason::kFlush, s, checksum_value, checksum_method,
blob_count, total_blob_bytes);
if (!s.ok()) {
return s;
}
}
*addition =
BlobFileAddition(file_number, blob_count, total_blob_bytes,
std::move(checksum_method), std::move(checksum_value));
return Status::OK();
}
void BlobFilePartitionManager::AddSealedFileGarbage(
const SealedFile& sealed_file, std::vector<BlobFileGarbage>* garbages) {
assert(garbages != nullptr);
if (sealed_file.garbage_blob_count == 0) {
assert(sealed_file.garbage_blob_bytes == 0);
return;
}
garbages->emplace_back(sealed_file.addition.GetBlobFileNumber(),
sealed_file.garbage_blob_count,
sealed_file.garbage_blob_bytes);
}
bool BlobFilePartitionManager::MarkPartitionGarbage(Partition* partition,
uint64_t file_number,
uint64_t blob_count,
uint64_t blob_bytes) {
assert(partition != nullptr);
if (!partition->writer || partition->file_number != file_number) {
return false;
}
partition->garbage_blob_count += blob_count;
partition->garbage_blob_bytes += blob_bytes;
assert(partition->garbage_blob_count <= partition->blob_count);
assert(partition->garbage_blob_bytes <= partition->total_blob_bytes);
return true;
}
bool BlobFilePartitionManager::MarkDeferredFileGarbage(DeferredFile* deferred,
uint64_t file_number,
uint64_t blob_count,
uint64_t blob_bytes) {
assert(deferred != nullptr);
if (deferred->file_number != file_number) {
return false;
}
deferred->garbage_blob_count += blob_count;
deferred->garbage_blob_bytes += blob_bytes;
assert(deferred->garbage_blob_count <= deferred->blob_count);
assert(deferred->garbage_blob_bytes <= deferred->total_blob_bytes);
return true;
}
bool BlobFilePartitionManager::MarkSealedFileGarbage(
std::vector<SealedFile>* sealed_files, uint64_t file_number,
uint64_t blob_count, uint64_t blob_bytes) {
assert(sealed_files != nullptr);
for (auto& sealed_file : *sealed_files) {
if (sealed_file.addition.GetBlobFileNumber() != file_number) {
continue;
}
sealed_file.garbage_blob_count += blob_count;
sealed_file.garbage_blob_bytes += blob_bytes;
assert(sealed_file.garbage_blob_count <=
sealed_file.addition.GetTotalBlobCount());
assert(sealed_file.garbage_blob_bytes <=
sealed_file.addition.GetTotalBlobBytes());
return true;
}
return false;
}
Status BlobFilePartitionManager::MaybePrepopulateBlobCache(
const BlobDirectWriteSettings& settings, const Slice& original_value,
uint64_t blob_file_number, uint64_t blob_offset) {
if (settings.blob_cache == nullptr ||
settings.prepopulate_blob_cache != PrepopulateBlobCache::kFlushOnly) {
return Status::OK();
}
FullTypedCacheInterface<BlobContents, BlobContentsCreator> blob_cache{
settings.blob_cache};
const OffsetableCacheKey base_cache_key(db_id_, db_session_id_,
blob_file_number);
const CacheKey cache_key = base_cache_key.WithOffset(blob_offset);
return blob_cache.InsertSaved(cache_key.AsSlice(), original_value, nullptr,
Cache::Priority::BOTTOM,
CacheTier::kVolatileTier);
}
uint32_t BlobFilePartitionManager::SelectWideColumnPartition(
uint32_t column_family_id, const Slice& key,
const WideColumns& columns) const {
return strategy_->SelectPartition(num_partitions_, column_family_id, key,
columns) %
num_partitions_;
}
Status BlobFilePartitionManager::WriteBlob(
const WriteOptions& write_options, uint32_t column_family_id,
CompressionType compression, const Slice& key, const Slice& value,
uint64_t* blob_file_number, uint64_t* blob_offset, uint64_t* blob_size,
const BlobDirectWriteSettings& settings, const uint32_t* partition_idx) {
assert(blob_file_number != nullptr);
assert(blob_offset != nullptr);
assert(blob_size != nullptr);
// Do compression before taking the partition mutex so large-value CPU work
// does not serialize writers. A concurrent file rollover can still cause
// this compressed buffer to be discarded below, which is acceptable on this
// non-hot failure/configuration-change path.
GrowableBuffer compressed_value;
Slice write_value = value;
if (compression != kNoCompression) {
auto& compression_state =
GetDirectWriteCompressionState(compression, settings.compression_opts);
if (compression_state.compressor == nullptr) {
return Status::NotSupported(
"Blob direct write compression type not supported");
}
Status s = LegacyForceBuiltinCompression(*compression_state.compressor,
&compression_state.working_area,
value, &compressed_value);
if (!s.ok()) {
return s;
}
write_value = Slice(compressed_value);
}
// Partition selection is based on the logical write inputs. In particular,
// strategies that inspect value contents or size see the original
// uncompressed value rather than `write_value`. The modulo here is
// intentional so custom strategies can return arbitrary hashed or sentinel
// values without violating the partition bounds.
const uint32_t selected_partition_idx =
partition_idx != nullptr
? (*partition_idx % num_partitions_)
: (strategy_->SelectPartition(num_partitions_, column_family_id, key,
value) %
num_partitions_);
{
MutexLock lock(&mutex_);
Partition* partition = partitions_[selected_partition_idx].get();
auto seal_current_file = [&]() -> Status {
if (!partition->writer) {
return Status::OK();
}
SealedFile sealed_file;
Status s = SealActiveBlobFile(write_options, partition, &sealed_file);
if (s.ok()) {
current_generation_sealed_files_.push_back(std::move(sealed_file));
}
return s;
};
const uint64_t record_size =
BlobLogRecord::kHeaderSize + key.size() + write_value.size();
const uint64_t future_file_size =
partition->file_size + record_size + BlobLogFooter::kSize;
if (partition->writer &&
(partition->column_family_id != column_family_id ||
partition->compression != compression ||
(partition->blob_count > 0 && future_file_size > blob_file_size_))) {
Status s = seal_current_file();
if (!s.ok()) {
return s;
}
}
if (!partition->writer) {
Status s = OpenNewBlobFile(partition, column_family_id, compression,
selected_partition_idx);
if (!s.ok()) {
return s;
}
}
uint64_t key_offset = 0;
Status s = partition->writer->AddRecord(write_options, key, write_value,
&key_offset, blob_offset);
if (!s.ok()) {
return s;
}
partition->sync_required = true;
partition->blob_count += 1;
partition->total_blob_bytes += record_size;
partition->file_size = BlobLogHeader::kSize + partition->total_blob_bytes;
*blob_file_number = partition->file_number;
*blob_size = write_value.size();
}
Status prepopulate_s = MaybePrepopulateBlobCache(
settings, value, *blob_file_number, *blob_offset);
if (!prepopulate_s.ok() && info_log_ != nullptr) {
ROCKS_LOG_WARN(info_log_,
"Failed to pre-populate direct-write blob cache entry: %s",
prepopulate_s.ToString().c_str());
}
return Status::OK();
}
void BlobFilePartitionManager::RotateCurrentGeneration() {
MutexLock lock(&mutex_);
GenerationBatch batch;
batch.sealed_files = std::move(current_generation_sealed_files_);
current_generation_sealed_files_.clear();
for (auto& partition : partitions_) {
if (!partition->writer) {
continue;
}
DeferredFile deferred;
deferred.writer = std::move(partition->writer);
deferred.file_number = partition->file_number;
deferred.blob_count = partition->blob_count;
deferred.total_blob_bytes = partition->total_blob_bytes;
deferred.garbage_blob_count = partition->garbage_blob_count;
deferred.garbage_blob_bytes = partition->garbage_blob_bytes;
ResetPartitionState(partition.get());
batch.deferred_files.emplace_back(std::move(deferred));
}
pending_generations_.emplace_back(std::move(batch));
}
Status BlobFilePartitionManager::PrepareFlushAdditions(
const WriteOptions& write_options, size_t num_generations,
std::vector<BlobFileAddition>* additions,
std::vector<BlobFileGarbage>* garbages,
std::vector<std::vector<uint64_t>>* generation_blob_file_numbers) {
assert(additions != nullptr);
assert(garbages != nullptr);
additions->clear();
garbages->clear();
if (generation_blob_file_numbers != nullptr) {
generation_blob_file_numbers->clear();
}
MutexLock lock(&mutex_);
if (num_generations > pending_generations_.size()) {
return Status::Corruption(
"Missing blob direct write generation metadata for flush");
}
for (size_t i = 0; i < num_generations; ++i) {
GenerationBatch& batch = pending_generations_[i];
while (!batch.deferred_files.empty()) {
DeferredFile deferred = std::move(batch.deferred_files.front());
batch.deferred_files.pop_front();
SealedFile sealed_file;
Status s = SealDeferredFile(write_options, &deferred, &sealed_file);
if (!s.ok()) {
return s;
}
// Keep each successfully sealed file attached to the generation
// immediately. If a later seal or the flush job itself fails, retry must
// reuse these exact on-disk files instead of finalizing replacements.
batch.sealed_files.push_back(std::move(sealed_file));
}
std::vector<uint64_t>* generation_file_numbers = nullptr;
if (generation_blob_file_numbers != nullptr) {
generation_blob_file_numbers->emplace_back();
generation_file_numbers = &generation_blob_file_numbers->back();
generation_file_numbers->reserve(batch.sealed_files.size());
}
for (const auto& sealed_file : batch.sealed_files) {
additions->push_back(sealed_file.addition);
AddSealedFileGarbage(sealed_file, garbages);
if (generation_file_numbers != nullptr) {
generation_file_numbers->push_back(
sealed_file.addition.GetBlobFileNumber());
}
}
}
return Status::OK();
}
Status BlobFilePartitionManager::MarkBlobWriteAsGarbage(uint64_t file_number,
uint64_t blob_count,
uint64_t blob_bytes) {
if (blob_count == 0) {
assert(blob_bytes == 0);
return Status::OK();
}
MutexLock lock(&mutex_);
for (auto& partition : partitions_) {
if (MarkPartitionGarbage(partition.get(), file_number, blob_count,
blob_bytes)) {
return Status::OK();
}
}
if (MarkSealedFileGarbage(&current_generation_sealed_files_, file_number,
blob_count, blob_bytes)) {
return Status::OK();
}
for (auto& batch : pending_generations_) {
for (auto& deferred : batch.deferred_files) {
if (MarkDeferredFileGarbage(&deferred, file_number, blob_count,
blob_bytes)) {
return Status::OK();
}
}
if (MarkSealedFileGarbage(&batch.sealed_files, file_number, blob_count,
blob_bytes)) {
return Status::OK();
}
}
const std::string message =
"Could not match failed blob direct-write rollback for file #" +
std::to_string(file_number);
if (info_log_ != nullptr) {
ROCKS_LOG_ERROR(info_log_, "%s", message.c_str());
}
return Status::Corruption(message);
}
void BlobFilePartitionManager::CommitPreparedGenerations(
size_t num_generations) {
MutexLock lock(&mutex_);
while (num_generations-- > 0 && !pending_generations_.empty()) {
pending_generations_.pop_front();
}
}
Status BlobFilePartitionManager::SyncAllOpenFiles(
const WriteOptions& write_options) {
MutexLock lock(&mutex_);
for (const auto& partition : partitions_) {
if (!partition->writer || !partition->sync_required) {
continue;
}
Status s = partition->writer->Sync(write_options);
if (!s.ok()) {
return s;
}
partition->sync_required = false;
}
return Status::OK();
}
void BlobFilePartitionManager::GetActiveBlobFileNumbers(
UnorderedSet<uint64_t>* file_numbers) const {
assert(file_numbers != nullptr);
ReadLock lock(&file_partition_mutex_);
for (const auto& entry : file_to_partition_) {
file_numbers->insert(entry.first);
}
}
void BlobFilePartitionManager::GetProtectedBlobFileNumbers(
UnorderedSet<uint64_t>* file_numbers) const {
assert(file_numbers != nullptr);
ReadLock lock(&file_partition_mutex_);
for (const auto& entry : protected_blob_file_refs_) {
file_numbers->insert(entry.first);
}
}
bool BlobFilePartitionManager::IsTrackedBlobFileNumber(
uint64_t file_number) const {
ReadLock lock(&file_partition_mutex_);
return file_to_partition_.find(file_number) != file_to_partition_.end() ||
protected_blob_file_refs_.find(file_number) !=
protected_blob_file_refs_.end();
}
void BlobFilePartitionManager::ProtectSealedBlobFileNumbers(
const std::vector<uint64_t>& file_numbers) {
if (file_numbers.empty()) {
return;
}
WriteLock lock(&file_partition_mutex_);
for (uint64_t file_number : file_numbers) {
++protected_blob_file_refs_[file_number];
}
}
void BlobFilePartitionManager::UnprotectSealedBlobFileNumbers(
const std::vector<uint64_t>& file_numbers) {
if (file_numbers.empty()) {
return;
}
WriteLock lock(&file_partition_mutex_);
for (uint64_t file_number : file_numbers) {
auto it = protected_blob_file_refs_.find(file_number);
if (it == protected_blob_file_refs_.end()) {
if (info_log_ != nullptr) {
ROCKS_LOG_ERROR(info_log_,
"Memtable blob protection underflow for file #%" PRIu64,
file_number);
}
continue;
}
assert(it->second > 0);
--it->second;
if (it->second == 0) {
protected_blob_file_refs_.erase(it);
if (blob_file_cache_ != nullptr) {
// Once the last memtable-backed reference goes away, any cached reader
// for this file is either obsolete or can be reopened through the
// manifest-visible path. Evict it here so delayed protection does not
// leave an obsolete blob reader behind until DB close.
blob_file_cache_->Evict(file_number);
}
}
}
}
void BlobFilePartitionManager::RemoveFilePartitionMappings(
const std::vector<uint64_t>& file_numbers) {
if (file_numbers.empty()) {
return;
}
WriteLock lock(&file_partition_mutex_);
for (uint64_t file_number : file_numbers) {
file_to_partition_.erase(file_number);
if (blob_file_cache_ != nullptr) {
// In-flight direct-write reads may have cached a footer-less reader for
// this file before it was sealed. Drop it now so future manifest-visible
// reads reopen against the finalized on-disk size and footer state.
blob_file_cache_->Evict(file_number);
}
}
}
Status BlobFilePartitionManager::ResolveBlobDirectWriteIndex(
const ReadOptions& read_options, const Slice& user_key,
const BlobIndex& blob_idx, const Version* version,
BlobFileCache* blob_file_cache, FilePrefetchBuffer* prefetch_buffer,
PinnableSlice* blob_value, uint64_t* bytes_read) {
assert(blob_value != nullptr);
if (version != nullptr) {
// Only fall back when the blob file is still owned exclusively by the
// write path and therefore absent from Version metadata. Once Version
// knows the file number, every result from Version::GetBlob(), including
// corruption, must propagate directly.
if (blob_idx.HasTTL() || blob_idx.IsInlined() ||
version->storage_info()->GetBlobFileMetaData(blob_idx.file_number()) !=
nullptr) {
return version->GetBlob(read_options, user_key, blob_idx, prefetch_buffer,
blob_value, bytes_read);
}
}
if (blob_file_cache == nullptr) {
return version != nullptr ? Status::Corruption("Invalid blob file number")
: Status::NotFound();
}
if (read_options.read_tier == kBlockCacheTier) {
// The direct-write fallback below may need to open the blob file reader,
// which `kBlockCacheTier` forbids. Keep the normal Version-backed path
// above eligible for cache-only hits.
return Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
}
Status s;
CacheHandleGuard<BlobFileReader> reader;
s = blob_file_cache->GetBlobFileReader(read_options, blob_idx.file_number(),
&reader,
/*allow_footer_skip_retry=*/true);
if (!s.ok()) {
return s;
}
std::unique_ptr<BlobContents> blob_contents;
s = reader.GetValue()->GetBlob(read_options, user_key, blob_idx.offset(),
blob_idx.size(), blob_idx.compression(),
prefetch_buffer, nullptr, &blob_contents,
bytes_read);
if (s.ok()) {
blob_value->PinSelf(blob_contents->data());
return s;
}
if (!s.IsCorruption()) {
return s;
}
reader.Reset();
blob_file_cache->Evict(blob_idx.file_number());
std::unique_ptr<BlobFileReader> fresh_reader;
s = blob_file_cache->OpenBlobFileReaderUncached(
read_options, blob_idx.file_number(), &fresh_reader,
/*allow_footer_skip_retry=*/true);
if (!s.ok()) {
return s;
}
std::unique_ptr<BlobContents> fresh_contents;
s = fresh_reader->GetBlob(read_options, user_key, blob_idx.offset(),
blob_idx.size(), blob_idx.compression(),
prefetch_buffer, nullptr, &fresh_contents,
bytes_read);
if (s.ok()) {
blob_value->PinSelf(fresh_contents->data());
CacheHandleGuard<BlobFileReader> ignored;
blob_file_cache
->RefreshBlobFileReader(blob_idx.file_number(), &fresh_reader, &ignored)
.PermitUncheckedError();
}
return s;
}
} // namespace ROCKSDB_NAMESPACE
+294
View File
@@ -0,0 +1,294 @@
// 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).
#pragma once
#include <cstdint>
#include <deque>
#include <functional>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
#include "db/blob/blob_file_addition.h"
#include "db/blob/blob_file_garbage.h"
#include "db/blob/blob_log_format.h"
#include "db/blob/blob_write_batch_transformer.h"
#include "port/port.h"
#include "rocksdb/compression_type.h"
#include "rocksdb/env.h"
#include "rocksdb/file_system.h"
#include "rocksdb/options.h"
#include "rocksdb/rocksdb_namespace.h"
#include "rocksdb/slice.h"
#include "rocksdb/status.h"
#include "util/hash_containers.h"
namespace ROCKSDB_NAMESPACE {
class BlobFileCache;
class BlobFileCompletionCallback;
class BlobIndex;
class BlobLogWriter;
class FilePrefetchBuffer;
class IOTracer;
class Logger;
class PinnableSlice;
class Version;
class WritableFileWriter;
struct FileOptions;
struct ReadOptions;
// Manages per-partition blob files for write-path blob direct write.
//
// The v1 design keeps each memtable switch as one FIFO generation batch.
// Direct-write files for that memtable are sealed and registered when the
// matching flush commits. The current implementation still serializes blob
// appends through one manager mutex; follow-up PRs can add independent
// per-partition locks to allow concurrent blob writes without changing the
// generation/flush contract. Follow-up PRs can also broaden compatibility as
// the feature matures.
class BlobFilePartitionManager {
public:
using FileNumberAllocator = std::function<uint64_t()>;
// Creates the per-column-family manager for write-path blob files.
BlobFilePartitionManager(
uint32_t num_partitions,
std::shared_ptr<BlobFilePartitionStrategy> strategy,
FileNumberAllocator file_number_allocator, FileSystem* fs,
SystemClock* clock, Statistics* statistics,
const FileOptions& file_options, std::string db_path,
std::string column_family_name, uint64_t blob_file_size, bool use_fsync,
BlobFileCache* blob_file_cache, BlobFileCompletionCallback* blob_callback,
const std::vector<std::shared_ptr<EventListener>>& listeners,
FileChecksumGenFactory* file_checksum_gen_factory,
const FileTypeSet& checksum_handoff_file_types,
const std::shared_ptr<IOTracer>& io_tracer, std::string db_id,
std::string db_session_id, Logger* info_log);
// Evicts cached readers for manager-owned files during shutdown.
~BlobFilePartitionManager();
// Appends one blob record to a partition file and returns the resulting
// BlobIndex location metadata.
Status WriteBlob(const WriteOptions& write_options, uint32_t column_family_id,
CompressionType compression, const Slice& key,
const Slice& value, uint64_t* blob_file_number,
uint64_t* blob_offset, uint64_t* blob_size,
const BlobDirectWriteSettings& settings,
const uint32_t* partition_idx = nullptr);
// Selects the partition to use for all blob-backed columns of one PutEntity
// operation. The return value is already normalized to [0, num_partitions_).
uint32_t SelectWideColumnPartition(uint32_t column_family_id,
const Slice& key,
const WideColumns& columns) const;
// Move the current active partition files into the next immutable
// memtable-generation batch. Called from SwitchMemtable() while DB mutex is
// held. No I/O is performed here.
void RotateCurrentGeneration();
// Seal the first `num_generations` queued immutable generations and return
// all blob additions and initial-garbage updates that must be registered
// with the matching flush. Generations stay queued until
// CommitPreparedGenerations() is called. If `generation_blob_file_numbers`
// is non-null, it receives the sealed blob file numbers for each prepared
// generation in the same FIFO order.
Status PrepareFlushAdditions(const WriteOptions& write_options,
size_t num_generations,
std::vector<BlobFileAddition>* additions,
std::vector<BlobFileGarbage>* garbages,
std::vector<std::vector<uint64_t>>*
generation_blob_file_numbers = nullptr);
// Remove the first `num_generations` prepared immutable generations after
// their blob-file additions and garbage edits were committed to MANIFEST.
void CommitPreparedGenerations(size_t num_generations);
// In v1 flush-on-write mode each AddRecord flushes to the OS immediately, so
// there is nothing extra to do for the non-sync write path.
Status FlushAllOpenFiles(const WriteOptions& /*write_options*/) {
return Status::OK();
}
// Sync the active open blob files before a sync WAL write.
Status SyncAllOpenFiles(const WriteOptions& write_options);
// Returns blob files still owned by the manager and therefore not yet safe
// to consider obsolete.
void GetActiveBlobFileNumbers(UnorderedSet<uint64_t>* file_numbers) const;
// Returns sealed blob files that are still reachable through live memtables
// or old SuperVersions and therefore must not be purged yet.
void GetProtectedBlobFileNumbers(UnorderedSet<uint64_t>* file_numbers) const;
// Returns true when the blob file is still owned by the write path or
// protected by a live memtable / old SuperVersion.
bool IsTrackedBlobFileNumber(uint64_t file_number) const;
// Increments / decrements memtable-held protection on sealed blob files.
void ProtectSealedBlobFileNumbers(const std::vector<uint64_t>& file_numbers);
void UnprotectSealedBlobFileNumbers(
const std::vector<uint64_t>& file_numbers);
// Stops protecting file numbers that are now registered in MANIFEST.
void RemoveFilePartitionMappings(const std::vector<uint64_t>& file_numbers);
// Marks blob records from a failed transformed write as initial garbage for
// the target blob file while keeping the physical bytes in place.
// Returns Corruption if the manager can no longer match the blob file.
Status MarkBlobWriteAsGarbage(uint64_t file_number, uint64_t blob_count,
uint64_t blob_bytes);
// Resolves a direct-write BlobIndex by consulting manifest-visible state
// first, then falling back to direct blob-file reads only when the target
// blob file is still write-path-owned and therefore not yet tracked by
// Version. Existing manifest-visible read results, including I/O failures,
// are returned directly rather than masked by fallback logic.
static Status ResolveBlobDirectWriteIndex(
const ReadOptions& read_options, const Slice& user_key,
const BlobIndex& blob_idx, const Version* version,
BlobFileCache* blob_file_cache, FilePrefetchBuffer* prefetch_buffer,
PinnableSlice* blob_value, uint64_t* bytes_read);
private:
struct Partition {
// Active writer for this partition, or null when the partition is idle.
std::unique_ptr<BlobLogWriter> writer;
// Metadata tracked while the active file remains open.
uint64_t file_number = 0;
uint64_t file_size = 0;
uint64_t blob_count = 0;
uint64_t total_blob_bytes = 0;
// Failed transformed writes already appended to this physical file.
uint64_t garbage_blob_count = 0;
uint64_t garbage_blob_bytes = 0;
uint32_t column_family_id = 0;
CompressionType compression = kNoCompression;
bool sync_required = false;
};
struct DeferredFile {
// Blob writer moved out of an active partition at memtable rotation.
std::unique_ptr<BlobLogWriter> writer;
// Metadata preserved until flush preparation seals the file.
uint64_t file_number = 0;
uint64_t blob_count = 0;
uint64_t total_blob_bytes = 0;
// Failed transformed writes already appended before the file is sealed.
uint64_t garbage_blob_count = 0;
uint64_t garbage_blob_bytes = 0;
};
struct SealedFile {
// Blob-file metadata ready to be added to MANIFEST.
BlobFileAddition addition;
// Initial unreachable bytes that should be registered as garbage
// alongside the file addition.
uint64_t garbage_blob_count = 0;
uint64_t garbage_blob_bytes = 0;
};
struct GenerationBatch {
// Files that were still open when the memtable became immutable.
std::deque<DeferredFile> deferred_files;
// Files already sealed for this memtable generation.
std::vector<SealedFile> sealed_files;
};
// Clears all active-file bookkeeping from a partition after sealing.
void ResetPartitionState(Partition* partition);
// Marks a blob file as manager-owned until it is committed or abandoned.
void AddFilePartitionMapping(uint64_t file_number, uint32_t partition_idx);
// Stops tracking a blob file after open/seal failure.
void RemoveFilePartitionMapping(uint64_t file_number);
// Opens a new active blob file for one partition.
Status OpenNewBlobFile(Partition* partition, uint32_t column_family_id,
CompressionType compression, uint32_t partition_idx);
// Finalizes one active partition file and returns the MANIFEST addition.
Status SealActiveBlobFile(const WriteOptions& write_options,
Partition* partition, SealedFile* sealed_file);
// Finalizes one deferred file that was carried over to flush preparation.
Status SealDeferredFile(const WriteOptions& write_options,
DeferredFile* deferred, SealedFile* sealed_file);
// Appends the footer, reports file completion, and builds the resulting
// BlobFileAddition for one sealed blob file.
Status FinalizeBlobFile(const WriteOptions& write_options,
BlobLogWriter* writer, uint64_t file_number,
uint64_t blob_count, uint64_t total_blob_bytes,
BlobFileAddition* addition);
// Appends one initial-garbage record to the flush output if any failed
// transformed writes targeted the sealed blob file.
static void AddSealedFileGarbage(const SealedFile& sealed_file,
std::vector<BlobFileGarbage>* garbages);
// Returns true if the active open file matches `file_number` and consumes
// the failed-write garbage accounting.
static bool MarkPartitionGarbage(Partition* partition, uint64_t file_number,
uint64_t blob_count, uint64_t blob_bytes);
// Returns true if the deferred flush-preparation file matches `file_number`
// and consumes the failed-write garbage accounting.
static bool MarkDeferredFileGarbage(DeferredFile* deferred,
uint64_t file_number, uint64_t blob_count,
uint64_t blob_bytes);
// Returns true if one already-sealed file matches `file_number` and
// consumes the failed-write garbage accounting.
static bool MarkSealedFileGarbage(std::vector<SealedFile>* sealed_files,
uint64_t file_number, uint64_t blob_count,
uint64_t blob_bytes);
// Seeds the blob cache with the original uncompressed value when configured.
Status MaybePrepopulateBlobCache(const BlobDirectWriteSettings& settings,
const Slice& original_value,
uint64_t blob_file_number,
uint64_t blob_offset);
// Configured partition fanout and write-selection strategy.
const uint32_t num_partitions_;
std::shared_ptr<BlobFilePartitionStrategy> strategy_;
// Shared services needed to create, track, and cache blob files.
FileNumberAllocator file_number_allocator_;
FileSystem* fs_;
SystemClock* clock_;
Statistics* statistics_;
FileOptions file_options_;
// Column-family context used for file naming and callbacks.
std::string db_path_;
std::string column_family_name_;
// File creation policy and shared blob-file infrastructure.
uint64_t blob_file_size_;
bool use_fsync_;
BlobFileCache* blob_file_cache_;
BlobFileCompletionCallback* blob_callback_;
std::vector<std::shared_ptr<EventListener>> listeners_;
FileChecksumGenFactory* file_checksum_gen_factory_;
FileTypeSet checksum_handoff_file_types_;
std::shared_ptr<IOTracer> io_tracer_;
// DB identity used when constructing cache keys.
std::string db_id_;
std::string db_session_id_;
Logger* info_log_;
// One active writer slot per configured partition.
std::vector<std::unique_ptr<Partition>> partitions_;
// Protects partition state and generation queues.
mutable port::Mutex mutex_;
// Files already sealed while the current mutable memtable was active.
std::vector<SealedFile> current_generation_sealed_files_;
// FIFO immutable memtable generations waiting to be flushed.
std::deque<GenerationBatch> pending_generations_;
// Tracks blob files still owned by active or deferred write-path state until
// MANIFEST commit publishes them.
std::unordered_map<uint64_t, uint32_t> file_to_partition_;
// Sealed direct-write files that remain reachable from live memtables, such
// as old SuperVersions serving lazy iterator reads after flush commit.
std::unordered_map<uint64_t, uint32_t> protected_blob_file_refs_;
// Protects file_to_partition_ and protected_blob_file_refs_.
mutable port::RWMutex file_partition_mutex_;
};
} // namespace ROCKSDB_NAMESPACE
+36 -14
View File
@@ -29,7 +29,7 @@ Status BlobFileReader::Create(
const ImmutableOptions& immutable_options, const ReadOptions& read_options,
const FileOptions& file_options, uint32_t column_family_id,
HistogramImpl* blob_file_read_hist, uint64_t blob_file_number,
const std::shared_ptr<IOTracer>& io_tracer,
const std::shared_ptr<IOTracer>& io_tracer, bool skip_footer_validation,
std::unique_ptr<BlobFileReader>* blob_file_reader) {
assert(blob_file_reader);
assert(!*blob_file_reader);
@@ -40,7 +40,8 @@ Status BlobFileReader::Create(
{
const Status s =
OpenFile(immutable_options, file_options, blob_file_read_hist,
blob_file_number, io_tracer, &file_size, &file_reader);
blob_file_number, io_tracer, &file_size, &file_reader,
/*skip_footer_size_check=*/skip_footer_validation);
if (!s.ok()) {
return s;
}
@@ -61,7 +62,7 @@ Status BlobFileReader::Create(
}
}
{
if (!skip_footer_validation) {
const Status s =
ReadFooter(file_reader.get(), read_options, file_size, statistics);
if (!s.ok()) {
@@ -76,9 +77,10 @@ Status BlobFileReader::Create(
compression_type);
}
blob_file_reader->reset(new BlobFileReader(
std::move(file_reader), file_size, compression_type,
std::move(decompressor), immutable_options.clock, statistics));
blob_file_reader->reset(
new BlobFileReader(std::move(file_reader), file_size, compression_type,
std::move(decompressor), immutable_options.clock,
statistics, !skip_footer_validation));
return Status::OK();
}
@@ -87,7 +89,8 @@ Status BlobFileReader::OpenFile(
const ImmutableOptions& immutable_options, const FileOptions& file_opts,
HistogramImpl* blob_file_read_hist, uint64_t blob_file_number,
const std::shared_ptr<IOTracer>& io_tracer, uint64_t* file_size,
std::unique_ptr<RandomAccessFileReader>* file_reader) {
std::unique_ptr<RandomAccessFileReader>* file_reader,
bool skip_footer_size_check) {
assert(file_size);
assert(file_reader);
@@ -112,17 +115,26 @@ Status BlobFileReader::OpenFile(
}
}
if (*file_size < BlobLogHeader::kSize + BlobLogFooter::kSize) {
if (!skip_footer_size_check &&
*file_size < BlobLogHeader::kSize + BlobLogFooter::kSize) {
return Status::Corruption("Malformed blob file");
}
if (skip_footer_size_check && *file_size < BlobLogHeader::kSize) {
return Status::Corruption("Malformed blob file");
}
std::unique_ptr<FSRandomAccessFile> file;
FileOptions reader_file_opts = file_opts;
if (skip_footer_size_check && reader_file_opts.use_direct_reads) {
reader_file_opts.use_direct_reads = false;
}
{
TEST_SYNC_POINT("BlobFileReader::OpenFile:NewRandomAccessFile");
const Status s =
fs->NewRandomAccessFile(blob_file_path, file_opts, &file, dbg);
fs->NewRandomAccessFile(blob_file_path, reader_file_opts, &file, dbg);
if (!s.ok()) {
return s;
}
@@ -291,13 +303,14 @@ BlobFileReader::BlobFileReader(
std::unique_ptr<RandomAccessFileReader>&& file_reader, uint64_t file_size,
CompressionType compression_type,
std::shared_ptr<Decompressor> decompressor, SystemClock* clock,
Statistics* statistics)
Statistics* statistics, bool has_footer)
: file_reader_(std::move(file_reader)),
file_size_(file_size),
compression_type_(compression_type),
decompressor_(std::move(decompressor)),
clock_(clock),
statistics_(statistics) {
statistics_(statistics),
has_footer_(has_footer) {
assert(file_reader_);
}
@@ -312,7 +325,8 @@ Status BlobFileReader::GetBlob(
const uint64_t key_size = user_key.size();
if (!IsValidBlobOffset(offset, key_size, value_size, file_size_)) {
if (!IsValidBlobOffset(offset, key_size, value_size, file_size_,
has_footer_)) {
return Status::Corruption("Invalid blob offset");
}
@@ -428,7 +442,8 @@ void BlobFileReader::MultiGetBlob(
const uint64_t offset = req->offset;
const uint64_t value_size = req->len;
if (!IsValidBlobOffset(offset, key_size, value_size, file_size_)) {
if (!IsValidBlobOffset(offset, key_size, value_size, file_size_,
has_footer_)) {
*req->status = Status::Corruption("Invalid blob offset");
continue;
}
@@ -454,6 +469,13 @@ void BlobFileReader::MultiGetBlob(
RecordTick(statistics_, BLOB_DB_BLOB_FILE_BYTES_READ, total_len);
if (read_reqs.empty()) {
if (bytes_read) {
*bytes_read = 0;
}
return;
}
Buffer buf;
AlignedBuf aligned_buf;
@@ -533,7 +555,7 @@ void BlobFileReader::MultiGetBlob(
}
// Uncompress blob if needed
Slice value_slice(record_slice.data() + adjustments[i], req->len);
Slice value_slice(record_slice.data() + adjustments[j - 1], req->len);
*req->status = UncompressBlobIfNeeded(
value_slice, compression_type_, decompressor_.get(), allocator, clock_,
statistics_, &blob_reqs[i].second);
+22 -3
View File
@@ -36,7 +36,20 @@ class BlobFileReader {
HistogramImpl* blob_file_read_hist,
uint64_t blob_file_number,
const std::shared_ptr<IOTracer>& io_tracer,
std::unique_ptr<BlobFileReader>* reader);
std::unique_ptr<BlobFileReader>* reader) {
return Create(immutable_options, read_options, file_options,
column_family_id, blob_file_read_hist, blob_file_number,
io_tracer, /*skip_footer_validation=*/false, reader);
}
// Allows opening in-flight direct-write blob files by optionally skipping
// footer validation.
static Status Create(
const ImmutableOptions& immutable_options,
const ReadOptions& read_options, const FileOptions& file_options,
uint32_t column_family_id, HistogramImpl* blob_file_read_hist,
uint64_t blob_file_number, const std::shared_ptr<IOTracer>& io_tracer,
bool skip_footer_validation, std::unique_ptr<BlobFileReader>* reader);
BlobFileReader(const BlobFileReader&) = delete;
BlobFileReader& operator=(const BlobFileReader&) = delete;
@@ -63,18 +76,22 @@ class BlobFileReader {
uint64_t GetFileSize() const { return file_size_; }
private:
// `has_footer` tracks whether offset validation should reserve footer space.
BlobFileReader(std::unique_ptr<RandomAccessFileReader>&& file_reader,
uint64_t file_size, CompressionType compression_type,
std::shared_ptr<Decompressor> decompressor, SystemClock* clock,
Statistics* statistics);
Statistics* statistics, bool has_footer);
// `skip_footer_size_check` is used for direct-write files that are still
// missing their footer at open time.
static Status OpenFile(const ImmutableOptions& immutable_options,
const FileOptions& file_opts,
HistogramImpl* blob_file_read_hist,
uint64_t blob_file_number,
const std::shared_ptr<IOTracer>& io_tracer,
uint64_t* file_size,
std::unique_ptr<RandomAccessFileReader>* file_reader);
std::unique_ptr<RandomAccessFileReader>* file_reader,
bool skip_footer_size_check);
static Status ReadHeader(const RandomAccessFileReader* file_reader,
const ReadOptions& read_options,
@@ -110,6 +127,8 @@ class BlobFileReader {
std::shared_ptr<Decompressor> decompressor_;
SystemClock* clock_;
Statistics* statistics_;
// False when the reader was opened before the blob file footer was written.
bool has_footer_;
};
} // namespace ROCKSDB_NAMESPACE
+86
View File
@@ -1011,6 +1011,92 @@ TEST_P(BlobFileReaderDecodingErrorTest, DecodingError) {
SyncPoint::GetInstance()->ClearAllCallBacks();
}
TEST_F(BlobFileReaderTest, MultiGetBlobWithFailedValidation) {
// Test that MultiGetBlob correctly handles the case where some requests
// fail validation. The adjustments vector is only populated for requests
// that pass validation, so the indices must track correctly.
Options options;
options.env = mock_env_.get();
options.cf_paths.emplace_back(
test::PerThreadDBPath(
mock_env_.get(),
"BlobFileReaderTest_MultiGetBlobWithFailedValidation"),
0);
options.enable_blob_files = true;
ImmutableOptions immutable_options(options);
constexpr uint32_t column_family_id = 1;
constexpr bool has_ttl = false;
constexpr ExpirationRange expiration_range;
constexpr uint64_t blob_file_number = 1;
constexpr size_t num_blobs = 3;
const std::vector<std::string> key_strs = {"key1", "key2", "key3"};
const std::vector<std::string> blob_strs = {"blob1", "blob2", "blob3"};
const std::vector<Slice> keys = {key_strs[0], key_strs[1], key_strs[2]};
const std::vector<Slice> blobs = {blob_strs[0], blob_strs[1], blob_strs[2]};
std::vector<uint64_t> blob_offsets(keys.size());
std::vector<uint64_t> blob_sizes(keys.size());
WriteBlobFile(immutable_options, column_family_id, has_ttl, expiration_range,
expiration_range, blob_file_number, keys, blobs, kNoCompression,
blob_offsets, blob_sizes);
constexpr HistogramImpl* blob_file_read_hist = nullptr;
std::unique_ptr<BlobFileReader> reader;
ReadOptions read_options;
ASSERT_OK(BlobFileReader::Create(
immutable_options, read_options, FileOptions(), column_family_id,
blob_file_read_hist, blob_file_number, nullptr /*IOTracer*/, &reader));
// Enable checksum verification so adjustments are non-zero
read_options.verify_checksums = true;
constexpr MemoryAllocator* allocator = nullptr;
// Fail the first request by giving it an invalid offset (too small).
// This causes the first request to be skipped in the adjustments vector,
// so adjustments has only 2 entries (for blobs 1 and 2), while blob_reqs
// has 3 entries. Without the fix, adjustments[i] would use the wrong
// index for the remaining valid requests.
std::array<Status, num_blobs> statuses_buf;
std::array<BlobReadRequest, num_blobs> requests_buf;
autovector<std::pair<BlobReadRequest*, std::unique_ptr<BlobContents>>>
blob_reqs;
// First request: invalid offset (too small, will fail validation)
requests_buf[0] = BlobReadRequest(keys[0], /*offset=*/0, blob_sizes[0],
kNoCompression, nullptr, &statuses_buf[0]);
// Second and third requests: valid
requests_buf[1] = BlobReadRequest(keys[1], blob_offsets[1], blob_sizes[1],
kNoCompression, nullptr, &statuses_buf[1]);
requests_buf[2] = BlobReadRequest(keys[2], blob_offsets[2], blob_sizes[2],
kNoCompression, nullptr, &statuses_buf[2]);
for (size_t i = 0; i < num_blobs; ++i) {
blob_reqs.emplace_back(&requests_buf[i], std::unique_ptr<BlobContents>());
}
uint64_t bytes_read = 0;
reader->MultiGetBlob(read_options, allocator, blob_reqs, &bytes_read);
// First request should fail validation
ASSERT_TRUE(statuses_buf[0].IsCorruption());
ASSERT_EQ(blob_reqs[0].second, nullptr);
// Second and third requests should succeed with correct data
ASSERT_OK(statuses_buf[1]);
ASSERT_NE(blob_reqs[1].second, nullptr);
ASSERT_EQ(blob_reqs[1].second->data(), blobs[1]);
ASSERT_OK(statuses_buf[2]);
ASSERT_NE(blob_reqs[2].second, nullptr);
ASSERT_EQ(blob_reqs[2].second->data(), blobs[2]);
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
+88 -55
View File
@@ -8,70 +8,48 @@
#include "db/blob/blob_index.h"
#include "db/blob/blob_log_format.h"
#include "db/dbformat.h"
#include "db/wide/wide_column_serialization.h"
namespace ROCKSDB_NAMESPACE {
Status BlobGarbageMeter::ProcessInFlow(const Slice& key, const Slice& value) {
uint64_t blob_file_number = kInvalidBlobFileNumber;
uint64_t bytes = 0;
const Status s = Parse(key, value, &blob_file_number, &bytes);
if (!s.ok()) {
return s;
}
if (blob_file_number == kInvalidBlobFileNumber) {
return Status::OK();
}
flows_[blob_file_number].AddInFlow(bytes);
return Status::OK();
return ProcessFlow(key, value, /*is_inflow=*/true);
}
Status BlobGarbageMeter::ProcessOutFlow(const Slice& key, const Slice& value) {
uint64_t blob_file_number = kInvalidBlobFileNumber;
uint64_t bytes = 0;
const Status s = Parse(key, value, &blob_file_number, &bytes);
if (!s.ok()) {
return s;
}
if (blob_file_number == kInvalidBlobFileNumber) {
return Status::OK();
}
// Note: in order to measure the amount of additional garbage, we only need to
// track the outflow for preexisting files, i.e. those that also had inflow.
// (Newly written files would only have outflow.)
auto it = flows_.find(blob_file_number);
if (it == flows_.end()) {
return Status::OK();
}
it->second.AddOutFlow(bytes);
return Status::OK();
return ProcessFlow(key, value, /*is_inflow=*/false);
}
Status BlobGarbageMeter::Parse(const Slice& key, const Slice& value,
uint64_t* blob_file_number, uint64_t* bytes) {
Status BlobGarbageMeter::GetBlobReferenceDetails(const ParsedInternalKey& ikey,
const BlobIndex& blob_index,
uint64_t* blob_file_number,
uint64_t* bytes) {
assert(blob_file_number);
assert(*blob_file_number == kInvalidBlobFileNumber);
assert(bytes);
assert(*bytes == 0);
ParsedInternalKey ikey;
{
constexpr bool log_err_key = false;
const Status s = ParseInternalKey(key, &ikey, log_err_key);
if (!s.ok()) {
return s;
}
if (blob_index.IsInlined() || blob_index.HasTTL()) {
return Status::Corruption("Unexpected TTL/inlined blob index");
}
*blob_file_number = blob_index.file_number();
*bytes =
blob_index.size() +
BlobLogRecord::CalculateAdjustmentForRecordHeader(ikey.user_key.size());
return Status::OK();
}
Status BlobGarbageMeter::ParseBlobIndexReference(const ParsedInternalKey& ikey,
const Slice& value,
uint64_t* blob_file_number,
uint64_t* bytes) {
assert(blob_file_number);
assert(*blob_file_number == kInvalidBlobFileNumber);
assert(bytes);
assert(*bytes == 0);
if (ikey.type != kTypeBlobIndex) {
return Status::OK();
}
@@ -85,16 +63,71 @@ Status BlobGarbageMeter::Parse(const Slice& key, const Slice& value,
}
}
if (blob_index.IsInlined() || blob_index.HasTTL()) {
return Status::Corruption("Unexpected TTL/inlined blob index");
return GetBlobReferenceDetails(ikey, blob_index, blob_file_number, bytes);
}
void BlobGarbageMeter::AddFlow(uint64_t blob_file_number, uint64_t bytes,
bool is_inflow) {
if (is_inflow) {
flows_[blob_file_number].AddInFlow(bytes);
return;
}
*blob_file_number = blob_index.file_number();
*bytes =
blob_index.size() +
BlobLogRecord::CalculateAdjustmentForRecordHeader(ikey.user_key.size());
// Note: in order to measure the amount of additional garbage, we only need to
// track the outflow for preexisting files, i.e. those that also had inflow.
// (Newly written files would only have outflow.)
auto it = flows_.find(blob_file_number);
if (it != flows_.end()) {
it->second.AddOutFlow(bytes);
}
}
return Status::OK();
Status BlobGarbageMeter::ProcessFlow(const Slice& key, const Slice& value,
bool is_inflow) {
ParsedInternalKey ikey;
{
constexpr bool log_err_key = false;
const Status s = ParseInternalKey(key, &ikey, log_err_key);
if (!s.ok()) {
return s;
}
}
uint64_t blob_file_number = kInvalidBlobFileNumber;
uint64_t bytes = 0;
if (Status s =
ParseBlobIndexReference(ikey, value, &blob_file_number, &bytes);
!s.ok()) {
return s;
}
if (blob_file_number != kInvalidBlobFileNumber) {
AddFlow(blob_file_number, bytes, is_inflow);
return Status::OK();
}
return ProcessEntityBlobReferences(ikey, value, is_inflow);
}
Status BlobGarbageMeter::ProcessEntityBlobReferences(
const ParsedInternalKey& ikey, const Slice& value, bool is_inflow) {
if (ikey.type != kTypeWideColumnEntity) {
return Status::OK();
}
return WideColumnSerialization::ForEachBlobFileNumber(
value, [&](const BlobIndex& blob_index) -> Status {
uint64_t file_number = kInvalidBlobFileNumber;
uint64_t blob_bytes = 0;
if (const Status s = GetBlobReferenceDetails(ikey, blob_index,
&file_number, &blob_bytes);
!s.ok()) {
return s;
}
AddFlow(file_number, blob_bytes, is_inflow);
return Status::OK();
});
}
} // namespace ROCKSDB_NAMESPACE
+15 -2
View File
@@ -15,6 +15,8 @@
namespace ROCKSDB_NAMESPACE {
class BlobIndex;
struct ParsedInternalKey;
class Slice;
// A class that can be used to compute the amount of additional garbage
@@ -93,8 +95,19 @@ class BlobGarbageMeter {
}
private:
static Status Parse(const Slice& key, const Slice& value,
uint64_t* blob_file_number, uint64_t* bytes);
static Status GetBlobReferenceDetails(const ParsedInternalKey& ikey,
const BlobIndex& blob_index,
uint64_t* blob_file_number,
uint64_t* bytes);
static Status ParseBlobIndexReference(const ParsedInternalKey& ikey,
const Slice& value,
uint64_t* blob_file_number,
uint64_t* bytes);
void AddFlow(uint64_t blob_file_number, uint64_t bytes, bool is_inflow);
Status ProcessFlow(const Slice& key, const Slice& value, bool is_inflow);
Status ProcessEntityBlobReferences(const ParsedInternalKey& ikey,
const Slice& value, bool is_inflow);
std::unordered_map<uint64_t, BlobInOutFlow> flows_;
};
+74
View File
@@ -11,10 +11,23 @@
#include "db/blob/blob_index.h"
#include "db/blob/blob_log_format.h"
#include "db/dbformat.h"
#include "db/wide/wide_column_serialization.h"
#include "test_util/testharness.h"
namespace ROCKSDB_NAMESPACE {
namespace {
BlobIndex MakeBlobIndex(uint64_t file_number, uint64_t offset, uint64_t size) {
std::string encoded;
BlobIndex::EncodeBlob(&encoded, file_number, offset, size, kNoCompression);
BlobIndex blob_index;
EXPECT_OK(blob_index.DecodeFrom(encoded));
return blob_index;
}
} // namespace
TEST(BlobGarbageMeterTest, MeasureGarbage) {
BlobGarbageMeter blob_garbage_meter;
@@ -188,6 +201,67 @@ TEST(BlobGarbageMeterTest, InlinedTTLBlobIndex) {
ASSERT_NOK(blob_garbage_meter.ProcessOutFlow(key_slice, value_slice));
}
TEST(BlobGarbageMeterTest, WideColumnEntity) {
constexpr char user_key[] = "entity_key";
constexpr SequenceNumber seq = 123;
const InternalKey key(user_key, seq, kTypeWideColumnEntity);
const Slice key_slice = key.Encode();
const std::vector<std::pair<std::string, std::string>> columns = {
{"", "default_inline"},
{"blob_attr", "blob_inline"},
{"ttl", "00000001"},
};
const BlobIndex default_blob = MakeBlobIndex(4, 123, 555);
const BlobIndex attr_blob = MakeBlobIndex(5, 456, 777);
std::string inflow_value;
ASSERT_OK(WideColumnSerialization::SerializeV2(
columns, {{0, default_blob}, {1, attr_blob}}, inflow_value));
std::string outflow_value;
ASSERT_OK(WideColumnSerialization::SerializeV2(columns, {{0, default_blob}},
outflow_value));
BlobGarbageMeter blob_garbage_meter;
ASSERT_OK(blob_garbage_meter.ProcessInFlow(key_slice, inflow_value));
ASSERT_OK(blob_garbage_meter.ProcessOutFlow(key_slice, outflow_value));
const auto& flows = blob_garbage_meter.flows();
ASSERT_EQ(flows.size(), 2);
constexpr size_t kUserKeySize = sizeof(user_key) - 1;
const uint64_t default_bytes =
default_blob.size() +
BlobLogRecord::CalculateAdjustmentForRecordHeader(kUserKeySize);
const uint64_t attr_bytes =
attr_blob.size() +
BlobLogRecord::CalculateAdjustmentForRecordHeader(kUserKeySize);
{
const auto it = flows.find(default_blob.file_number());
ASSERT_NE(it, flows.end());
ASSERT_EQ(it->second.GetInFlow().GetCount(), 1);
ASSERT_EQ(it->second.GetInFlow().GetBytes(), default_bytes);
ASSERT_EQ(it->second.GetOutFlow().GetCount(), 1);
ASSERT_EQ(it->second.GetOutFlow().GetBytes(), default_bytes);
ASSERT_FALSE(it->second.HasGarbage());
}
{
const auto it = flows.find(attr_blob.file_number());
ASSERT_NE(it, flows.end());
ASSERT_EQ(it->second.GetInFlow().GetCount(), 1);
ASSERT_EQ(it->second.GetInFlow().GetBytes(), attr_bytes);
ASSERT_EQ(it->second.GetOutFlow().GetCount(), 0);
ASSERT_EQ(it->second.GetOutFlow().GetBytes(), 0);
ASSERT_TRUE(it->second.HasGarbage());
ASSERT_EQ(it->second.GetGarbageCount(), 1);
ASSERT_EQ(it->second.GetGarbageBytes(), attr_bytes);
}
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
+17 -4
View File
@@ -148,13 +148,26 @@ struct BlobLogRecord {
// Checks whether a blob offset is potentially valid or not.
inline bool IsValidBlobOffset(uint64_t value_offset, uint64_t key_size,
uint64_t value_size, uint64_t file_size) {
if (value_offset <
BlobLogHeader::kSize + BlobLogRecord::kHeaderSize + key_size) {
uint64_t value_size, uint64_t file_size,
bool has_footer = true) {
constexpr uint64_t kMinPrefix =
BlobLogHeader::kSize + BlobLogRecord::kHeaderSize;
if (value_offset < kMinPrefix) {
return false;
}
if (value_offset - kMinPrefix < key_size) {
return false;
}
if (value_offset + value_size + BlobLogFooter::kSize > file_size) {
const uint64_t footer_size = has_footer ? BlobLogFooter::kSize : 0;
if (file_size < footer_size) {
return false;
}
const uint64_t max_value_end = file_size - footer_size;
if (value_size > max_value_end) {
return false;
}
if (value_offset > max_value_end - value_size) {
return false;
}
+130
View File
@@ -20,6 +20,21 @@
namespace ROCKSDB_NAMESPACE {
namespace {
Status AppendBlobRefreshRetryFailure(const Status& stale_status,
const Status& retry_status) {
assert(stale_status.IsCorruption());
assert(!retry_status.ok());
if (retry_status.IsCorruption()) {
return retry_status;
}
return Status::CopyAppendMessage(
stale_status, "; refresh retry failed: ", retry_status.ToString());
}
} // namespace
BlobSource::BlobSource(const ImmutableOptions& immutable_options,
const MutableCFOptions& mutable_cf_options,
const std::string& db_id,
@@ -231,6 +246,38 @@ Status BlobSource::GetBlob(const ReadOptions& read_options,
s = blob_file_reader.GetValue()->GetBlob(
read_options, user_key, offset, value_size, compression_type,
prefetch_buffer, allocator, &blob_contents, &read_size);
if (s.IsCorruption()) {
const Status stale_status = s;
blob_file_reader.Reset();
blob_file_cache_->Evict(file_number);
std::unique_ptr<BlobFileReader> fresh_reader;
s = blob_file_cache_->OpenBlobFileReaderUncached(
read_options, file_number, &fresh_reader,
/*allow_footer_skip_retry=*/false);
if (!s.ok()) {
return AppendBlobRefreshRetryFailure(stale_status, s);
}
if (compression_type != fresh_reader->GetCompressionType()) {
return Status::Corruption(
"Compression type mismatch when reading blob");
}
blob_contents.reset();
read_size = 0;
s = fresh_reader->GetBlob(read_options, user_key, offset, value_size,
compression_type, prefetch_buffer, allocator,
&blob_contents, &read_size);
if (!s.ok()) {
s = AppendBlobRefreshRetryFailure(stale_status, s);
} else {
CacheHandleGuard<BlobFileReader> ignored_reader;
blob_file_cache_
->RefreshBlobFileReader(file_number, &fresh_reader, &ignored_reader)
.PermitUncheckedError();
}
}
if (!s.ok()) {
return s;
}
@@ -397,6 +444,89 @@ void BlobSource::MultiGetBlobFromOneFile(const ReadOptions& read_options,
blob_file_reader.GetValue()->MultiGetBlob(read_options, allocator,
_blob_reqs, &_bytes_read);
bool needs_reader_refresh = false;
for (const auto& blob_req : _blob_reqs) {
BlobReadRequest* const req = blob_req.first;
assert(req != nullptr);
assert(req->status != nullptr);
if (req->status->IsCorruption()) {
needs_reader_refresh = true;
break;
}
}
if (needs_reader_refresh) {
blob_file_reader.Reset();
blob_file_cache_->Evict(file_number);
std::unique_ptr<BlobFileReader> fresh_reader;
s = blob_file_cache_->OpenBlobFileReaderUncached(
read_options, file_number, &fresh_reader,
/*allow_footer_skip_retry=*/false);
if (!s.ok()) {
for (const auto& blob_req : _blob_reqs) {
BlobReadRequest* const req = blob_req.first;
assert(req != nullptr);
assert(req->status != nullptr);
if (req->status->IsCorruption()) {
*req->status = AppendBlobRefreshRetryFailure(*req->status, s);
}
}
return;
}
autovector<std::pair<BlobReadRequest*, std::unique_ptr<BlobContents>>>
retry_blob_reqs;
autovector<Status> stale_statuses;
for (auto& blob_req : _blob_reqs) {
BlobReadRequest* const req = blob_req.first;
assert(req != nullptr);
assert(req->status != nullptr);
if (!req->status->IsCorruption()) {
continue;
}
stale_statuses.emplace_back(*req->status);
*req->status = Status::OK();
blob_req.second.reset();
retry_blob_reqs.emplace_back(req, std::unique_ptr<BlobContents>());
}
uint64_t refreshed_bytes_read = 0;
fresh_reader->MultiGetBlob(read_options, allocator, retry_blob_reqs,
&refreshed_bytes_read);
_bytes_read += refreshed_bytes_read;
bool install_fresh_reader = false;
for (size_t i = 0; i < retry_blob_reqs.size(); ++i) {
auto& retried_blob_req = retry_blob_reqs[i];
BlobReadRequest* const retried_req = retried_blob_req.first;
assert(retried_req != nullptr);
if (retried_req->status->ok()) {
install_fresh_reader = true;
} else {
*retried_req->status = AppendBlobRefreshRetryFailure(
stale_statuses[i], *retried_req->status);
}
for (auto& blob_req : _blob_reqs) {
if (blob_req.first != retried_req) {
continue;
}
blob_req.second = std::move(retried_blob_req.second);
break;
}
}
if (install_fresh_reader) {
CacheHandleGuard<BlobFileReader> ignored_reader;
blob_file_cache_
->RefreshBlobFileReader(file_number, &fresh_reader, &ignored_reader)
.PermitUncheckedError();
}
}
if (blob_cache_ && read_options.fill_cache) {
// If filling cache is allowed and a cache is configured, try to put
// the blob(s) to the cache.
+150
View File
@@ -1044,6 +1044,156 @@ TEST_F(BlobSourceTest, MultiGetBlobsFromCache) {
}
}
TEST_F(BlobSourceTest, GetBlobPreservesCorruptionDetailsWhenRefreshOpenFails) {
options_.cf_paths.emplace_back(
test::PerThreadDBPath(env_,
"BlobSourceTest_GetBlobPreservesCorruptionDetails"),
0);
DestroyAndReopen(options_);
ImmutableOptions immutable_options(options_);
MutableCFOptions mutable_cf_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;
const std::string key_str = "key0";
const std::string blob_str = "blob0";
std::vector<Slice> keys{Slice(key_str)};
std::vector<Slice> blobs{Slice(blob_str)};
std::vector<uint64_t> blob_offsets(1);
std::vector<uint64_t> blob_sizes(1);
const uint64_t file_size = BlobLogHeader::kSize + BlobLogRecord::kHeaderSize +
key_str.size() + blob_str.size() +
BlobLogFooter::kSize;
WriteBlobFile(immutable_options, column_family_id, has_ttl, expiration_range,
expiration_range, blob_file_number, keys, blobs, kNoCompression,
blob_offsets, blob_sizes);
constexpr size_t capacity = 1024;
std::shared_ptr<Cache> backing_cache = NewLRUCache(capacity);
FileOptions file_options;
std::unique_ptr<BlobFileCache> blob_file_cache =
std::make_unique<BlobFileCache>(
backing_cache.get(), &immutable_options, &file_options,
column_family_id, nullptr /*HistogramImpl*/, nullptr /*IOTracer*/);
BlobSource blob_source(immutable_options, mutable_cf_options, db_id_,
db_session_id_, blob_file_cache.get());
ReadOptions read_options;
read_options.verify_checksums = true;
constexpr FilePrefetchBuffer* prefetch_buffer = nullptr;
PinnableSlice value;
uint64_t bytes_read = 0;
ASSERT_OK(blob_source.GetBlob(
read_options, keys[0], blob_file_number, blob_offsets[0], file_size,
blob_sizes[0], kNoCompression, prefetch_buffer, &value, &bytes_read));
ASSERT_EQ(value, blobs[0]);
const std::string blob_file_path =
BlobFileName(immutable_options.cf_paths.front().path, blob_file_number);
ASSERT_OK(env_->DeleteFile(blob_file_path));
PinnableSlice invalid_value;
bytes_read = 0;
Status s =
blob_source.GetBlob(read_options, keys[0], blob_file_number, file_size,
file_size, blob_sizes[0], kNoCompression,
prefetch_buffer, &invalid_value, &bytes_read);
ASSERT_TRUE(s.IsCorruption());
ASSERT_EQ(bytes_read, 0);
const std::string status_str = s.ToString();
ASSERT_NE(status_str.find("Invalid blob offset"), std::string::npos);
ASSERT_NE(status_str.find("refresh retry failed"), std::string::npos);
ASSERT_NE(status_str.find("IO error"), std::string::npos);
}
TEST_F(BlobSourceTest,
MultiGetBlobPreservesCorruptionDetailsWhenRefreshOpenFails) {
options_.cf_paths.emplace_back(
test::PerThreadDBPath(
env_, "BlobSourceTest_MultiGetBlobPreservesCorruptionDetails"),
0);
DestroyAndReopen(options_);
ImmutableOptions immutable_options(options_);
MutableCFOptions mutable_cf_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;
const std::string key_str = "key0";
const std::string blob_str = "blob0";
std::vector<Slice> keys{Slice(key_str)};
std::vector<Slice> blobs{Slice(blob_str)};
std::vector<uint64_t> blob_offsets(1);
std::vector<uint64_t> blob_sizes(1);
const uint64_t file_size = BlobLogHeader::kSize + BlobLogRecord::kHeaderSize +
key_str.size() + blob_str.size() +
BlobLogFooter::kSize;
WriteBlobFile(immutable_options, column_family_id, has_ttl, expiration_range,
expiration_range, blob_file_number, keys, blobs, kNoCompression,
blob_offsets, blob_sizes);
constexpr size_t capacity = 1024;
std::shared_ptr<Cache> backing_cache = NewLRUCache(capacity);
FileOptions file_options;
std::unique_ptr<BlobFileCache> blob_file_cache =
std::make_unique<BlobFileCache>(
backing_cache.get(), &immutable_options, &file_options,
column_family_id, nullptr /*HistogramImpl*/, nullptr /*IOTracer*/);
BlobSource blob_source(immutable_options, mutable_cf_options, db_id_,
db_session_id_, blob_file_cache.get());
ReadOptions read_options;
read_options.verify_checksums = true;
constexpr FilePrefetchBuffer* prefetch_buffer = nullptr;
PinnableSlice value;
uint64_t bytes_read = 0;
ASSERT_OK(blob_source.GetBlob(
read_options, keys[0], blob_file_number, blob_offsets[0], file_size,
blob_sizes[0], kNoCompression, prefetch_buffer, &value, &bytes_read));
ASSERT_EQ(value, blobs[0]);
const std::string blob_file_path =
BlobFileName(immutable_options.cf_paths.front().path, blob_file_number);
ASSERT_OK(env_->DeleteFile(blob_file_path));
std::array<Status, 1> statuses;
std::array<PinnableSlice, 1> values;
autovector<BlobReadRequest> blob_reqs;
blob_reqs.emplace_back(keys[0], file_size, blob_sizes[0], kNoCompression,
&values[0], &statuses[0]);
bytes_read = 0;
blob_source.MultiGetBlobFromOneFile(read_options, blob_file_number, file_size,
blob_reqs, &bytes_read);
ASSERT_TRUE(statuses[0].IsCorruption());
ASSERT_EQ(bytes_read, 0);
const std::string status_str = statuses[0].ToString();
ASSERT_NE(status_str.find("Invalid blob offset"), std::string::npos);
ASSERT_NE(status_str.find("refresh retry failed"), std::string::npos);
ASSERT_NE(status_str.find("IO error"), std::string::npos);
}
class BlobSecondaryCacheTest : public DBTestBase {
protected:
public:
+316
View File
@@ -0,0 +1,316 @@
// 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).
// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#include "db/blob/blob_write_batch_transformer.h"
#include "db/blob/blob_file_partition_manager.h"
#include "db/blob/blob_index.h"
#include "db/blob/blob_log_format.h"
#include "db/wide/wide_column_serialization.h"
#include "db/write_batch_internal.h"
#include "port/likely.h"
namespace ROCKSDB_NAMESPACE {
BlobWriteBatchTransformer::BlobWriteBatchTransformer(
const BlobPartitionManagerProvider& partition_mgr_provider,
WriteBatch* output_batch,
const BlobDirectWriteSettingsProvider& settings_provider,
const WriteOptions& write_options)
: partition_mgr_provider_(partition_mgr_provider),
output_batch_(output_batch),
settings_provider_(settings_provider),
write_options_(write_options) {
assert(partition_mgr_provider_);
assert(output_batch_);
assert(settings_provider_);
}
Status BlobWriteBatchTransformer::TransformBatch(
const WriteOptions& write_options, WriteBatch* input_batch,
WriteBatch* output_batch,
const BlobPartitionManagerProvider& partition_mgr_provider,
const BlobDirectWriteSettingsProvider& settings_provider, bool* transformed,
std::vector<BlobFilePartitionManager*>* used_managers,
std::vector<RollbackInfo>* rollback_infos) {
assert(input_batch);
assert(output_batch);
assert(transformed);
output_batch->Clear();
*transformed = false;
BlobWriteBatchTransformer transformer(partition_mgr_provider, output_batch,
settings_provider, write_options);
Status s = input_batch->Iterate(&transformer);
*transformed = transformer.HasTransformed();
if (used_managers) {
used_managers->assign(transformer.used_managers_.begin(),
transformer.used_managers_.end());
}
if (rollback_infos) {
*rollback_infos = std::move(transformer.rollback_infos_);
}
return s;
}
Status BlobWriteBatchTransformer::MaybePreprocessWideColumns(
const WriteOptions& write_options, uint32_t column_family_id,
const Slice& key, const WideColumns& columns,
BlobFilePartitionManager* partition_mgr,
const BlobDirectWriteSettings& settings, bool serialize_inline_entity,
std::string* entity, bool* transformed,
std::vector<RollbackInfo>* rollback_infos) {
assert(entity != nullptr);
assert(transformed != nullptr);
*transformed = false;
if (partition_mgr == nullptr || !settings.enable_blob_direct_write) {
if (!serialize_inline_entity) {
return Status::OK();
}
entity->clear();
return WideColumnSerialization::Serialize(columns, *entity);
}
std::vector<std::pair<size_t, BlobIndex>> new_blob_columns;
new_blob_columns.reserve(columns.size());
std::string blob_index_buf;
bool has_entity_partition = false;
uint32_t entity_partition = 0;
for (size_t column_idx = 0; column_idx < columns.size(); ++column_idx) {
const Slice& column_value = columns[column_idx].value();
if (column_value.size() < settings.min_blob_size) {
continue;
}
if (!has_entity_partition) {
entity_partition = partition_mgr->SelectWideColumnPartition(
column_family_id, key, columns);
has_entity_partition = true;
}
uint64_t blob_file_number = 0;
uint64_t blob_offset = 0;
uint64_t blob_size = 0;
Status s = partition_mgr->WriteBlob(
write_options, column_family_id, settings.compression_type, key,
column_value, &blob_file_number, &blob_offset, &blob_size, settings,
&entity_partition);
if (!s.ok()) {
return s;
}
if (rollback_infos != nullptr) {
const uint64_t record_bytes =
BlobLogRecord::kHeaderSize + key.size() + blob_size;
rollback_infos->push_back(
{partition_mgr, blob_file_number, /*count=*/1, record_bytes});
}
BlobIndex blob_index;
BlobIndex::EncodeBlob(&blob_index_buf, blob_file_number, blob_offset,
blob_size, settings.compression_type);
s = blob_index.DecodeFrom(blob_index_buf);
if (!s.ok()) {
return s;
}
new_blob_columns.emplace_back(column_idx, blob_index);
}
if (new_blob_columns.empty()) {
if (!serialize_inline_entity) {
return Status::OK();
}
entity->clear();
return WideColumnSerialization::Serialize(columns, *entity);
}
entity->clear();
Status s =
WideColumnSerialization::SerializeV2(columns, new_blob_columns, *entity);
if (s.ok()) {
*transformed = true;
}
return s;
}
Status BlobWriteBatchTransformer::PutCF(uint32_t column_family_id,
const Slice& key, const Slice& value) {
// Use cached settings/manager for the same CF to avoid per-entry lookup.
if (column_family_id != cached_cf_id_) {
cached_settings_ = settings_provider_(column_family_id);
cached_partition_mgr_ = partition_mgr_provider_(column_family_id);
cached_cf_id_ = column_family_id;
}
const auto& settings = cached_settings_;
if (!cached_partition_mgr_ || !settings.enable_blob_direct_write ||
value.size() < settings.min_blob_size) {
return WriteBatchInternal::Put(output_batch_, column_family_id, key, value);
}
uint64_t blob_file_number = 0;
uint64_t blob_offset = 0;
uint64_t blob_size = 0;
Status s = cached_partition_mgr_->WriteBlob(
write_options_, column_family_id, settings.compression_type, key, value,
&blob_file_number, &blob_offset, &blob_size, settings);
if (!s.ok()) {
return s;
}
used_managers_.insert(cached_partition_mgr_);
// Track the exact file so stale transformed attempts can rollback
// per-file rather than smearing bytes across all partitions at seal time.
const uint64_t record_bytes =
BlobLogRecord::kHeaderSize + key.size() + blob_size;
rollback_infos_.push_back(
{cached_partition_mgr_, blob_file_number, /*count=*/1, record_bytes});
BlobIndex::EncodeBlob(&blob_index_buf_, blob_file_number, blob_offset,
blob_size, settings.compression_type);
has_transformed_ = true;
return WriteBatchInternal::PutBlobIndex(output_batch_, column_family_id, key,
blob_index_buf_);
}
Status BlobWriteBatchTransformer::TimedPutCF(uint32_t column_family_id,
const Slice& key,
const Slice& value,
uint64_t write_time) {
// TimedPut: pass through without blob separation for now.
return WriteBatchInternal::TimedPut(output_batch_, column_family_id, key,
value, write_time);
}
Status BlobWriteBatchTransformer::PutEntityCF(uint32_t column_family_id,
const Slice& key,
const Slice& entity) {
if (column_family_id != cached_cf_id_) {
cached_settings_ = settings_provider_(column_family_id);
cached_partition_mgr_ = partition_mgr_provider_(column_family_id);
cached_cf_id_ = column_family_id;
}
const auto& settings = cached_settings_;
if (!cached_partition_mgr_ || !settings.enable_blob_direct_write) {
return WriteBatchInternal::PutEntitySerialized(
output_batch_, column_family_id, key, entity);
}
Slice entity_ref = entity;
std::vector<WideColumn> columns;
std::vector<std::pair<size_t, BlobIndex>> existing_blob_columns;
Status status = WideColumnSerialization::DeserializeV2(entity_ref, columns,
existing_blob_columns);
if (status.ok()) {
// Reject pre-serialized entities that already contain blob references.
// Passing them through would preserve references to blob files outside this
// batch's direct-write lifetime tracking, which can later turn into stale
// reads after blob GC.
if (UNLIKELY(!existing_blob_columns.empty())) {
return Status::NotSupported(
"Blob direct write does not support pre-serialized wide entities "
"with blob references");
}
std::string rewritten_entity;
bool transformed = false;
status = MaybePreprocessWideColumns(
write_options_, column_family_id, key, columns, cached_partition_mgr_,
settings, /*serialize_inline_entity=*/false, &rewritten_entity,
&transformed, &rollback_infos_);
if (status.ok()) {
if (!transformed) {
return WriteBatchInternal::PutEntitySerialized(
output_batch_, column_family_id, key, entity);
}
has_transformed_ = true;
used_managers_.insert(cached_partition_mgr_);
return WriteBatchInternal::PutEntitySerialized(
output_batch_, column_family_id, key, rewritten_entity);
}
}
return status;
}
Status BlobWriteBatchTransformer::DeleteCF(uint32_t column_family_id,
const Slice& key) {
return WriteBatchInternal::Delete(output_batch_, column_family_id, key);
}
Status BlobWriteBatchTransformer::SingleDeleteCF(uint32_t column_family_id,
const Slice& key) {
return WriteBatchInternal::SingleDelete(output_batch_, column_family_id, key);
}
Status BlobWriteBatchTransformer::DeleteRangeCF(uint32_t column_family_id,
const Slice& begin_key,
const Slice& end_key) {
return WriteBatchInternal::DeleteRange(output_batch_, column_family_id,
begin_key, end_key);
}
Status BlobWriteBatchTransformer::MergeCF(uint32_t column_family_id,
const Slice& key,
const Slice& value) {
return WriteBatchInternal::Merge(output_batch_, column_family_id, key, value);
}
Status BlobWriteBatchTransformer::PutBlobIndexCF(uint32_t column_family_id,
const Slice& key,
const Slice& value) {
// Already a blob index -- pass through unchanged.
return WriteBatchInternal::PutBlobIndex(output_batch_, column_family_id, key,
value);
}
void BlobWriteBatchTransformer::LogData(const Slice& blob) {
output_batch_->PutLogData(blob).PermitUncheckedError();
}
Status BlobWriteBatchTransformer::MarkBeginPrepare(bool unprepared) {
return WriteBatchInternal::InsertBeginPrepare(
output_batch_, !unprepared /* write_after_commit */, unprepared);
}
Status BlobWriteBatchTransformer::MarkEndPrepare(const Slice& xid) {
return WriteBatchInternal::InsertEndPrepare(output_batch_, xid);
}
Status BlobWriteBatchTransformer::MarkCommit(const Slice& xid) {
return WriteBatchInternal::MarkCommit(output_batch_, xid);
}
Status BlobWriteBatchTransformer::MarkCommitWithTimestamp(const Slice& xid,
const Slice& ts) {
return WriteBatchInternal::MarkCommitWithTimestamp(output_batch_, xid, ts);
}
Status BlobWriteBatchTransformer::MarkRollback(const Slice& xid) {
return WriteBatchInternal::MarkRollback(output_batch_, xid);
}
Status BlobWriteBatchTransformer::MarkNoop(bool /*empty_batch*/) {
return WriteBatchInternal::InsertNoop(output_batch_);
}
} // namespace ROCKSDB_NAMESPACE
+176
View File
@@ -0,0 +1,176 @@
// 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).
#pragma once
#include <cstdint>
#include <functional>
#include <memory>
#include <string>
#include <unordered_map>
#include <vector>
#include "rocksdb/advanced_options.h"
#include "rocksdb/compression_type.h"
#include "rocksdb/options.h"
#include "rocksdb/rocksdb_namespace.h"
#include "rocksdb/slice.h"
#include "rocksdb/status.h"
#include "rocksdb/write_batch.h"
#include "util/hash_containers.h"
namespace ROCKSDB_NAMESPACE {
class BlobFilePartitionManager;
class Cache;
// Callback to look up per-CF blob settings.
struct BlobDirectWriteSettings {
// Whether this column family should attempt direct-write blob separation.
bool enable_blob_direct_write = false;
// Minimum value size eligible for direct-write blob separation.
uint64_t min_blob_size = 0;
// Compression to use for newly written blob records.
CompressionType compression_type = kNoCompression;
// Compression options for newly written blob records.
CompressionOptions compression_opts;
// Raw pointer -- the Cache is owned by ColumnFamilyOptions and outlives all
// settings snapshots. Using raw avoids 2 atomic ref-count ops per Put().
Cache* blob_cache = nullptr;
// Blob-cache prepopulation policy for direct-write records.
PrepopulateBlobCache prepopulate_blob_cache = PrepopulateBlobCache::kDisable;
};
using BlobDirectWriteSettingsProvider =
std::function<BlobDirectWriteSettings(uint32_t column_family_id)>;
// Callback to look up per-CF partition manager.
using BlobPartitionManagerProvider =
std::function<BlobFilePartitionManager*(uint32_t column_family_id)>;
// Transforms a WriteBatch by writing large values directly to blob files
// and replacing them with BlobIndex entries. Non-qualifying entries
// (small values, deletes, merges, etc.) are passed through unchanged.
class BlobWriteBatchTransformer : public WriteBatch::Handler {
public:
struct RollbackInfo {
// Manager that owns the file updated during transform.
BlobFilePartitionManager* partition_mgr = nullptr;
// Blob file number written during transform.
uint64_t file_number = 0;
// Number of blob records appended to that file.
uint64_t count = 0;
// Number of bytes appended to that file.
uint64_t bytes = 0;
};
// Builds the final serialized entity for one PutEntity write.
//
// `columns` must already be sorted by column name. When direct-write blob
// separation is enabled and one or more columns meet `min_blob_size`, this
// writes those columns to blob files and returns a V2 entity containing
// BlobIndex references. Otherwise, if `serialize_inline_entity` is true, it
// returns a normal V1 entity with all columns inline.
//
// `rollback_infos`, when provided, is appended with the exact blob writes
// performed before the method returns, even if it later fails, so callers
// can account those bytes as initial garbage.
static Status MaybePreprocessWideColumns(
const WriteOptions& write_options, uint32_t column_family_id,
const Slice& key, const WideColumns& columns,
BlobFilePartitionManager* partition_mgr,
const BlobDirectWriteSettings& settings, bool serialize_inline_entity,
std::string* entity, bool* transformed,
std::vector<RollbackInfo>* rollback_infos = nullptr);
// Constructs a per-batch transformer. Most callers should use
// TransformBatch() instead of driving the handler directly.
BlobWriteBatchTransformer(
const BlobPartitionManagerProvider& partition_mgr_provider,
WriteBatch* output_batch,
const BlobDirectWriteSettingsProvider& settings_provider,
const WriteOptions& write_options);
// Transform a WriteBatch. If no values qualify for blob separation,
// output_batch will be empty and the caller should use the original batch.
// If any values are separated, output_batch contains the full transformed
// batch. used_managers (if non-null) receives the set of partition managers
// that had data written to them, so the caller can flush/sync them.
// rollback_infos (if non-null) receives the exact file/count/byte writes so
// a failed transformed attempt can be accounted as initial garbage. Partial
// results are returned even if this method fails after some blob writes.
static Status TransformBatch(
const WriteOptions& write_options, WriteBatch* input_batch,
WriteBatch* output_batch,
const BlobPartitionManagerProvider& partition_mgr_provider,
const BlobDirectWriteSettingsProvider& settings_provider,
bool* transformed,
std::vector<BlobFilePartitionManager*>* used_managers = nullptr,
std::vector<RollbackInfo>* rollback_infos = nullptr);
// WriteBatch::Handler overrides
Status PutCF(uint32_t column_family_id, const Slice& key,
const Slice& value) override;
Status TimedPutCF(uint32_t column_family_id, const Slice& key,
const Slice& value, uint64_t write_time) override;
Status PutEntityCF(uint32_t column_family_id, const Slice& key,
const Slice& entity) override;
Status DeleteCF(uint32_t column_family_id, const Slice& key) override;
Status SingleDeleteCF(uint32_t column_family_id, const Slice& key) override;
Status DeleteRangeCF(uint32_t column_family_id, const Slice& begin_key,
const Slice& end_key) override;
Status MergeCF(uint32_t column_family_id, const Slice& key,
const Slice& value) override;
Status PutBlobIndexCF(uint32_t column_family_id, const Slice& key,
const Slice& value) override;
void LogData(const Slice& blob) override;
Status MarkBeginPrepare(bool unprepared = false) override;
Status MarkEndPrepare(const Slice& xid) override;
Status MarkCommit(const Slice& xid) override;
Status MarkCommitWithTimestamp(const Slice& xid, const Slice& ts) override;
Status MarkRollback(const Slice& xid) override;
Status MarkNoop(bool empty_batch) override;
// Returns true once at least one value has been rewritten to a BlobIndex.
bool HasTransformed() const { return has_transformed_; }
private:
// Callback to look up the partition manager for a given column family ID.
BlobPartitionManagerProvider partition_mgr_provider_;
// Output batch that receives transformed entries (BlobIndex for qualifying
// values, passthrough for everything else).
WriteBatch* output_batch_;
// Callback to look up blob direct write settings for a given CF ID.
BlobDirectWriteSettingsProvider settings_provider_;
// Write options from the caller, forwarded to WriteBlob calls.
const WriteOptions& write_options_;
// True once at least one value has been separated into a blob file.
bool has_transformed_ = false;
// Reusable buffer for encoding BlobIndex entries (avoids per-Put alloc).
std::string blob_index_buf_;
// Per-batch cache of the last CF's settings and manager, avoiding
// redundant provider lookups when consecutive entries share the same CF.
uint32_t cached_cf_id_ = UINT32_MAX;
// Cached settings for `cached_cf_id_`.
BlobDirectWriteSettings cached_settings_;
// Cached partition manager for `cached_cf_id_`.
BlobFilePartitionManager* cached_partition_mgr_ = nullptr;
// Set of partition managers that received data during this batch,
// returned to the caller so it can flush/sync them.
UnorderedSet<BlobFilePartitionManager*> used_managers_;
// Exact blob writes performed during this batch. We only aggregate these
// entries if rollback is needed so the normal path keeps minimal overhead.
std::vector<RollbackInfo> rollback_infos_;
};
} // namespace ROCKSDB_NAMESPACE
+222
View File
@@ -3,17 +3,29 @@
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#include <algorithm>
#include <array>
#include <set>
#include <sstream>
#include <string>
#include "cache/compressed_secondary_cache.h"
#include "db/blob/blob_file_partition_manager.h"
#include "db/blob/blob_index.h"
#include "db/blob/blob_log_format.h"
#include "db/blob/blob_log_sequential_reader.h"
#include "db/column_family.h"
#include "db/db_test_util.h"
#include "db/db_with_timestamp_test_util.h"
#include "file/filename.h"
#include "file/random_access_file_reader.h"
#include "port/stack_trace.h"
#include "rocksdb/convenience.h"
#include "rocksdb/trace_reader_writer.h"
#include "rocksdb/trace_record.h"
#include "rocksdb/utilities/replayer.h"
#include "test_util/sync_point.h"
#include "util/compression.h"
#include "utilities/fault_injection_env.h"
namespace ROCKSDB_NAMESPACE {
@@ -51,6 +63,43 @@ TEST_F(DBBlobBasicTest, GetBlob) {
.IsIncomplete());
}
TEST_F(DBBlobBasicTest, EmptyValueNotStoredAsBlob) {
// Regression test for crash when empty blob value is evicted to
// CompressedSecondaryCache (T261142690). Empty values should always be
// stored inline in the SST, never as blobs, even with min_blob_size=0.
// A BlobIndex for an empty value is strictly larger than the value itself,
// so storing it as a blob is pure overhead.
Options options = GetDefaultOptions();
options.enable_blob_files = true;
options.min_blob_size = 0;
options.disable_auto_compactions = true;
Reopen(options);
// Write an empty value and a non-empty value.
ASSERT_OK(Put("empty_key", ""));
constexpr char blob_value[] = "blob_value";
ASSERT_OK(Put("nonempty_key", blob_value));
ASSERT_OK(Flush());
// Both values should be readable.
ASSERT_EQ(Get("empty_key"), "");
ASSERT_EQ(Get("nonempty_key"), blob_value);
// The empty value should be stored inline (readable from block cache
// without blob file I/O), while the non-empty value requires blob I/O.
ReadOptions ro;
ro.read_tier = kBlockCacheTier;
PinnableSlice result;
ASSERT_OK(db_->Get(ro, db_->DefaultColumnFamily(), "empty_key", &result));
ASSERT_EQ(result, "");
result.Reset();
ASSERT_TRUE(db_->Get(ro, db_->DefaultColumnFamily(), "nonempty_key", &result)
.IsIncomplete());
}
TEST_F(DBBlobBasicTest, GetBlobFromCache) {
Options options = GetDefaultOptions();
@@ -1571,6 +1620,47 @@ TEST_P(DBBlobBasicIOErrorTest, GetBlob_IOError) {
SyncPoint::GetInstance()->ClearAllCallBacks();
}
TEST_P(DBBlobBasicIOErrorTest, GetEntityMergeWithBlobBaseIOError) {
// Goal: verify GetEntity preserves injected blob-read IOErrors when merge
// reads a blob-backed base value, instead of laundering them into Corruption.
// The test writes a blob-backed base value plus a merge operand, then injects
// an IOError at blob read time and checks both GetEntity and Get see it.
Options options;
options.env = fault_injection_env_.get();
options.enable_blob_files = true;
options.min_blob_size = 0;
options.merge_operator = MergeOperators::CreateStringAppendOperator();
Reopen(options);
constexpr char key[] = "key";
constexpr char base_value[] = "base_value";
ASSERT_OK(Put(key, base_value));
ASSERT_OK(Flush());
ASSERT_OK(Merge(key, "merge_operand"));
ASSERT_OK(Flush());
SyncPoint::GetInstance()->SetCallBack(sync_point_, [this](void* /* arg */) {
fault_injection_env_->SetFilesystemActive(false,
Status::IOError(sync_point_));
});
SyncPoint::GetInstance()->EnableProcessing();
PinnableWideColumns entity_result;
Status s = db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(), key,
&entity_result);
ASSERT_TRUE(s.IsIOError()) << "Expected IOError but got: " << s.ToString();
PinnableSlice get_result;
s = db_->Get(ReadOptions(), db_->DefaultColumnFamily(), key, &get_result);
ASSERT_TRUE(s.IsIOError()) << "Expected IOError but got: " << s.ToString();
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
}
TEST_P(DBBlobBasicIOErrorMultiGetTest, MultiGetBlobs_IOError) {
Options options = GetDefaultOptions();
options.env = fault_injection_env_.get();
@@ -2459,6 +2549,138 @@ TEST_F(DBBlobWithTimestampTest, IterateBlobs) {
}
}
TEST_F(DBBlobBasicTest, GetApproximateSizesIncludingBlobFiles) {
Options options = GetDefaultOptions();
options.enable_blob_files = true;
options.min_blob_size = 0;
Reopen(options);
// Write some key-value pairs with blob values and flush to create blob files.
constexpr int kNumKeys = 1000;
constexpr int kValueSize = 1024;
Random rnd(301);
for (int i = 0; i < kNumKeys; ++i) {
ASSERT_OK(Put(Key(i), rnd.RandomString(kValueSize)));
}
ASSERT_OK(Flush());
// Verify blob files exist.
std::vector<std::string> files;
ASSERT_OK(env_->GetChildren(dbname_, &files));
bool has_blob_files = false;
for (const auto& f : files) {
if (f.size() > 5 && f.substr(f.size() - 5) == ".blob") {
has_blob_files = true;
break;
}
}
ASSERT_TRUE(has_blob_files);
// Query the full range - all keys are covered.
std::string start = Key(0);
std::string end = Key(kNumKeys);
Range r(start, end);
// Without include_blob_files (default behavior): should not include blob
// file sizes.
uint64_t size_without_blobs = 0;
{
SizeApproximationOptions size_approx_options;
size_approx_options.include_files = true;
size_approx_options.include_blob_files = false;
ASSERT_OK(db_->GetApproximateSizes(size_approx_options,
db_->DefaultColumnFamily(), &r, 1,
&size_without_blobs));
ASSERT_GT(size_without_blobs, 0);
}
// With include_blob_files: should be strictly larger.
{
SizeApproximationOptions size_approx_options;
size_approx_options.include_files = true;
size_approx_options.include_blob_files = true;
uint64_t size_with_blobs = 0;
ASSERT_OK(db_->GetApproximateSizes(size_approx_options,
db_->DefaultColumnFamily(), &r, 1,
&size_with_blobs));
ASSERT_GT(size_with_blobs, size_without_blobs);
}
// Range that doesn't overlap any data should return 0.
{
std::string no_start = Key(kNumKeys + 100);
std::string no_end = Key(kNumKeys + 200);
Range no_r(no_start, no_end);
SizeApproximationOptions size_approx_options;
size_approx_options.include_files = true;
size_approx_options.include_blob_files = true;
uint64_t no_size = 0;
ASSERT_OK(db_->GetApproximateSizes(
size_approx_options, db_->DefaultColumnFamily(), &no_r, 1, &no_size));
ASSERT_EQ(no_size, 0);
}
// Partial range should return proportionally less blob size than full range.
{
SizeApproximationOptions size_approx_options;
size_approx_options.include_files = true;
size_approx_options.include_blob_files = true;
uint64_t full_size = 0;
ASSERT_OK(db_->GetApproximateSizes(
size_approx_options, db_->DefaultColumnFamily(), &r, 1, &full_size));
// Query roughly the first half of keys.
std::string half_end = Key(kNumKeys / 2);
Range half_r(start, half_end);
uint64_t half_size = 0;
ASSERT_OK(db_->GetApproximateSizes(size_approx_options,
db_->DefaultColumnFamily(), &half_r, 1,
&half_size));
ASSERT_GT(half_size, 0);
ASSERT_LT(half_size, full_size);
}
// Via SizeApproximationFlags API.
{
uint64_t size_flags = 0;
ASSERT_OK(db_->GetApproximateSizes(
db_->DefaultColumnFamily(), &r, 1, &size_flags,
DB::SizeApproximationFlags::INCLUDE_FILES |
DB::SizeApproximationFlags::INCLUDE_BLOB_FILES));
ASSERT_GT(size_flags, size_without_blobs);
}
// Multi-range query: two non-overlapping sub-ranges should sum to
// approximately the full-range result.
{
SizeApproximationOptions size_approx_options;
size_approx_options.include_files = true;
size_approx_options.include_blob_files = true;
std::string mid = Key(kNumKeys / 2);
std::string r1_start = Key(0);
std::string r1_end = mid;
std::string r2_start = mid;
std::string r2_end = Key(kNumKeys);
Range ranges[2] = {Range(r1_start, r1_end), Range(r2_start, r2_end)};
uint64_t sizes[2] = {0, 0};
ASSERT_OK(db_->GetApproximateSizes(
size_approx_options, db_->DefaultColumnFamily(), ranges, 2, sizes));
// Each sub-range should return a positive size.
ASSERT_GT(sizes[0], 0);
ASSERT_GT(sizes[1], 0);
// Sum of sub-ranges should be close to the full-range result.
uint64_t full_size = 0;
ASSERT_OK(db_->GetApproximateSizes(
size_approx_options, db_->DefaultColumnFamily(), &r, 1, &full_size));
ASSERT_NEAR(static_cast<double>(sizes[0] + sizes[1]),
static_cast<double>(full_size), full_size * 0.1);
}
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
File diff suppressed because it is too large. Load diff
File diff suppressed because it is too large. Load diff
+74 -8
View File
@@ -13,7 +13,10 @@
#include <deque>
#include <vector>
#include "db/blob/blob_counting_iterator.h"
#include "db/blob/blob_file_builder.h"
#include "db/blob/blob_garbage_meter.h"
#include "db/column_family.h"
#include "db/compaction/compaction_iterator.h"
#include "db/dbformat.h"
#include "db/event_helpers.h"
@@ -87,7 +90,8 @@ Status BuildTable(
Env::WriteLifeTimeHint write_hint, const std::string* full_history_ts_low,
BlobFileCompletionCallback* blob_callback, Version* version,
uint64_t* memtable_payload_bytes, uint64_t* memtable_garbage_bytes,
InternalStats::CompactionStats* flush_stats) {
InternalStats::CompactionStats* flush_stats,
std::vector<BlobFileGarbage>* blob_file_garbages, bool fast_sst_open) {
assert((tboptions.column_family_id ==
TablePropertiesCollectorFactory::Context::kUnknownColumnFamily) ==
tboptions.column_family_name.empty());
@@ -99,7 +103,24 @@ Status BuildTable(
/*enable_hash=*/paranoid_file_checks);
Status s;
meta->fd.file_size = 0;
iter->SeekToFirst();
if (blob_file_garbages != nullptr) {
blob_file_garbages->clear();
}
InternalIterator* input = iter;
std::unique_ptr<BlobGarbageMeter> blob_garbage_meter;
std::unique_ptr<BlobCountingIterator> blob_counting_iter;
if (blob_file_garbages != nullptr) {
// Flush can drop blob-backed records via overwrite elision and flush-time
// compaction filters. Track input and surviving output refs just like the
// compaction path so the manifest learns about the new garbage.
blob_garbage_meter.reset(new BlobGarbageMeter());
blob_counting_iter.reset(
new BlobCountingIterator(iter, blob_garbage_meter.get()));
input = blob_counting_iter.get();
}
input->SeekToFirst();
std::unique_ptr<CompactionRangeDelAggregator> range_del_agg(
new CompactionRangeDelAggregator(&tboptions.internal_comparator,
snapshots, full_history_ts_low));
@@ -128,7 +149,7 @@ Status BuildTable(
TableProperties tp;
bool table_file_created = false;
if (iter->Valid() || !range_del_agg->IsEmpty()) {
if (input->Valid() || !range_del_agg->IsEmpty()) {
std::unique_ptr<CompactionFilter> compaction_filter;
if (ioptions.compaction_filter_factory != nullptr &&
ioptions.compaction_filter_factory->ShouldFilterTableFileCreation(
@@ -211,7 +232,7 @@ Status BuildTable(
const std::atomic<bool> kManualCompactionCanceledFalse{false};
CompactionIterator c_iter(
iter, ucmp, &merge, kMaxSequenceNumber, &snapshots, earliest_snapshot,
input, ucmp, &merge, kMaxSequenceNumber, &snapshots, earliest_snapshot,
earliest_write_conflict_snapshot, job_snapshot, snapshot_checker, env,
ShouldReportDetailedTime(env, ioptions.stats), range_del_agg.get(),
blob_file_builder.get(), ioptions.allow_data_in_errors,
@@ -219,7 +240,17 @@ Status BuildTable(
/*manual_compaction_canceled=*/kManualCompactionCanceledFalse,
true /* must_count_input_entries */,
/*compaction=*/nullptr, compaction_filter.get(),
/*shutting_down=*/nullptr, db_options.info_log, full_history_ts_low);
/*shutting_down=*/nullptr, db_options.info_log, full_history_ts_low,
std::nullopt, version, tboptions.read_options.io_activity);
if (version != nullptr) {
ColumnFamilyData* const cfd = version->cfd();
c_iter.SetBlobFetcher(
version, cfd != nullptr ? cfd->blob_file_cache() : nullptr,
tboptions.read_options.io_activity,
tboptions.reason == TableFileCreationReason::kFlush ||
tboptions.reason == TableFileCreationReason::kRecovery);
}
SequenceNumber smallest_preferred_seqno = kMaxSequenceNumber;
std::string key_after_flush_buf;
@@ -268,6 +299,14 @@ Status BuildTable(
flush_stats->num_output_records++;
}
if (blob_garbage_meter != nullptr) {
s = blob_garbage_meter->ProcessOutFlow(key_after_flush,
value_after_flush);
if (!s.ok()) {
break;
}
}
s = meta->UpdateBoundaries(key_after_flush, value_after_flush,
ikey.sequence, ikey.type);
if (!s.ok()) {
@@ -322,6 +361,8 @@ Status BuildTable(
}
TEST_SYNC_POINT("BuildTable:BeforeFinishBuildTable");
TEST_SYNC_POINT_CALLBACK("BuildTable:BeforeCheckEmpty",
static_cast<TableBuilder*>(builder));
const bool empty = builder->IsEmpty();
if (flush_stats) {
assert(c_iter.HasNumInputEntryScanned());
@@ -330,6 +371,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) {
@@ -447,6 +496,8 @@ Status BuildTable(
// here because this is a special case after we finish the table building.
// No matter whether use_direct_io_for_flush_and_compaction is true,
// the goal is to cache it here for further user reads.
std::string* file_open_metadata_ptr =
fast_sst_open ? &meta->file_open_metadata : nullptr;
std::unique_ptr<InternalIterator> it(table_cache->NewIterator(
tboptions.read_options, file_options, tboptions.internal_comparator,
*meta, nullptr /* range_del_agg */, mutable_cf_options, nullptr,
@@ -457,7 +508,10 @@ Status BuildTable(
MaxFileSizeForL0MetaPin(mutable_cf_options),
/*smallest_compaction_key=*/nullptr,
/*largest_compaction_key*/ nullptr,
/*allow_unprepared_value*/ false));
/*allow_unprepared_value*/ false,
/*range_del_read_seqno=*/nullptr,
/*range_del_iter=*/nullptr,
/*maybe_pin_table_handle=*/false, file_open_metadata_ptr));
s = it->status();
if (s.ok() && paranoid_file_checks) {
OutputValidator file_validator(tboptions.internal_comparator,
@@ -475,8 +529,20 @@ Status BuildTable(
}
// Check for input iterator errors
if (!iter->status().ok()) {
s = iter->status();
if (!input->status().ok()) {
s = input->status();
}
if (s.ok() && blob_file_garbages != nullptr &&
blob_garbage_meter != nullptr) {
for (const auto& pair : blob_garbage_meter->flows()) {
const uint64_t blob_file_number = pair.first;
const BlobGarbageMeter::BlobInOutFlow& flow = pair.second;
if (flow.HasGarbage()) {
blob_file_garbages->emplace_back(
blob_file_number, flow.GetGarbageCount(), flow.GetGarbageBytes());
}
}
}
if (!s.ok() || meta->fd.GetFileSize() == 0) {
+4 -1
View File
@@ -31,6 +31,7 @@ struct FileMetaData;
class VersionSet;
class BlobFileAddition;
class BlobFileGarbage;
class SnapshotChecker;
class TableCache;
class TableBuilder;
@@ -79,6 +80,8 @@ Status BuildTable(
BlobFileCompletionCallback* blob_callback = nullptr,
Version* version = nullptr, uint64_t* memtable_payload_bytes = nullptr,
uint64_t* memtable_garbage_bytes = nullptr,
InternalStats::CompactionStats* flush_stats = nullptr);
InternalStats::CompactionStats* flush_stats = nullptr,
std::vector<BlobFileGarbage>* blob_file_garbages = nullptr,
bool fast_sst_open = false);
} // namespace ROCKSDB_NAMESPACE
+70 -6
View File
@@ -1759,9 +1759,13 @@ rocksdb_column_family_handle_t* rocksdb_create_column_family(
rocksdb_t* db, const rocksdb_options_t* column_family_options,
const char* column_family_name, char** errptr) {
rocksdb_column_family_handle_t* handle = new rocksdb_column_family_handle_t;
SaveError(errptr, db->rep->CreateColumnFamily(
ColumnFamilyOptions(column_family_options->rep),
std::string(column_family_name), &(handle->rep)));
handle->rep = nullptr;
if (SaveError(errptr, db->rep->CreateColumnFamily(
ColumnFamilyOptions(column_family_options->rep),
std::string(column_family_name), &(handle->rep)))) {
delete handle;
return nullptr;
}
handle->immortal = false;
return handle;
}
@@ -3052,6 +3056,7 @@ class H : public WriteBatch::Handler {
(*log_data_)(state_, blob.data(), blob.size());
}
}
Status MarkNoop(bool /* empty_batch */) override { return Status::OK(); }
};
class HCF : public WriteBatch::Handler {
@@ -3085,6 +3090,7 @@ class HCF : public WriteBatch::Handler {
(*log_data_)(state_, blob.data(), blob.size());
}
}
Status MarkNoop(bool /* empty_batch */) override { return Status::OK(); }
};
void rocksdb_writebatch_iterate(rocksdb_writebatch_t* b, void* state,
@@ -4351,6 +4357,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) {
@@ -4635,6 +4650,16 @@ uint32_t rocksdb_options_get_memtable_avg_op_scan_flush_trigger(
return opt->rep.memtable_avg_op_scan_flush_trigger;
}
void rocksdb_options_set_min_tombstones_for_range_conversion(
rocksdb_options_t* opt, uint32_t n) {
opt->rep.min_tombstones_for_range_conversion = n;
}
uint32_t rocksdb_options_get_min_tombstones_for_range_conversion(
rocksdb_options_t* opt) {
return opt->rep.min_tombstones_for_range_conversion;
}
void rocksdb_options_enable_statistics(rocksdb_options_t* opt) {
opt->rep.statistics = ROCKSDB_NAMESPACE::CreateDBStatistics();
}
@@ -4734,6 +4759,26 @@ double rocksdb_options_get_blob_gc_force_threshold(rocksdb_options_t* opt) {
return opt->rep.blob_garbage_collection_force_threshold;
}
void rocksdb_options_set_read_triggered_compaction_threshold(
rocksdb_options_t* opt, double val) {
opt->rep.read_triggered_compaction_threshold = val;
}
double rocksdb_options_get_read_triggered_compaction_threshold(
rocksdb_options_t* opt) {
return opt->rep.read_triggered_compaction_threshold;
}
void rocksdb_options_set_max_compaction_trigger_wakeup_seconds(
rocksdb_options_t* opt, uint64_t val) {
opt->rep.max_compaction_trigger_wakeup_seconds = val;
}
uint64_t rocksdb_options_get_max_compaction_trigger_wakeup_seconds(
rocksdb_options_t* opt) {
return opt->rep.max_compaction_trigger_wakeup_seconds;
}
void rocksdb_options_set_blob_compaction_readahead_size(rocksdb_options_t* opt,
uint64_t val) {
opt->rep.blob_compaction_readahead_size = val;
@@ -5789,6 +5834,16 @@ uint64_t rocksdb_perfcontext_metric(rocksdb_perfcontext_t* context,
return rep->internal_range_del_reseek_count;
case rocksdb_block_read_cpu_time:
return rep->block_read_cpu_time;
case rocksdb_data_block_read_byte:
return rep->data_block_read_byte;
case rocksdb_index_block_read_byte:
return rep->index_block_read_byte;
case rocksdb_filter_block_read_byte:
return rep->filter_block_read_byte;
case rocksdb_compression_dict_block_read_byte:
return rep->compression_dict_block_read_byte;
case rocksdb_metadata_block_read_byte:
return rep->metadata_block_read_byte;
default:
break;
}
@@ -7525,9 +7580,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;
}
@@ -8257,6 +8316,11 @@ void rocksdb_transaction_delete_cf(
SaveError(errptr, txn->rep->Delete(column_family->rep, Slice(key, klen)));
}
void rocksdb_transaction_put_log_data(rocksdb_transaction_t* txn,
const char* blob, size_t len) {
txn->rep->PutLogData(Slice(blob, len));
}
// Delete a key outside a transaction
void rocksdb_transactiondb_delete(rocksdb_transactiondb_t* txn_db,
const rocksdb_writeoptions_t* options,
+79
View File
@@ -205,6 +205,27 @@ static void CheckDel(void* ptr, const char* k, size_t klen) {
(*state)++;
}
static void NopPut(void* ptr, const char* k, size_t klen, const char* v,
size_t vlen) {
(void)ptr;
(void)k;
(void)klen;
(void)v;
(void)vlen;
}
static void NopDel(void* ptr, const char* k, size_t klen) {
(void)ptr;
(void)k;
(void)klen;
}
static void CheckLogData(void* ptr, const char* blob, size_t blen) {
CheckEqual("log_blob", blob, blen);
int* found = (int*)ptr;
*found = 1;
}
// Callback from rocksdb_writebatch_iterate_cf()
static void CheckPutCF(void* ptr, uint32_t cfid, const char* k, size_t klen,
const char* v, size_t vlen) {
@@ -2643,6 +2664,10 @@ int main(int argc, char** argv) {
CheckCondition(150 ==
rocksdb_options_get_memtable_avg_op_scan_flush_trigger(o));
rocksdb_options_set_min_tombstones_for_range_conversion(o, 200);
CheckCondition(200 ==
rocksdb_options_get_min_tombstones_for_range_conversion(o));
rocksdb_options_set_ttl(o, 5000);
CheckCondition(5000 == rocksdb_options_get_ttl(o));
@@ -2867,6 +2892,14 @@ int main(int argc, char** argv) {
rocksdb_options_set_blob_gc_force_threshold(o, 0.75);
CheckCondition(0.75 == rocksdb_options_get_blob_gc_force_threshold(o));
rocksdb_options_set_read_triggered_compaction_threshold(o, 0.001);
CheckCondition(0.001 ==
rocksdb_options_get_read_triggered_compaction_threshold(o));
rocksdb_options_set_max_compaction_trigger_wakeup_seconds(o, 3600);
CheckCondition(
3600 == rocksdb_options_get_max_compaction_trigger_wakeup_seconds(o));
rocksdb_options_set_blob_compaction_readahead_size(o, 262144);
CheckCondition(262144 ==
rocksdb_options_get_blob_compaction_readahead_size(o));
@@ -3086,6 +3119,12 @@ int main(int argc, char** argv) {
CheckCondition(150 ==
rocksdb_options_get_memtable_avg_op_scan_flush_trigger(o));
rocksdb_options_set_min_tombstones_for_range_conversion(copy, 1000);
CheckCondition(
1000 == rocksdb_options_get_min_tombstones_for_range_conversion(copy));
CheckCondition(200 ==
rocksdb_options_get_min_tombstones_for_range_conversion(o));
rocksdb_options_set_ttl(copy, 8000);
CheckCondition(8000 == rocksdb_options_get_ttl(copy));
CheckCondition(5000 == rocksdb_options_get_ttl(o));
@@ -3850,10 +3889,35 @@ int main(int argc, char** argv) {
CheckMultiGetValues(3, vals, vals_sizes, errs, expected);
}
rocksdb_transaction_put_log_data(txn, "log_blob", 8);
// record sequence number before commit so we can scan the WAL after
rocksdb_t* base_db_ld = rocksdb_transactiondb_get_base_db(txn_db);
uint64_t seq_before = rocksdb_get_latest_sequence_number(base_db_ld);
// commit
rocksdb_transaction_commit(txn, &err);
CheckNoError(err);
// verify log data was written to WAL by scanning batches since seq_before
{
rocksdb_wal_iterator_t* wal_iter =
rocksdb_get_updates_since(base_db_ld, seq_before, NULL, &err);
CheckNoError(err);
int log_found = 0;
for (; rocksdb_wal_iter_valid(wal_iter) && !log_found;
rocksdb_wal_iter_next(wal_iter)) {
uint64_t seq;
rocksdb_writebatch_t* wal_wb =
rocksdb_wal_iter_get_batch(wal_iter, &seq);
rocksdb_writebatch_iterate_ld(wal_wb, &log_found, NopPut, NopDel,
CheckLogData);
rocksdb_writebatch_destroy(wal_wb);
}
CheckCondition(log_found == 1);
rocksdb_wal_iter_destroy(wal_iter);
}
rocksdb_transactiondb_close_base_db(base_db_ld);
// read from outside transaction, after commit
CheckTxnDBGet(txn_db, roptions, "foo", "hello");
CheckTxnDBPinGet(txn_db, roptions, "foo", "hello");
@@ -4880,6 +4944,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);
+12 -2
View File
@@ -12,6 +12,7 @@ namespace ROCKSDB_NAMESPACE {
void CoalescingIterator::Coalesce(
const autovector<MultiCfIteratorInfo>& items) {
assert(wide_columns_.empty());
assert(owned_columns_.empty());
MinHeap heap;
for (const auto& item : items) {
assert(item.iterator);
@@ -22,13 +23,22 @@ void CoalescingIterator::Coalesce(
if (heap.empty()) {
return;
}
// Own the coalesced bytes so the result stays valid even if a child iterator
// refreshes or reuses its internal buffers.
owned_columns_.reserve(heap.size());
wide_columns_.reserve(heap.size());
auto add_column = [this](const WideColumn& column) {
owned_columns_.emplace_back(column.name().ToString(),
column.value().ToString());
const auto& owned = owned_columns_.back();
wide_columns_.emplace_back(owned.first, owned.second);
};
auto current = heap.top();
heap.pop();
while (!heap.empty()) {
int comparison = current.column->name().compare(heap.top().column->name());
if (comparison < 0) {
wide_columns_.push_back(*current.column);
add_column(*current.column);
} else if (comparison > 0) {
// Shouldn't reach here.
// Current item in the heap is greater than the top item in the min heap
@@ -37,7 +47,7 @@ void CoalescingIterator::Coalesce(
current = heap.top();
heap.pop();
}
wide_columns_.push_back(*current.column);
add_column(*current.column);
if (WideColumnsHelper::HasDefaultColumn(wide_columns_)) {
value_ = WideColumnsHelper::GetDefaultColumn(wide_columns_);
+6
View File
@@ -5,6 +5,10 @@
#pragma once
#include <string>
#include <utility>
#include <vector>
#include "db/multi_cf_iterator_impl.h"
namespace ROCKSDB_NAMESPACE {
@@ -45,6 +49,7 @@ class CoalescingIterator : public Iterator {
void Reset() {
value_.clear();
wide_columns_.clear();
owned_columns_.clear();
}
bool PrepareValue() override { return impl_.PrepareValue(); }
@@ -79,6 +84,7 @@ class CoalescingIterator : public Iterator {
MultiCfIteratorImpl<ResetFunc, PopulateFunc> impl_;
Slice value_;
WideColumns wide_columns_;
std::vector<std::pair<std::string, std::string>> owned_columns_;
struct WideColumnWithOrder {
const WideColumn* column;
+85 -14
View File
@@ -11,12 +11,14 @@
#include <algorithm>
#include <cinttypes>
#include <cmath>
#include <limits>
#include <sstream>
#include <string>
#include <vector>
#include "db/blob/blob_file_cache.h"
#include "db/blob/blob_file_partition_manager.h"
#include "db/blob/blob_source.h"
#include "db/compaction/compaction_picker.h"
#include "db/compaction/compaction_picker_fifo.h"
@@ -495,6 +497,15 @@ ColumnFamilyOptions SanitizeCfOptions(const ImmutableDBOptions& db_options,
result.memtable_avg_op_scan_flush_trigger = 0;
}
}
if (result.enable_blob_direct_write && !result.enable_blob_files) {
ROCKS_LOG_WARN(db_options.info_log.get(),
"enable_blob_direct_write requires enable_blob_files=true. "
"Disabling blob direct write.");
result.enable_blob_direct_write = false;
}
if (result.blob_direct_write_partitions == 0) {
result.blob_direct_write_partitions = 1;
}
return result;
}
@@ -601,7 +612,7 @@ ColumnFamilyData::ColumnFamilyData(
const FileOptions* file_options, ColumnFamilySet* column_family_set,
BlockCacheTracer* const block_cache_tracer,
const std::shared_ptr<IOTracer>& io_tracer, const std::string& db_id,
const std::string& db_session_id, bool read_only)
const std::string& db_session_id, bool read_only, bool fast_sst_open)
: id_(id),
name_(name),
dummy_versions_(_dummy_versions),
@@ -661,7 +672,7 @@ ColumnFamilyData::ColumnFamilyData(
new InternalStats(ioptions_.num_levels, ioptions_.clock, this));
table_cache_.reset(new TableCache(ioptions_, file_options, _table_cache,
block_cache_tracer, io_tracer,
db_session_id));
db_session_id, fast_sst_open));
blob_file_cache_.reset(
new BlobFileCache(_table_cache, &ioptions(), soptions(), id_,
internal_stats_->GetBlobFileReadHist(), io_tracer));
@@ -782,6 +793,11 @@ ColumnFamilyData::~ColumnFamilyData() {
}
}
void ColumnFamilyData::SetBlobPartitionManager(
std::shared_ptr<BlobFilePartitionManager> mgr) {
blob_partition_manager_ = std::move(mgr);
}
bool ColumnFamilyData::UnrefAndTryDelete() {
int old_refs = refs_.fetch_sub(1);
assert(old_refs > 0);
@@ -1523,6 +1539,32 @@ Status ColumnFamilyData::ValidateOptions(
const auto* ucmp = cf_options.comparator;
assert(ucmp);
if (cf_options.enable_blob_direct_write) {
if (db_options.enable_pipelined_write) {
return Status::NotSupported(
"Blob direct write v1 does not support pipelined writes.");
}
if (db_options.allow_concurrent_memtable_write) {
return Status::NotSupported(
"Blob direct write v1 does not support concurrent memtable writes.");
}
if (db_options.unordered_write) {
return Status::NotSupported(
"Blob direct write v1 does not support unordered writes.");
}
if (db_options.two_write_queues) {
return Status::NotSupported(
"Blob direct write v1 does not support two write queues.");
}
if (cf_options.experimental_mempurge_threshold > 0.0) {
return Status::NotSupported(
"Blob direct write does not support MemPurge.");
}
if (ucmp->timestamp_size() > 0) {
return Status::NotSupported(
"Blob direct write does not support user-defined timestamps.");
}
}
if (ucmp->timestamp_size() > 0 &&
!cf_options.persist_user_defined_timestamps) {
if (db_options.atomic_flush) {
@@ -1563,12 +1605,42 @@ Status ColumnFamilyData::ValidateOptions(
}
}
if (cf_options.read_triggered_compaction_threshold < 0.0 ||
std::isnan(cf_options.read_triggered_compaction_threshold) ||
std::isinf(cf_options.read_triggered_compaction_threshold)) {
return Status::InvalidArgument(
"read_triggered_compaction_threshold must be >= 0.0 and finite. "
"Use 0.0 to disable.");
}
if (cf_options.compaction_style == kCompactionStyleFIFO &&
db_options.max_open_files != -1 && cf_options.ttl > 0) {
return Status::NotSupported(
"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) ==
@@ -1737,16 +1809,14 @@ void ColumnFamilyData::RecoverEpochNumbers() {
vstorage->RecoverEpochNumbers(this);
}
ColumnFamilySet::ColumnFamilySet(const std::string& dbname,
const ImmutableDBOptions* db_options,
const FileOptions& file_options,
Cache* table_cache,
WriteBufferManager* _write_buffer_manager,
WriteController* _write_controller,
BlockCacheTracer* const block_cache_tracer,
const std::shared_ptr<IOTracer>& io_tracer,
const std::string& db_id,
const std::string& db_session_id)
ColumnFamilySet::ColumnFamilySet(
const std::string& dbname, const ImmutableDBOptions* db_options,
const FileOptions& file_options, Cache* table_cache,
WriteBufferManager* _write_buffer_manager,
WriteController* _write_controller,
BlockCacheTracer* const block_cache_tracer,
const std::shared_ptr<IOTracer>& io_tracer, const std::string& db_id,
const std::string& db_session_id, bool fast_sst_open)
: max_column_family_(0),
file_options_(file_options),
dummy_cfd_(new ColumnFamilyData(
@@ -1763,7 +1833,8 @@ ColumnFamilySet::ColumnFamilySet(const std::string& dbname,
block_cache_tracer_(block_cache_tracer),
io_tracer_(io_tracer),
db_id_(db_id),
db_session_id_(db_session_id) {
db_session_id_(db_session_id),
fast_sst_open_(fast_sst_open) {
// initialize linked list
dummy_cfd_->prev_ = dummy_cfd_;
dummy_cfd_->next_ = dummy_cfd_;
@@ -1830,7 +1901,7 @@ ColumnFamilyData* ColumnFamilySet::CreateColumnFamily(
ColumnFamilyData* new_cfd = new ColumnFamilyData(
id, name, dummy_versions, table_cache_, write_buffer_manager_, options,
*db_options_, &file_options_, this, block_cache_tracer_, io_tracer_,
db_id_, db_session_id_, read_only);
db_id_, db_session_id_, read_only, fast_sst_open_);
column_families_.insert({name, id});
column_family_data_.insert({id, new_cfd});
auto ucmp = new_cfd->user_comparator();
+41 -9
View File
@@ -49,6 +49,7 @@ class InstrumentedMutex;
class InstrumentedMutexLock;
struct SuperVersionContext;
class BlobFileCache;
class BlobFilePartitionManager;
class BlobSource;
extern const double kIncSlowdownRatio;
@@ -415,6 +416,19 @@ class ColumnFamilyData {
TableCache* table_cache() const { return table_cache_.get(); }
BlobFileCache* blob_file_cache() const { return blob_file_cache_.get(); }
BlobSource* blob_source() const { return blob_source_.get(); }
// Returns the write-path blob partition manager for this CF, or null if BDW
// is disabled.
BlobFilePartitionManager* blob_partition_manager() const {
return blob_partition_manager_.get();
}
// Returns a shared ownership handle for cold paths that must keep the
// manager alive beyond the lifetime of this ColumnFamilyData.
std::shared_ptr<BlobFilePartitionManager> blob_partition_manager_handle()
const {
return blob_partition_manager_;
}
// Installs the write-path blob partition manager for this CF.
void SetBlobPartitionManager(std::shared_ptr<BlobFilePartitionManager> mgr);
// See documentation in compaction_picker.h
// REQUIRES: DB mutex held
@@ -605,16 +619,23 @@ class ColumnFamilyData {
return ioptions_.cf_allow_ingest_behind || ioptions_.allow_ingest_behind;
}
// Per-CF reader-writer lock that serializes IngestExternalFiles with range
// tombstone conversion.
port::RWMutex& GetIngestSstLock() { return ingest_sst_lock_; }
private:
friend class ColumnFamilySet;
ColumnFamilyData(
uint32_t id, const std::string& name, Version* dummy_versions,
Cache* table_cache, WriteBufferManager* write_buffer_manager,
const ColumnFamilyOptions& options, const ImmutableDBOptions& db_options,
const FileOptions* file_options, ColumnFamilySet* column_family_set,
BlockCacheTracer* const block_cache_tracer,
const std::shared_ptr<IOTracer>& io_tracer, const std::string& db_id,
const std::string& db_session_id, bool read_only);
ColumnFamilyData(uint32_t id, const std::string& name,
Version* dummy_versions, Cache* table_cache,
WriteBufferManager* write_buffer_manager,
const ColumnFamilyOptions& options,
const ImmutableDBOptions& db_options,
const FileOptions* file_options,
ColumnFamilySet* column_family_set,
BlockCacheTracer* const block_cache_tracer,
const std::shared_ptr<IOTracer>& io_tracer,
const std::string& db_id, const std::string& db_session_id,
bool read_only, bool fast_sst_open = false);
std::vector<std::string> GetDbPaths() const;
@@ -648,6 +669,8 @@ class ColumnFamilyData {
std::unique_ptr<TableCache> table_cache_;
std::unique_ptr<BlobFileCache> blob_file_cache_;
std::unique_ptr<BlobSource> blob_source_;
// Per-CF manager for write-path blob direct-write files.
std::shared_ptr<BlobFilePartitionManager> blob_partition_manager_;
std::unique_ptr<InternalStats> internal_stats_;
@@ -713,6 +736,10 @@ class ColumnFamilyData {
bool mempurge_used_;
std::atomic<uint64_t> next_epoch_number_;
// Used to synchronize IngestExternalFile with range tombstone conversion. See
// also Memtable::ingest_seqno_barrier_.
port::RWMutex ingest_sst_lock_;
};
// ColumnFamilySet has interesting thread-safety requirements
@@ -758,7 +785,8 @@ class ColumnFamilySet {
WriteController* _write_controller,
BlockCacheTracer* const block_cache_tracer,
const std::shared_ptr<IOTracer>& io_tracer,
const std::string& db_id, const std::string& db_session_id);
const std::string& db_id, const std::string& db_session_id,
bool fast_sst_open = false);
~ColumnFamilySet();
ColumnFamilyData* GetDefault() const;
@@ -794,6 +822,9 @@ class ColumnFamilySet {
Cache* get_table_cache() { return table_cache_; }
bool GetFastSstOpen() const { return fast_sst_open_; }
void SetFastSstOpen(bool v) { fast_sst_open_ = v; }
WriteBufferManager* write_buffer_manager() { return write_buffer_manager_; }
WriteController* write_controller() { return write_controller_; }
@@ -842,6 +873,7 @@ class ColumnFamilySet {
std::shared_ptr<IOTracer> io_tracer_;
const std::string& db_id_;
std::string db_session_id_;
bool fast_sst_open_;
};
// A wrapper for ColumnFamilySet that supports releasing DB mutex during each
+36
View File
@@ -734,6 +734,42 @@ TEST_P(ColumnFamilyTest, AddDrop) {
std::vector<std::string>({"default", "four", "three"}));
}
TEST_P(ColumnFamilyTest, EmptyNameRejected) {
// Creating a column family with an empty-string name should be rejected with
// InvalidArgument. Empty strings are reserved by various RocksDB APIs and
// serialization formats to mean "unknown/unspecified column family" (e.g.
// TablePropertiesCollectorFactory::Context::kUnknownColumnFamily, table
// properties).
//
// Prior behavior was silently broken: CreateColumnFamily("") would return
// OK and a usable handle, but the CF was not persisted in the manifest --
// data written to it was lost on DB reopen.
Open();
ColumnFamilyHandle* handle = nullptr;
Status s =
db_->CreateColumnFamily(column_family_options_, std::string(), &handle);
ASSERT_TRUE(s.IsInvalidArgument()) << s.ToString();
ASSERT_EQ(handle, nullptr);
// Same via the bulk-by-names API. The first valid name should still get
// created; the failure should come on the empty one.
std::vector<ColumnFamilyHandle*> handles;
s = db_->CreateColumnFamilies(column_family_options_,
{"ok_name", "", "another"}, &handles);
ASSERT_TRUE(s.IsInvalidArgument()) << s.ToString();
// Clean up the partially-created handle ("ok_name") before the failure.
ASSERT_EQ(handles.size(), 1U);
ASSERT_OK(db_->DropColumnFamily(handles[0]));
ASSERT_OK(db_->DestroyColumnFamilyHandle(handles[0]));
handles.clear();
// Same via the bulk-by-descriptor API.
s = db_->CreateColumnFamilies(
{ColumnFamilyDescriptor("", column_family_options_)}, &handles);
ASSERT_TRUE(s.IsInvalidArgument()) << s.ToString();
ASSERT_TRUE(handles.empty());
}
TEST_P(ColumnFamilyTest, BulkAddDrop) {
constexpr int kNumCF = 1000;
ColumnFamilyOptions cf_options;
+664 -34
View File
@@ -7,12 +7,14 @@
#include <iterator>
#include <limits>
#include <memory>
#include "db/blob/blob_fetcher.h"
#include "db/blob/blob_file_builder.h"
#include "db/blob/blob_index.h"
#include "db/blob/prefetch_buffer_collection.h"
#include "db/snapshot_checker.h"
#include "db/wide/blob_column_resolver_util.h"
#include "db/wide/wide_column_serialization.h"
#include "db/wide/wide_columns_helper.h"
#include "logging/logging.h"
@@ -20,8 +22,106 @@
#include "rocksdb/listener.h"
#include "table/internal_iterator.h"
#include "test_util/sync_point.h"
#include "util/autovector.h"
namespace ROCKSDB_NAMESPACE {
// CompactionBlobResolver implementation
void CompactionBlobResolver::Init(BlobFetcher* blob_fetcher,
PrefetchBufferCollection* prefetch_buffers,
CompactionIterationStats* iter_stats) {
blob_fetcher_ = blob_fetcher;
prefetch_buffers_ = prefetch_buffers;
iter_stats_ = iter_stats;
}
void CompactionBlobResolver::Reset(
const Slice& user_key, const std::vector<WideColumn>* columns,
const std::vector<std::pair<size_t, BlobIndex>>* blob_columns,
bool track_resolve_error) {
user_key_ = user_key;
columns_ = columns;
blob_columns_ = blob_columns;
track_resolve_error_ = track_resolve_error;
resolved_cache_.clear();
resolve_error_.reset();
}
Status CompactionBlobResolver::ResolveColumn(size_t column_index,
Slice* resolved_value) {
Status status = Status::OK();
if (columns_ == nullptr || column_index >= columns_->size()) {
status = Status::InvalidArgument("Column index out of bounds");
} else {
const BlobIndex* blob_index_ptr =
blob_resolver_util::FindBlobColumn(blob_columns_, column_index);
if (blob_index_ptr == nullptr) {
// Not a blob column - return the inline value directly
*resolved_value = (*columns_)[column_index].value();
} else {
// Check if we've already resolved this column
PinnableSlice* cached =
blob_resolver_util::FindInCache(resolved_cache_, column_index);
if (cached != nullptr) {
*resolved_value = Slice(*cached);
} else if (blob_fetcher_ == nullptr) {
status = Status::NotSupported("Blob fetcher not available");
} else {
const BlobIndex& blob_index = *blob_index_ptr;
// Handle inlined blobs (e.g., from legacy stacked BlobDB with TTL)
if (blob_index.IsInlined()) {
*resolved_value = blob_resolver_util::CacheInlinedBlob(
resolved_cache_, column_index, blob_index);
} else {
FilePrefetchBuffer* prefetch_buffer =
prefetch_buffers_ ? prefetch_buffers_->GetOrCreatePrefetchBuffer(
blob_index.file_number())
: nullptr;
uint64_t bytes_read = 0;
resolved_cache_.emplace_back(column_index,
std::make_unique<PinnableSlice>());
auto& new_entry = resolved_cache_.back();
status =
blob_fetcher_->FetchBlob(user_key_, blob_index, prefetch_buffer,
new_entry.second.get(), &bytes_read);
if (!status.ok()) {
resolved_cache_.pop_back();
} else {
if (iter_stats_ != nullptr) {
++iter_stats_->num_blobs_read;
iter_stats_->total_blob_bytes_read += bytes_read;
}
*resolved_value = Slice(*new_entry.second);
}
}
}
}
}
if (!status.ok() && track_resolve_error_ && !resolve_error_.has_value()) {
resolve_error_.emplace(status);
}
return status;
}
bool CompactionBlobResolver::IsBlobColumn(size_t column_index) const {
if (columns_ == nullptr || column_index >= columns_->size()) {
return false;
}
return blob_resolver_util::IsBlobColumnIndex(blob_columns_, column_index);
}
size_t CompactionBlobResolver::NumColumns() const {
if (columns_ == nullptr) {
return 0;
}
return columns_->size();
}
CompactionIterator::CompactionIterator(
InternalIterator* input, const Comparator* cmp, MergeHelper* merge_helper,
SequenceNumber last_sequence, std::vector<SequenceNumber>* snapshots,
@@ -38,7 +138,8 @@ CompactionIterator::CompactionIterator(
const std::atomic<bool>* shutting_down,
const std::shared_ptr<Logger> info_log,
const std::string* full_history_ts_low,
std::optional<SequenceNumber> preserve_seqno_min)
std::optional<SequenceNumber> preserve_seqno_min,
const Version* input_version, Env::IOActivity blob_read_io_activity)
: CompactionIterator(
input, cmp, merge_helper, last_sequence, snapshots, earliest_snapshot,
earliest_write_conflict_snapshot, job_snapshot, snapshot_checker, env,
@@ -47,7 +148,8 @@ CompactionIterator::CompactionIterator(
manual_compaction_canceled,
compaction ? std::make_unique<RealCompaction>(compaction) : nullptr,
must_count_input_entries, compaction_filter, shutting_down, info_log,
full_history_ts_low, preserve_seqno_min) {}
full_history_ts_low, preserve_seqno_min, input_version,
blob_read_io_activity) {}
CompactionIterator::CompactionIterator(
InternalIterator* input, const Comparator* cmp, MergeHelper* merge_helper,
@@ -65,7 +167,8 @@ CompactionIterator::CompactionIterator(
const std::atomic<bool>* shutting_down,
const std::shared_ptr<Logger> info_log,
const std::string* full_history_ts_low,
std::optional<SequenceNumber> preserve_seqno_min)
std::optional<SequenceNumber> preserve_seqno_min,
const Version* input_version, Env::IOActivity blob_read_io_activity)
: input_(input, cmp, must_count_input_entries),
cmp_(cmp),
merge_helper_(merge_helper),
@@ -80,6 +183,8 @@ CompactionIterator::CompactionIterator(
blob_file_builder_(blob_file_builder),
compaction_(std::move(compaction)),
compaction_filter_(compaction_filter),
filter_supports_v4_(
compaction_filter_ ? compaction_filter_->SupportsFilterV4() : false),
shutting_down_(shutting_down),
manual_compaction_canceled_(manual_compaction_canceled),
bottommost_level_(compaction_ && compaction_->bottommost_level() &&
@@ -98,7 +203,8 @@ CompactionIterator::CompactionIterator(
merge_out_iter_(merge_helper_),
blob_garbage_collection_cutoff_file_number_(
ComputeBlobGarbageCollectionCutoffFileNumber(compaction_.get())),
blob_fetcher_(CreateBlobFetcherIfNeeded(compaction_.get())),
blob_fetcher_(CreateBlobFetcherIfNeeded(compaction_.get(), input_version,
blob_read_io_activity)),
prefetch_buffers_(
CreatePrefetchBufferCollectionIfNeeded(compaction_.get())),
current_key_committed_(false),
@@ -120,6 +226,8 @@ CompactionIterator::CompactionIterator(
timestamp_size_ == full_history_ts_low_->size());
#endif
input_.SetPinnedItersMgr(&pinned_iters_mgr_);
blob_resolver_.Init(blob_fetcher_.get(), prefetch_buffers_.get(),
&iter_stats_);
// The default `merge_until_status_` does not need to be checked since it is
// overwritten as soon as `MergeUntil()` is called
merge_until_status_.PermitUncheckedError();
@@ -131,6 +239,24 @@ CompactionIterator::~CompactionIterator() {
input_.SetPinnedItersMgr(nullptr);
}
void CompactionIterator::SetBlobFetcher(const Version* version,
BlobFileCache* blob_file_cache,
Env::IOActivity io_activity,
bool allow_write_path_fallback) {
if (blob_fetcher_ != nullptr || version == nullptr) {
return;
}
ReadOptions read_options;
read_options.io_activity = io_activity;
read_options.fill_cache = false;
blob_fetcher_ = std::make_unique<BlobFetcher>(
version, read_options, blob_file_cache, allow_write_path_fallback);
blob_resolver_.Init(blob_fetcher_.get(), prefetch_buffers_.get(),
&iter_stats_);
}
void CompactionIterator::ResetRecordCounts() {
iter_stats_.num_record_drop_user = 0;
iter_stats_.num_record_drop_hidden = 0;
@@ -258,9 +384,10 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
compaction_filter_skip_until_.rep());
if (decision == CompactionFilter::Decision::kUndetermined &&
!compaction_filter_->IsStackedBlobDbInternalCompactionFilter()) {
if (!compaction_) {
status_ =
Status::Corruption("Unexpected blob index outside of compaction");
if (!blob_fetcher_) {
status_ = Status::NotSupported(
"Blob-backed value filtering requires blob read support in the "
"current table-file creation context");
validity_info_.Invalidate();
return false;
}
@@ -287,8 +414,6 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
uint64_t bytes_read = 0;
assert(blob_fetcher_);
s = blob_fetcher_->FetchBlob(ikey_.user_key, blob_index,
prefetch_buffer, &blob_value_,
&bytes_read);
@@ -309,7 +434,19 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
const Slice* existing_val = nullptr;
const WideColumns* existing_col = nullptr;
WideColumns existing_columns;
// Reuse member variable to avoid per-key heap allocation.
filter_existing_columns_.clear();
WideColumnBlobResolver* blob_resolver_ptr = nullptr;
// Use member variables for entity column storage to avoid per-key
// heap allocations. Cleared at start of each use.
entity_columns_.clear();
entity_blob_columns_.clear();
// Storage for eagerly resolved blob values used by the FilterV3
// compatibility path. Keep this in the same scope as the FilterV4()
// invocation below because filter_existing_columns_ stores Slices into
// these buffers.
autovector<PinnableSlice, 4> resolved_blobs_storage;
if (ikey_.type != kTypeWideColumnEntity) {
if (!blob_value_.empty()) {
@@ -318,23 +455,125 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
existing_val = &value_;
}
} else {
Slice value_copy = value_;
const Status s =
WideColumnSerialization::Deserialize(value_copy, existing_columns);
// Check if entity has blob columns that need resolution
bool has_blob_columns = false;
{
Status s_hbc =
WideColumnSerialization::HasBlobColumns(value_, has_blob_columns);
if (!s_hbc.ok()) {
status_ = s_hbc;
validity_info_.Invalidate();
return false;
}
}
if (UNLIKELY(has_blob_columns)) {
// Entity has blob columns. DeserializeV2() populates
// entity_columns_ with the full column set; blob-backed entries still
// carry the serialized BlobIndex bytes in value(). The companion
// entity_blob_columns_ side list identifies which column indexes are
// blob references and provides the decoded BlobIndex objects.
Slice input_copy = value_;
Status s = WideColumnSerialization::DeserializeV2(
input_copy, entity_columns_, entity_blob_columns_);
if (!s.ok()) {
status_ = s;
validity_info_.Invalidate();
return false;
}
// Mark as deserialized so PrepareOutput can skip re-deserialization
// if the filter returns kKeep.
entity_deserialized_ = true;
if (!s.ok()) {
status_ = s;
// Build filter_existing_columns_ with raw values (blob columns
// have blob index as value, not the actual blob data)
for (size_t i = 0; i < entity_columns_.size(); ++i) {
filter_existing_columns_.emplace_back(entity_columns_[i].name(),
entity_columns_[i].value());
}
if (filter_supports_v4_) {
// Reset member blob resolver for this entity - filter can call
// resolver->ResolveColumn() to fetch blob values on-demand
blob_resolver_.Reset(ikey_.user_key, &entity_columns_,
&entity_blob_columns_,
/*track_resolve_error=*/true);
blob_resolver_ptr = &blob_resolver_;
} else {
// FilterV3 compatibility: eagerly resolve all blob columns so
// the filter sees actual values (not raw BlobIndex bytes).
// resolved_blobs_storage is in the outer scope so the data
// outlives filter_existing_columns_ (which holds Slices into
// it).
resolved_blobs_storage.resize(entity_blob_columns_.size());
for (size_t bi = 0; bi < entity_blob_columns_.size(); ++bi) {
const size_t col_idx = entity_blob_columns_[bi].first;
const BlobIndex& blob_idx = entity_blob_columns_[bi].second;
if (blob_idx.IsInlined()) {
resolved_blobs_storage[bi].PinSelf(blob_idx.value());
} else {
FilePrefetchBuffer* prefetch_buffer =
prefetch_buffers_
? prefetch_buffers_->GetOrCreatePrefetchBuffer(
blob_idx.file_number())
: nullptr;
uint64_t bytes_read = 0;
assert(blob_fetcher_);
Status s_fetch = blob_fetcher_->FetchBlob(
ikey_.user_key, blob_idx, prefetch_buffer,
&resolved_blobs_storage[bi], &bytes_read);
if (!s_fetch.ok()) {
status_ = s_fetch;
validity_info_.Invalidate();
return false;
}
++iter_stats_.num_blobs_read;
iter_stats_.total_blob_bytes_read += bytes_read;
}
// Replace the blob index value in filter_existing_columns_
// with the resolved blob value
filter_existing_columns_[col_idx] =
WideColumn(entity_columns_[col_idx].name(),
Slice(resolved_blobs_storage[bi]));
}
}
} else {
// No blob columns, use fast path
Slice value_copy = value_;
const Status s = WideColumnSerialization::Deserialize(
value_copy, filter_existing_columns_);
if (!s.ok()) {
status_ = s;
validity_info_.Invalidate();
return false;
}
}
existing_col = &filter_existing_columns_;
}
// existing_val / existing_col point into value_, blob_value_,
// entity_columns_, and resolved_blobs_storage, all of which stay alive
// until this call returns.
decision = compaction_filter_->FilterV4(
level_, filter_key, value_type, existing_val, existing_col,
&compaction_filter_value_, &new_columns,
compaction_filter_skip_until_.rep(), blob_resolver_ptr);
if (blob_resolver_ptr != nullptr) {
Status resolve_status = blob_resolver_.resolve_status();
if (!resolve_status.ok()) {
// Keep lazy FilterV4 failure semantics aligned with the eager
// FilterV3 compatibility path: if blob resolution fails while the
// filter is inspecting the entry, fail compaction even if the
// filter returned kKeep after noticing the error.
status_ = std::move(resolve_status);
validity_info_.Invalidate();
return false;
}
existing_col = &existing_columns;
}
decision = compaction_filter_->FilterV3(
level_, filter_key, value_type, existing_val, existing_col,
&compaction_filter_value_, &new_columns,
compaction_filter_skip_until_.rep());
}
iter_stats_.total_filter_time +=
@@ -342,10 +581,10 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
}
if (decision == CompactionFilter::Decision::kUndetermined) {
// Should not reach here, since FilterV2/FilterV3 should never return
// Should not reach here, since FilterV2/V3/V4 should never return
// kUndetermined.
status_ = Status::NotSupported(
"FilterV2/FilterV3 should never return kUndetermined");
"FilterV2/V3/V4 should never return kUndetermined");
validity_info_.Invalidate();
return false;
}
@@ -354,7 +593,7 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
cmp_->Compare(*compaction_filter_skip_until_.rep(), ikey_.user_key) <=
0) {
// Can't skip to a key smaller than the current one.
// Keep the key as per FilterV2/FilterV3 documentation.
// Keep the key as per FilterV2/V3/V4 documentation.
decision = CompactionFilter::Decision::kKeep;
}
@@ -442,6 +681,16 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
}
value_ = compaction_filter_value_;
// Value changed, force re-deserialization in PrepareOutput
entity_deserialized_ = false;
}
if (UNLIKELY(compaction_filter_value_.size() >
std::numeric_limits<uint32_t>::max())) {
status_ = Status::Corruption(
"CompactionFilter result value size exceeds 4GB limit");
validity_info_.Invalidate();
return false;
}
return true;
@@ -450,9 +699,14 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
void CompactionIterator::NextFromInput() {
at_next_ = false;
validity_info_.Invalidate();
entity_deserialized_ = false;
while (!Valid() && input_.Valid() && !IsPausingManualCompaction() &&
!IsShuttingDown()) {
// A filtered-out key can advance directly to the next input record without
// returning to PrepareOutput(), so clear the per-record deserialization
// cache at the start of each loop iteration.
entity_deserialized_ = false;
key_ = input_.key();
value_ = input_.value();
blob_value_.Reset();
@@ -1266,6 +1520,340 @@ void CompactionIterator::GarbageCollectBlobIfNeeded() {
}
}
// Wide-column entity compaction flow after InvokeFilterIfNeeded():
// 1. PrepareOutput() is the only caller of the helper block below.
// 2. PrepareOutput() ensures entity_columns_/entity_blob_columns_ describe
// the current entity, either by reusing the filter-time deserialization or
// by deserializing once itself.
// 3. PrepareOutput() then picks exactly one follow-up path:
// - inline-only entity:
// ExtractLargeColumnValuesIfNeeded()
// This is the flush/compaction extraction path. It may rewrite large
// inline columns into blob references.
// - entity already containing blob references:
// GarbageCollectEntityBlobsIfNeeded()
// This is the blob-GC path. It only fetches / relocates references
// that fall below the current GC cutoff.
// Both helpers leave value_ unchanged when no rewrite is needed.
void CompactionIterator::ExtractLargeColumnValuesIfNeeded() {
assert(ikey_.type == kTypeWideColumnEntity);
// Check if blob extraction is enabled
if (!blob_file_builder_) {
return;
}
// Deserialize using member variables (already populated by PrepareOutput
// for the combined deserialization path). If not yet populated, deserialize
// here.
if (entity_columns_.empty()) {
Slice entity_slice = value_;
entity_columns_.clear();
entity_blob_columns_.clear();
Status s = WideColumnSerialization::DeserializeV2(
entity_slice, entity_columns_, entity_blob_columns_);
if (!s.ok()) {
status_ = s;
validity_info_.Invalidate();
return;
}
}
// IMPORTANT: If there are already blob columns, we skip extraction entirely.
// This means new large inline columns added to such entities (e.g., via
// PutEntity after a previous PutEntity that already had blob columns) will
// NOT be extracted to blob files until the entity undergoes a full rewrite
// (e.g., via compaction that GCs all existing blob refs).
//
// This is a deliberate simplification: supporting partial extraction
// (extracting only new large inline columns while preserving existing
// blob refs) would require merging old and new blob column sets during
// serialization, and the benefit is marginal since full rewrites happen
// naturally during blob GC.
if (UNLIKELY(!entity_blob_columns_.empty())) {
return;
}
// Try to extract large column values to blob files
// Track which columns were extracted and their blob indices
std::vector<std::pair<size_t, BlobIndex>> new_blob_columns;
std::string temp_blob_index;
for (size_t i = 0; i < entity_columns_.size(); ++i) {
const Slice& col_value = entity_columns_[i].value();
// Clear the temporary buffer for each column
temp_blob_index.clear();
// Try to add the column value to blob file
// blob_file_builder_->Add() will check min_blob_size internally
// and only write to blob file if the value is large enough
const Status add_status =
blob_file_builder_->Add(user_key(), col_value, &temp_blob_index);
if (!add_status.ok()) {
status_ = add_status;
validity_info_.Invalidate();
return;
}
// If blob_index is not empty, the value was extracted to a blob file
if (!temp_blob_index.empty()) {
BlobIndex blob_idx;
const Status decode_status = blob_idx.DecodeFrom(temp_blob_index);
if (!decode_status.ok()) {
status_ = decode_status;
validity_info_.Invalidate();
return;
}
new_blob_columns.emplace_back(i, blob_idx);
}
}
// If no columns were extracted, nothing to do
if (new_blob_columns.empty()) {
return;
}
// Re-serialize the entity with blob indices using the Slice-based overload
// to avoid copying column names and values to strings.
entity_wide_columns_.clear();
entity_wide_columns_.reserve(entity_columns_.size());
for (const auto& col : entity_columns_) {
entity_wide_columns_.emplace_back(col.name(), col.value());
}
rewritten_entity_.clear();
const Status serialize_status = WideColumnSerialization::SerializeV2(
entity_wide_columns_, new_blob_columns, rewritten_entity_);
if (!serialize_status.ok()) {
status_ = serialize_status;
validity_info_.Invalidate();
return;
}
// Update value_ to point to the rewritten entity
value_ = rewritten_entity_;
}
bool CompactionIterator::FetchBlobsNeedingGC(
const std::vector<WideColumn>& /*columns*/,
const std::vector<std::pair<size_t, BlobIndex>>& blob_columns,
std::vector<std::pair<size_t, PinnableSlice>>* fetched_blob_values) {
assert(fetched_blob_values != nullptr);
for (const auto& blob_col : blob_columns) {
const size_t col_idx = blob_col.first;
const BlobIndex& blob_index = blob_col.second;
// Skip inlined blobs - they don't need GC
if (blob_index.IsInlined()) {
continue;
}
// Check if this blob file needs garbage collection
if (blob_index.file_number() >=
blob_garbage_collection_cutoff_file_number_) {
continue;
}
// This blob needs to be relocated - fetch its value
FilePrefetchBuffer* prefetch_buffer =
prefetch_buffers_ ? prefetch_buffers_->GetOrCreatePrefetchBuffer(
blob_index.file_number())
: nullptr;
uint64_t bytes_read = 0;
assert(blob_fetcher_);
PinnableSlice blob_value;
Status s = blob_fetcher_->FetchBlob(user_key(), blob_index, prefetch_buffer,
&blob_value, &bytes_read);
if (!s.ok()) {
status_ = s;
validity_info_.Invalidate();
return false;
}
++iter_stats_.num_blobs_read;
iter_stats_.total_blob_bytes_read += bytes_read;
fetched_blob_values->emplace_back(col_idx, std::move(blob_value));
}
return !fetched_blob_values->empty();
}
std::vector<std::pair<size_t, BlobIndex>>
CompactionIterator::RelocateBlobValues(
const std::vector<std::pair<size_t, PinnableSlice>>& fetched_blob_values) {
std::vector<std::pair<size_t, BlobIndex>> new_blob_columns;
if (blob_file_builder_) {
for (const auto& fv : fetched_blob_values) {
const size_t col_idx = fv.first;
const Slice col_value(fv.second);
std::string temp_blob_index;
Status status =
blob_file_builder_->Add(user_key(), col_value, &temp_blob_index);
if (!status.ok()) {
status_ = status;
validity_info_.Invalidate();
new_blob_columns.clear();
break;
}
if (!temp_blob_index.empty()) {
BlobIndex blob_idx;
status = blob_idx.DecodeFrom(temp_blob_index);
if (!status.ok()) {
status_ = status;
validity_info_.Invalidate();
new_blob_columns.clear();
break;
}
new_blob_columns.emplace_back(col_idx, blob_idx);
// Track relocation stats here (after successful relocation)
++iter_stats_.num_blobs_relocated;
iter_stats_.total_blob_bytes_relocated += col_value.size();
}
// If temp_blob_index is empty, the value will be inlined
}
}
return new_blob_columns;
}
void CompactionIterator::SerializeEntityAfterGC(
const std::vector<WideColumn>& columns,
const std::vector<std::pair<size_t, BlobIndex>>& original_blob_columns,
const std::vector<std::pair<size_t, PinnableSlice>>& fetched_blob_values,
const std::vector<std::pair<size_t, BlobIndex>>& new_blob_columns) {
// Collect all blob columns for final serialization: include non-GC'd
// originals and newly relocated. Use linear scans since these
// collections are typically small (<5 elements).
std::vector<std::pair<size_t, BlobIndex>> final_blob_columns;
for (const auto& blob_col : original_blob_columns) {
// Check if this column was fetched (GC'd) via linear scan
bool was_fetched = false;
for (const auto& fv : fetched_blob_values) {
if (fv.first == blob_col.first) {
was_fetched = true;
break;
}
}
if (!was_fetched) {
// This blob column wasn't GC'd - keep original
final_blob_columns.push_back(blob_col);
}
}
// Add newly relocated blobs
for (const auto& nb : new_blob_columns) {
final_blob_columns.push_back(nb);
}
// Build WideColumns for serialization, replacing fetched blob values
// with their resolved inline values.
WideColumns wide_columns;
wide_columns.reserve(columns.size());
for (size_t i = 0; i < columns.size(); ++i) {
// Linear scan to find if this column was fetched
const PinnableSlice* fetched_value = nullptr;
for (const auto& fv : fetched_blob_values) {
if (fv.first == i) {
fetched_value = &fv.second;
break;
}
}
if (fetched_value != nullptr) {
// This blob column was fetched for GC; use the resolved inline value.
wide_columns.emplace_back(columns[i].name(), Slice(*fetched_value));
} else {
// For inline columns, this is the actual value. For non-fetched blob
// columns, this contains the raw serialized blob index, which is fine
// because SerializeV2() re-encodes from the BlobIndex
// object in final_blob_columns, ignoring this value for blob-type
// columns.
wide_columns.emplace_back(columns[i].name(), columns[i].value());
}
}
// Serialize the entity
rewritten_entity_.clear();
Status s;
if (final_blob_columns.empty()) {
s = WideColumnSerialization::Serialize(wide_columns, rewritten_entity_);
} else {
s = WideColumnSerialization::SerializeV2(wide_columns, final_blob_columns,
rewritten_entity_);
}
if (!s.ok()) {
status_ = s;
validity_info_.Invalidate();
return;
}
value_ = rewritten_entity_;
}
void CompactionIterator::GarbageCollectEntityBlobsIfNeeded() {
assert(ikey_.type == kTypeWideColumnEntity);
if (!compaction_ || !compaction_->enable_blob_garbage_collection()) {
return;
}
// This is the second branch in the flow above: the entity already has V2
// blob references, so compaction considers only relocating the subset of
// referenced blob files that are old enough for blob GC.
// Use member variables already populated by PrepareOutput's combined
// deserialization path. If not yet populated, deserialize here.
if (entity_columns_.empty()) {
Slice entity_slice = value_;
entity_columns_.clear();
entity_blob_columns_.clear();
Status s = WideColumnSerialization::DeserializeV2(
entity_slice, entity_columns_, entity_blob_columns_);
if (!s.ok()) {
status_ = s;
validity_info_.Invalidate();
return;
}
}
// The caller (PrepareOutput) already verified blob columns exist.
assert(!entity_blob_columns_.empty());
if (entity_blob_columns_.empty()) {
return;
}
// Fetch blobs that need GC
std::vector<std::pair<size_t, PinnableSlice>> fetched_blob_values;
if (!FetchBlobsNeedingGC(entity_columns_, entity_blob_columns_,
&fetched_blob_values)) {
// Either no blobs needed GC (status_ is OK) or an error occurred
// (status_ is already set by FetchBlobsNeedingGC). In both cases,
// we return without modifying the entity.
return;
}
// Relocate fetched values to new blob files
std::vector<std::pair<size_t, BlobIndex>> new_blob_columns =
RelocateBlobValues(fetched_blob_values);
if (!status_.ok()) {
return; // Error occurred during relocation
}
// Serialize the final entity
SerializeEntityAfterGC(entity_columns_, entity_blob_columns_,
fetched_blob_values, new_blob_columns);
}
void CompactionIterator::PrepareOutput() {
if (Valid()) {
if (LIKELY(!is_range_del_)) {
@@ -1273,6 +1861,37 @@ void CompactionIterator::PrepareOutput() {
ExtractLargeValueIfNeeded();
} else if (ikey_.type == kTypeBlobIndex) {
GarbageCollectBlobIfNeeded();
} else if (ikey_.type == kTypeWideColumnEntity) {
// If entity was already deserialized (e.g., by InvokeFilterIfNeeded)
// and the filter returned kKeep, skip redundant re-deserialization.
if (!entity_deserialized_) {
TEST_SYNC_POINT(
"CompactionIterator::PrepareOutput:DeserializeEntity");
// Deserialize entity once into member variables, then decide between
// blob GC and extraction based on whether blob columns exist.
// This avoids the double parse of HasBlobColumns() + DeserializeV2().
entity_columns_.clear();
entity_blob_columns_.clear();
Slice entity_slice = value_;
{
Status s_deser = WideColumnSerialization::DeserializeV2(
entity_slice, entity_columns_, entity_blob_columns_);
if (!s_deser.ok()) {
status_ = s_deser;
validity_info_.Invalidate();
return;
}
}
} else {
TEST_SYNC_POINT(
"CompactionIterator::PrepareOutput:SkipDeserializeEntity");
}
entity_deserialized_ = false; // Reset for next iteration
if (UNLIKELY(!entity_blob_columns_.empty())) {
GarbageCollectEntityBlobsIfNeeded();
} else {
ExtractLargeColumnValuesIfNeeded();
}
}
}
@@ -1421,21 +2040,32 @@ uint64_t CompactionIterator::ComputeBlobGarbageCollectionCutoffFileNumber(
}
std::unique_ptr<BlobFetcher> CompactionIterator::CreateBlobFetcherIfNeeded(
const CompactionProxy* compaction) {
if (!compaction) {
return nullptr;
}
const Version* const version = compaction->input_version();
if (!version) {
const CompactionProxy* compaction, const Version* input_version,
Env::IOActivity blob_read_io_activity) {
const Version* const version =
compaction != nullptr ? compaction->input_version() : input_version;
if (version == nullptr) {
return nullptr;
}
ReadOptions read_options;
read_options.io_activity = Env::IOActivity::kCompaction;
read_options.io_activity = compaction != nullptr
? Env::IOActivity::kCompaction
: blob_read_io_activity;
read_options.fill_cache = false;
return std::unique_ptr<BlobFetcher>(new BlobFetcher(version, read_options));
BlobFileCache* blob_file_cache = nullptr;
bool allow_write_path_fallback = false;
if (compaction == nullptr) {
allow_write_path_fallback = true;
auto* cfd = version->cfd();
if (cfd != nullptr) {
blob_file_cache = cfd->blob_file_cache();
}
}
return std::unique_ptr<BlobFetcher>(new BlobFetcher(
version, read_options, blob_file_cache, allow_write_path_fallback));
}
std::unique_ptr<PrefetchBufferCollection>
+158 -23
View File
@@ -7,10 +7,12 @@
#include <algorithm>
#include <cinttypes>
#include <deque>
#include <memory>
#include <optional>
#include <string>
#include <unordered_set>
#include <vector>
#include "db/blob/blob_index.h"
#include "db/compaction/compaction.h"
#include "db/compaction/compaction_iteration_stats.h"
#include "db/merge_helper.h"
@@ -19,12 +21,69 @@
#include "db/snapshot_checker.h"
#include "options/cf_options.h"
#include "rocksdb/compaction_filter.h"
#include "rocksdb/slice.h"
namespace ROCKSDB_NAMESPACE {
class BlobFileBuilder;
class BlobFileCache;
class BlobFetcher;
class Version;
class PrefetchBufferCollection;
class FilePrefetchBuffer;
class Version;
// Internal implementation of WideColumnBlobResolver for compaction.
// This class enables lazy loading of blob column values during compaction
// filter processing.
class CompactionBlobResolver : public WideColumnBlobResolver {
public:
CompactionBlobResolver()
: columns_(nullptr),
blob_columns_(nullptr),
blob_fetcher_(nullptr),
prefetch_buffers_(nullptr),
iter_stats_(nullptr) {}
// Set the fixed context (blob fetcher, prefetch buffers, stats) once.
void Init(BlobFetcher* blob_fetcher,
PrefetchBufferCollection* prefetch_buffers,
CompactionIterationStats* iter_stats);
Status ResolveColumn(size_t column_index, Slice* resolved_value) override;
bool IsBlobColumn(size_t column_index) const override;
size_t NumColumns() const override;
Status resolve_status() const {
if (!resolve_error_.has_value()) {
return Status::OK();
}
return *resolve_error_;
}
// Reset the resolver for a new entity. Clears resolved cache.
void Reset(const Slice& user_key, const std::vector<WideColumn>* columns,
const std::vector<std::pair<size_t, BlobIndex>>* blob_columns,
bool track_resolve_error = false);
private:
Slice user_key_;
const std::vector<WideColumn>* columns_;
const std::vector<std::pair<size_t, BlobIndex>>* blob_columns_;
BlobFetcher* blob_fetcher_;
PrefetchBufferCollection* prefetch_buffers_;
CompactionIterationStats* iter_stats_;
bool track_resolve_error_ = false;
// Cache for resolved blob values to avoid re-fetching. Uses a vector of
// (column_index, PinnableSlice) pairs -- typical entities have few blob
// columns (<5), making linear scan cheaper than hash map overhead.
std::vector<std::pair<size_t, std::unique_ptr<PinnableSlice>>>
resolved_cache_;
// Sticky resolver error for the current entity. This is enabled only for the
// lazy FilterV4 path so compaction can still fail even if the filter
// notices the error and returns kKeep.
std::optional<Status> resolve_error_;
};
// A wrapper of internal iterator whose purpose is to count how
// many entries there are in the iterator.
@@ -213,34 +272,43 @@ class CompactionIterator {
const std::atomic<bool>* shutting_down = nullptr,
const std::shared_ptr<Logger> info_log = nullptr,
const std::string* full_history_ts_low = nullptr,
std::optional<SequenceNumber> preserve_seqno_min = {});
std::optional<SequenceNumber> preserve_seqno_min = {},
const Version* input_version = nullptr,
Env::IOActivity blob_read_io_activity = Env::IOActivity::kCompaction);
// Constructor with custom CompactionProxy, used for tests.
CompactionIterator(InternalIterator* input, const Comparator* cmp,
MergeHelper* merge_helper, SequenceNumber last_sequence,
std::vector<SequenceNumber>* snapshots,
SequenceNumber earliest_snapshot,
SequenceNumber earliest_write_conflict_snapshot,
SequenceNumber job_snapshot,
const SnapshotChecker* snapshot_checker, Env* env,
bool report_detailed_time,
CompactionRangeDelAggregator* range_del_agg,
BlobFileBuilder* blob_file_builder,
bool allow_data_in_errors,
bool enforce_single_del_contracts,
const std::atomic<bool>& manual_compaction_canceled,
std::unique_ptr<CompactionProxy> compaction,
bool must_count_input_entries,
const CompactionFilter* compaction_filter = nullptr,
const std::atomic<bool>* shutting_down = nullptr,
const std::shared_ptr<Logger> info_log = nullptr,
const std::string* full_history_ts_low = nullptr,
std::optional<SequenceNumber> preserve_seqno_min = {});
CompactionIterator(
InternalIterator* input, const Comparator* cmp, MergeHelper* merge_helper,
SequenceNumber last_sequence, std::vector<SequenceNumber>* snapshots,
SequenceNumber earliest_snapshot,
SequenceNumber earliest_write_conflict_snapshot,
SequenceNumber job_snapshot, const SnapshotChecker* snapshot_checker,
Env* env, bool report_detailed_time,
CompactionRangeDelAggregator* range_del_agg,
BlobFileBuilder* blob_file_builder, bool allow_data_in_errors,
bool enforce_single_del_contracts,
const std::atomic<bool>& manual_compaction_canceled,
std::unique_ptr<CompactionProxy> compaction,
bool must_count_input_entries,
const CompactionFilter* compaction_filter = nullptr,
const std::atomic<bool>* shutting_down = nullptr,
const std::shared_ptr<Logger> info_log = nullptr,
const std::string* full_history_ts_low = nullptr,
std::optional<SequenceNumber> preserve_seqno_min = {},
const Version* input_version = nullptr,
Env::IOActivity blob_read_io_activity = Env::IOActivity::kCompaction);
~CompactionIterator();
void ResetRecordCounts();
// Non-compaction table-file creation (e.g., flush/recovery) can still need
// blob resolution when direct-write wide entities already contain blob
// references. Plumb a fetcher explicitly in those cases.
void SetBlobFetcher(const Version* version, BlobFileCache* blob_file_cache,
Env::IOActivity io_activity,
bool allow_write_path_fallback);
// Seek to the beginning of the compaction iterator output.
//
// REQUIRED: Call only once.
@@ -302,6 +370,12 @@ class CompactionIterator {
// for regular values (kTypeValue).
void ExtractLargeValueIfNeeded();
// Extracts large column values from wide column entities to blob files.
// For each column whose value size >= min_blob_size, the value is written
// to a blob file and replaced with a blob index reference. Should only be
// called for wide column entities (kTypeWideColumnEntity).
void ExtractLargeColumnValuesIfNeeded();
// Relocates valid blobs residing in the oldest blob files if garbage
// collection is enabled. Relocated blobs are written to new blob files or
// inlined in the LSM tree depending on the current settings (i.e.
@@ -312,6 +386,39 @@ class CompactionIterator {
// algorithm is also called from here.
void GarbageCollectBlobIfNeeded();
// Garbage collects blob references within wide column entities. For entities
// that contain blob column references, this method checks if any of the
// referenced blob files need garbage collection. If so, the blob values are
// fetched and either relocated to new blob files or inlined based on
// current settings. Should only be called for wide column entities
// (kTypeWideColumnEntity) that have blob columns.
void GarbageCollectEntityBlobsIfNeeded();
// Helper for GarbageCollectEntityBlobsIfNeeded: Fetches blob values that need
// garbage collection based on the cutoff file number.
// Returns true if any blobs were fetched, false otherwise.
// On error, sets status_ and returns false.
bool FetchBlobsNeedingGC(
const std::vector<WideColumn>& columns,
const std::vector<std::pair<size_t, BlobIndex>>& blob_columns,
std::vector<std::pair<size_t, PinnableSlice>>* fetched_blob_values);
// Helper for GarbageCollectEntityBlobsIfNeeded: Relocates fetched blob
// values to new blob files if blob_file_builder_ is available.
// Returns the new blob column indices. On error, sets status_ and returns
// empty.
std::vector<std::pair<size_t, BlobIndex>> RelocateBlobValues(
const std::vector<std::pair<size_t, PinnableSlice>>& fetched_blob_values);
// Helper for GarbageCollectEntityBlobsIfNeeded: Serializes the entity after
// garbage collection, combining original columns with fetched values and
// new blob indices. On error, sets status_.
void SerializeEntityAfterGC(
const std::vector<WideColumn>& columns,
const std::vector<std::pair<size_t, BlobIndex>>& original_blob_columns,
const std::vector<std::pair<size_t, PinnableSlice>>& fetched_blob_values,
const std::vector<std::pair<size_t, BlobIndex>>& new_blob_columns);
// Invoke compaction filter if needed.
// Return true on success, false on failures (e.g.: kIOError).
bool InvokeFilterIfNeeded(bool* need_skip, Slice* skip_until);
@@ -352,7 +459,8 @@ class CompactionIterator {
static uint64_t ComputeBlobGarbageCollectionCutoffFileNumber(
const CompactionProxy* compaction);
static std::unique_ptr<BlobFetcher> CreateBlobFetcherIfNeeded(
const CompactionProxy* compaction);
const CompactionProxy* compaction, const Version* input_version,
Env::IOActivity blob_read_io_activity);
static std::unique_ptr<PrefetchBufferCollection>
CreatePrefetchBufferCollectionIfNeeded(const CompactionProxy* compaction);
@@ -376,6 +484,7 @@ class CompactionIterator {
BlobFileBuilder* blob_file_builder_;
std::unique_ptr<CompactionProxy> compaction_;
const CompactionFilter* compaction_filter_;
const bool filter_supports_v4_;
const std::atomic<bool>* shutting_down_;
const std::atomic<bool>& manual_compaction_canceled_;
const bool bottommost_level_;
@@ -479,6 +588,32 @@ class CompactionIterator {
PinnableSlice blob_value_;
std::string compaction_filter_value_;
InternalKey compaction_filter_skip_until_;
// Buffer for storing rewritten entity with blob references after
// extracting large column values to blob files.
std::string rewritten_entity_;
// Cached deserialized columns for the current wide-column entity, reused
// across iterations to avoid per-key heap allocations. This is the full
// sorted column set. For blob-backed columns, value() initially contains the
// serialized BlobIndex bytes from the entity.
std::vector<WideColumn> entity_columns_;
// Side list for the blob-backed subset of entity_columns_. Each pair is
// (column_index_in_entity_columns_, decoded_blob_index), so callers can tell
// which entries in entity_columns_ are blob references without reparsing.
std::vector<std::pair<size_t, BlobIndex>> entity_blob_columns_;
// Temporary Slice-based view over entity_columns_ used when reserializing an
// entity after blob extraction, to avoid copying names/values into strings.
WideColumns entity_wide_columns_;
// Reusable storage for existing_columns in InvokeFilterIfNeeded,
// avoiding per-key heap allocations.
WideColumns filter_existing_columns_;
// Reusable blob resolver for compaction filter, avoiding per-key heap
// allocation. Init() is called once; Reset() is called per entity.
CompactionBlobResolver blob_resolver_;
// True when entity_columns_/entity_blob_columns_ already describe the
// current input record (e.g., because InvokeFilterIfNeeded() deserialized
// it) and PrepareOutput() can skip redundant work. Reset for each candidate
// input record and whenever the filter rewrites the entity value.
bool entity_deserialized_{false};
// "level_ptrs" holds indices that remember which file of an associated
// level we were last checking during the last call to compaction->
// KeyNotExistsBeyondOutputLevel(). This allows future calls to the function
File diff suppressed because it is too large. Load diff
+105 -15
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"
@@ -57,6 +58,7 @@
#include "table/table_builder.h"
#include "table/unique_id_impl.h"
#include "test_util/sync_point.h"
#include "util/hash_containers.h"
#include "util/stop_watch.h"
namespace ROCKSDB_NAMESPACE {
@@ -103,6 +105,8 @@ const char* GetCompactionReasonString(CompactionReason compaction_reason) {
return "RoundRobinTtl";
case CompactionReason::kRefitLevel:
return "RefitLevel";
case CompactionReason::kReadTriggered:
return "ReadTriggered";
case CompactionReason::kNumOfReasons:
// fall through
default:
@@ -895,6 +899,10 @@ Status CompactionJob::VerifyOutputFiles() {
}
auto verify_table = [&](SubcompactionState& subcompaction_state) {
// Collect file open metadata during verification when fast_sst_open
// is enabled, keyed by file number.
UnorderedMap<uint64_t, std::string> file_open_metadata_map;
for (const auto& output_file : subcompaction_state.GetOutputs()) {
// Verify that the table is usable
// We set for_compaction to false and don't
@@ -903,17 +911,21 @@ 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();
std::string file_open_metadata;
std::string* file_open_metadata_ptr =
mutable_db_options_copy_.fast_sst_open ? &file_open_metadata
: nullptr;
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,
@@ -924,7 +936,10 @@ Status CompactionJob::VerifyOutputFiles() {
MaxFileSizeForL0MetaPin(compact_->compaction->mutable_cf_options()),
/*smallest_compaction_key=*/nullptr,
/*largest_compaction_key=*/nullptr,
/*allow_unprepared_value=*/false);
/*allow_unprepared_value=*/false,
/*range_del_read_seqno=*/nullptr,
/*range_del_iter=*/nullptr,
/*maybe_pin_table_handle=*/false, file_open_metadata_ptr);
auto s = iter->status();
if (s.ok()) {
// Check for remote/local compaction and verify_output_flags flags
@@ -941,12 +956,17 @@ Status CompactionJob::VerifyOutputFiles() {
!!(verify_output_flags & VerifyOutputFlags::kVerifyBlockChecksum);
const bool should_verify_iteration =
!!(verify_output_flags & VerifyOutputFlags::kVerifyIteration);
const bool should_verify_file_checksum =
!!(verify_output_flags &
VerifyOutputFlags::kVerifyFileChecksum) &&
db_options_.file_checksum_gen_factory != nullptr &&
output_file.meta.file_checksum != kUnknownFileChecksum;
if (should_verify_block_checksum) {
assert(table_reader_ptr != nullptr);
// If verifying iteration as well, verify meta blocks here only to
// avoid redundant checks on data blocks
s = table_reader_ptr->VerifyChecksum(
verify_table_read_options, TableReaderCaller::kCompaction,
verification_read_options, TableReaderCaller::kCompaction,
/*meta_blocks_only=*/should_verify_iteration);
}
if (s.ok() && should_verify_iteration) {
@@ -967,6 +987,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);
}
}
}
}
@@ -976,6 +1014,27 @@ Status CompactionJob::VerifyOutputFiles() {
subcompaction_state.status = s;
break;
}
if (!file_open_metadata.empty()) {
file_open_metadata_map[output_file.meta.fd.GetNumber()] =
std::move(file_open_metadata);
}
}
// Apply collected file open metadata to mutable outputs
if (!file_open_metadata_map.empty()) {
auto apply_metadata =
[&file_open_metadata_map](
std::vector<CompactionOutputs::Output>& outputs) {
for (auto& output : outputs) {
auto it = file_open_metadata_map.find(output.meta.fd.GetNumber());
if (it != file_open_metadata_map.end()) {
output.meta.file_open_metadata = std::move(it->second);
}
}
};
apply_metadata(subcompaction_state.GetMutableCompactionOutputs());
apply_metadata(subcompaction_state.GetMutableProximalOutputs());
}
};
for (size_t i = 1; i < compact_->sub_compact_states.size(); i++) {
@@ -1092,6 +1151,9 @@ Status CompactionJob::Run() {
status = VerifyOutputFiles();
}
TEST_SYNC_POINT_CALLBACK("CompactionJob::Run():AfterVerifyOutputFiles",
&status);
if (status.ok()) {
SetOutputTableProperties();
}
@@ -1278,6 +1340,16 @@ Status CompactionJob::Install(bool* compaction_released) {
<< pl_stats.bytes_written_blob;
}
// Propagate Install failure to compact_->status so that
// CleanupCompaction() -> SubcompactionState::Cleanup() sees the failure and
// calls ReleaseObsolete on output files' table cache entries. Without this,
// if Run() succeeds but InstallCompactionResults() fails, Cleanup would see
// overall_status = OK and skip ReleaseObsolete, leaking entries for output
// files that were never installed into any Version.
if (!status.ok() && compact_->status.ok()) {
compact_->status = status;
}
CleanupCompaction();
return status;
}
@@ -2326,11 +2398,16 @@ Status CompactionJob::InstallCompactionResults(bool* compaction_released) {
*compaction_released = true;
};
return versions_->LogAndApply(compaction->column_family_data(), read_options,
write_options, edit, db_mutex_, db_directory_,
/*new_descriptor_log=*/false,
/*column_family_options=*/nullptr,
manifest_wcb);
Status s;
TEST_SYNC_POINT_CALLBACK(
"CompactionJob::InstallCompactionResults:BeforeLogAndApply", &s);
if (s.ok()) {
s = versions_->LogAndApply(compaction->column_family_data(), read_options,
write_options, edit, db_mutex_, db_directory_,
/*new_descriptor_log=*/false,
/*column_family_options=*/nullptr, manifest_wcb);
}
return s;
}
void CompactionJob::RecordCompactionIOStats() {
@@ -2470,8 +2547,16 @@ Status CompactionJob::OpenCompactionOutputFile(SubcompactionState* sub_compact,
return s;
}
// Enable hash computation if paranoid_file_checks is on or if
// verify_output_flags includes kVerifyIteration, so that
// VerifyOutputFiles() can compare the hash of the written data
// against a re-read of the output file.
bool enable_output_hash =
paranoid_file_checks_ ||
!!(sub_compact->compaction->mutable_cf_options().verify_output_flags &
VerifyOutputFlags::kVerifyIteration);
outputs.AddOutput(std::move(meta), cfd->internal_comparator(),
paranoid_file_checks_);
enable_output_hash);
}
writable_file->SetIOPriority(GetRateLimiterPriority());
@@ -2512,7 +2597,7 @@ Status CompactionJob::OpenCompactionOutputFile(SubcompactionState* sub_compact,
void CompactionJob::CleanupCompaction() {
for (SubcompactionState& sub_compact : compact_->sub_compact_states) {
sub_compact.Cleanup(table_cache_.get());
sub_compact.Cleanup(table_cache_.get(), compact_->status);
}
delete compact_;
compact_ = nullptr;
@@ -2904,12 +2989,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]);
+1
View File
@@ -60,6 +60,7 @@ class CompactionOutputs {
}
const std::vector<Output>& GetOutputs() const { return outputs_; }
std::vector<Output>& GetMutableOutputs() { return outputs_; }
// Set new table builder for the current output
void NewBuilder(const TableBuilderOptions& tboptions);
+15 -14
View File
@@ -102,10 +102,10 @@ Compaction* FIFOCompactionPicker::PickTTLCompaction(
for (auto ritr = level_files.rbegin(); ritr != level_files.rend(); ++ritr) {
FileMetaData* f = *ritr;
assert(f);
if (f->fd.table_reader && f->fd.table_reader->GetTableProperties()) {
TableReader* reader = f->fd.pinned_reader.Get();
if (reader != nullptr && reader->GetTableProperties() != nullptr) {
uint64_t newest_key_time = f->TryGetNewestKeyTime();
uint64_t creation_time =
f->fd.table_reader->GetTableProperties()->creation_time;
uint64_t creation_time = reader->GetTableProperties()->creation_time;
uint64_t est_newest_key_time = newest_key_time == kUnknownNewestKeyTime
? creation_time
: newest_key_time;
@@ -166,8 +166,9 @@ Compaction* FIFOCompactionPicker::PickTTLCompaction(
assert(f);
uint64_t newest_key_time = f->TryGetNewestKeyTime();
uint64_t creation_time = 0;
if (f->fd.table_reader && f->fd.table_reader->GetTableProperties()) {
creation_time = f->fd.table_reader->GetTableProperties()->creation_time;
TableReader* reader = f->fd.pinned_reader.Get();
if (reader != nullptr && reader->GetTableProperties() != nullptr) {
creation_time = reader->GetTableProperties()->creation_time;
}
uint64_t est_newest_key_time = newest_key_time == kUnknownNewestKeyTime
? creation_time
@@ -250,7 +251,7 @@ Compaction* FIFOCompactionPicker::PickSizeCompaction(
// 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,
// -- 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();
@@ -476,12 +477,12 @@ Compaction* FIFOCompactionPicker::PickIntraL0Compaction(
if (fifo_opts.max_data_files_size == 0) {
ROCKS_LOG_BUFFER(
log_buffer,
"[%s] FIFO kv-ratio compaction: skipping — "
"[%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 — "
"[%s] FIFO kv-ratio compaction: skipping -- "
"max_data_files_size (%" PRIu64
") < max_table_files_size "
"(%" PRIu64 ").",
@@ -546,7 +547,7 @@ Compaction* FIFOCompactionPicker::PickRatioBasedIntraL0Compaction(
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 "
"[%s] FIFO kv-ratio compaction: skipping -- non-L0 "
"level %d still has %" ROCKSDB_PRIszt
" files (migration in progress)",
cf_name.c_str(), level,
@@ -586,7 +587,7 @@ Compaction* FIFOCompactionPicker::PickRatioBasedIntraL0Compaction(
// 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
// 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
@@ -641,7 +642,7 @@ Compaction* FIFOCompactionPicker::PickRatioBasedIntraL0Compaction(
// of linear) write amplification.
// Build tier boundaries from smallest to largest.
// Stop at 10KB minimum — SST files of most workloads are larger than
// 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
@@ -650,7 +651,7 @@ Compaction* FIFOCompactionPicker::PickRatioBasedIntraL0Compaction(
boundaries.push_back(b);
}
if (boundaries.empty()) {
// target itself is below kMinTierBoundary — use target as the
// target itself is below kMinTierBoundary -- use target as the
// sole boundary so we can still compact at the target size.
boundaries.push_back(target);
}
@@ -667,7 +668,7 @@ Compaction* FIFOCompactionPicker::PickRatioBasedIntraL0Compaction(
continue;
}
// Found a file < boundary — collect contiguous batch
// Found a file < boundary -- collect contiguous batch
std::vector<FileMetaData*> batch;
uint64_t accumulated = 0;
size_t pos = scan;
@@ -712,7 +713,7 @@ Compaction* FIFOCompactionPicker::PickRatioBasedIntraL0Compaction(
return c;
}
// This batch wasn't enough — advance past it
// This batch wasn't enough -- advance past it
scan = pos;
}
}
+20 -1
View File
@@ -36,6 +36,9 @@ bool LevelCompactionPicker::NeedsCompaction(
if (!vstorage->FilesMarkedForForcedBlobGC().empty()) {
return true;
}
if (!vstorage->ReadTriggeredCompactionFiles().empty()) {
return true;
}
for (int i = 0; i <= vstorage->MaxInputLevel(); i++) {
if (vstorage->CompactionScore(i) >= 1) {
return true;
@@ -325,6 +328,14 @@ void LevelCompactionBuilder::SetupInitialFiles() {
compaction_reason_ = CompactionReason::kForcedBlobGC;
return;
}
// Read-triggered compaction
PickFileToCompact(vstorage_->ReadTriggeredCompactionFiles(),
CompactToNextLevel::kYes);
if (!start_level_inputs_.empty()) {
compaction_reason_ = CompactionReason::kReadTriggered;
return;
}
}
bool LevelCompactionBuilder::SetupOtherL0FilesIfNeeded() {
@@ -404,7 +415,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
+185 -3
View File
@@ -178,7 +178,8 @@ class CompactionPickerTestBase : public testing::Test {
}
TableProperties tp;
tp.newest_key_time = newest_key_time;
f->fd.table_reader = new mock::MockTableReader(mock::KVVector{}, tp);
f->fd.pinned_reader.TEST_SetReader(
new mock::MockTableReader(mock::KVVector{}, tp));
vstorage->AddFile(level, f);
files_.emplace_back(f);
@@ -272,8 +273,9 @@ class CompactionPickerTestBase : public testing::Test {
void ClearFiles() {
for (auto& file : files_) {
if (file->fd.table_reader != nullptr) {
delete file->fd.table_reader;
TableReader* t = file->fd.pinned_reader.Get();
if (t != nullptr) {
delete t;
}
}
files_.clear();
@@ -982,6 +984,186 @@ TEST_F(CompactionPickerTest, UniversalIncrementalSpace1) {
ASSERT_EQ(14U, compaction->input(1, 3)->fd.GetNumber());
}
TEST_F(CompactionPickerTest, ReadTriggeredCompactionDisabled) {
NewVersionStorage(6, kCompactionStyleLevel);
// threshold=0 means disabled, no files should be marked
mutable_cf_options_.read_triggered_compaction_threshold = 0.0;
Add(0, 1U, "100", "200", 1000U);
file_map_[1U].first->stats.num_collapsible_entry_reads_sampled.store(999999);
UpdateVersionStorageInfo();
ASSERT_TRUE(vstorage_->ReadTriggeredCompactionFiles().empty());
}
TEST_F(CompactionPickerTest, ReadTriggeredCompactionBelowThreshold) {
NewVersionStorage(6, kCompactionStyleLevel);
mutable_cf_options_.read_triggered_compaction_threshold = 1.0;
// file_size=1000, reads=500 => reads_per_byte=0.5 < 1.0
Add(0, 1U, "100", "200", 1000U);
file_map_[1U].first->stats.num_collapsible_entry_reads_sampled.store(500);
UpdateVersionStorageInfo();
ASSERT_TRUE(vstorage_->ReadTriggeredCompactionFiles().empty());
}
TEST_F(CompactionPickerTest, ReadTriggeredCompactionAboveThreshold) {
NewVersionStorage(6, kCompactionStyleLevel);
mutable_cf_options_.read_triggered_compaction_threshold = 0.5;
// file_size=1000, reads=600 => reads_per_byte=0.6 > 0.5
Add(0, 1U, "100", "200", 1000U);
file_map_[1U].first->stats.num_collapsible_entry_reads_sampled.store(600);
// file_size=1000, reads=300 => reads_per_byte=0.3 < 0.5 (not marked)
Add(1, 2U, "300", "400", 1000U);
file_map_[2U].first->stats.num_collapsible_entry_reads_sampled.store(300);
// file_size=1000, reads=800 => reads_per_byte=0.8 > 0.5 (hottest)
Add(2, 3U, "500", "600", 1000U);
file_map_[3U].first->stats.num_collapsible_entry_reads_sampled.store(800);
// Add a file at the bottom so L2 is not the last non-empty level
Add(4, 4U, "700", "800", 1000U);
UpdateVersionStorageInfo();
const auto& marked = vstorage_->ReadTriggeredCompactionFiles();
ASSERT_EQ(marked.size(), 2);
// Sorted by reads_per_byte descending: file 3 (0.8) then file 1 (0.6)
ASSERT_EQ(marked[0].second->fd.GetNumber(), 3U);
ASSERT_EQ(marked[1].second->fd.GetNumber(), 1U);
}
TEST_F(CompactionPickerTest, NeedsCompactionReadTriggered) {
NewVersionStorage(6, kCompactionStyleLevel);
mutable_cf_options_.read_triggered_compaction_threshold = 0.1;
Add(1, 1U, "100", "200", 1000U);
file_map_[1U].first->stats.num_collapsible_entry_reads_sampled.store(500);
Add(3, 2U, "300", "400", 1000U);
UpdateVersionStorageInfo();
ASSERT_FALSE(vstorage_->ReadTriggeredCompactionFiles().empty());
ASSERT_TRUE(level_compaction_picker.NeedsCompaction(vstorage_.get()));
}
TEST_F(CompactionPickerTest, ReadTriggeredPicksFile) {
NewVersionStorage(6, kCompactionStyleLevel);
mutable_cf_options_.read_triggered_compaction_threshold = 0.1;
Add(1, 1U, "100", "200", 1000U);
file_map_[1U].first->stats.num_collapsible_entry_reads_sampled.store(500);
Add(3, 2U, "300", "400", 1000U);
UpdateVersionStorageInfo();
std::unique_ptr<Compaction> compaction(level_compaction_picker.PickCompaction(
cf_name_, mutable_cf_options_, mutable_db_options_,
/*existing_snapshots=*/{}, /*snapshot_checker=*/nullptr, vstorage_.get(),
&log_buffer_, /*full_history_ts_low=*/""));
ASSERT_TRUE(compaction.get() != nullptr);
ASSERT_EQ(compaction->compaction_reason(), CompactionReason::kReadTriggered);
}
TEST_F(CompactionPickerTest, UniversalReadTriggeredCompaction) {
const uint64_t kFileSize = 100000;
mutable_cf_options_.read_triggered_compaction_threshold = 0.001;
// Set trigger high so size amp / sorted run pickers don't fire
mutable_cf_options_.level0_file_num_compaction_trigger = 10;
UniversalCompactionPicker universal_compaction_picker(ioptions_, &icmp_);
NewVersionStorage(5, kCompactionStyleUniversal);
// Hot file at L2 with data at L4 below it
Add(0, 1U, "150", "200", kFileSize, 0, 500, 550);
Add(2, 2U, "301", "350", kFileSize, 0, 201, 250);
Add(4, 3U, "301", "400", kFileSize, 0, 101, 150);
// Mark file 2 (L2) as having high reads
file_map_[2U].first->stats.num_collapsible_entry_reads_sampled.store(
kFileSize);
UpdateVersionStorageInfo();
ASSERT_TRUE(universal_compaction_picker.NeedsCompaction(vstorage_.get()));
std::unique_ptr<Compaction> compaction(
universal_compaction_picker.PickCompaction(
cf_name_, mutable_cf_options_, mutable_db_options_,
/*existing_snapshots=*/{}, /*snapshot_checker=*/nullptr,
vstorage_.get(), &log_buffer_, /*full_history_ts_low=*/""));
ASSERT_TRUE(compaction);
ASSERT_EQ(compaction->compaction_reason(), CompactionReason::kReadTriggered);
ASSERT_EQ(compaction->start_level(), 2);
ASSERT_EQ(compaction->output_level(), 4);
}
TEST_F(CompactionPickerTest, ReadTriggeredSkipsLastLevel) {
const uint64_t kFileSize = 100000;
mutable_cf_options_.read_triggered_compaction_threshold = 0.001;
mutable_cf_options_.level0_file_num_compaction_trigger = 10;
UniversalCompactionPicker universal_compaction_picker(ioptions_, &icmp_);
NewVersionStorage(5, kCompactionStyleUniversal);
Add(0, 1U, "150", "200", kFileSize, 0, 500, 550);
Add(4, 3U, "301", "350", kFileSize, 0, 101, 150);
// File 3 is at the last non-empty level -- should NOT be marked for
// read-triggered compaction. Bottommost file cleanup is handled
// separately by ComputeBottommostFilesMarkedForCompaction().
file_map_[3U].first->stats.num_collapsible_entry_reads_sampled.store(
kFileSize);
UpdateVersionStorageInfo();
ASSERT_TRUE(vstorage_->ReadTriggeredCompactionFiles().empty());
}
TEST_F(CompactionPickerTest, UniversalReadTriggeredNoPickWhenNotMarked) {
const uint64_t kFileSize = 100000;
mutable_cf_options_.read_triggered_compaction_threshold = 0.001;
UniversalCompactionPicker universal_compaction_picker(ioptions_, &icmp_);
NewVersionStorage(5, kCompactionStyleUniversal);
Add(0, 1U, "150", "200", kFileSize, 0, 500, 550);
Add(4, 3U, "301", "350", kFileSize, 0, 101, 150);
// No reads on any file
UpdateVersionStorageInfo();
ASSERT_TRUE(vstorage_->ReadTriggeredCompactionFiles().empty());
// Not enough sorted runs to trigger compaction either
ASSERT_FALSE(universal_compaction_picker.NeedsCompaction(vstorage_.get()));
}
TEST_F(CompactionPickerTest, UniversalReadTriggeredIntraL0) {
const uint64_t kFileSize = 100000;
mutable_cf_options_.read_triggered_compaction_threshold = 0.001;
mutable_cf_options_.level0_file_num_compaction_trigger = 10;
UniversalCompactionPicker universal_compaction_picker(ioptions_, &icmp_);
// Single-level universal with overlapping L0 files
NewVersionStorage(1, kCompactionStyleUniversal);
// Two L0 files with overlapping key ranges
Add(0, 1U, "100", "300", kFileSize, 0, 500, 550);
Add(0, 2U, "200", "400", kFileSize, 0, 400, 450);
// Mark file 1 as hot
file_map_[1U].first->stats.num_collapsible_entry_reads_sampled.store(
kFileSize);
UpdateVersionStorageInfo();
ASSERT_FALSE(vstorage_->ReadTriggeredCompactionFiles().empty());
ASSERT_TRUE(universal_compaction_picker.NeedsCompaction(vstorage_.get()));
std::unique_ptr<Compaction> compaction(
universal_compaction_picker.PickCompaction(
cf_name_, mutable_cf_options_, mutable_db_options_,
/*existing_snapshots=*/{}, /*snapshot_checker=*/nullptr,
vstorage_.get(), &log_buffer_, /*full_history_ts_low=*/""));
ASSERT_TRUE(compaction);
ASSERT_EQ(compaction->compaction_reason(), CompactionReason::kReadTriggered);
ASSERT_EQ(compaction->start_level(), 0);
ASSERT_EQ(compaction->output_level(), 0);
}
TEST_F(CompactionPickerTest, UniversalIncrementalSpace2) {
const uint64_t kFileSize = 100000;
+178 -101
View File
@@ -275,6 +275,21 @@ class UniversalCompactionBuilder {
return c;
}
Compaction* MaybePickReadTriggeredCompaction(
Compaction* const prev_picked_c) {
if (prev_picked_c != nullptr ||
vstorage_->ReadTriggeredCompactionFiles().empty()) {
return prev_picked_c;
}
Compaction* c = PickReadTriggeredCompaction();
if (c != nullptr) {
ROCKS_LOG_BUFFER(log_buffer_,
"[%s] Universal: picked for read triggered compaction\n",
cf_name_.c_str());
}
return c;
}
// Pick Universal compaction to limit read amplification
Compaction* PickCompactionToReduceSortedRuns(
unsigned int ratio, unsigned int max_number_of_files_to_compact);
@@ -292,6 +307,14 @@ class UniversalCompactionBuilder {
Compaction* PickDeleteTriggeredCompaction();
Compaction* PickReadTriggeredCompaction();
// Given already-selected start_level_inputs, find the first non-empty output
// level, compute overlapping inputs, and create a Compaction. Can produce
// intra-L0 compaction if there is only level 0.
Compaction* BuildCompactionToNextLevel(
CompactionInputFiles& start_level_inputs, CompactionReason reason);
// Returns true if this given file (that is marked be compaction) should be
// skipped from being picked for now. We do this to best use standalone range
// tombstone files.
@@ -594,6 +617,9 @@ bool UniversalCompactionPicker::NeedsCompaction(
if (!vstorage->FilesMarkedForCompaction().empty()) {
return true;
}
if (!vstorage->ReadTriggeredCompactionFiles().empty()) {
return true;
}
return false;
}
@@ -759,6 +785,7 @@ Compaction* UniversalCompactionBuilder::PickCompaction() {
if (sorted_runs_.size() == 0 ||
(vstorage_->FilesMarkedForPeriodicCompaction().empty() &&
vstorage_->FilesMarkedForCompaction().empty() &&
vstorage_->ReadTriggeredCompactionFiles().empty() &&
sorted_runs_.size() < (unsigned int)file_num_compaction_trigger)) {
ROCKS_LOG_BUFFER(log_buffer_, "[%s] Universal: nothing to do\n",
cf_name_.c_str());
@@ -782,6 +809,7 @@ Compaction* UniversalCompactionBuilder::PickCompaction() {
c = MaybePickCompactionToReduceSortedRuns(c, file_num_compaction_trigger,
ratio);
c = MaybePickDeleteTriggeredCompaction(c);
c = MaybePickReadTriggeredCompaction(c);
if (c == nullptr) {
TEST_SYNC_POINT_CALLBACK(
@@ -1461,9 +1489,6 @@ Compaction* UniversalCompactionBuilder::PickIncrementalForReduceSizeAmp(
// CompactOnDeleteCollector due to the presence of tombstones.
Compaction* UniversalCompactionBuilder::PickDeleteTriggeredCompaction() {
CompactionInputFiles start_level_inputs;
int output_level;
std::vector<CompactionInputFiles> inputs;
std::vector<FileMetaData*> grandparents;
if (vstorage_->num_levels() == 1) {
// This is single level universal. Since we're basically trying to reclaim
@@ -1474,7 +1499,6 @@ Compaction* UniversalCompactionBuilder::PickDeleteTriggeredCompaction() {
start_level_inputs.level = 0;
start_level_inputs.files.clear();
output_level = 0;
// Find the first file marked for compaction. Ignore the last file
for (size_t loop = 0; loop + 1 < sorted_runs_.size(); loop++) {
SortedRun* sr = &sorted_runs_[loop];
@@ -1503,13 +1527,9 @@ Compaction* UniversalCompactionBuilder::PickDeleteTriggeredCompaction() {
FileMetaData* f = vstorage_->LevelFiles(0)[loop];
start_level_inputs.files.push_back(f);
}
if (start_level_inputs.size() <= 1) {
// If only the last file in L0 is marked for compaction, ignore it
return nullptr;
}
inputs.push_back(start_level_inputs);
} else {
int start_level;
int output_level;
// For multi-level universal, the strategy is to make this look more like
// leveled. We pick one of the files marked for compaction and compact with
@@ -1522,100 +1542,10 @@ Compaction* UniversalCompactionBuilder::PickDeleteTriggeredCompaction() {
if (start_level_inputs.empty()) {
return nullptr;
}
int max_output_level = vstorage_->MaxOutputLevel(allow_ingest_behind_);
// Pick the first non-empty level after the start_level
for (output_level = start_level + 1; output_level <= max_output_level;
output_level++) {
if (vstorage_->NumLevelFiles(output_level) != 0) {
break;
}
}
// If all higher levels are empty, pick the highest level as output level
if (output_level > max_output_level) {
if (start_level == 0) {
output_level = max_output_level;
} else {
// If start level is non-zero and all higher levels are empty, this
// compaction will translate into a trivial move. Since the idea is
// to reclaim space and trivial move doesn't help with that, we
// skip compaction in this case and return nullptr
return nullptr;
}
}
assert(output_level <= max_output_level);
if (!MeetsOutputLevelRequirements(output_level)) {
return nullptr;
}
if (output_level != 0) {
// For standalone range deletion, we don't want to compact it with newer
// L0 files that it doesn't cover.
const FileMetaData* starting_l0_file =
(start_level == 0 && start_level_inputs.size() == 1 &&
start_level_inputs.files[0]->FileIsStandAloneRangeTombstone())
? start_level_inputs.files[0]
: nullptr;
if (start_level == 0) {
if (!picker_->GetOverlappingL0Files(vstorage_, &start_level_inputs,
output_level, nullptr,
starting_l0_file)) {
return nullptr;
}
}
CompactionInputFiles output_level_inputs;
int parent_index = -1;
output_level_inputs.level = output_level;
if (!picker_->SetupOtherInputs(cf_name_, mutable_cf_options_, vstorage_,
&start_level_inputs, &output_level_inputs,
&parent_index, -1, false,
starting_l0_file)) {
return nullptr;
}
inputs.push_back(start_level_inputs);
if (!output_level_inputs.empty()) {
inputs.push_back(output_level_inputs);
}
if (picker_->FilesRangeOverlapWithCompaction(
inputs, output_level,
Compaction::EvaluateProximalLevel(vstorage_, mutable_cf_options_,
ioptions_, start_level,
output_level))) {
return nullptr;
}
picker_->GetGrandparents(vstorage_, start_level_inputs,
output_level_inputs, &grandparents);
} else {
inputs.push_back(start_level_inputs);
}
}
uint64_t estimated_total_size = 0;
// Use size of the output level as estimated file size
for (FileMetaData* f : vstorage_->LevelFiles(output_level)) {
estimated_total_size += f->fd.GetFileSize();
}
uint32_t path_id =
GetPathId(ioptions_, mutable_cf_options_, estimated_total_size);
return new Compaction(
vstorage_, ioptions_, mutable_cf_options_, mutable_db_options_,
std::move(inputs), output_level,
MaxFileSizeForLevel(mutable_cf_options_, output_level,
kCompactionStyleUniversal),
/* max_grandparent_overlap_bytes */ GetMaxOverlappingBytes(), path_id,
GetCompressionType(vstorage_, mutable_cf_options_, output_level, 1),
GetCompressionOptions(mutable_cf_options_, vstorage_, output_level),
Temperature::kUnknown,
/* max_subcompactions */ 0, grandparents, earliest_snapshot_,
snapshot_checker_, CompactionReason::kFilesMarkedForCompaction,
/* trim_ts */ "", score_,
/* l0_files_might_overlap */ true);
return BuildCompactionToNextLevel(
start_level_inputs, CompactionReason::kFilesMarkedForCompaction);
}
Compaction* UniversalCompactionBuilder::PickCompactionToOldest(
@@ -1782,6 +1712,153 @@ Compaction* UniversalCompactionBuilder::PickPeriodicCompaction() {
return c;
}
Compaction* UniversalCompactionBuilder::PickReadTriggeredCompaction() {
ROCKS_LOG_BUFFER(log_buffer_, "[%s] Universal: Read Triggered Compaction",
cf_name_.c_str());
if (vstorage_->ReadTriggeredCompactionFiles().empty()) {
return nullptr;
}
CompactionInputFiles start_level_inputs;
int start_level = -1;
for (const auto& level_file : vstorage_->ReadTriggeredCompactionFiles()) {
assert(!level_file.second->being_compacted);
start_level = level_file.first;
if (start_level == 0 &&
!picker_->level0_compactions_in_progress()->empty()) {
continue;
}
start_level_inputs.files = {level_file.second};
start_level_inputs.level = start_level;
if (picker_->ExpandInputsToCleanCut(cf_name_, vstorage_,
&start_level_inputs)) {
break;
}
start_level_inputs.files.clear();
}
if (start_level_inputs.empty()) {
return nullptr;
}
// For intra L0 compactions, pick up all overlapping files
if (vstorage_->num_levels() == 1 && start_level_inputs.level == 0) {
if (!picker_->GetOverlappingL0Files(vstorage_, &start_level_inputs,
/*output_level=*/0, nullptr, nullptr)) {
return nullptr;
}
}
return BuildCompactionToNextLevel(start_level_inputs,
CompactionReason::kReadTriggered);
}
Compaction* UniversalCompactionBuilder::BuildCompactionToNextLevel(
CompactionInputFiles& start_level_inputs, CompactionReason reason) {
int start_level = start_level_inputs.level;
int max_output_level = vstorage_->MaxOutputLevel(allow_ingest_behind_);
// Pick the first non-empty level after start_level as output.
int output_level;
for (output_level = start_level + 1; output_level <= max_output_level;
output_level++) {
if (vstorage_->NumLevelFiles(output_level) != 0) {
break;
}
}
// If all higher levels are empty, pick the highest level as output level
// for L0 start. For non-L0 start, a trivial move doesn't help reclaim
// space, so skip.
if (output_level > max_output_level) {
if (start_level == 0) {
output_level = max_output_level;
} else {
return nullptr;
}
}
if (start_level_inputs.size() <= 1 && output_level == 0) {
// If only the last file in L0 is marked for compaction, ignore it
return nullptr;
}
std::vector<CompactionInputFiles> inputs;
std::vector<FileMetaData*> grandparents;
if (output_level != 0) {
if (!MeetsOutputLevelRequirements(output_level)) {
return nullptr;
}
// For standalone range deletion, we don't want to compact it with newer
// L0 files that it doesn't cover.
const FileMetaData* starting_l0_file =
(start_level == 0 && start_level_inputs.size() == 1 &&
start_level_inputs.files[0]->FileIsStandAloneRangeTombstone())
? start_level_inputs.files[0]
: nullptr;
if (start_level == 0) {
if (!picker_->GetOverlappingL0Files(vstorage_, &start_level_inputs,
output_level, nullptr,
starting_l0_file)) {
return nullptr;
}
}
CompactionInputFiles output_level_inputs;
int parent_index = -1;
output_level_inputs.level = output_level;
if (!picker_->SetupOtherInputs(
cf_name_, mutable_cf_options_, vstorage_, &start_level_inputs,
&output_level_inputs, &parent_index, -1, false, starting_l0_file)) {
return nullptr;
}
inputs.push_back(start_level_inputs);
if (!output_level_inputs.empty()) {
inputs.push_back(output_level_inputs);
}
if (picker_->FilesRangeOverlapWithCompaction(
inputs, output_level,
Compaction::EvaluateProximalLevel(vstorage_, mutable_cf_options_,
ioptions_, start_level,
output_level))) {
return nullptr;
}
picker_->GetGrandparents(vstorage_, start_level_inputs, output_level_inputs,
&grandparents);
} else {
inputs.push_back(start_level_inputs);
}
uint64_t estimated_total_size = 0;
for (FileMetaData* f : vstorage_->LevelFiles(output_level)) {
estimated_total_size += f->fd.GetFileSize();
}
uint32_t path_id =
GetPathId(ioptions_, mutable_cf_options_, estimated_total_size);
return new Compaction(
vstorage_, ioptions_, mutable_cf_options_, mutable_db_options_,
std::move(inputs), output_level,
MaxFileSizeForLevel(mutable_cf_options_, output_level,
kCompactionStyleUniversal),
/* max_grandparent_overlap_bytes */ GetMaxOverlappingBytes(), path_id,
GetCompressionType(vstorage_, mutable_cf_options_, output_level, 1),
GetCompressionOptions(mutable_cf_options_, vstorage_, output_level),
Temperature::kUnknown,
/* max_subcompactions */ 0, grandparents, earliest_snapshot_,
snapshot_checker_, reason,
/* trim_ts */ "", score_,
/* l0_files_might_overlap */ true);
}
uint64_t UniversalCompactionBuilder::GetMaxOverlappingBytes() const {
if (!mutable_cf_options_.compaction_options_universal.incremental) {
return std::numeric_limits<uint64_t>::max();
+10 -5
View File
@@ -846,11 +846,16 @@ static std::unordered_map<std::string, OptionTypeInfo>
{offsetof(struct InternalStats::CompactionStats, count),
OptionType::kUInt64T, OptionVerificationType::kNormal,
OptionTypeFlags::kNone}},
{"counts", OptionTypeInfo::Array<
int, static_cast<int>(CompactionReason::kNumOfReasons)>(
offsetof(struct InternalStats::CompactionStats, counts),
OptionVerificationType::kNormal, OptionTypeFlags::kNone,
{0, OptionType::kInt})},
{"counts",
OptionTypeInfo::Array<
/* In release 11.2, a new compaction reason was added. This broken
* the reader and writer. To unblock release 11.1, we temporarily
* reduce the count array size to the old one. TODO add a proper
* serialization and deserialization method. */
int, static_cast<int>(CompactionReason::kNumOfReasons) - 1>(
offsetof(struct InternalStats::CompactionStats, counts),
OptionVerificationType::kNormal, OptionTypeFlags::kNone,
{0, OptionType::kInt})},
};
static std::unordered_map<std::string, OptionTypeInfo>
+35 -1
View File
@@ -409,6 +409,34 @@ TEST_F(CompactionServiceTest, BasicCompactions) {
SyncPoint::GetInstance()->DisableProcessing();
}
TEST_F(CompactionServiceTest, SkipWALRecoveryInOpenAndCompact) {
// Verify that OpenAndCompact skips WAL recovery when opening the secondary
// instance. WAL replay is unnecessary for remote compaction since it only
// needs the LSM state from MANIFEST.
Options options = CurrentOptions();
ReopenWithCompactionService(&options);
// Track whether FindAndRecoverLogFiles is called during compaction
std::atomic_bool wal_recovery_called{false};
SyncPoint::GetInstance()->SetCallBack(
"DBImplSecondary::FindAndRecoverLogFiles:Begin",
[&](void* /* arg */) { wal_recovery_called.store(true); });
SyncPoint::GetInstance()->EnableProcessing();
// Generate data and trigger compaction (which uses OpenAndCompact)
GenerateTestData();
ASSERT_OK(dbfull()->TEST_WaitForCompact());
// WAL recovery should NOT have been called during OpenAndCompact
ASSERT_FALSE(wal_recovery_called.load());
// Data should still be correct (compaction worked without WAL recovery)
VerifyTestData();
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
}
TEST_F(CompactionServiceTest, ManualCompaction) {
Options options = CurrentOptions();
options.disable_auto_compactions = true;
@@ -1113,6 +1141,8 @@ TEST_F(CompactionServiceTest, CorruptedOutputVerifyOutputFlags) {
VerifyOutputFlags::kVerifyBlockChecksum,
VerifyOutputFlags::kEnableForRemoteCompaction |
VerifyOutputFlags::kVerifyIteration,
VerifyOutputFlags::kEnableForRemoteCompaction |
VerifyOutputFlags::kVerifyFileChecksum,
VerifyOutputFlags::kVerifyAll}) {
SCOPED_TRACE(
"verify_output_flags=" +
@@ -1124,6 +1154,7 @@ TEST_F(CompactionServiceTest, CorruptedOutputVerifyOutputFlags) {
options.disable_auto_compactions = true;
options.paranoid_file_checks = false;
options.verify_output_flags = verify_output_flags;
options.file_checksum_gen_factory = GetFileChecksumGenCrc32cFactory();
ReopenWithCompactionService(&options);
GenerateTestData();
@@ -1159,10 +1190,13 @@ TEST_F(CompactionServiceTest, CorruptedOutputVerifyOutputFlags) {
!!(verify_output_flags & VerifyOutputFlags::kVerifyBlockChecksum);
const bool should_verify_iteration =
!!(verify_output_flags & VerifyOutputFlags::kVerifyIteration);
const bool should_verify_file_checksum =
!!(verify_output_flags & VerifyOutputFlags::kVerifyFileChecksum);
Status s = db_->CompactRange(CompactRangeOptions(), &start, &end);
if (is_enabled_for_remote_compaction &&
(should_verify_block_checksum || should_verify_iteration)) {
(should_verify_block_checksum || should_verify_iteration ||
should_verify_file_checksum)) {
ASSERT_NOK(s);
ASSERT_TRUE(s.IsCorruption());
} else {
+2 -2
View File
@@ -38,11 +38,11 @@ OutputIterator SubcompactionState::GetOutputs() const {
compaction_outputs_.outputs_);
}
void SubcompactionState::Cleanup(Cache* cache) {
void SubcompactionState::Cleanup(Cache* cache, const Status& overall_status) {
proximal_level_outputs_.Cleanup();
compaction_outputs_.Cleanup();
if (!status.ok()) {
if (!status.ok() || !overall_status.ok()) {
for (const auto& out : GetOutputs()) {
// If this file was inserted into the table cache then remove it here
// because this compaction was not committed. This is not strictly
+10 -1
View File
@@ -81,6 +81,15 @@ class SubcompactionState {
// it returns both the last level outputs and proximal level outputs.
OutputIterator GetOutputs() const;
// Get mutable access to outputs for post-processing (e.g., retrieving
// file open metadata).
std::vector<CompactionOutputs::Output>& GetMutableCompactionOutputs() {
return compaction_outputs_.GetMutableOutputs();
}
std::vector<CompactionOutputs::Output>& GetMutableProximalOutputs() {
return proximal_level_outputs_.GetMutableOutputs();
}
// Assign range dels aggregator. The various tombstones will potentially
// be filtered to different outputs.
void AssignRangeDelAggregator(
@@ -170,7 +179,7 @@ class SubcompactionState {
}
}
void Cleanup(Cache* cache);
void Cleanup(Cache* cache, const Status& overall_status);
void AggregateCompactionOutputStats(
InternalStats::CompactionStatsFull& internal_stats) const;
+1064
View File
File diff suppressed because it is too large. Load diff
+114
View File
@@ -466,6 +466,90 @@ TEST_F(DBBlockCacheTest, WarmCacheWithDataBlocksDuringFlush) {
options.statistics->getTickerCount(BLOCK_CACHE_DATA_ADD));
}
// Cache wrapper that tracks the priority of each Insert call.
namespace {
class PriorityTrackingCache : public CacheWrapper {
public:
explicit PriorityTrackingCache(std::shared_ptr<Cache> target)
: CacheWrapper(std::move(target)) {}
const char* Name() const override { return "PriorityTrackingCache"; }
Status Insert(const Slice& key, ObjectPtr value,
const CacheItemHelper* helper, size_t charge,
Handle** handle = nullptr, Priority priority = Priority::LOW,
const Slice& compressed_value = Slice(),
CompressionType type = kNoCompression) override {
insert_priorities_.push_back(priority);
return CacheWrapper::Insert(key, value, helper, charge, handle, priority,
compressed_value, type);
}
void ResetPriorities() { insert_priorities_.clear(); }
bool HasPriority(Priority p) const {
return std::find(insert_priorities_.begin(), insert_priorities_.end(), p) !=
insert_priorities_.end();
}
private:
std::vector<Priority> insert_priorities_;
};
} // namespace
TEST_F(DBBlockCacheTest, WarmCacheWithDataBlocksDuringCompaction) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.statistics = ROCKSDB_NAMESPACE::CreateDBStatistics();
options.disable_auto_compactions = true;
auto tracking_cache =
std::make_shared<PriorityTrackingCache>(NewLRUCache(1 << 25, 0, false));
BlockBasedTableOptions table_options;
table_options.block_cache = tracking_cache;
table_options.cache_index_and_filter_blocks = false;
table_options.prepopulate_block_cache =
BlockBasedTableOptions::PrepopulateBlockCache::kFlushAndCompaction;
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
DestroyAndReopen(options);
std::string value(kValueSize, 'a');
// Flush warming: inserts should use LOW priority.
tracking_cache->ResetPriorities();
ASSERT_OK(Put("key", value));
ASSERT_OK(Flush());
EXPECT_TRUE(tracking_cache->HasPriority(Cache::Priority::LOW));
EXPECT_FALSE(tracking_cache->HasPriority(Cache::Priority::BOTTOM));
// Write overlapping key to force a real merge compaction (not trivial move).
ASSERT_OK(Put("key", value + "2"));
ASSERT_OK(Flush());
auto data_add_before =
options.statistics->getTickerCount(BLOCK_CACHE_DATA_ADD);
// Compaction warming: data block inserts should use BOTTOM priority.
// Internal cache bookkeeping (e.g., cache entry stats) may insert at HIGH,
// so we check for BOTTOM presence and LOW absence.
tracking_cache->ResetPriorities();
CompactRangeOptions cro;
cro.bottommost_level_compaction = BottommostLevelCompaction::kForceOptimized;
ASSERT_OK(db_->CompactRange(cro, /*begin=*/nullptr, /*end=*/nullptr));
EXPECT_GT(options.statistics->getTickerCount(BLOCK_CACHE_DATA_ADD),
data_add_before);
EXPECT_TRUE(tracking_cache->HasPriority(Cache::Priority::BOTTOM));
EXPECT_FALSE(tracking_cache->HasPriority(Cache::Priority::LOW));
// Compaction output is in cache -- reads should have zero misses.
auto data_miss_before =
options.statistics->getTickerCount(BLOCK_CACHE_DATA_MISS);
ASSERT_EQ(value + "2", Get("key"));
EXPECT_EQ(data_miss_before,
options.statistics->getTickerCount(BLOCK_CACHE_DATA_MISS));
}
// This test cache data, index and filter blocks during flush.
class DBBlockCacheTest1 : public DBTestBase,
public ::testing::WithParamInterface<uint32_t> {
@@ -620,6 +704,36 @@ TEST_F(DBBlockCacheTest, DynamicOptions) {
ASSERT_EQ(0, st->getAndResetTickerCount(BLOCK_CACHE_DATA_MISS));
ASSERT_EQ(1, st->getAndResetTickerCount(BLOCK_CACHE_DATA_HIT));
// Switch to kFlushAndCompaction
++i;
ASSERT_OK(dbfull()->SetOptions(
{{"block_based_table_factory",
"{prepopulate_block_cache=kFlushAndCompaction;}"}}));
ASSERT_OK(Put(std::to_string(i), value));
ASSERT_OK(Flush());
// Flush warming still works
ASSERT_EQ(1, st->getAndResetTickerCount(BLOCK_CACHE_DATA_ADD));
ASSERT_EQ(value, Get(std::to_string(i)));
ASSERT_EQ(0, st->getAndResetTickerCount(BLOCK_CACHE_DATA_ADD));
ASSERT_EQ(0, st->getAndResetTickerCount(BLOCK_CACHE_DATA_MISS));
ASSERT_EQ(1, st->getAndResetTickerCount(BLOCK_CACHE_DATA_HIT));
// Switch back to kDisable
++i;
ASSERT_OK(dbfull()->SetOptions(
{{"block_based_table_factory", "{prepopulate_block_cache=kDisable;}"}}));
ASSERT_OK(Put(std::to_string(i), value));
ASSERT_OK(Flush());
ASSERT_EQ(0, st->getAndResetTickerCount(BLOCK_CACHE_DATA_ADD));
ASSERT_EQ(value, Get(std::to_string(i)));
ASSERT_EQ(1, st->getAndResetTickerCount(BLOCK_CACHE_DATA_ADD));
ASSERT_EQ(1, st->getAndResetTickerCount(BLOCK_CACHE_DATA_MISS));
ASSERT_EQ(0, st->getAndResetTickerCount(BLOCK_CACHE_DATA_HIT));
++i;
// NOT YET SUPPORTED
// FIXME: find a way to make this fail again (until well supported)
+656 -10
View File
@@ -14,11 +14,13 @@
#include "db/db_test_util.h"
#include "db/dbformat.h"
#include "env/mock_env.h"
#include "file/filename.h"
#include "port/port.h"
#include "port/stack_trace.h"
#include "rocksdb/advanced_options.h"
#include "rocksdb/concurrent_task_limiter.h"
#include "rocksdb/experimental.h"
#include "rocksdb/file_checksum.h"
#include "rocksdb/iostats_context.h"
#include "rocksdb/sst_file_writer.h"
#include "test_util/mock_time_env.h"
@@ -117,6 +119,17 @@ class DBCompactionTest : public DBTestBase {
DBCompactionTest()
: DBTestBase("db_compaction_test", /*env_do_fsync=*/false) {}
void TearDown() override {
// Reset Env::Default() thread pool state that tests may have modified.
// Under sharded execution multiple tests share a process, so leaked
// thread-pool sizes (especially BOTTOM) cause later tests to see
// unexpected compaction scheduling (e.g. ForwardToBottomPriPool).
Env::Default()->SetBackgroundThreads(0, Env::Priority::BOTTOM);
Env::Default()->SetBackgroundThreads(1, Env::Priority::LOW);
Env::Default()->SetBackgroundThreads(1, Env::Priority::HIGH);
DBTestBase::TearDown();
}
protected:
/*
* Verifies compaction stats of cfd are valid.
@@ -797,6 +810,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 +835,7 @@ TEST_F(DBCompactionTest, CompactRangeBottomPri) {
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), nullptr, nullptr));
ASSERT_EQ(1, low_pri_count);
ASSERT_EQ(1, bottom_pri_count);
ASSERT_TRUE(bottom_running_seen);
ASSERT_EQ("0,0,2", FilesPerLevel(0));
// Recompact bottom most level uses bottom pool
@@ -11488,20 +11509,24 @@ TEST_F(DBCompactionTest, RecordNewestKeyTimeForTtlCompaction) {
// Check that we are populating newest_key_time on flush
std::vector<FileMetaData*> file_metadatas = GetLevelFileMetadatas(0);
ASSERT_EQ(file_metadatas.size(), 4);
uint64_t first_newest_key_time =
file_metadatas[0]->fd.table_reader->GetTableProperties()->newest_key_time;
uint64_t first_newest_key_time = file_metadatas[0]
->fd.pinned_reader.Get()
->GetTableProperties()
->newest_key_time;
ASSERT_NE(first_newest_key_time, kUnknownNewestKeyTime);
// Check that the newest_key_times are in expected ordering
uint64_t prev_newest_key_time = first_newest_key_time;
for (size_t idx = 1; idx < file_metadatas.size(); idx++) {
uint64_t newest_key_time = file_metadatas[idx]
->fd.table_reader->GetTableProperties()
->fd.pinned_reader.Get()
->GetTableProperties()
->newest_key_time;
ASSERT_LT(newest_key_time, prev_newest_key_time);
prev_newest_key_time = newest_key_time;
ASSERT_EQ(newest_key_time, file_metadatas[idx]
->fd.table_reader->GetTableProperties()
->fd.pinned_reader.Get()
->GetTableProperties()
->creation_time);
}
// The delta between the first and last newest_key_times is 15s
@@ -11516,14 +11541,18 @@ TEST_F(DBCompactionTest, RecordNewestKeyTimeForTtlCompaction) {
ASSERT_EQ(NumTableFilesAtLevel(0), 1);
file_metadatas = GetLevelFileMetadatas(0);
ASSERT_EQ(file_metadatas.size(), 1);
ASSERT_EQ(
file_metadatas[0]->fd.table_reader->GetTableProperties()->newest_key_time,
first_newest_key_time);
ASSERT_EQ(file_metadatas[0]
->fd.pinned_reader.Get()
->GetTableProperties()
->newest_key_time,
first_newest_key_time);
// Contrast newest_key_time with creation_time, which records the oldest
// ancestor time (15s older than newest_key_time)
ASSERT_EQ(
file_metadatas[0]->fd.table_reader->GetTableProperties()->creation_time,
last_newest_key_time);
ASSERT_EQ(file_metadatas[0]
->fd.pinned_reader.Get()
->GetTableProperties()
->creation_time,
last_newest_key_time);
ASSERT_EQ(file_metadatas[0]->oldest_ancester_time, last_newest_key_time);
// Make sure TTL of 5s causes compaction
@@ -11663,6 +11692,623 @@ TEST_F(DBCompactionTest, PeriodicTask) {
ASSERT_EQ(listener->num_periodic_compactions, 1);
Close();
}
TEST_F(DBCompactionTest, ReadTriggeredCompaction) {
Options options = CurrentOptions();
options.num_levels = 3;
options.level0_file_num_compaction_trigger = 10;
options.read_triggered_compaction_threshold = 0.001;
options.disable_auto_compactions = true;
DestroyAndReopen(options);
// Write data at L2 first so L1 is not the bottommost level
Random rnd(301);
for (int i = 0; i < 5; ++i) {
ASSERT_OK(Put(Key(i), rnd.RandomString(1024)));
}
ASSERT_OK(Flush());
MoveFilesToLevel(2);
ASSERT_EQ("0,0,1", FilesPerLevel());
// Write more data and move to L1 (the hot level)
for (int i = 5; i < 10; ++i) {
ASSERT_OK(Put(Key(i), rnd.RandomString(1024)));
}
ASSERT_OK(Flush());
MoveFilesToLevel(1);
ASSERT_EQ("0,1,1", FilesPerLevel());
ColumnFamilyMetaData cf_meta;
dbfull()->GetColumnFamilyMetaData(dbfull()->DefaultColumnFamily(), &cf_meta);
ASSERT_EQ(cf_meta.levels[1].files.size(), 1);
uint64_t file_size = cf_meta.levels[1].files[0].size;
// Set reads high enough to exceed threshold (0.001 * file_size)
uint64_t reads_needed =
static_cast<uint64_t>(0.002 * static_cast<double>(file_size));
{
auto* cfd = static_cast_with_check<ColumnFamilyHandleImpl>(
dbfull()->DefaultColumnFamily())
->cfd();
auto* vstorage = cfd->current()->storage_info();
for (auto* f : vstorage->LevelFiles(1)) {
f->stats.num_collapsible_entry_reads_sampled.store(reads_needed);
}
}
std::atomic<int> read_triggered_compactions{0};
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"LevelCompactionPicker::PickCompaction:Return", [&](void* arg) {
Compaction* compaction = static_cast<Compaction*>(arg);
if (compaction->compaction_reason() ==
CompactionReason::kReadTriggered) {
read_triggered_compactions.fetch_add(1, std::memory_order_relaxed);
}
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
// Enable auto compactions to trigger
ASSERT_OK(dbfull()->SetOptions({{"disable_auto_compactions", "false"}}));
ASSERT_OK(dbfull()->TEST_WaitForCompact());
ASSERT_GE(read_triggered_compactions, 1);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
// Verify the L1 file was compacted down (L1 should be empty now)
ASSERT_EQ(0, NumTableFilesAtLevel(1));
// Verify data integrity: all keys are still readable
for (int i = 0; i < 10; ++i) {
ASSERT_NE(Get(Key(i)), "NOT_FOUND");
}
}
// 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;
}
// Regression test for table cache leak when InstallCompactionResults fails.
// Similar to LeakedTableCacheEntryOnCompactionFailure but the failure occurs
// during Install() (MANIFEST write) rather than during Run().
// 1. Compaction Run() succeeds: all subcompactions OK, VerifyOutputFiles
// inserts output files into the table cache, compact_->status = OK.
// 2. Install() calls InstallCompactionResults() -> LogAndApply(), which
// fails (injected MANIFEST write error).
// 3. Install()'s local status captures the error, but compact_->status
// was never updated (THE BUG). CleanupCompaction passes compact_->status
// (OK) to Cleanup(), which skips ReleaseObsolete.
// 4. FaultInjection prevents FindObsoleteFiles backstop from working.
// 5. Close()'s TEST_VerifyNoObsoleteFilesCached finds the leaked entry.
TEST_F(DBCompactionTest, LeakedTableCacheEntryOnInstallFailure) {
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) and Run()
// completes successfully, inject an error into InstallCompactionResults()
// so that Install() fails while compact_->status remains OK.
// Also enable metadata read faults to prevent the FindObsoleteFiles
// backstop from evicting the leaked cache entries.
std::atomic<bool> inject_error{true};
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"CompactionJob::InstallCompactionResults:BeforeLogAndApply",
[&](void* arg) {
if (inject_error.exchange(false)) {
*static_cast<Status*>(arg) = Status::IOError("injected");
}
});
// After the compaction job finishes (including Install), enable metadata
// read faults so FindObsoleteFiles backstop cannot scan the directory.
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 -- Run() succeeds but Install() fails.
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;
}
// Regression test for ReleaseObsolete failing to erase table cache entries
// when concurrent readers hold references.
// 1. Flush creates an L0 SST file with an entry in the table cache.
// 2. A concurrent reader acquires a cache reference on the file (simulated
// with a direct cache Lookup).
// 3. ReleaseObsolete is called (simulating what PurgeObsoleteFiles does when
// the file becomes obsolete). With the old code, this calls
// ReleaseAndEraseIfLastRef which fails because of the concurrent ref.
// 4. The concurrent reader releases its reference.
// 5. Without the fix, the entry remains in the cache (leak). With the fix,
// ReleaseObsolete's Erase() marked the entry Invisible, so it was cleaned
// up when the concurrent ref was released.
TEST_F(DBCompactionTest, ObsoleteFileTableCacheEntryWithConcurrentRef) {
Options options = CurrentOptions();
options.disable_auto_compactions = true;
DestroyAndReopen(options);
// Create an L0 file.
ASSERT_OK(Put("a", std::string(1024, 'x')));
ASSERT_OK(Put("z", std::string(1024, 'x')));
ASSERT_OK(Flush());
ASSERT_EQ(NumTableFilesAtLevel(0), 1);
// Get the file number of the L0 file.
std::vector<LiveFileMetaData> files;
dbfull()->GetLiveFilesMetaData(&files);
ASSERT_EQ(files.size(), 1);
uint64_t target_file_number = files[0].file_number;
// Ensure the file is in the table cache by reading from it.
ASSERT_EQ(Get("a"), std::string(1024, 'x'));
Cache* table_cache = dbfull()->TEST_table_cache();
// Simulate a concurrent reader acquiring a cache reference.
Cache::Handle* concurrent_handle =
TableCache::Lookup(table_cache, target_file_number);
ASSERT_NE(concurrent_handle, nullptr);
// Call ReleaseObsolete directly -- this is what PurgeObsoleteFiles calls
// when a file becomes obsolete. Pass nullptr for the handle to make
// ReleaseObsolete do its own Lookup internally.
TableCache::ReleaseObsolete(table_cache, target_file_number,
/*handle=*/nullptr,
/*uncache_aggressiveness=*/0);
// The concurrent reader releases its reference.
table_cache->Release(concurrent_handle);
// With the fix, the entry should be gone: ReleaseObsolete called Erase()
// which marked it Invisible, so it was freed when the concurrent ref was
// released. Without the fix, the entry leaks here.
Cache::Handle* leaked = TableCache::Lookup(table_cache, target_file_number);
ASSERT_EQ(leaked, nullptr);
}
// Regression test for the atomic flush path leaking table cache entries when
// InstallMemtableAtomicFlushResults (MANIFEST write) fails.
// 1. atomic_flush is enabled with two column families.
// 2. Both CFs have data, a flush is triggered.
// 3. FlushJob::Run() succeeds for both (files are added to table cache by
// BuildTable), but write_manifest=false so no install happens in Run().
// 4. The combined MANIFEST write (InstallMemtableAtomicFlushResults) fails
// due to injected I/O error.
// 5. Without the fix, the files remain in the table cache but are not in
// any Version, causing TEST_VerifyNoObsoleteFilesCached to fire during
// Close() (especially when metadata_read_fault_one_in disables the
// FindObsoleteFiles backstop).
TEST_F(DBCompactionTest, LeakedTableCacheEntryOnAtomicFlushInstallFailure) {
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.atomic_flush = true;
options.create_if_missing = true;
options.disable_auto_compactions = true;
// Enough buffer so auto-flush doesn't trigger prematurely.
options.write_buffer_size = 64 << 20;
DestroyAndReopen(options);
// Create a second column family.
CreateAndReopenWithCF({"pikachu"}, options);
ASSERT_EQ(2, handles_.size());
// Write data to both CFs.
ASSERT_OK(Put(0, "key0", std::string(1024, 'a')));
ASSERT_OK(Put(1, "key1", std::string(1024, 'b')));
// Inject a write error during the combined MANIFEST write that happens
// inside InstallMemtableAtomicFlushResults (via LogAndApply).
std::atomic<bool> inject_error{true};
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"VersionSet::LogAndApply:WriteManifest", [&](void*) {
if (inject_error.exchange(false)) {
fault_fs->SetThreadLocalErrorContext(
FaultInjectionIOType::kWrite, /*seed=*/0, /*one_in=*/1,
/*retryable=*/false, /*has_data_loss=*/false);
fault_fs->EnableThreadLocalErrorInjection(
FaultInjectionIOType::kWrite);
}
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
// Trigger atomic flush - Run() succeeds but MANIFEST write fails.
FlushOptions flush_opts;
flush_opts.wait = true;
Status s = db_->Flush(flush_opts, handles_);
ASSERT_NOK(s);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
// Disable the write fault so Close() can proceed.
fault_fs->DisableThreadLocalErrorInjection(FaultInjectionIOType::kWrite);
// Enable metadata read fault injection on the main thread so that
// Close()'s FindObsoleteFiles full-scan backstop cannot find the orphan
// files (simulating metadata_read_fault_one_in from the stress test).
fault_fs->SetThreadLocalErrorContext(
FaultInjectionIOType::kMetadataRead, /*seed=*/0, /*one_in=*/1,
/*retryable=*/false, /*has_data_loss=*/false);
fault_fs->EnableThreadLocalErrorInjection(
FaultInjectionIOType::kMetadataRead);
// Destroy column family handles before closing.
for (auto h : handles_) {
ASSERT_OK(db_->DestroyColumnFamilyHandle(h));
}
handles_.clear();
// Close the DB. Without the fix, TEST_VerifyNoObsoleteFilesCached fires.
s = db_->Close();
ASSERT_OK(s);
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);
}
}
// Requires ~8GB+ RAM because the compaction filter internally creates a ~4GB
// value via std::string::assign().
TEST_F(DBCompactionTest, CompactionFilterLargeValueRejected) {
if (!test::HasBigMem()) {
ROCKSDB_GTEST_BYPASS("insufficient memory for reliable continuous testing");
return;
}
// A compaction filter that inflates every value to a configurable size
class InflatingFilter : public CompactionFilter {
public:
std::atomic<size_t> target_size{0};
Decision FilterV2(int /*level*/, const Slice& /*key*/,
ValueType /*value_type*/, const Slice& /*existing_value*/,
std::string* new_value,
std::string* /*skip_until*/) const override {
new_value->assign(target_size.load(), 'X');
return Decision::kChangeValue;
}
const char* Name() const override { return "InflatingFilter"; }
};
InflatingFilter inflating_filter;
Options options = CurrentOptions();
options.compaction_filter = &inflating_filter;
options.disable_auto_compactions = true;
DestroyAndReopen(options);
// Control: value at exactly 4GB - 1 should be accepted
inflating_filter.target_size = size_t{std::numeric_limits<uint32_t>::max()};
ASSERT_OK(Put("key", "small_value"));
ASSERT_OK(Flush());
ASSERT_OK(Put("key", "small_value2"));
ASSERT_OK(Flush());
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
// Now test rejection: value at 4GB should be rejected
inflating_filter.target_size =
size_t{std::numeric_limits<uint32_t>::max()} + 1;
DestroyAndReopen(options);
ASSERT_OK(Put("key", "small_value"));
ASSERT_OK(Flush());
ASSERT_OK(Put("key", "small_value2"));
ASSERT_OK(Flush());
Status s = db_->CompactRange(CompactRangeOptions(), nullptr, nullptr);
ASSERT_TRUE(s.IsCorruption()) << s.ToString();
ASSERT_TRUE(s.ToString().find("4GB") != std::string::npos) << s.ToString();
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
+35 -22
View File
@@ -28,9 +28,10 @@
namespace ROCKSDB_NAMESPACE {
Status DBImpl::FlushForGetLiveFiles() {
return DBImpl::FlushAllColumnFamilies(FlushOptions(),
FlushReason::kGetLiveFiles);
Status DBImpl::FlushForGetLiveFiles(bool force_atomic_flush) {
FlushOptions flush_opts;
flush_opts.force_atomic_flush = force_atomic_flush;
return DBImpl::FlushAllColumnFamilies(flush_opts, FlushReason::kGetLiveFiles);
}
Status DBImpl::GetLiveFiles(std::vector<std::string>& ret,
@@ -204,7 +205,6 @@ Status DBImpl::GetLiveFilesStorageInfo(
// NOTE: This implementation was largely migrated from Checkpoint.
Status s;
VectorWalPtr live_wal_files;
bool flush_memtable = true;
if (!immutable_db_options_.allow_2pc) {
@@ -216,12 +216,12 @@ Status DBImpl::GetLiveFilesStorageInfo(
// Don't take archived log size into account when calculating wal
// size for flush, and don't need to verify consistency with manifest
// here & now.
s = wal_manager_.GetSortedWalFiles(live_wal_files,
/* need_seqnos */ false,
/*include_archived*/ false);
Status wal_s = wal_manager_.GetSortedWalFiles(live_wal_files,
/* need_seqnos */ false,
/*include_archived*/ false);
if (!s.ok()) {
return s;
if (!wal_s.ok()) {
return wal_s;
}
// Don't flush column families if total log size is smaller than
@@ -242,13 +242,28 @@ Status DBImpl::GetLiveFilesStorageInfo(
// This is a modified version of GetLiveFiles, to get access to more
// metadata.
mutex_.Lock();
bool wal_locked = false;
const bool needs_blob_direct_write_flush =
HasInFlightBlobDirectWriteFilesWithLockHeld();
if (needs_blob_direct_write_flush && !flush_memtable) {
mutex_.Unlock();
return Status::NotSupported(
"Blob direct write requires flushing active blob files before "
"capturing live files. Retry with flush enabled.");
}
if (flush_memtable) {
bool wal_locked = lock_wal_count_ > 0;
wal_locked = lock_wal_count_ > 0;
if (wal_locked) {
if (needs_blob_direct_write_flush) {
mutex_.Unlock();
return Status::NotSupported(
"Blob direct write requires flushing active blob files before "
"capturing live files. Retry with WAL unlocked.");
}
ROCKS_LOG_INFO(immutable_db_options_.info_log,
"Can't FlushForGetLiveFiles while WAL is locked");
} else {
Status status = FlushForGetLiveFiles();
Status status = FlushForGetLiveFiles(opts.atomic_flush);
if (!status.ok()) {
mutex_.Unlock();
ROCKS_LOG_ERROR(immutable_db_options_.info_log,
@@ -392,17 +407,15 @@ Status DBImpl::GetLiveFilesStorageInfo(
TEST_SYNC_POINT("CheckpointImpl::CreateCheckpoint:SavedLiveFiles1");
TEST_SYNC_POINT("CheckpointImpl::CreateCheckpoint:SavedLiveFiles2");
if (s.ok()) {
// FlushWAL is required to ensure we can physically copy everything
// logically written to the WAL. (Sync not strictly required for
// active WAL to be copied rather than hard linked, even when
// Checkpoint guarantees that the copied-to file is sync-ed. Plus we can't
// help track_and_verify_wals_in_manifest after manifest_size is
// already determined.)
s = FlushWAL(/*sync=*/false);
if (s.IsNotSupported()) { // read-only DB or similar
s = Status::OK();
}
// FlushWAL is required to ensure we can physically copy everything
// logically written to the WAL. (Sync not strictly required for
// active WAL to be copied rather than hard linked, even when
// Checkpoint guarantees that the copied-to file is sync-ed. Plus we can't
// help track_and_verify_wals_in_manifest after manifest_size is
// already determined.)
Status s = FlushWAL(/*sync=*/false);
if (s.IsNotSupported()) { // read-only DB or similar
s = Status::OK();
}
TEST_SYNC_POINT("CheckpointImpl::CreateCustomCheckpoint:AfterGetLive1");
+269
View File
@@ -17,6 +17,7 @@
#include "port/port.h"
#include "port/stack_trace.h"
#include "rocksdb/utilities/transaction_db.h"
#include "table/block_based/block_based_table_builder.h"
#include "test_util/sync_point.h"
#include "test_util/testutil.h"
#include "util/cast_util.h"
@@ -33,6 +34,17 @@ static std::string NEW_VALUE = "NewValue";
class DBFlushTest : public DBTestBase {
public:
DBFlushTest() : DBTestBase("db_flush_test", /*env_do_fsync=*/true) {}
// Wait for all background flush callbacks to complete before Close().
// Close() calls db_.reset() which nulls the unique_ptr before the destructor
// runs, so a concurrent OnFlushCompleted reading test_->db_ would race.
// WaitForCompact() waits for bg_flush_scheduled_==0 (which is decremented
// after listener callbacks return) without setting shutting_down_ (so
// callbacks are not skipped) and without requiring NumNotFlushed()==0 (which
// would hang when mempurge leaves a memtable in imm()).
Status WaitForFlushCallbacks() {
return db_->WaitForCompact(WaitForCompactOptions());
}
};
class DBFlushDirectIOTest : public DBFlushTest,
@@ -1055,6 +1067,7 @@ TEST_F(DBFlushTest, MemPurgeBasic) {
ASSERT_EQ(Get(RNDKEY2), p_rv2);
ASSERT_EQ(Get(RNDKEY3), p_rv3);
ASSERT_OK(WaitForFlushCallbacks());
Close();
}
@@ -1169,6 +1182,7 @@ TEST_F(DBFlushTest, MemPurgeBasicToggle) {
// We expect no mempurge at all.
EXPECT_EQ(mempurge_count.exchange(0), ZERO);
ASSERT_OK(WaitForFlushCallbacks());
Close();
}
// End of MemPurgeBasicToggle, which is not
@@ -1425,6 +1439,7 @@ TEST_F(DBFlushTest, MemPurgeDeleteAndDeleteRange) {
delete iter;
}
ASSERT_OK(WaitForFlushCallbacks());
Close();
}
@@ -1837,6 +1852,8 @@ TEST_F(DBFlushTest, MemPurgeCorrectLogNumberAndSSTFileCreation) {
}
}
ASSERT_OK(WaitForFlushCallbacks());
// Check that there was at least one mempurge
uint32_t expected_min_mempurge_count = 1;
// Check that there was no SST files created during flush.
@@ -1857,6 +1874,8 @@ TEST_F(DBFlushTest, MemPurgeCorrectLogNumberAndSSTFileCreation) {
ASSERT_EQ(Get(key), value);
}
ASSERT_OK(WaitForFlushCallbacks());
// Check that there was at least one SST files created during flush.
expected_sst_count = 1;
EXPECT_GE(sst_count.load(), expected_sst_count);
@@ -1876,6 +1895,88 @@ TEST_F(DBFlushTest, MemPurgeCorrectLogNumberAndSSTFileCreation) {
// Extra check of database consistency.
ASSERT_EQ(Get(key), value);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
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();
}
@@ -3706,6 +3807,174 @@ 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();
}
// Regression test for table cache leak when flush install fails.
// BuildTable caches the flush output file (for user reads) but if the
// subsequent TryInstallMemtableFlushResults (LogAndApply) fails, the
// cache entry was never evicted. The FindObsoleteFiles backstop normally
// catches this, but fails under metadata read fault injection
// (open_metadata_read_fault_one_in), causing
// TEST_VerifyNoObsoleteFilesCached to fire during Close().
TEST_F(DBFlushTest, LeakedTableCacheEntryOnFlushInstallFailure) {
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;
// Disable auto-recovery so it doesn't clean up the orphan on a separate
// thread where metadata faults aren't enabled.
options.max_bgerror_resume_count = 0;
DestroyAndReopen(options);
// Write data so flush produces a non-empty file.
ASSERT_OK(Put("a", std::string(1024, 'x')));
// After BuildTable succeeds (caching the output file), inject a failure
// before the install-or-rollback decision. This simulates scenarios like
// CF dropped or shutdown after BuildTable. Also deactivate the filesystem
// so the post-flush FindObsoleteFiles backstop cannot scan the directory.
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"FlushJob::Run:PostBuildTable", [&](void* arg) {
*static_cast<Status*>(arg) = Status::IOError("injected");
fault_fs->SetFilesystemActive(false);
});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
// Trigger flush -- BuildTable succeeds but LogAndApply fails.
Status s = Flush();
ASSERT_NOK(s);
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
// With the fix, the leaked cache entry was already evicted by
// ReleaseObsolete in FlushJob::Run(). Without the fix, it persists.
// We can verify by counting non-live cache entries. The quarantine
// includes the file (BG error is active), but that's only a workaround --
// the cache entry should not exist at all with the fix.
//
// Check: with the fix, the table cache should be empty (the flush output
// was never installed and the fix evicted it). Without the fix, there's
// a leaked entry.
int cache_entries = 0;
auto count_fn = [&cache_entries](const Slice&, Cache::ObjectPtr, size_t,
const Cache::CacheItemHelper*) {
cache_entries++;
};
dbfull()->TEST_table_cache()->ApplyToAllEntries(count_fn, {});
ASSERT_EQ(cache_entries, 0)
<< "Leaked table cache entry for flush output that was never installed";
// Reactivate filesystem for clean shutdown.
fault_fs->SetFilesystemActive(true);
db_ = nullptr; // destructor handles Close
}
// Verify that ConstructFragmentedRangeTombstones runs after MarkImmutable
// in SwitchMemtable. A range tombstone inserted between construction and
// immutability would cause a flush entry count mismatch.
TEST_F(DBFlushTest, FlushAfterReadPathRangeTombstoneInsertion) {
Options options = CurrentOptions();
options.min_tombstones_for_range_conversion = 2;
options.flush_verify_memtable_count = true;
options.statistics = CreateDBStatistics();
DestroyAndReopen(options);
// Write base data and flush to L0.
for (char c = 'a'; c <= 'h'; c++) {
ASSERT_OK(Put(std::string(1, c), std::string("v") + c));
}
ASSERT_OK(Flush());
// Delete contiguous keys in the memtable.
for (const auto& key : {"b", "c", "d", "e"}) {
ASSERT_OK(Delete(key));
}
// When SwitchMemtable hits the sync point after
// ConstructFragmentedRangeTombstones, directly call
// AddLogicallyRedundantRangeTombstone on the memtable being switched.
// We can't create an iterator here (would deadlock on the DB mutex).
auto* cfh = static_cast<ColumnFamilyHandleImpl*>(db_->DefaultColumnFamily());
SyncPoint::GetInstance()->SetCallBack(
"DBImpl::SwitchMemtable:AfterConstructFragmentedRangeTombstones",
[&](void*) {
// Try to insert a range tombstone. With the fix (Construct after
// MarkImmutable), this returns false because the memtable is
// already immutable. Without the fix, this succeeds and creates
// an entry count mismatch.
// Try to insert a range tombstone. With the fix (Construct after
// MarkImmutable), this returns false because the memtable is
// already immutable. Without the fix, this succeeds and creates
// an entry count mismatch.
cfh->cfd()->mem()->AddLogicallyRedundantRangeTombstone(
1 /* seq */, "b", "f", cfh->cfd()->GetIngestSstLock());
});
SyncPoint::GetInstance()->EnableProcessing();
Status s = Flush();
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
ASSERT_OK(s);
ASSERT_EQ(Get("a"), "va");
ASSERT_EQ(Get("b"), "NOT_FOUND");
ASSERT_EQ(Get("d"), "NOT_FOUND");
ASSERT_EQ(Get("f"), "vf");
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
+62 -34
View File
@@ -5,6 +5,7 @@
#include "db/db_impl/compacted_db_impl.h"
#include "db/blob/blob_fetcher.h"
#include "db/db_impl/db_impl.h"
#include "db/version_set.h"
#include "logging/logging.h"
@@ -75,13 +76,16 @@ Status CompactedDBImpl::Get(const ReadOptions& _read_options,
}
GetWithTimestampReadCallback read_cb(kMaxSequenceNumber);
BlobFetcher blob_fetcher(version_, read_options);
std::string* ts =
user_comparator_->timestamp_size() > 0 ? timestamp : nullptr;
LookupKey lkey(key, kMaxSequenceNumber, read_options.timestamp);
GetContext get_context(user_comparator_, nullptr, nullptr, nullptr,
GetContext::kNotFound, lkey.user_key(), value,
/*columns=*/nullptr, ts, nullptr, nullptr, true,
nullptr, nullptr, nullptr, nullptr, &read_cb);
nullptr, nullptr, nullptr, nullptr, &read_cb,
nullptr /* is_blob_index */, 0 /* tracing_get_id */,
&blob_fetcher);
const FdWithKeyRange& f = files_.files[FindFile(lkey.user_key())];
if (user_comparator_->CompareWithoutTimestamp(
@@ -91,8 +95,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 +169,7 @@ void CompactedDBImpl::MultiGet(const ReadOptions& _read_options,
}
GetWithTimestampReadCallback read_cb(kMaxSequenceNumber);
autovector<TableReader*, 16> reader_list;
BlobFetcher blob_fetcher(version_, read_options);
for (size_t i = 0; i < num_keys; ++i) {
const Slice& key = keys[i];
LookupKey lkey(key, kMaxSequenceNumber, read_options.timestamp);
@@ -162,39 +179,41 @@ 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);
statuses[i] = Status::NotFound();
continue;
}
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,
nullptr /* is_blob_index */, 0 /* tracing_get_id */, &blob_fetcher);
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 {
f.fd.table_reader->Prepare(lkey.internal_key());
reader_list.push_back(f.fd.table_reader);
statuses[i] = Status::NotFound();
}
}
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;
} else {
if (get_context.State() == GetContext::kFound) {
statuses[idx] = Status::OK();
}
}
}
++idx;
}
}
Status CompactedDBImpl::Init(const Options& options) {
@@ -230,6 +249,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 +262,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 +282,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()) {
+2 -1
View File
@@ -129,7 +129,7 @@ class CompactedDBImpl : public DBImpl {
// Share with DBImplReadOnly?
protected:
Status FlushForGetLiveFiles() override {
Status FlushForGetLiveFiles(bool /*force_atomic_flush*/) override {
// No-op for read-only DB
return Status::OK();
}
@@ -143,5 +143,6 @@ class CompactedDBImpl : public DBImpl {
Version* version_;
const Comparator* user_comparator_;
LevelFilesBrief files_;
int files_level_{0};
};
} // namespace ROCKSDB_NAMESPACE
+943 -54
View File
File diff suppressed because it is too large. Load diff
+286 -19
View File
@@ -260,6 +260,10 @@ class DBImpl : public DB {
const WriteOptions& options,
std::shared_ptr<WriteBatchWithIndex> wbwi) override;
// Returns true if any live column family currently has blob direct write
// enabled.
bool HasAnyBlobDirectWriteColumnFamily();
using DB::Get;
Status Get(const ReadOptions& _read_options,
ColumnFamilyHandle* column_family, const Slice& key,
@@ -1092,6 +1096,7 @@ class DBImpl : public DB {
superversions_to_free_queue_.push_back(sv);
}
void EnableTrackPublishedSeqInSnapshotContext();
void SetSnapshotChecker(SnapshotChecker* snapshot_checker);
// Fill JobContext with snapshot information needed by flush and compaction.
@@ -1248,6 +1253,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;
@@ -1530,7 +1536,7 @@ class DBImpl : public DB {
Status FlushAllColumnFamilies(const FlushOptions& flush_options,
FlushReason flush_reason);
virtual Status FlushForGetLiveFiles();
virtual Status FlushForGetLiveFiles(bool force_atomic_flush = false);
void NewThreadStatusCfInfo(ColumnFamilyData* cfd) const;
@@ -1550,10 +1556,17 @@ class DBImpl : public DB {
// @param memtable_updated Whether the same write that ingests wbwi has
// updated memtable. This is useful for determining whether to set bg
// error when IngestWBWIAsMemtable fails.
// @param ingest_wbwi_for_commit Whether wbwi ingestion is publishing the
// committed data of a prepared transaction. This means a failure can leave
// committed data durable in WAL but not published in memtables.
// @param ignore_missing_cf If true, skip column families not found in the DB
// instead of returning an error.
Status IngestWBWIAsMemtable(std::shared_ptr<WriteBatchWithIndex> wbwi,
const WBWIMemTable::SeqnoRange& assigned_seqno,
uint64_t min_prep_log, SequenceNumber last_seqno,
bool memtable_updated, bool ignore_missing_cf);
bool memtable_updated,
bool ingest_wbwi_for_commit,
bool ignore_missing_cf);
// If disable_memtable is set the application logic must guarantee that the
// batch will still be skipped from memtable during the recovery. An excption
@@ -1576,21 +1589,131 @@ class DBImpl : public DB {
// See more in comment above PreReleaseCallback::Callback().
// post_memtable_callback is called after memtable write but before publishing
// the sequence number to readers.
// `trace_batch_override`, when non-null, supplies the logical batch to trace
// instead of `updates`. Fast paths may rewrite or rebuild `updates` before
// apply, but tracing should still record the original user-visible batch.
// `skip_blob_direct_write_transform` indicates the caller has already
// performed any needed batch-level blob direct-write preprocessing.
// `deferred_put_entities`, when non-null, carries structured `PutEntity()`
// ops that should be materialized into `updates` after `PreprocessWrite()`
// has selected the target memtable/blob generation.
//
// The main write queue. This is the only write queue that updates
// LastSequence. When using one write queue, the same sequence also indicates
// the last published sequence.
Status WriteImpl(const WriteOptions& options, WriteBatch* updates,
WriteCallback* callback = nullptr,
UserWriteCallback* user_write_cb = nullptr,
uint64_t* wal_used = nullptr, uint64_t log_ref = 0,
bool disable_memtable = false, uint64_t* seq_used = nullptr,
size_t batch_cnt = 0,
PreReleaseCallback* pre_release_callback = nullptr,
PostMemTableCallback* post_memtable_callback = nullptr,
std::shared_ptr<WriteBatchWithIndex> wbwi = nullptr);
struct DeferredPutEntityBatch;
Status WriteImpl(
const WriteOptions& options, WriteBatch* updates,
WriteCallback* callback = nullptr,
UserWriteCallback* user_write_cb = nullptr, uint64_t* wal_used = nullptr,
uint64_t log_ref = 0, bool disable_memtable = false,
uint64_t* seq_used = nullptr, size_t batch_cnt = 0,
PreReleaseCallback* pre_release_callback = nullptr,
PostMemTableCallback* post_memtable_callback = nullptr,
std::shared_ptr<WriteBatchWithIndex> wbwi = nullptr,
WriteBatch* trace_batch_override = nullptr,
bool skip_blob_direct_write_transform = false,
const DeferredPutEntityBatch* deferred_put_entities = nullptr);
// Per-WriteImpl state that keeps BDW column families pinned through
// referenced SuperVersions until the transformed write either commits or
// rolls back.
struct BlobDirectWriteContext;
// High-level `PutEntity()` fast-path flow:
//
// non-BDW target CF:
// PutEntityFastPath()
// -> AppendPreprocessedPutEntityToBatch()
// -> WritePreprocessedPutEntityBatch()
// -> WriteImpl(skip_blob_direct_write_transform = true)
//
// BDW-enabled target CF:
// PutEntityFastPath()
// -> sort columns and stash DeferredPutEntityBatch::Op
// -> WritePreprocessedPutEntityBatch()
// -> WriteImpl(..., deferred_put_entities)
// -> after PreprocessWrite() picks the memtable/blob generation:
// AppendSortedPutEntityToBatch()
//
// The BDW branch defers final serialization until `WriteImpl()` knows the
// target memtable/blob generation, avoiding the old
// serialize -> deserialize -> serialize cycle on the write hot path.
// Staging buffer for the `PutEntity()` fast path when blob direct write may
// need to rewrite wide-column values. `PutEntityFastPath()` records the
// logical operations here instead of serializing them into a `WriteBatch`
// immediately, then passes this struct to `WriteImpl()`. Once
// `PreprocessWrite()` has selected the memtable/blob-file generation,
// `WriteImpl()` materializes each op directly into the final batch with blob
// direct-write preprocessing applied. This avoids the old
// serialize-deserialize-serialize cycle on the write hot path.
struct DeferredPutEntityBatch {
// The fast path sorts columns up front so `WriteImpl()` can append the
// final serialized entity without redoing that work.
struct Op {
// Target column family for the logical `PutEntity()` op.
uint32_t column_family_id = 0;
// Owned key bytes kept alive until `WriteImpl()` rebuilds the batch.
std::string key;
// Pre-sorted columns ready for final serialization and blob
// direct-write preprocessing.
WideColumns sorted_columns;
};
// Logical `PutEntity()` ops collected by the fast path and replayed into
// the final `WriteBatch` inside `WriteImpl()`.
std::vector<Op> ops;
};
// Rewrites a write batch for blob direct write when the current DB and batch
// shape are compatible, recording touched managers and rollback metadata in
// `blob_direct_write_ctx`.
Status MaybeTransformBatchForBlobDirectWrite(
const WriteOptions& write_options, WriteBatch** batch,
bool should_write_to_memtable, bool lookup_from_write_thread,
WriteBatch* transformed_storage,
BlobDirectWriteContext* blob_direct_write_ctx);
// Sorts and preprocesses one `PutEntity()` op, then appends the final
// serialized form to `batch`.
Status AppendPreprocessedPutEntityToBatch(
const WriteOptions& write_options, WriteBatch* batch,
ColumnFamilyHandle* column_family, const Slice& key,
const WideColumns& columns,
BlobDirectWriteContext* blob_direct_write_ctx);
// Appends a `PutEntity()` op whose columns are already sorted, allowing the
// fast path to avoid re-sorting before final serialization.
Status AppendSortedPutEntityToBatch(
const WriteOptions& write_options, WriteBatch* batch,
uint32_t column_family_id, const Slice& key,
const WideColumns& sorted_columns,
BlobDirectWriteContext* blob_direct_write_ctx);
// Specialized `PutEntity()` write path for a single column family. This
// avoids building an intermediate serialized entity when blob direct write
// needs to rewrite the final batch inside `WriteImpl()`.
Status PutEntityFastPath(const WriteOptions& write_options,
ColumnFamilyHandle* column_family, const Slice& key,
const WideColumns& columns);
// Specialized `PutEntity(AttributeGroups)` path that defers final batch
// materialization until `WriteImpl()` when any target CF may use blob direct
// write.
Status PutEntityFastPath(const WriteOptions& write_options, const Slice& key,
const AttributeGroups& attribute_groups);
// Writes a batch prepared by the `PutEntity()` fast path. When
// `deferred_put_entities` is present, `WriteImpl()` rebuilds `batch`
// internally after `PreprocessWrite()` and disables normal write batching to
// keep the staging state local to this write. `trace_batch` stays separate
// so tracing can still emit the logical `PutEntity()` batch even when the
// applied batch is materialized later inside `WriteImpl()`.
Status WritePreprocessedPutEntityBatch(
const WriteOptions& write_options, WriteBatch* batch,
WriteBatch* trace_batch,
const DeferredPutEntityBatch* deferred_put_entities = nullptr);
// Flushes or syncs all blob direct-write managers touched by the current
// transformed write before the write can proceed.
Status SyncBlobDirectWriteManagers(
const WriteOptions& write_options,
const BlobDirectWriteContext& blob_direct_write_ctx);
Status PipelinedWriteImpl(const WriteOptions& options, WriteBatch* updates,
WriteBatch* trace_batch,
WriteCallback* callback = nullptr,
UserWriteCallback* user_write_cb = nullptr,
uint64_t* wal_used = nullptr, uint64_t log_ref = 0,
@@ -1619,7 +1742,7 @@ class DBImpl : public DB {
// marks start of a new sub-batch.
Status WriteImplWALOnly(
WriteThread* write_thread, const WriteOptions& options,
WriteBatch* updates, WriteCallback* callback,
WriteBatch* updates, WriteBatch* trace_batch, WriteCallback* callback,
UserWriteCallback* user_write_cb, uint64_t* wal_used,
const uint64_t log_ref, uint64_t* seq_used, const size_t sub_batch_cnt,
PreReleaseCallback* pre_release_callback, const AssignOrder assign_order,
@@ -1699,6 +1822,9 @@ class DBImpl : public DB {
Status FailIfCfHasTs(const ColumnFamilyHandle* column_family) const;
Status FailIfTsMismatchCf(ColumnFamilyHandle* column_family,
const Slice& ts) const;
Status FailIfTableFilterWithRangeConversion(
const ReadOptions& read_options,
const MutableCFOptions& mutable_cf_options) const;
// Check that the read timestamp `ts` is at or above the `full_history_ts_low`
// timestamp in a `SuperVersion`. It's necessary to do this check after
@@ -1718,6 +1844,22 @@ 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);
// Block the until any in-flight async file open work has
// completed. No-op when open_files_async is false. Returns early if
// shutdown begins.
void WaitForAsyncFileOpen();
void InvokeWalFilterIfNeededOnColumnFamilyToWalNumberMap();
// Return true to proceed with current WAL record whose content is stored in
@@ -1728,8 +1870,13 @@ 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 DBImplReadOnly;
friend class DBImplSecondary;
friend class ErrorHandler;
friend class InternalStats;
friend class PessimisticTransaction;
@@ -1762,6 +1909,21 @@ class DBImpl : public DB {
friend class CompactionServiceTest_PreservedOptionsRemoteCompaction_Test;
#endif
// Same as HasAnyBlobDirectWriteColumnFamily(), but requires `mutex_` held.
bool HasAnyBlobDirectWriteColumnFamilyWithLockHeld();
// Returns true if any BDW column family still owns blob files that have not
// yet been made visible through MANIFEST. Requires `mutex_` held.
bool HasInFlightBlobDirectWriteFilesWithLockHeld();
// Creates and attaches the per-CF blob direct-write partition manager when
// the column family is opened with the feature enabled.
void MaybeInitBlobDirectWriteColumnFamily(
ColumnFamilyData* cfd, const ColumnFamilyOptions& cf_options,
const std::string& column_family_name);
// Increments the DB-level count of live blob direct-write column families.
void RegisterBlobDirectWriteColumnFamily();
// Decrements the DB-level count of live blob direct-write column families.
void UnregisterBlobDirectWriteColumnFamily();
struct CompactionState;
struct PrepickedCompaction;
struct PurgeFileInfo;
@@ -1899,11 +2061,12 @@ class DBImpl : public DB {
flush_reason_(FlushReason::kOthers) {}
BGFlushArg(ColumnFamilyData* cfd, uint64_t max_memtable_id,
SuperVersionContext* superversion_context,
FlushReason flush_reason)
FlushReason flush_reason, bool atomic_flush)
: cfd_(cfd),
max_memtable_id_(max_memtable_id),
superversion_context_(superversion_context),
flush_reason_(flush_reason) {}
flush_reason_(flush_reason),
atomic_flush_(atomic_flush) {}
// Column family to flush.
ColumnFamilyData* cfd_;
@@ -1915,6 +2078,8 @@ class DBImpl : public DB {
// requires a SuperVersionContext object (currently embedded in JobContext).
SuperVersionContext* superversion_context_;
FlushReason flush_reason_;
// Whether this flush should use atomic flush code path.
bool atomic_flush_ = false;
};
// Argument passed to flush thread.
@@ -2034,6 +2199,10 @@ class DBImpl : public DB {
void DeleteObsoleteFileImpl(int job_id, const std::string& fname,
const std::string& path_to_sync, FileType type,
uint64_t number);
// Returns true when a blob file must be preserved because it is still
// tracked by blob direct write or is still footer-less on disk.
bool ShouldKeepBlobFileDuringPurge(uint64_t number,
const std::string& blob_file_path);
// Background process needs to call
// auto x = CaptureCurrentFileNumberInPendingOutputs()
@@ -2150,7 +2319,7 @@ class DBImpl : public DB {
Status InsertLogRecordToMemtable(WriteBatch* batch_to_use,
uint64_t wal_number,
SequenceNumber* next_sequence,
bool* has_valid_writes);
bool* has_valid_writes, bool read_only);
Status MaybeWriteLevel0TableForRecovery(
bool has_valid_writes, bool read_only, uint64_t wal_number, int job_id,
@@ -2416,8 +2585,13 @@ class DBImpl : public DB {
void MaybeScheduleFlushOrCompaction();
BackgroundJobPressure CaptureBackgroundJobPressure() const;
void NotifyOnBackgroundJobPressureChanged();
struct FlushRequest {
FlushReason flush_reason;
// Whether this flush request should use the atomic flush code path.
bool atomic_flush = false;
// A map from column family to flush to largest memtable id to persist for
// each column family. Once all the memtables whose IDs are smaller than or
// equal to this per-column-family specified value, this flush request is
@@ -2506,6 +2680,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();
@@ -2569,6 +2750,8 @@ class DBImpl : public DB {
Status MaybeReleaseTimestampedSnapshotsAndCheck();
Status MaybeWriteWalMarkersToManifestOnClose();
Status CloseHelper();
void WaitForBackgroundWork();
@@ -2746,6 +2929,53 @@ class DBImpl : public DB {
std::string ts_low);
bool ShouldReferenceSuperVersion(const MergeContext& merge_context);
// Resolves a plain value whose current payload is an encoded direct-write
// blob index, either through `value` directly or through the default-column
// view layered on `columns`.
static Status ResolveDirectWritePlainValue(const ReadOptions& read_options,
const Slice& key,
const Version* current,
ColumnFamilyData* cfd,
PinnableSlice* value,
PinnableWideColumns* columns);
// Resolves each unresolved direct-write blob-valued column in `columns` and
// rebuilds the serialized wide-column entity in place.
static Status ResolveDirectWriteWideColumns(const ReadOptions& read_options,
const Slice& key,
const Version* current,
ColumnFamilyData* cfd,
PinnableWideColumns* columns);
// Dispatches between plain-value and wide-column direct-write resolution for
// read results observed before flush makes the corresponding blob file
// visible through normal Version metadata.
static bool MaybeResolveDirectWriteValue(
const ReadOptions& read_options, const Slice& key,
bool resolve_direct_write_value, const Version* current,
ColumnFamilyData* cfd, PinnableSlice* value, PinnableWideColumns* columns,
Status* s, bool* is_blob_index, bool* value_found = nullptr);
// Resolves memtable read results that still carry blob references through
// either a raw blob-index payload in `value` or unresolved blob columns in
// `columns`. Unlike the direct-write helper above, this path only depends on
// a BlobFetcher and therefore works for read-only/secondary DBs.
static bool MaybeResolveMemtableBlobValue(const Slice& key,
const BlobFetcher* blob_fetcher,
PinnableSlice* value,
PinnableWideColumns* columns,
Status* s, bool* is_blob_index,
bool* value_found = nullptr);
// Completes read-only/secondary memtable Get()/GetEntity() hits by resolving
// blob-backed payloads when `resolve_blob_backed_memtable_value` is true,
// pinning plain values on success, and clearing outputs on error. When the
// caller explicitly requested raw blob indices via
// `GetImplOptions::is_blob_index`, this helper leaves that payload
// untouched. `memtable_blob_fetcher` may be null when blob support is
// disabled for the column family.
static void PostprocessMemtableValueRead(
const Slice& key, const std::string* timestamp,
bool resolve_blob_backed_memtable_value,
const BlobFetcher* memtable_blob_fetcher, PinnableSlice* value,
PinnableWideColumns* columns, Status* s, bool* is_blob_index,
bool* value_found = nullptr);
template <typename IterType, typename ImplType,
typename ErrorIteratorFuncType>
@@ -3051,6 +3281,9 @@ class DBImpl : public DB {
// stores the number of compactions are currently running
int num_running_compactions_ = 0;
// stores the number of BOTTOM-priority compactions currently running
int num_running_bottom_compactions_ = 0;
// number of background memtable flush jobs, submitted to the HIGH pool
int bg_flush_scheduled_ = 0;
@@ -3060,6 +3293,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
@@ -3091,6 +3337,11 @@ class DBImpl : public DB {
// Used when disableWAL is true.
std::atomic<bool> has_unpersisted_data_{false};
// Number of live column families with an active blob direct write partition
// manager. This keeps the read/write fast paths at O(1) when the feature is
// completely disabled for the DB.
std::atomic<uint32_t> blob_direct_write_cf_count_{0};
// if an attempt was made to flush all column families that
// the oldest log depends on but uncommitted data in the oldest
// log prevents the log from being released.
@@ -3115,9 +3366,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;
@@ -3132,6 +3380,13 @@ class DBImpl : public DB {
// Callback for compaction to check if a key is visible to a snapshot.
// REQUIRES: mutex held
std::unique_ptr<SnapshotChecker> snapshot_checker_;
// When set, InitSnapshotContext() appends GetLastPublishedSequence() to the
// job's snapshot_seqs (if not already present) so that flush/compaction
// preserves the published-sequence boundary even when no explicit user
// snapshot exists there. Unlike taking a real ManagedSnapshot, this just
// pins the seqno into the snapshot list: no allocation, no snapshot list
// mutation, and no SnapshotChecker.
bool track_published_seq_in_snapshot_context_ = false;
// Callback for when the cached_recoverable_state_ is written to memtable
// Only to be set during initialization
@@ -3185,7 +3440,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_;
@@ -3322,6 +3577,18 @@ inline Status DBImpl::FailIfTsMismatchCf(ColumnFamilyHandle* column_family,
return Status::OK();
}
inline Status DBImpl::FailIfTableFilterWithRangeConversion(
const ReadOptions& read_options,
const MutableCFOptions& mutable_cf_options) const {
if (read_options.table_filter &&
mutable_cf_options.min_tombstones_for_range_conversion > 0) {
return Status::InvalidArgument(
"ReadOptions::table_filter is not supported when "
"min_tombstones_for_range_conversion > 0");
}
return Status::OK();
}
inline Status DBImpl::FailIfReadCollapsedHistory(const ColumnFamilyData* cfd,
const SuperVersion* sv,
const Slice& ts) const {
+302 -17
View File
@@ -8,7 +8,9 @@
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#include <cinttypes>
#include <deque>
#include <unordered_map>
#include "db/blob/blob_file_partition_manager.h"
#include "db/builder.h"
#include "db/db_impl/db_impl.h"
#include "db/error_handler.h"
@@ -215,7 +217,8 @@ Status DBImpl::FlushMemTableToOutputFile(
&event_logger_, mutable_cf_options.report_bg_io_stats,
true /* sync_output_directory */, true /* write_manifest */, thread_pri,
io_tracer_, cfd->GetSuperVersion()->ShareSeqnoToTimeMapping(), db_id_,
db_session_id_, cfd->GetFullHistoryTsLow(), &blob_callback_);
db_session_id_, cfd->GetFullHistoryTsLow(), &blob_callback_,
mutable_db_options_.fast_sst_open);
FileMetaData file_meta;
Status s;
@@ -279,12 +282,49 @@ Status DBImpl::FlushMemTableToOutputFile(
flush_reason);
bool switched_to_mempurge = false;
size_t prepared_blob_generations = 0;
std::vector<uint64_t> sealed_blob_numbers;
// Within flush_job.Run, rocksdb may call event listener to notify
// file creation and deletion.
//
// Note that flush_job.Run will unlock and lock the db_mutex,
// and EventListener callback will be called when the db_mutex
// is unlocked by the current thread.
if (s.ok()) {
auto mgr_handle = cfd->blob_partition_manager_handle();
auto* mgr = mgr_handle.get();
if (mgr != nullptr) {
prepared_blob_generations = flush_job.GetMemTables().size();
std::vector<BlobFileAddition> write_path_additions;
std::vector<BlobFileGarbage> write_path_garbages;
std::vector<std::vector<uint64_t>> generation_blob_file_numbers;
mutex_.Unlock();
s = mgr->PrepareFlushAdditions(
write_options, prepared_blob_generations, &write_path_additions,
&write_path_garbages, &generation_blob_file_numbers);
mutex_.Lock();
if (!s.ok()) {
prepared_blob_generations = 0;
} else if (!write_path_additions.empty()) {
const auto& memtables = flush_job.GetMemTables();
assert(generation_blob_file_numbers.size() == memtables.size());
for (size_t i = 0; i < generation_blob_file_numbers.size(); ++i) {
// Old SuperVersions can keep these memtables alive after the flush
// commits, so keep their sealed direct-write blob files protected
// from obsolete-file purge until the memtable is finally released.
memtables[i]->ProtectSealedBlobFiles(mgr_handle,
generation_blob_file_numbers[i]);
}
for (const auto& addition : write_path_additions) {
sealed_blob_numbers.push_back(addition.GetBlobFileNumber());
}
flush_job.AddExternalBlobFileAdditions(std::move(write_path_additions));
flush_job.AddExternalBlobFileGarbages(std::move(write_path_garbages));
} else if (!write_path_garbages.empty()) {
flush_job.AddExternalBlobFileGarbages(std::move(write_path_garbages));
}
}
}
if (s.ok()) {
s = flush_job.Run(&logs_with_prep_tracker_, &file_meta,
&switched_to_mempurge, &skip_set_bg_error,
@@ -292,10 +332,33 @@ Status DBImpl::FlushMemTableToOutputFile(
need_cancel = false;
}
if (cfd->blob_partition_manager() != nullptr &&
prepared_blob_generations > 0) {
auto unconsumed_additions = flush_job.TakeExternalBlobFileAdditions();
auto unconsumed_garbages = flush_job.TakeExternalBlobFileGarbages();
if (!s.ok() || !unconsumed_additions.empty() ||
!unconsumed_garbages.empty()) {
// Skip committing this attempt, but keep the prepared generations queued
// in pending_generations_. The same immutable memtables still reference
// these sealed blob files, so a later flush retry must publish the exact
// same additions rather than abandoning them here.
prepared_blob_generations = 0;
sealed_blob_numbers.clear();
}
}
if (!s.ok() && need_cancel) {
flush_job.Cancel();
}
if (s.ok() && prepared_blob_generations > 0) {
auto* mgr = cfd->blob_partition_manager();
mgr->CommitPreparedGenerations(prepared_blob_generations);
if (!sealed_blob_numbers.empty()) {
mgr->RemoveFilePartitionMappings(sealed_blob_numbers);
}
}
if (s.ok()) {
InstallSuperVersionAndScheduleWork(cfd, superversion_context);
if (made_progress) {
@@ -388,7 +451,12 @@ Status DBImpl::FlushMemTableToOutputFile(
Status DBImpl::FlushMemTablesToOutputFiles(
const autovector<BGFlushArg>& bg_flush_args, bool* made_progress,
JobContext* job_context, LogBuffer* log_buffer, Env::Priority thread_pri) {
if (immutable_db_options_.atomic_flush) {
#ifndef NDEBUG
for (const auto& bg_flush_arg : bg_flush_args) {
assert(bg_flush_arg.atomic_flush_ == bg_flush_args[0].atomic_flush_);
}
#endif /* !NDEBUG */
if (bg_flush_args[0].atomic_flush_) {
return AtomicFlushMemTablesToOutputFiles(
bg_flush_args, made_progress, job_context, log_buffer, thread_pri);
}
@@ -481,7 +549,8 @@ Status DBImpl::AtomicFlushMemTablesToOutputFiles(
false /* sync_output_directory */, false /* write_manifest */,
thread_pri, io_tracer_,
cfd->GetSuperVersion()->ShareSeqnoToTimeMapping(), db_id_,
db_session_id_, cfd->GetFullHistoryTsLow(), &blob_callback_));
db_session_id_, cfd->GetFullHistoryTsLow(), &blob_callback_,
mutable_db_options_.fast_sst_open));
}
std::vector<FileMetaData> file_meta(num_cfs);
@@ -530,11 +599,13 @@ Status DBImpl::AtomicFlushMemTablesToOutputFiles(
// <bool /* executed */, Status /* status code */>
autovector<std::pair<bool, Status>> exec_status;
std::vector<bool> pick_status;
std::vector<size_t> prepared_blob_generations(num_cfs, 0);
for (int i = 0; i != num_cfs; ++i) {
// Initially all jobs are not executed, with status OK.
exec_status.emplace_back(false, Status::OK());
pick_status.push_back(false);
}
std::unordered_map<int, std::vector<uint64_t>> sealed_blob_numbers_by_cf;
bool flush_for_recovery =
bg_flush_args[0].flush_reason_ == FlushReason::kErrorRecovery ||
@@ -558,6 +629,46 @@ Status DBImpl::AtomicFlushMemTablesToOutputFiles(
}
}
if (s.ok()) {
for (int i = 0; i != num_cfs; ++i) {
auto mgr_handle = cfds[i]->blob_partition_manager_handle();
auto* mgr = mgr_handle.get();
if (mgr == nullptr) {
continue;
}
prepared_blob_generations[i] = jobs[i]->GetMemTables().size();
std::vector<BlobFileAddition> write_path_additions;
std::vector<BlobFileGarbage> write_path_garbages;
std::vector<std::vector<uint64_t>> generation_blob_file_numbers;
mutex_.Unlock();
s = mgr->PrepareFlushAdditions(
write_options, prepared_blob_generations[i], &write_path_additions,
&write_path_garbages, &generation_blob_file_numbers);
mutex_.Lock();
if (!s.ok()) {
prepared_blob_generations[i] = 0;
break;
}
if (!write_path_additions.empty()) {
const auto& memtables = jobs[i]->GetMemTables();
assert(generation_blob_file_numbers.size() == memtables.size());
for (size_t j = 0; j < generation_blob_file_numbers.size(); ++j) {
memtables[j]->ProtectSealedBlobFiles(mgr_handle,
generation_blob_file_numbers[j]);
}
auto& sealed_blob_numbers = sealed_blob_numbers_by_cf[i];
for (const auto& addition : write_path_additions) {
sealed_blob_numbers.push_back(addition.GetBlobFileNumber());
}
jobs[i]->AddExternalBlobFileAdditions(std::move(write_path_additions));
}
if (!write_path_garbages.empty()) {
jobs[i]->AddExternalBlobFileGarbages(std::move(write_path_garbages));
}
}
}
if (s.ok()) {
assert(switched_to_mempurge.size() ==
static_cast<long unsigned int>(num_cfs));
@@ -763,6 +874,48 @@ Status DBImpl::AtomicFlushMemTablesToOutputFiles(
directories_.GetDbDir(), log_buffer);
}
if (!s.ok()) {
// If the atomic flush's combined MANIFEST write failed, the output files
// are cached in the table cache (added by BuildTable during FlushJob::Run)
// but never installed into any Version. Evict them now so that
// TEST_VerifyNoObsoleteFilesCached does not fire during Close() when the
// FindObsoleteFiles full-scan backstop is disabled by
// metadata_read_fault_one_in.
for (int i = 0; i != num_cfs; ++i) {
if (exec_status[i].first && exec_status[i].second.ok() &&
!switched_to_mempurge[i] && file_meta[i].fd.GetFileSize() > 0) {
TableCache::ReleaseObsolete(
cfds[i]->table_cache()->get_cache().get(),
file_meta[i].fd.GetNumber(), nullptr /*handle*/,
all_mutable_cf_options[i].uncache_aggressiveness);
}
}
}
for (int i = 0; i != num_cfs; ++i) {
auto* mgr = cfds[i]->blob_partition_manager();
if (mgr == nullptr || prepared_blob_generations[i] == 0) {
continue;
}
auto unconsumed_additions = jobs[i]->TakeExternalBlobFileAdditions();
auto unconsumed_garbages = jobs[i]->TakeExternalBlobFileGarbages();
if (!s.ok() || !unconsumed_additions.empty() ||
!unconsumed_garbages.empty()) {
// Same retry semantics as the single-CF flush path above: do not
// consume these prepared generations yet, because the rolled-back
// memtables still need to reuse the same sealed blob files on retry.
prepared_blob_generations[i] = 0;
continue;
}
mgr->CommitPreparedGenerations(prepared_blob_generations[i]);
auto it = sealed_blob_numbers_by_cf.find(i);
if (it != sealed_blob_numbers_by_cf.end() && !it->second.empty()) {
mgr->RemoveFilePartitionMappings(it->second);
}
}
if (s.ok()) {
assert(num_cfs ==
static_cast<int>(job_context->superversion_contexts.size()));
@@ -2037,7 +2190,7 @@ Status DBImpl::FlushAllColumnFamilies(const FlushOptions& flush_options,
FlushReason flush_reason) {
mutex_.AssertHeld();
Status status;
if (immutable_db_options_.atomic_flush) {
if (immutable_db_options_.atomic_flush || flush_options.force_atomic_flush) {
mutex_.Unlock();
status = AtomicFlushMemTables(flush_options, flush_reason);
if (status.IsColumnFamilyDropped()) {
@@ -2070,7 +2223,7 @@ Status DBImpl::Flush(const FlushOptions& flush_options,
ROCKS_LOG_INFO(immutable_db_options_.info_log, "[%s] Manual flush start.",
cfh->GetName().c_str());
Status s;
if (immutable_db_options_.atomic_flush) {
if (immutable_db_options_.atomic_flush || flush_options.force_atomic_flush) {
s = AtomicFlushMemTables(flush_options, FlushReason::kManualFlush,
{cfh->cfd()});
} else {
@@ -2086,7 +2239,8 @@ Status DBImpl::Flush(const FlushOptions& flush_options,
Status DBImpl::Flush(const FlushOptions& flush_options,
const std::vector<ColumnFamilyHandle*>& column_families) {
Status s;
if (!immutable_db_options_.atomic_flush) {
if (!immutable_db_options_.atomic_flush &&
!flush_options.force_atomic_flush) {
for (auto cfh : column_families) {
s = Flush(flush_options, cfh);
if (!s.ok()) {
@@ -2381,7 +2535,7 @@ void DBImpl::GenerateFlushRequest(const autovector<ColumnFamilyData*>& cfds,
continue;
}
uint64_t max_memtable_id = cfd->imm()->GetLatestMemTableID(
immutable_db_options_.atomic_flush /* for_atomic_flush */);
req->atomic_flush /* for_atomic_flush */);
req->cfd_to_max_mem_id_to_persist.emplace(cfd, max_memtable_id);
}
}
@@ -2451,7 +2605,8 @@ Status DBImpl::FlushMemTable(ColumnFamilyData* cfd,
if (cfd->imm()->NumNotFlushed() != 0 || !cfd->mem()->IsEmpty() ||
!cached_recoverable_state_empty_.load() ||
IsRecoveryFlush(flush_reason)) {
FlushRequest req{flush_reason, {{cfd, flush_memtable_id}}};
FlushRequest req{
flush_reason, false /* atomic_flush */, {{cfd, flush_memtable_id}}};
flush_reqs.emplace_back(std::move(req));
memtable_ids_to_wait.emplace_back(
cfd->imm()->GetLatestMemTableID(false /* for_atomic_flush */));
@@ -2479,7 +2634,9 @@ Status DBImpl::FlushMemTable(ColumnFamilyData* cfd,
"to avoid holding old logs",
cfd->GetName().c_str());
s = SwitchMemtable(cfd_stats, &context);
FlushRequest req{flush_reason, {{cfd_stats, flush_memtable_id}}};
FlushRequest req{flush_reason,
false /* atomic_flush */,
{{cfd_stats, flush_memtable_id}}};
flush_reqs.emplace_back(std::move(req));
memtable_ids_to_wait.emplace_back(
cfd_stats->imm()->GetLatestMemTableID(
@@ -2558,7 +2715,6 @@ Status DBImpl::AtomicFlushMemTables(
const FlushOptions& flush_options, FlushReason flush_reason,
const autovector<ColumnFamilyData*>& provided_candidate_cfds,
bool entered_write_thread) {
assert(immutable_db_options_.atomic_flush);
if (!flush_options.wait && write_controller_.IsStopped()) {
std::ostringstream oss;
oss << "Writes have been stopped, thus unable to perform manual flush. "
@@ -2613,6 +2769,7 @@ Status DBImpl::AtomicFlushMemTables(
}
const bool needs_to_join_write_thread = !entered_write_thread;
FlushRequest flush_req;
flush_req.atomic_flush = true;
autovector<ColumnFamilyData*> cfds;
{
WriteContext context;
@@ -2720,6 +2877,7 @@ Status DBImpl::RetryFlushesForErrorRecovery(FlushReason flush_reason,
autovector<uint64_t> flush_memtable_ids;
if (immutable_db_options_.atomic_flush) {
FlushRequest flush_req;
flush_req.atomic_flush = true;
GenerateFlushRequest(cfds, flush_reason, &flush_req);
EnqueuePendingFlush(flush_req);
for (auto& iter : flush_req.cfd_to_max_mem_id_to_persist) {
@@ -2733,6 +2891,7 @@ Status DBImpl::RetryFlushesForErrorRecovery(FlushReason flush_reason,
// reason to do so outside of atomic flush.
FlushRequest flush_req{
flush_reason,
false /* atomic_flush */,
{{cfd,
std::numeric_limits<uint64_t>::max() /* max_mem_id_to_persist */}}};
if (EnqueuePendingFlush(flush_req)) {
@@ -3145,6 +3304,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();
@@ -3166,11 +3412,11 @@ DBImpl::FlushRequest DBImpl::PopFirstFromFlushQueue() {
assert(!flush_queue_.empty());
FlushRequest flush_req = std::move(flush_queue_.front());
flush_queue_.pop_front();
if (!immutable_db_options_.atomic_flush) {
if (!flush_req.atomic_flush) {
assert(flush_req.cfd_to_max_mem_id_to_persist.size() == 1);
}
for (const auto& elem : flush_req.cfd_to_max_mem_id_to_persist) {
if (!immutable_db_options_.atomic_flush) {
if (!flush_req.atomic_flush) {
ColumnFamilyData* cfd = elem.first;
assert(cfd);
assert(cfd->queued_for_flush());
@@ -3215,7 +3461,7 @@ bool DBImpl::EnqueuePendingFlush(const FlushRequest& flush_req) {
if (flush_req.cfd_to_max_mem_id_to_persist.empty()) {
return enqueued;
}
if (!immutable_db_options_.atomic_flush) {
if (!flush_req.atomic_flush) {
// For the non-atomic flush case, we never schedule multiple column
// families in the same flush request.
assert(flush_req.cfd_to_max_mem_id_to_persist.size() == 1);
@@ -3231,6 +3477,11 @@ bool DBImpl::EnqueuePendingFlush(const FlushRequest& flush_req) {
enqueued = true;
}
} else {
// Atomic flush requests bypass the queued_for_flush() deduplication guard.
// This means an atomic request and a non-atomic request for the same CF
// can coexist in the flush queue. This is safe because PickMemtablesToFlush
// uses flush_in_progress_ to prevent double-picking of memtables, and
// BackgroundFlush filters out CFs where !IsFlushPending().
for (auto& iter : flush_req.cfd_to_max_mem_id_to_persist) {
ColumnFamilyData* cfd = iter.first;
cfd->Ref();
@@ -3394,7 +3645,7 @@ Status DBImpl::BackgroundFlush(bool* made_progress, JobContext* job_context,
}
return status;
}
if (!immutable_db_options_.atomic_flush &&
if (!flush_req.atomic_flush &&
ShouldRescheduleFlushRequestToRetainUDT(flush_req)) {
assert(flush_req.cfd_to_max_mem_id_to_persist.size() == 1);
ColumnFamilyData* cfd =
@@ -3443,7 +3694,8 @@ Status DBImpl::BackgroundFlush(bool* made_progress, JobContext* job_context,
}
superversion_contexts.emplace_back(true);
bg_flush_args.emplace_back(cfd, max_memtable_id,
&(superversion_contexts.back()), flush_reason);
&(superversion_contexts.back()), flush_reason,
flush_req.atomic_flush);
}
// `MaybeScheduleFlushOrCompaction` schedules as many `BackgroundCallFlush`
// jobs as the number of `FlushRequest` in the `flush_queue_`, a.k.a
@@ -3580,6 +3832,9 @@ void DBImpl::BackgroundCallFlush(Env::Priority thread_pri) {
bg_flush_scheduled_--;
// See if there's more work to be done
MaybeScheduleFlushOrCompaction();
NotifyOnBackgroundJobPressureChanged();
atomic_flush_install_cv_.SignalAll();
bg_cv_.SignalAll();
// IMPORTANT: there should be no code after calling SignalAll. This call may
@@ -3604,6 +3859,9 @@ void DBImpl::BackgroundCallCompaction(PrepickedCompaction* prepicked_compaction,
InstrumentedMutexLock l(&mutex_);
num_running_compactions_++;
if (bg_thread_pri == Env::Priority::BOTTOM) {
num_running_bottom_compactions_++;
}
std::unique_ptr<std::list<uint64_t>::iterator>
pending_outputs_inserted_elem(new std::list<uint64_t>::iterator(
@@ -3686,11 +3944,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
@@ -4870,6 +5132,11 @@ void DBImpl::MarkAsGrabbedForPurge(uint64_t file_number) {
files_grabbed_for_purge_.insert(file_number);
}
void DBImpl::EnableTrackPublishedSeqInSnapshotContext() {
InstrumentedMutexLock l(&mutex_);
track_published_seq_in_snapshot_context_ = true;
}
void DBImpl::SetSnapshotChecker(SnapshotChecker* snapshot_checker) {
InstrumentedMutexLock l(&mutex_);
// snapshot_checker_ should only set once. If we need to set it multiple
@@ -4903,6 +5170,19 @@ void DBImpl::InitSnapshotContext(JobContext* job_context) {
SequenceNumber earliest_write_conflict_snapshot = kMaxSequenceNumber;
std::vector<SequenceNumber> snapshot_seqs =
snapshots_.GetAll(&earliest_write_conflict_snapshot);
// The SnapshotChecker path above already adds a managed snapshot to
// snapshot_seqs.
if (track_published_seq_in_snapshot_context_ && !snapshot_checker) {
// Pin the published-sequence boundary into snapshot_seqs without taking
// a real snapshot. This prevents flush/compaction from collapsing a version
// visible at the published boundary into a newer unpublished seqno.
const SequenceNumber published_seq = GetLastPublishedSequence();
if (snapshot_seqs.empty() || snapshot_seqs.back() < published_seq) {
snapshot_seqs.push_back(published_seq);
}
}
TEST_SYNC_POINT("DBImpl::InitSnapshotContext:BeforeInit");
job_context->InitSnapshotContext(
snapshot_checker, std::move(managed_snapshot),
earliest_write_conflict_snapshot, std::move(snapshot_seqs));
@@ -4911,6 +5191,10 @@ void DBImpl::InitSnapshotContext(JobContext* job_context) {
Status DBImpl::WaitForCompact(
const WaitForCompactOptions& wait_for_compact_options) {
InstrumentedMutexLock l(&mutex_);
// `close_db=true` needs the same direct-write cleanup guarantee as DB close:
// register live blob files before relying on the DB being reopenable.
const bool flush_for_blob_direct_write =
HasAnyBlobDirectWriteColumnFamilyWithLockHeld();
if (wait_for_compact_options.flush) {
Status s = DBImpl::FlushAllColumnFamilies(FlushOptions(),
FlushReason::kManualFlush);
@@ -4918,8 +5202,9 @@ Status DBImpl::WaitForCompact(
return s;
}
} else if (wait_for_compact_options.close_db &&
has_unpersisted_data_.load(std::memory_order_relaxed) &&
!mutable_db_options_.avoid_flush_during_shutdown) {
(flush_for_blob_direct_write ||
(has_unpersisted_data_.load(std::memory_order_relaxed) &&
!mutable_db_options_.avoid_flush_during_shutdown))) {
Status s =
DBImpl::FlushAllColumnFamilies(FlushOptions(), FlushReason::kShutDown);
if (!s.ok()) {
+13 -1
View File
@@ -8,14 +8,17 @@
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#ifndef NDEBUG
#include <iostream>
#include <unordered_set>
#include "db/blob/blob_file_cache.h"
#include "db/blob/blob_file_partition_manager.h"
#include "db/column_family.h"
#include "db/db_impl/db_impl.h"
#include "db/error_handler.h"
#include "db/periodic_task_scheduler.h"
#include "monitoring/thread_status_updater.h"
#include "util/cast_util.h"
#include "util/hash_containers.h"
namespace ROCKSDB_NAMESPACE {
uint64_t DBImpl::TEST_GetLevel0TotalSize() {
@@ -282,6 +285,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();
@@ -352,11 +360,15 @@ void DBImpl::TEST_VerifyNoObsoleteFilesCached(
}
// Live and "quarantined" files are allowed to be open in table cache
std::set<uint64_t> live_and_quar_files;
UnorderedSet<uint64_t> live_and_quar_files;
for (auto cfd : *versions_->GetColumnFamilySet()) {
if (cfd->IsDropped()) {
continue;
}
if (auto* mgr = cfd->blob_partition_manager(); mgr != nullptr) {
mgr->GetActiveBlobFileNumbers(&live_and_quar_files);
mgr->GetProtectedBlobFileNumbers(&live_and_quar_files);
}
// Iterate over live versions
Version* current = cfd->current();
Version* ver = current;
Loaded 100 of 408 files, more files were not shown because too many files have changed in this diff. Show more