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
148 changed files with 7041 additions and 1692 deletions

No files matched your search

@@ -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');
};
+33 -30
View File
@@ -7,11 +7,25 @@
// Parameters:
// executionFile - path to the JSON execution log from claude-code-base-action
// conclusion - 'success' or 'failure' from the action output
// meta - { trigger, headSha, reviewer, isQuery, isPartial }
// 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 {
@@ -80,36 +94,25 @@ module.exports = function parseClaude({executionFile, conclusion, meta}) {
const isPartial = !!meta.isPartial;
const icon = isPartial ? '🟡' : (conclusion === 'success' ? '✅' : '⚠️');
const headerTitle = meta.isQuery ? 'Claude Response' : 'Claude Code Review';
const triggerLine = meta.trigger === 'auto' ?
`*Auto-triggered after CI passed — reviewing commit ${
meta.headSha.substring(0, 7)}*` :
`*Requested by @${meta.reviewer}*`;
const triggerLine = getTriggerLine();
return [
`## ${icon} ${headerTitle}`,
'',
return buildComment({
icon,
headerTitle,
triggerLine,
'',
'---',
'',
responseBody,
'',
'---',
'',
'<details>',
'<summary>ℹ️ About this response</summary>',
'',
'Generated by [Claude Code](https://github.com/anthropics/claude-code-action).',
'Review methodology: `claude_md/code_review.md`',
'',
'**Limitations:**',
'- Claude may miss context from files not in the diff',
'- Large PRs may be truncated',
'- Always apply human judgment to AI suggestions',
'',
'**Commands:**',
'- `/claude-review [context]` — Request a code review',
'- `/claude-query <question>` — Ask about the PR or codebase',
'</details>',
].join('\n');
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',
],
});
};
+165 -18
View File
@@ -14,9 +14,31 @@
// 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}) {
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;
@@ -28,30 +50,155 @@ module.exports = async function postPrComment(
// Ensure marker is embedded in the body
const markedBody = body.includes(marker) ? body : `${marker}\n${body}`;
// Search for existing comment with this marker
const {data: comments} = await github.rest.issues.listComments({
owner,
repo,
issue_number: prNumber,
per_page: 100,
});
const existing = comments.find(c => c.body.includes(marker));
if (existing) {
await github.rest.issues.updateComment({
async function listComments() {
return await github.paginate(github.rest.issues.listComments, {
owner,
repo,
comment_id: existing.id,
body: markedBody,
issue_number: prNumber,
per_page: 100,
});
core.info(`Updated existing comment ${existing.id}`);
} else {
await github.rest.issues.createComment({
}
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.'
);
+10 -131
View File
@@ -1,12 +1,8 @@
# Claude Code Review — Comment Posting Workflow
#
# Companion to claude-review.yml. Downloads the review artifact and posts
# it as a PR comment. This workflow has write permissions but never runs
# AI or processes untrusted code.
#
# Security: Separating analysis (has ANTHROPIC_API_KEY, no write perms)
# from posting (has GITHUB_TOKEN write perms, no AI) prevents a crafted
# PR from tricking Claude into exfiltrating tokens.
# 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
@@ -20,127 +16,10 @@ permissions:
issues: write
jobs:
comment:
if: github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
steps:
- name: Checkout scripts
uses: actions/checkout@v4
with:
sparse-checkout: .github/scripts
sparse-checkout-cone-mode: true
- name: Download review artifact
id: download
uses: actions/download-artifact@v4
with:
name: claude-review-result
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
continue-on-error: true
- name: Post or update PR comment
if: steps.download.outcome == 'success'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
if (!fs.existsSync('claude-review-comment.md') || !fs.existsSync('pr_number.txt')) {
core.info('No review results found; skipping.');
return;
}
const body = fs.readFileSync('claude-review-comment.md', 'utf8');
const prNumber = parseInt(fs.readFileSync('pr_number.txt', 'utf8').trim());
const trigger = fs.existsSync('trigger_type.txt')
? fs.readFileSync('trigger_type.txt', 'utf8').trim()
: 'auto';
// Use different markers for auto vs manual so they don't
// overwrite each other. Auto-reviews update in place (one
// per PR). Manual reviews always create new comments.
const marker = trigger === 'auto'
? '<!-- claude-review-auto -->'
: `<!-- claude-review-manual-${Date.now()} -->`;
const post = require('./.github/scripts/post-pr-comment.js');
await post({
github,
context,
core,
prNumber,
body,
marker,
});
- name: Add reaction to trigger comment
if: steps.download.outcome == 'success'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
if (!fs.existsSync('trigger_type.txt')) return;
const trigger = fs.readFileSync('trigger_type.txt', 'utf8').trim();
if (trigger !== 'manual') return;
if (!fs.existsSync('comment_id.txt')) return;
const commentId = parseInt(fs.readFileSync('comment_id.txt', 'utf8').trim());
if (!commentId) return;
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: commentId,
content: 'rocket'
});
# Notify on failure (for manual triggers)
failure-notice:
if: github.event.workflow_run.conclusion == 'failure'
runs-on: ubuntu-latest
steps:
- name: Download artifact for PR number
id: download
uses: actions/download-artifact@v4
with:
name: claude-review-result
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
continue-on-error: true
- name: React with failure
if: steps.download.outcome == 'success'
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
if (!fs.existsSync('comment_id.txt')) return;
const commentId = parseInt(fs.readFileSync('comment_id.txt', 'utf8').trim());
if (!commentId) return;
await github.rest.reactions.createForIssueComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: commentId,
content: 'confused'
});
# Unauthorized user notice
unauthorized-notice:
runs-on: ubuntu-latest
if: >-
github.event.workflow_run.event == 'issue_comment' &&
github.event.workflow_run.conclusion == 'skipped'
steps:
- name: Check if unauthorized
id: check
uses: actions/github-script@v7
with:
script: |
// This job only runs when the analysis workflow was skipped,
// which happens when the user is not in the authorized list.
// We cannot easily get the original comment from workflow_run,
// so this is a best-effort notice via workflow logs.
core.info('Analysis workflow was skipped — likely unauthorized user.');
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
+38 -561
View File
@@ -1,20 +1,8 @@
# Claude Code Review — Analysis Workflow
#
# This workflow runs Claude to review PRs. It produces a markdown review
# as an artifact but does NOT post comments (no write permissions).
# The companion workflow (claude-review-comment.yml) handles posting.
#
# Triggers:
# 1. workflow_run: Auto-review after pr-jobs passes
# 2. issue_comment: Manual /claude-review or /claude-query by maintainers
# 3. workflow_dispatch: Manual testing
#
# Security:
# - This job has contents:read ONLY (no PR write, no issue write)
# - ANTHROPIC_API_KEY is used here but never coexists with write tokens
# - Claude uses read-only tools (View, GlobTool, GrepTool) plus Write
# (only for incremental findings file, not repo modifications)
# - Review methodology: claude_md/code_review.md
# 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
@@ -23,6 +11,11 @@ on:
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]
@@ -33,560 +26,44 @@ on:
required: true
type: number
model:
description: Claude model to use
description: Claude model to use (defaults to latest Opus)
required: false
type: choice
options:
- claude-opus-4-6
- claude-sonnet-4-20250514
- 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:
# ======================== Auto Review ======================== #
auto-review:
name: Auto Claude Review
runs-on: ubuntu-latest
permissions:
contents: read
actions: read
review:
if: >-
github.event_name == 'workflow_run' &&
github.event.workflow_run.conclusion == 'success' &&
github.event.workflow_run.event == 'pull_request'
steps:
- name: Get PR info
id: pr_info
uses: actions/github-script@v7
with:
script: |
const headSha = context.payload.workflow_run.head_sha;
let prNumber = null;
const prs = context.payload.workflow_run.pull_requests;
if (prs && prs.length > 0) {
prNumber = prs[0].number;
} else {
// PR may not be registered yet if push and PR creation raced.
// Retry up to 5 times with 10s delay.
for (let attempt = 0; attempt < 5; attempt++) {
if (attempt > 0) {
core.info(`PR not found for SHA ${headSha}, retrying in 10s (attempt ${attempt + 1}/5)...`);
await new Promise(r => setTimeout(r, 10000));
}
const { data: pulls } = await github.rest.pulls.list({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'open',
sort: 'updated',
direction: 'desc',
per_page: 100
});
const match = pulls.find(p => p.head.sha === headSha);
if (match) {
prNumber = match.number;
break;
}
}
}
if (!prNumber) {
core.warning(
`Could not find PR for SHA ${headSha} after retries. ` +
`This can happen when the PR head advanced before this ` +
`workflow_run-triggered job started. Skipping stale review run.`
);
core.setOutput('skip', 'true');
core.setOutput('skip_reason', `stale_sha:${headSha}`);
return;
}
const pr = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber
});
core.setOutput('skip', 'false');
core.setOutput('pr_number', prNumber.toString());
core.setOutput('head_repo', pr.data.head.repo.clone_url);
core.setOutput('head_ref', pr.data.head.ref);
core.setOutput('head_sha', pr.data.head.sha);
core.setOutput('base_sha', pr.data.base.sha);
core.setOutput('base_ref', pr.data.base.ref);
core.setOutput('pr_title', pr.data.title);
core.setOutput('pr_body', pr.data.body || '');
core.setOutput('pr_author', pr.data.user.login);
- name: Skip stale run
if: steps.pr_info.outputs.skip == 'true'
run: |-
echo "Skipping stale auto-review run: ${{ steps.pr_info.outputs.skip_reason }}"
- name: Check for existing review
if: steps.pr_info.outputs.skip != 'true'
id: check_existing
uses: actions/github-script@v7
env:
PR_NUMBER: ${{ steps.pr_info.outputs.pr_number }}
HEAD_SHA: ${{ steps.pr_info.outputs.head_sha }}
with:
script: |
const comments = await github.rest.issues.listComments({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: parseInt(process.env.PR_NUMBER),
per_page: 100
});
const shortSha = process.env.HEAD_SHA.substring(0, 7);
const existing = comments.data.find(c =>
c.body.includes('<!-- claude-review -->') &&
c.body.includes(shortSha)
);
core.setOutput('skip', existing ? 'true' : 'false');
- name: Checkout base repository
if: steps.pr_info.outputs.skip != 'true' && steps.check_existing.outputs.skip != 'true'
uses: actions/checkout@v4
with:
ref: ${{ steps.pr_info.outputs.base_ref }}
fetch-depth: 0
persist-credentials: false
- name: Generate diff and prompt
if: steps.pr_info.outputs.skip != 'true' && steps.check_existing.outputs.skip != 'true'
env:
HEAD_REPO: ${{ steps.pr_info.outputs.head_repo }}
HEAD_REF: ${{ steps.pr_info.outputs.head_ref }}
HEAD_SHA: ${{ steps.pr_info.outputs.head_sha }}
BASE_SHA: ${{ steps.pr_info.outputs.base_sha }}
PR_TITLE: ${{ steps.pr_info.outputs.pr_title }}
PR_BODY: ${{ steps.pr_info.outputs.pr_body }}
PR_AUTHOR: ${{ steps.pr_info.outputs.pr_author }}
run: |
git remote add fork "${HEAD_REPO}" || true
git fetch fork "${HEAD_REF}" --depth=100
git diff "${BASE_SHA}...${HEAD_SHA}" > /tmp/pr.diff
DIFF_STATS=$(git diff --stat "${BASE_SHA}...${HEAD_SHA}" | tail -1)
DIFF_SIZE=$(wc -c < /tmp/pr.diff)
IS_TRUNCATED=false
if [ "$DIFF_SIZE" -gt 100000 ]; then
head -c 100000 /tmp/pr.diff > /tmp/pr_truncated.diff
mv /tmp/pr_truncated.diff /tmp/pr.diff
IS_TRUNCATED=true
fi
cat claude_md/ci_review_prompt.md > /tmp/prompt.txt
cat >> /tmp/prompt.txt << PROMPT_EOF
## Pull Request Information
- **Title:** ${PR_TITLE}
- **Author:** ${PR_AUTHOR}
- **Changes:** ${DIFF_STATS}
PROMPT_EOF
if [ "${IS_TRUNCATED}" = "true" ]; then
echo "- **Note:** Diff truncated due to size." >> /tmp/prompt.txt
fi
printf '\n## PR Description\n' >> /tmp/prompt.txt
printf '%s\n' "${PR_BODY}" >> /tmp/prompt.txt
printf '\n---\n\n## Diff to Review\n\n' >> /tmp/prompt.txt
cat /tmp/pr.diff >> /tmp/prompt.txt
- name: Run Claude
if: steps.pr_info.outputs.skip != 'true' && steps.check_existing.outputs.skip != 'true'
id: claude
uses: anthropics/claude-code-base-action@beta
with:
prompt_file: /tmp/prompt.txt
model: claude-opus-4-6
max_turns: "300"
timeout_minutes: "60"
allowed_tools: "View,GlobTool,GrepTool,Write,Task"
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
- name: Recover partial review on turn limit
if: steps.pr_info.outputs.skip != 'true' && steps.check_existing.outputs.skip != 'true'
id: recover
uses: actions/github-script@v7
env:
EXECUTION_FILE: ${{ steps.claude.outputs.execution_file }}
with:
script: |
const fs = require('fs');
const log = JSON.parse(fs.readFileSync(process.env.EXECUTION_FILE, 'utf8'));
const result = log.find(m => m.type === 'result');
const hitLimit = result && result.subtype === 'error_max_turns';
const hasFindings = fs.existsSync('review-findings.md');
core.setOutput('hit_limit', hitLimit ? 'true' : 'false');
core.setOutput('has_findings', hasFindings ? 'true' : 'false');
core.setOutput('needs_recovery', (hitLimit && hasFindings) ? 'true' : 'false');
- name: Format partial findings
if: >-
steps.pr_info.outputs.skip != 'true' &&
steps.check_existing.outputs.skip != 'true' &&
steps.recover.outputs.needs_recovery == 'true'
id: format_recovery
uses: anthropics/claude-code-base-action@beta
with:
prompt_file: claude_md/ci_recovery_prompt.md
model: claude-sonnet-4-20250514
max_turns: "10"
allowed_tools: "View,GlobTool,GrepTool"
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
- name: Build review comment
if: steps.pr_info.outputs.skip != 'true' && steps.check_existing.outputs.skip != 'true'
uses: actions/github-script@v7
env:
EXECUTION_FILE: ${{ steps.claude.outputs.execution_file }}
RECOVERY_FILE: ${{ steps.format_recovery.outputs.execution_file }}
CONCLUSION: ${{ steps.claude.outputs.conclusion }}
HEAD_SHA: ${{ steps.pr_info.outputs.head_sha }}
NEEDS_RECOVERY: ${{ steps.recover.outputs.needs_recovery }}
with:
script: |
const parse = require('./.github/scripts/parse-claude-review.js');
const fs = require('fs');
let execFile = process.env.EXECUTION_FILE;
if (process.env.NEEDS_RECOVERY === 'true') {
const rf = process.env.RECOVERY_FILE;
if (rf && fs.existsSync(rf)) {
execFile = rf;
}
}
const body = parse({
executionFile: execFile,
conclusion: process.env.CONCLUSION,
meta: {
trigger: 'auto',
headSha: process.env.HEAD_SHA,
reviewer: '',
isQuery: false,
isPartial: process.env.NEEDS_RECOVERY === 'true',
}
});
fs.writeFileSync('claude-review-comment.md', body);
- name: Save PR number
if: steps.pr_info.outputs.skip != 'true' && steps.check_existing.outputs.skip != 'true'
run: echo "${{ steps.pr_info.outputs.pr_number }}" > pr_number.txt
- name: Save trigger metadata
if: steps.pr_info.outputs.skip != 'true' && steps.check_existing.outputs.skip != 'true'
run: echo "auto" > trigger_type.txt
- name: Upload review artifact
if: always() && steps.pr_info.outputs.skip != 'true' && steps.check_existing.outputs.skip != 'true'
uses: actions/upload-artifact@v4
with:
name: claude-review-result
path: |
claude-review-comment.md
pr_number.txt
trigger_type.txt
if-no-files-found: ignore
retention-days: 1
- name: Upload execution log
if: always() && steps.pr_info.outputs.skip != 'true' && steps.check_existing.outputs.skip != 'true'
uses: actions/upload-artifact@v4
with:
name: claude-review-execution-log
path: ${{ steps.claude.outputs.execution_file }}
retention-days: 7
if-no-files-found: warn
- name: Upload recovery execution log
if: always() && steps.pr_info.outputs.skip != 'true' && steps.recover.outputs.needs_recovery == 'true'
uses: actions/upload-artifact@v4
with:
name: claude-review-recovery-log
path: ${{ steps.format_recovery.outputs.execution_file }}
retention-days: 7
if-no-files-found: ignore
# ======================== Manual Review ======================== #
manual-review:
name: Manual Claude Review
runs-on: ubuntu-latest
permissions:
contents: read
if: >-
github.event_name == 'workflow_dispatch' ||
(
github.event_name == 'issue_comment' &&
github.event.issue.pull_request &&
(contains(github.event.comment.body, '/claude-review') || contains(github.event.comment.body, '/claude-query')) &&
contains(fromJSON('["pdillinger", "jaykorean", "hx235", "dannyhchen", "joshkang97", "zaidoon1", "omkarhgawde", "xingbowang", "anand1976", "virajthakur", "nmk70", "archang19", "mszeszko-meta", "yoshinorim", "cbi42"]'), github.event.comment.user.login)
)
steps:
- name: Detect command type
id: command
uses: actions/github-script@v7
with:
script: |
if (context.eventName === 'workflow_dispatch') {
core.setOutput('type', 'review');
core.setOutput('query', '');
return;
}
const body = context.payload.comment.body;
if (body.includes('/claude-review')) {
core.setOutput('type', 'review');
const match = body.match(/\/claude-review\s+([\s\S]*)/);
core.setOutput('query', match ? match[1].trim() : '');
} else if (body.includes('/claude-query')) {
const match = body.match(/\/claude-query\s+([\s\S]*)/);
const query = match ? match[1].trim() : '';
if (!query) {
core.setFailed('No query provided. Usage: /claude-query <your question>');
return;
}
core.setOutput('type', 'query');
core.setOutput('query', query);
} else {
core.setFailed('Unknown command');
}
- name: Get PR details
id: pr_info
uses: actions/github-script@v7
env:
INPUT_PR_NUMBER: ${{ inputs.pr_number }}
with:
script: |
const prNumber = context.eventName === 'workflow_dispatch'
? parseInt(process.env.INPUT_PR_NUMBER, 10)
: context.issue.number;
const pr = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: prNumber
});
core.setOutput('pr_number', prNumber.toString());
core.setOutput('head_repo', pr.data.head.repo.clone_url);
core.setOutput('head_ref', pr.data.head.ref);
core.setOutput('head_sha', pr.data.head.sha);
core.setOutput('base_sha', pr.data.base.sha);
core.setOutput('base_ref', pr.data.base.ref);
core.setOutput('pr_title', pr.data.title);
core.setOutput('pr_body', pr.data.body || '');
core.setOutput('pr_author', pr.data.user.login);
- name: Checkout base repository
uses: actions/checkout@v4
with:
ref: ${{ steps.pr_info.outputs.base_ref }}
fetch-depth: 0
persist-credentials: false
- name: Generate diff and prompt
env:
HEAD_REPO: ${{ steps.pr_info.outputs.head_repo }}
HEAD_REF: ${{ steps.pr_info.outputs.head_ref }}
HEAD_SHA: ${{ steps.pr_info.outputs.head_sha }}
BASE_SHA: ${{ steps.pr_info.outputs.base_sha }}
PR_TITLE: ${{ steps.pr_info.outputs.pr_title }}
PR_BODY: ${{ steps.pr_info.outputs.pr_body }}
PR_AUTHOR: ${{ steps.pr_info.outputs.pr_author }}
DIFF_STATS: ""
COMMAND_TYPE: ${{ steps.command.outputs.type }}
ADDITIONAL_CONTEXT: ${{ steps.command.outputs.query }}
USER_QUERY: ${{ steps.command.outputs.query }}
run: |
git remote add fork "${HEAD_REPO}" || true
git fetch fork "${HEAD_REF}" --depth=100
git diff "${BASE_SHA}...${HEAD_SHA}" > /tmp/pr.diff
DIFF_STATS=$(git diff --stat "${BASE_SHA}...${HEAD_SHA}" | tail -1)
DIFF_SIZE=$(wc -c < /tmp/pr.diff)
IS_TRUNCATED=false
if [ "$DIFF_SIZE" -gt 100000 ]; then
head -c 100000 /tmp/pr.diff > /tmp/pr_truncated.diff
mv /tmp/pr_truncated.diff /tmp/pr.diff
IS_TRUNCATED=true
fi
if [ "${COMMAND_TYPE}" = "query" ]; then
# Query prompt
cat claude_md/ci_query_prompt.md > /tmp/prompt.txt
printf '\n---\n\n' >> /tmp/prompt.txt
cat >> /tmp/prompt.txt << PROMPT_EOF
## Pull Request Context
- **Title:** ${PR_TITLE}
- **Author:** ${PR_AUTHOR}
- **Changes:** ${DIFF_STATS}
## PR Description
PROMPT_EOF
printf '%s\n' "${PR_BODY}" >> /tmp/prompt.txt
printf '\n## Question\n' >> /tmp/prompt.txt
printf '%s\n' "${USER_QUERY}" >> /tmp/prompt.txt
printf '\n---\n## PR Diff (for reference)\n' >> /tmp/prompt.txt
cat /tmp/pr.diff >> /tmp/prompt.txt
else
# Review prompt
cat claude_md/ci_review_prompt.md > /tmp/prompt.txt
cat >> /tmp/prompt.txt << PROMPT_EOF
## Pull Request Information
- **Title:** ${PR_TITLE}
- **Author:** ${PR_AUTHOR}
- **Changes:** ${DIFF_STATS}
PROMPT_EOF
if [ "${IS_TRUNCATED}" = "true" ]; then
echo "- **Note:** Diff truncated due to size." >> /tmp/prompt.txt
fi
printf '\n## PR Description\n' >> /tmp/prompt.txt
printf '%s\n' "${PR_BODY}" >> /tmp/prompt.txt
if [ -n "${ADDITIONAL_CONTEXT}" ]; then
printf '\n## Additional Instructions from Reviewer\n' >> /tmp/prompt.txt
printf '%s\n' "${ADDITIONAL_CONTEXT}" >> /tmp/prompt.txt
fi
printf '\n---\n\n## Diff to Review\n\n' >> /tmp/prompt.txt
cat /tmp/pr.diff >> /tmp/prompt.txt
fi
- name: Run Claude
id: claude
uses: anthropics/claude-code-base-action@beta
with:
prompt_file: /tmp/prompt.txt
model: ${{ github.event_name == 'workflow_dispatch' && inputs.model || 'claude-opus-4-6' }}
max_turns: "300"
timeout_minutes: "60"
allowed_tools: "View,GlobTool,GrepTool,Write,Task"
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
- name: Recover partial review on turn limit
id: recover
uses: actions/github-script@v7
env:
EXECUTION_FILE: ${{ steps.claude.outputs.execution_file }}
COMMAND_TYPE: ${{ steps.command.outputs.type }}
with:
script: |
const fs = require('fs');
// Skip recovery for queries — only reviews use incremental findings
if (process.env.COMMAND_TYPE === 'query') {
core.setOutput('needs_recovery', 'false');
return;
}
const log = JSON.parse(fs.readFileSync(process.env.EXECUTION_FILE, 'utf8'));
const result = log.find(m => m.type === 'result');
const hitLimit = result && result.subtype === 'error_max_turns';
const hasFindings = fs.existsSync('review-findings.md');
core.setOutput('hit_limit', hitLimit ? 'true' : 'false');
core.setOutput('has_findings', hasFindings ? 'true' : 'false');
core.setOutput('needs_recovery', (hitLimit && hasFindings) ? 'true' : 'false');
- name: Format partial findings
if: steps.recover.outputs.needs_recovery == 'true'
id: format_recovery
uses: anthropics/claude-code-base-action@beta
with:
prompt_file: claude_md/ci_recovery_prompt.md
model: claude-sonnet-4-20250514
max_turns: "10"
allowed_tools: "View,GlobTool,GrepTool"
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
- name: Build review comment
uses: actions/github-script@v7
env:
EXECUTION_FILE: ${{ steps.claude.outputs.execution_file }}
RECOVERY_FILE: ${{ steps.format_recovery.outputs.execution_file }}
CONCLUSION: ${{ steps.claude.outputs.conclusion }}
HEAD_SHA: ${{ steps.pr_info.outputs.head_sha }}
REVIEWER: ${{ github.event_name == 'workflow_dispatch' && github.actor || github.event.comment.user.login }}
COMMAND_TYPE: ${{ steps.command.outputs.type }}
NEEDS_RECOVERY: ${{ steps.recover.outputs.needs_recovery }}
with:
script: |
const parse = require('./.github/scripts/parse-claude-review.js');
const fs = require('fs');
const isReview = process.env.COMMAND_TYPE === 'review';
let execFile = process.env.EXECUTION_FILE;
if (process.env.NEEDS_RECOVERY === 'true') {
const rf = process.env.RECOVERY_FILE;
if (rf && fs.existsSync(rf)) {
execFile = rf;
}
}
const body = parse({
executionFile: execFile,
conclusion: process.env.CONCLUSION,
meta: {
trigger: 'manual',
headSha: process.env.HEAD_SHA,
reviewer: process.env.REVIEWER,
isQuery: !isReview,
isPartial: process.env.NEEDS_RECOVERY === 'true',
}
});
fs.writeFileSync('claude-review-comment.md', body);
- name: Save PR number
run: echo "${{ steps.pr_info.outputs.pr_number }}" > pr_number.txt
- name: Save trigger metadata
env:
REVIEWER: ${{ github.event_name == 'workflow_dispatch' && github.actor || github.event.comment.user.login }}
COMMENT_ID: ${{ github.event.comment.id }}
run: |
echo "manual" > trigger_type.txt
echo "${REVIEWER}" > reviewer.txt
echo "${COMMENT_ID}" > comment_id.txt
- name: Upload review artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: claude-review-result
path: |
claude-review-comment.md
pr_number.txt
trigger_type.txt
reviewer.txt
comment_id.txt
if-no-files-found: ignore
retention-days: 1
- name: Upload execution log
if: always()
uses: actions/upload-artifact@v4
with:
name: claude-review-execution-log
path: ${{ steps.claude.outputs.execution_file }}
retention-days: 7
if-no-files-found: warn
- name: Upload recovery execution log
if: always() && steps.recover.outputs.needs_recovery == 'true'
uses: actions/upload-artifact@v4
with:
name: claude-review-recovery-log
path: ${{ steps.format_recovery.outputs.execution_file }}
retention-days: 7
if-no-files-found: ignore
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
+1 -14
View File
@@ -1,20 +1,6 @@
# Rocksdb Change Log
> NOTE: Entries for next release do not go here. Follow instructions in `unreleased_history/README.txt`
## 11.2.4 (06/24/2026)
### Bug Fixes
* Fixed a bug where closing a read-only DB instance could delete live SST files created by a concurrent read-write DB sharing the same directory.
## 11.2.3 (05/07/2026)
### New Features
* Add experimental mutable DBOption `optimize_manifest_for_recovery` (default false). When enabled, RocksDB can reduce recovery work after a clean shutdown, which may lower DB::Open latency on warm reopens.
* Add experimental `reuse_manifest_on_open` DBOption (default false). When enabled, DB::Open reuses the existing MANIFEST file for append instead of creating a fresh one, avoiding the cost of serializing the entire database state into a new MANIFEST on the first post-open write.
## 11.2.2 (05/07/2026)
## 11.2.1 (04/29/2026)
### Bug Fixes
* Fix bug in range tombstone synthesis that covers live keys added during an IngestExternalFile
## 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.
@@ -35,6 +21,7 @@
* 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.
+7
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
@@ -1268,6 +1272,8 @@ 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 \
@@ -1283,6 +1289,7 @@ install-hooks:
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 \
+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
+7
View File
@@ -7,6 +7,13 @@ 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
+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.
+52 -12
View File
@@ -1,14 +1,54 @@
Read review-findings.md. This file contains partial code
review findings from a review session that ran out of turns before
finishing. Format them into a clean final review comment using the
output format defined in claude_md/code_review.md (Summary, Issues
Found, Cross-Component Analysis, Positive Observations).
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.
At the top of the review, add this notice:
> **Partial review** -- the analysis was interrupted before all
> perspectives were completed. The findings below cover the
> perspectives that were finished. You can request a full re-review
> with `/claude-review`.
## Required Output Structure
Write ONLY the formatted review as your final response -- no
commentary or explanation outside the review itself.
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.
+55
View File
@@ -481,6 +481,61 @@ Team lead synthesizes the debate into a consensus document:
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)
+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
+27 -16
View File
@@ -54,13 +54,14 @@ class RoundRobinBlobFilePartitionStrategy : public BlobFilePartitionStrategy {
};
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) {
CompressionType compression, const CompressionOptions& compression_opts) {
assert(compression <= kLastBuiltinCompression);
static thread_local std::array<DirectWriteCompressionState,
@@ -71,12 +72,15 @@ DirectWriteCompressionState& GetDirectWriteCompressionState(
auto& compression_state =
compression_states[static_cast<size_t>(compression)];
if (compression != kNoCompression &&
compression_state.compressor == nullptr) {
// BDW v1 mirrors BlobFileBuilder by using the built-in compressor with the
// default CompressionOptions only. Cache it per thread so repeated writes
// do not allocate a new compressor and working area every time.
(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(CompressionOptions{},
GetBuiltinV2CompressionManager()->GetCompressor(compression_opts,
compression);
if (compression_state.compressor != nullptr) {
compression_state.working_area =
@@ -390,15 +394,15 @@ bool BlobFilePartitionManager::MarkSealedFileGarbage(
}
Status BlobFilePartitionManager::MaybePrepopulateBlobCache(
const BlobDirectWriteSettings* settings, const Slice& original_value,
const BlobDirectWriteSettings& settings, const Slice& original_value,
uint64_t blob_file_number, uint64_t blob_offset) {
if (settings == nullptr || settings->blob_cache == nullptr ||
settings->prepopulate_blob_cache != PrepopulateBlobCache::kFlushOnly) {
if (settings.blob_cache == nullptr ||
settings.prepopulate_blob_cache != PrepopulateBlobCache::kFlushOnly) {
return Status::OK();
}
FullTypedCacheInterface<BlobContents, BlobContentsCreator> blob_cache{
settings->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);
@@ -419,7 +423,7 @@ 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) {
const BlobDirectWriteSettings& settings, const uint32_t* partition_idx) {
assert(blob_file_number != nullptr);
assert(blob_offset != nullptr);
assert(blob_size != nullptr);
@@ -431,7 +435,8 @@ Status BlobFilePartitionManager::WriteBlob(
GrowableBuffer compressed_value;
Slice write_value = value;
if (compression != kNoCompression) {
auto& compression_state = GetDirectWriteCompressionState(compression);
auto& compression_state =
GetDirectWriteCompressionState(compression, settings.compression_opts);
if (compression_state.compressor == nullptr) {
return Status::NotSupported(
"Blob direct write compression type not supported");
@@ -781,13 +786,19 @@ Status BlobFilePartitionManager::ResolveBlobDirectWriteIndex(
}
}
Status s = version != nullptr ? Status::Corruption("Invalid blob file number")
: Status::NotFound();
if (blob_file_cache == nullptr) {
return s;
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,
+3 -3
View File
@@ -77,7 +77,7 @@ class BlobFilePartitionManager {
CompressionType compression, const Slice& key,
const Slice& value, uint64_t* blob_file_number,
uint64_t* blob_offset, uint64_t* blob_size,
const BlobDirectWriteSettings* settings = nullptr,
const BlobDirectWriteSettings& settings,
const uint32_t* partition_idx = nullptr);
// Selects the partition to use for all blob-backed columns of one PutEntity
@@ -239,8 +239,8 @@ class BlobFilePartitionManager {
static bool MarkSealedFileGarbage(std::vector<SealedFile>* sealed_files,
uint64_t file_number, uint64_t blob_count,
uint64_t blob_bytes);
// Optionally seeds the blob cache with the original uncompressed value.
Status MaybePrepopulateBlobCache(const BlobDirectWriteSettings* settings,
// 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);
+3 -3
View File
@@ -107,7 +107,7 @@ Status BlobWriteBatchTransformer::MaybePreprocessWideColumns(
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,
column_value, &blob_file_number, &blob_offset, &blob_size, settings,
&entity_partition);
if (!s.ok()) {
return s;
@@ -169,7 +169,7 @@ Status BlobWriteBatchTransformer::PutCF(uint32_t column_family_id,
Status s = cached_partition_mgr_->WriteBlob(
write_options_, column_family_id, settings.compression_type, key, value,
&blob_file_number, &blob_offset, &blob_size, &settings);
&blob_file_number, &blob_offset, &blob_size, settings);
if (!s.ok()) {
return s;
}
@@ -278,7 +278,7 @@ Status BlobWriteBatchTransformer::MergeCF(uint32_t column_family_id,
Status BlobWriteBatchTransformer::PutBlobIndexCF(uint32_t column_family_id,
const Slice& key,
const Slice& value) {
// Already a blob index — pass through unchanged.
// Already a blob index -- pass through unchanged.
return WriteBatchInternal::PutBlobIndex(output_batch_, column_family_id, key,
value);
}
+3 -1
View File
@@ -33,7 +33,9 @@ struct BlobDirectWriteSettings {
uint64_t min_blob_size = 0;
// Compression to use for newly written blob records.
CompressionType compression_type = kNoCompression;
// Raw pointer — the Cache is owned by ColumnFamilyOptions and outlives all
// 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.
+41
View File
@@ -1620,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();
+106
View File
@@ -151,6 +151,48 @@ class DBBlobDirectWriteTest : public DBTestBase {
return reader.ReadHeader(header);
}
Status ReadBlobFileRecords(uint64_t blob_file_number, size_t num_records,
std::vector<BlobLogRecord>* records) {
assert(records != nullptr);
records->clear();
std::unique_ptr<FSRandomAccessFile> file;
FileOptions file_options;
constexpr IODebugContext* dbg = nullptr;
const std::string blob_file_path = BlobFileName(dbname_, blob_file_number);
FileSystem* fs = env_->GetFileSystem().get();
SystemClock* clock = env_->GetSystemClock().get();
Status s =
fs->NewRandomAccessFile(blob_file_path, file_options, &file, dbg);
if (!s.ok()) {
return s;
}
std::unique_ptr<RandomAccessFileReader> file_reader(
new RandomAccessFileReader(std::move(file), blob_file_path, clock));
BlobLogSequentialReader reader(std::move(file_reader), clock,
/*statistics=*/nullptr);
BlobLogHeader header;
s = reader.ReadHeader(&header);
if (!s.ok()) {
return s;
}
records->reserve(num_records);
for (size_t i = 0; i < num_records; ++i) {
BlobLogRecord record;
s = reader.ReadRecord(&record,
BlobLogSequentialReader::kReadHeaderKeyBlob);
if (!s.ok()) {
return s;
}
records->push_back(std::move(record));
}
return Status::OK();
}
CompressionType GetSupportedCompressedBlobCompression() {
static constexpr std::array<CompressionType, 6> kCandidates{
kSnappyCompression, kLZ4Compression, kZSTD,
@@ -944,6 +986,70 @@ TEST_F(DBBlobDirectWriteTest,
ASSERT_EQ(Get(third_key), third_value);
}
TEST_F(DBBlobDirectWriteTest,
DirectWriteCompressionOptionsUpdateRebuildsCachedCompressor) {
#ifndef ZSTD
ROCKSDB_GTEST_SKIP("This test requires ZSTD support");
return;
#else
// Goal: verify BDW picks up dynamic blob_compression_opts updates instead of
// reusing a stale per-thread compressor keyed only by compression type.
// We toggle the ZSTD frame checksum flag on and off, then inspect the raw
// blob records in one blob file. With identical inputs and level, enabling
// checksum should add exactly 4 bytes to the stored compressed blob.
Options options = GetDirectWriteOptions();
options.blob_file_size = 1 << 20;
options.blob_compression_type = kZSTD;
options.blob_compression_opts.level = 1;
options.blob_compression_opts.checksum = false;
Reopen(options);
const std::string first_key = "checksum_off_before_update";
const std::string second_key = "checksum_on_after_update";
const std::string third_key = "checksum_off_after_second_update";
const std::string value(256, 'v');
ASSERT_OK(Put(first_key, value));
ASSERT_EQ(Get(first_key), value);
ASSERT_OK(
db_->SetOptions(db_->DefaultColumnFamily(),
{{"blob_compression_opts", "{level=1;checksum=true}"}}));
ASSERT_OK(Put(second_key, value));
ASSERT_EQ(Get(second_key), value);
ASSERT_OK(
db_->SetOptions(db_->DefaultColumnFamily(),
{{"blob_compression_opts", "{level=1;checksum=false}"}}));
ASSERT_OK(Put(third_key, value));
ASSERT_EQ(Get(third_key), value);
ASSERT_OK(Flush());
ASSERT_EQ(CountBlobFiles(), 1U);
const std::vector<uint64_t> blob_file_numbers = GetBlobFileNumbers();
ASSERT_EQ(blob_file_numbers.size(), 1U);
std::vector<BlobLogRecord> records;
ASSERT_OK(
ReadBlobFileRecords(blob_file_numbers[0], /*num_records=*/3, &records));
ASSERT_EQ(records.size(), 3U);
ASSERT_EQ(records[0].key.ToString(), first_key);
ASSERT_EQ(records[1].key.ToString(), second_key);
ASSERT_EQ(records[2].key.ToString(), third_key);
ASSERT_EQ(records[0].value.size(), records[2].value.size());
ASSERT_EQ(records[1].value.size(), records[0].value.size() + 4);
Close();
Reopen(options);
ASSERT_EQ(Get(first_key), value);
ASSERT_EQ(Get(second_key), value);
ASSERT_EQ(Get(third_key), value);
#endif // ZSTD
}
TEST_F(DBBlobDirectWriteTest, DirectWriteFailedBatchTrackedAsInitialGarbage) {
Options options = GetDirectWriteOptions();
options.blob_file_size = 1 << 20;
+60 -1
View File
@@ -128,6 +128,16 @@ class DBBlobIndexTest : public DBTestBase {
columns, s, is_blob_index, value_found);
}
bool MaybeResolveMemtableBlobValueForTest(const Slice& key,
const BlobFetcher* blob_fetcher,
PinnableSlice* value,
PinnableWideColumns* columns,
Status* s, bool* is_blob_index,
bool* value_found = nullptr) {
return DBImpl::MaybeResolveMemtableBlobValue(
key, blob_fetcher, value, columns, s, is_blob_index, value_found);
}
Options GetTestOptions() {
Options options;
options.env = CurrentOptions().env;
@@ -271,6 +281,55 @@ TEST_F(DBBlobIndexTest,
ASSERT_EQ(static_cast<char>(BlobIndex::Type::kUnknown), blob_index.front());
}
TEST_F(DBBlobIndexTest,
MaybeResolveMemtableBlobValueWithoutFetcherFailsClosed) {
// Goal: if a readonly/secondary memtable hit produces a blob-backed payload
// but no BlobFetcher is available, the helper must fail closed instead of
// handing raw blob-index bytes back to the caller as if they were the value.
std::string blob_index;
BlobIndex::EncodeBlob(&blob_index, /*file_number=*/123, /*offset=*/456,
/*size=*/789, kNoCompression);
PinnableSlice value;
value.GetSelf()->assign(blob_index.data(), blob_index.size());
value.PinSelf();
Status s = Status::OK();
bool is_blob_index = true;
ASSERT_TRUE(MaybeResolveMemtableBlobValueForTest(
Slice("key"), /*blob_fetcher=*/nullptr, &value, /*columns=*/nullptr, &s,
&is_blob_index));
ASSERT_TRUE(s.IsNotSupported()) << s.ToString();
ASSERT_TRUE(value.empty());
ASSERT_FALSE(is_blob_index);
}
TEST_F(DBBlobIndexTest, ReadOnlyGetImplReturnsBlobIndexWhenRequested) {
// Goal: cover the internal read-only GetImpl contract when the caller
// explicitly asks for raw blob-index bytes via `is_blob_index`. Recovery
// keeps the blob index in the memtable, and the read-only path must preserve
// the encoded index instead of eagerly resolving or rejecting it.
Options options = GetTestOptions();
DestroyAndReopen(options);
std::string blob_index;
BlobIndex::EncodeInlinedTTL(&blob_index, /*expiration=*/9876543210, "blob");
WriteBatch batch;
ASSERT_OK(PutBlobIndex(&batch, "blob_key", blob_index));
ASSERT_OK(Write(&batch));
Close();
options.avoid_flush_during_recovery = true;
ASSERT_OK(ReadOnlyReopen(options));
bool is_blob_index = false;
ASSERT_EQ(blob_index, GetImpl("blob_key", &is_blob_index));
ASSERT_TRUE(is_blob_index);
}
class PlainBlobValueFilterV3 : public CompactionFilter {
public:
PlainBlobValueFilterV3(std::atomic<int>* filter_call_count,
@@ -2138,7 +2197,7 @@ TEST_F(DBBlobIndexTest, EntityBlobFilterV3Remove) {
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
ASSERT_OK(Flush());
// Compact to trigger the filter — entity should be removed
// Compact to trigger the filter -- entity should be removed
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
// Key should be gone
+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;
+1 -1
View File
@@ -75,7 +75,7 @@ class CompactionBlobResolver : public WideColumnBlobResolver {
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
// (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_;
+9 -9
View File
@@ -251,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();
@@ -477,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 ").",
@@ -547,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,
@@ -587,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
@@ -642,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
@@ -651,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);
}
@@ -668,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;
@@ -713,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;
}
}
+1 -1
View File
@@ -1101,7 +1101,7 @@ TEST_F(CompactionPickerTest, ReadTriggeredSkipsLastLevel) {
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
// 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(
+8 -8
View File
@@ -186,14 +186,14 @@ TEST_F(DBBasicTest,
}
// When the per-CF log_number actually advances, the per-CF skip must NOT
// fire — the edit carries real information and must be emitted.
// fire -- the edit carries real information and must be emitted.
TEST_F(DBBasicTest, OptimizeManifestForRecoveryEmitsPerCFWhenLogAdvances) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.optimize_manifest_for_recovery = true;
DestroyAndReopen(options);
ASSERT_OK(Put("k", "v"));
// Write data and close WITHOUT flushing — the WAL has un-replayed
// Write data and close WITHOUT flushing -- the WAL has un-replayed
// records, so on reopen recovery flushes the memtable and the per-CF
// edit's log_number must advance past the prior WAL.
Close();
@@ -230,7 +230,7 @@ TEST_F(DBBasicTest, OptimizeManifestForRecoveryPreservesWalTracking) {
// best_efforts_recovery requires a fresh MANIFEST + CURRENT to be
// produced on every open (the salvage contract). Even with the option
// on, none of the SkippedNoopEdit branches must fire under
// best_efforts_recovery — otherwise CURRENT can be left missing or
// best_efforts_recovery -- otherwise CURRENT can be left missing or
// stale (regression caught by DBBasicTest.RecoverWithNoCurrentFile and
// DBTest2.BestEffortsRecoveryWithSstUniqueIdVerification under an
// option-on default).
@@ -267,7 +267,7 @@ TEST_F(DBBasicTest, OptimizeManifestForRecoveryMultiCF) {
ASSERT_OK(Put(1, "k", "v1"));
ASSERT_OK(Put(2, "k", "v2"));
ASSERT_OK(Flush(0));
// CF 1 and 2 keep dirty memtables — recovery must emit their per-CF
// CF 1 and 2 keep dirty memtables -- recovery must emit their per-CF
// edits (log_number advance from WAL replay).
Close();
@@ -662,7 +662,7 @@ TEST_F(DBBasicTest, ReuseManifestOnOpenStillRotatesOnSizeCap) {
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
// The Flush after Reopen must have triggered a rotation given the
// tiny size cap — proves the size-driven rotation path still runs
// tiny size cap -- proves the size-driven rotation path still runs
// when descriptor_log_ is bound by reuse.
EXPECT_GT(rotations.load(), 0);
EXPECT_EQ("v", Get("k"));
@@ -685,7 +685,7 @@ TEST_F(DBBasicTest, ReuseManifestOnOpenMultiCF) {
Close();
ReopenWithColumnFamilies({"default", "alpha", "beta"}, options);
// Trigger more writes post-reopen on each CF — these get appended to
// Trigger more writes post-reopen on each CF -- these get appended to
// the reused MANIFEST.
ASSERT_OK(Put(0, "k2", "v0b"));
ASSERT_OK(Put(1, "k2", "v1b"));
@@ -777,7 +777,7 @@ TEST_F(DBBasicTest, WritableFileWriterInitialFileSizeAdoptsExistingSize) {
ASSERT_OK(raw->Close());
}
// Reopen and wrap in a WritableFileWriter without initial_file_size —
// Reopen and wrap in a WritableFileWriter without initial_file_size --
// GetFileSize() should be 0 (the constructor's default).
std::unique_ptr<FSWritableFile> fs_file;
ASSERT_OK(env->GetFileSystem()->ReopenWritableFile(
@@ -805,7 +805,7 @@ TEST_F(DBBasicTest, WritableFileWriterInitialFileSizeAdoptsExistingSize) {
}
// Tail corruption: appending garbage bytes to the MANIFEST after a
// clean close must prevent reuse — the physical size exceeds the
// clean close must prevent reuse -- the physical size exceeds the
// last valid record end.
TEST_F(DBBasicTest, ReuseManifestOnOpenSkipsOnTailCorruption) {
Options options = CurrentOptions();
+1 -1
View File
@@ -542,7 +542,7 @@ TEST_F(DBBlockCacheTest, WarmCacheWithDataBlocksDuringCompaction) {
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.
// 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"));
+143 -5
View File
@@ -11833,7 +11833,7 @@ TEST_F(DBCompactionTest, RoundRobinCleanCutWithSharedBoundary) {
// 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.
// 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
@@ -11864,7 +11864,7 @@ TEST_F(DBCompactionTest, LeakedTableCacheEntryOnCompactionFailure) {
// 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.
// 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) {
@@ -11876,7 +11876,7 @@ TEST_F(DBCompactionTest, LeakedTableCacheEntryOnCompactionFailure) {
// 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 —
// --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*) {
@@ -11889,7 +11889,7 @@ TEST_F(DBCompactionTest, LeakedTableCacheEntryOnCompactionFailure) {
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
// Trigger compaction — fails after VerifyOutputFiles.
// Trigger compaction -- fails after VerifyOutputFiles.
Status s = dbfull()->TEST_CompactRange(0, nullptr, nullptr);
ASSERT_NOK(s);
@@ -11971,7 +11971,7 @@ TEST_F(DBCompactionTest, LeakedTableCacheEntryOnInstallFailure) {
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
// Trigger compaction — Run() succeeds but Install() fails.
// Trigger compaction -- Run() succeeds but Install() fails.
Status s = dbfull()->TEST_CompactRange(0, nullptr, nullptr);
ASSERT_NOK(s);
@@ -11993,6 +11993,144 @@ TEST_F(DBCompactionTest, LeakedTableCacheEntryOnInstallFailure) {
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;
+8 -1
View File
@@ -1852,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.
@@ -1872,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);
@@ -1891,6 +1895,9 @@ 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();
}
@@ -3883,7 +3890,7 @@ TEST_F(DBFlushTest, LeakedTableCacheEntryOnFlushInstallFailure) {
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
// Trigger flush — BuildTable succeeds but LogAndApply fails.
// Trigger flush -- BuildTable succeeds but LogAndApply fails.
Status s = Flush();
ASSERT_NOK(s);
+229 -23
View File
@@ -30,6 +30,7 @@
#include "db/arena_wrapped_db_iter.h"
#include "db/attribute_group_iterator_impl.h"
#include "db/blob/blob_fetcher.h"
#include "db/blob/blob_file_partition_manager.h"
#include "db/blob/blob_index.h"
#include "db/builder.h"
@@ -174,7 +175,6 @@ DBImpl::DBImpl(const DBOptions& options, const std::string& dbname,
const bool seq_per_batch, const bool batch_per_txn,
bool read_only)
: dbname_(dbname),
read_only_(read_only),
own_info_log_(options.info_log == nullptr),
initial_db_options_(SanitizeOptions(dbname, options, read_only,
&init_logger_creation_s_)),
@@ -483,6 +483,18 @@ void DBImpl::WaitForBackgroundWork() {
}
}
void DBImpl::WaitForAsyncFileOpen() {
if (!immutable_db_options_.open_files_async) {
return;
}
InstrumentedMutexLock l(&mutex_);
TEST_SYNC_POINT("DBImpl::WaitForAsyncFileOpen::BeforeWait");
while (bg_async_file_open_state_ == AsyncFileOpenState::kScheduled &&
!shutting_down_.load(std::memory_order_acquire)) {
bg_cv_.Wait();
}
}
// Will lock the mutex_, will wait for completion if wait is true
void DBImpl::CancelAllBackgroundWork(bool wait) {
ROCKS_LOG_INFO(immutable_db_options_.info_log,
@@ -641,8 +653,7 @@ void DBImpl::UnregisterBlobDirectWriteColumnFamily() {
Status DBImpl::MaybeWriteWalMarkersToManifestOnClose() {
mutex_.AssertHeld();
if (!mutable_db_options_.optimize_manifest_for_recovery ||
!opened_successfully_ || read_only_ || versions_ == nullptr ||
logs_.empty()) {
!opened_successfully_ || versions_ == nullptr || logs_.empty()) {
return Status::OK();
}
@@ -829,7 +840,7 @@ Status DBImpl::CloseHelper() {
// manifest file), it is not able to identify live files correctly. As a
// result, all "live" files can get deleted by accident. However, corrupted
// manifest is recoverable by RepairDB().
if (opened_successfully_ && !read_only_) {
if (opened_successfully_) {
JobContext job_context(next_job_id_.fetch_add(1));
FindObsoleteFiles(&job_context, true);
@@ -2861,6 +2872,148 @@ Status DBImpl::ResolveDirectWriteWideColumns(const ReadOptions& read_options,
return status;
}
bool DBImpl::MaybeResolveMemtableBlobValue(const Slice& key,
const BlobFetcher* blob_fetcher,
PinnableSlice* value,
PinnableWideColumns* columns,
Status* s, bool* is_blob_index,
bool* value_found) {
if (!s->ok() || (!value && !columns)) {
return false;
}
auto reset_outputs = [&]() {
if (value != nullptr) {
value->Reset();
}
if (columns != nullptr) {
columns->Reset();
}
};
auto clear_blob_state = [&]() {
if (is_blob_index != nullptr) {
*is_blob_index = false;
}
};
const bool needs_plain_value_resolution =
is_blob_index != nullptr && *is_blob_index;
const bool needs_wide_column_resolution =
columns != nullptr && !columns->unresolved_blob_column_indices_.empty();
if (!needs_plain_value_resolution && !needs_wide_column_resolution) {
return false;
}
if (blob_fetcher == nullptr) {
reset_outputs();
*s = Status::NotSupported(
"Encountered blob-backed memtable value without blob fetcher.");
clear_blob_state();
return true;
}
if (needs_plain_value_resolution) {
Slice blob_index_slice;
std::string blob_index_storage;
if (value != nullptr) {
if (value->size() > 0) {
blob_index_slice = Slice(value->data(), value->size());
} else {
blob_index_slice = Slice(*(value->GetSelf()));
}
} else {
assert(columns != nullptr);
const WideColumns& plain_value_columns = columns->columns();
assert(plain_value_columns.size() == 1);
assert(plain_value_columns.front().name() == kDefaultWideColumnName);
blob_index_slice = plain_value_columns.front().value();
}
PinnableSlice resolved_value;
PinnableSlice* target = value != nullptr ? value : &resolved_value;
if (value != nullptr) {
// BlobIndex::DecodeFrom can retain Slices into the encoded bytes for
// inlined blob indices, so take an owned copy before resetting `value`
// and reusing the same PinnableSlice as the output target.
blob_index_storage.assign(blob_index_slice.data(),
blob_index_slice.size());
blob_index_slice = Slice(blob_index_storage);
value->Reset();
}
*s = blob_fetcher->FetchBlob(key, blob_index_slice,
nullptr /* prefetch_buffer */, target,
nullptr /* bytes_read */);
if (s->ok() && columns != nullptr) {
columns->SetPlainValue(std::move(*target));
} else if (!s->ok()) {
reset_outputs();
if (s->IsIncomplete() && value_found != nullptr) {
*value_found = false;
}
}
clear_blob_state();
return true;
}
assert(columns != nullptr);
std::string resolved_entity;
bool resolved = false;
*s = WideColumnSerialization::ResolveEntityBlobColumns(
columns->value_, key, blob_fetcher, nullptr /* prefetch_buffers */,
resolved_entity, resolved, nullptr /* total_bytes_read */,
nullptr /* num_blobs_resolved */);
if (s->ok()) {
assert(resolved);
if (resolved) {
*s = columns->SetWideColumnValue(std::move(resolved_entity));
}
}
if (!s->ok()) {
reset_outputs();
if (s->IsIncomplete() && value_found != nullptr) {
*value_found = false;
}
}
clear_blob_state();
return true;
}
void DBImpl::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) {
if (resolve_blob_backed_memtable_value) {
std::string blob_lookup_key_storage;
const bool value_resolved = MaybeResolveMemtableBlobValue(
GetBlobLookupUserKey(key, timestamp, &blob_lookup_key_storage),
memtable_blob_fetcher, value, columns, s, is_blob_index, value_found);
if (!value_resolved && value != nullptr && s->ok()) {
value->PinSelf();
}
return;
}
if (s->ok()) {
if (value != nullptr) {
value->PinSelf();
}
} else {
if (value != nullptr) {
value->Reset();
}
if (columns != nullptr) {
columns->Reset();
}
}
}
bool DBImpl::MaybeResolveDirectWriteValue(
const ReadOptions& read_options, const Slice& key,
bool resolve_direct_write_value, const Version* current,
@@ -3076,6 +3229,14 @@ Status DBImpl::GetImpl(const ReadOptions& read_options, const Slice& key,
}
const bool resolve_direct_write_value =
partition_mgr != nullptr && (is_blob_ptr == &is_blob_index);
std::optional<BlobFetcher> memtable_blob_fetcher;
if (partition_mgr != nullptr) {
memtable_blob_fetcher.emplace(sv->current, read_options,
cfd->blob_file_cache(),
/*allow_write_path_fallback=*/true);
}
const BlobFetcher* memtable_blob_fetcher_ptr =
memtable_blob_fetcher ? &*memtable_blob_fetcher : nullptr;
std::string blob_lookup_key_storage;
auto get_blob_lookup_key = [&]() -> Slice {
return GetBlobLookupUserKey(key, timestamp, &blob_lookup_key_storage);
@@ -3103,19 +3264,21 @@ Status DBImpl::GetImpl(const ReadOptions& read_options, const Slice& key,
get_impl_options.columns, timestamp, &s, &merge_context,
&max_covering_tombstone_seq, read_options,
false /* immutable_memtable */,
get_impl_options.callback, is_blob_ptr)) {
get_impl_options.callback, is_blob_ptr,
/*do_merge=*/true, memtable_blob_fetcher_ptr)) {
done = true;
maybe_resolve_memtable_value();
RecordTick(stats_, MEMTABLE_HIT);
} else if ((s.ok() || s.IsMergeInProgress()) &&
sv->imm->Get(
lkey,
get_impl_options.value ? get_impl_options.value->GetSelf()
: nullptr,
get_impl_options.columns, timestamp, &s, &merge_context,
&max_covering_tombstone_seq, read_options,
get_impl_options.callback, is_blob_ptr)) {
sv->imm->Get(lkey,
get_impl_options.value
? get_impl_options.value->GetSelf()
: nullptr,
get_impl_options.columns, timestamp, &s,
&merge_context, &max_covering_tombstone_seq,
read_options, get_impl_options.callback,
is_blob_ptr, memtable_blob_fetcher_ptr)) {
done = true;
maybe_resolve_memtable_value();
@@ -3127,14 +3290,14 @@ Status DBImpl::GetImpl(const ReadOptions& read_options, const Slice& key,
if (sv->mem->Get(lkey, /*value=*/nullptr, /*columns=*/nullptr,
/*timestamp=*/nullptr, &s, &merge_context,
&max_covering_tombstone_seq, read_options,
false /* immutable_memtable */, nullptr, nullptr,
false)) {
false /* immutable_memtable */, nullptr, nullptr, false,
memtable_blob_fetcher_ptr)) {
done = true;
RecordTick(stats_, MEMTABLE_HIT);
} else if ((s.ok() || s.IsMergeInProgress()) &&
sv->imm->GetMergeOperands(lkey, &s, &merge_context,
&max_covering_tombstone_seq,
read_options)) {
sv->imm->GetMergeOperands(
lkey, &s, &merge_context, &max_covering_tombstone_seq,
read_options, memtable_blob_fetcher_ptr)) {
done = true;
RecordTick(stats_, MEMTABLE_HIT);
}
@@ -3830,6 +3993,14 @@ Status DBImpl::MultiGetImpl(
(*sorted_keys)[start_key]->column_family);
ColumnFamilyData* cfd = cfh->cfd();
auto* partition_mgr = cfd->blob_partition_manager();
std::optional<BlobFetcher> memtable_blob_fetcher;
if (partition_mgr != nullptr) {
memtable_blob_fetcher.emplace(super_version->current, read_options,
cfd->blob_file_cache(),
/*allow_write_path_fallback=*/true);
}
const BlobFetcher* memtable_blob_fetcher_ptr =
memtable_blob_fetcher ? &*memtable_blob_fetcher : nullptr;
// Clear the timestamps for returning results so that we can distinguish
// between tombstone or key that has never been written
for (size_t i = start_key; i < start_key + num_keys; ++i) {
@@ -3876,9 +4047,11 @@ Status DBImpl::MultiGetImpl(
has_unpersisted_data_.load(std::memory_order_relaxed));
if (!skip_memtable) {
super_version->mem->MultiGet(read_options, &range, callback,
false /* immutable_memtable */);
false /* immutable_memtable */,
memtable_blob_fetcher_ptr);
if (!range.empty()) {
super_version->imm->MultiGet(read_options, &range, callback);
super_version->imm->MultiGet(read_options, &range, callback,
memtable_blob_fetcher_ptr);
}
if (!range.empty()) {
uint64_t left = range.KeysLeft();
@@ -4255,9 +4428,21 @@ Status DBImpl::CreateColumnFamilyImpl(const ReadOptions& read_options,
const std::string& column_family_name,
ColumnFamilyHandle** handle) {
options_mutex_.AssertHeld();
Status s;
*handle = nullptr;
// The empty string is reserved as a "no/unknown column family name" sentinel
// in various APIs and serialization formats (e.g.
// TablePropertiesCollectorFactory::Context::kUnknownColumnFamily, table
// properties), so reject it to avoid ambiguity. Note: prior to this check,
// CreateColumnFamily("") would silently "succeed" but the CF was not
// persisted in the manifest, causing data loss across DB reopens.
if (column_family_name.empty()) {
return Status::InvalidArgument(
"Column family name cannot be the empty string");
}
Status s;
DBOptions db_options =
BuildDBOptions(immutable_db_options_, mutable_db_options_);
s = ColumnFamilyData::ValidateOptions(db_options, cf_options);
@@ -6313,12 +6498,20 @@ Status DBImpl::GetLatestSequenceForKey(
*seq = kMaxSequenceNumber;
*found_record_for_key = false;
std::optional<BlobFetcher> memtable_blob_fetcher;
if (cfd->blob_partition_manager() != nullptr) {
memtable_blob_fetcher.emplace(sv->current, read_options,
cfd->blob_file_cache(),
/*allow_write_path_fallback=*/true);
}
const BlobFetcher* memtable_blob_fetcher_ptr =
memtable_blob_fetcher ? &*memtable_blob_fetcher : nullptr;
// Check if there is a record for this key in the latest memtable
sv->mem->Get(lkey, /*value=*/nullptr, /*columns=*/nullptr, timestamp, &s,
&merge_context, &max_covering_tombstone_seq, seq, read_options,
false /* immutable_memtable */, nullptr /*read_callback*/,
is_blob_index);
is_blob_index, /*do_merge=*/true, memtable_blob_fetcher_ptr);
if (!(s.ok() || s.IsNotFound() || s.IsMergeInProgress())) {
// unexpected error reading memtable.
@@ -6351,7 +6544,8 @@ Status DBImpl::GetLatestSequenceForKey(
// Check if there is a record for this key in the immutable memtables
sv->imm->Get(lkey, /*value=*/nullptr, /*columns=*/nullptr, timestamp, &s,
&merge_context, &max_covering_tombstone_seq, seq, read_options,
nullptr /*read_callback*/, is_blob_index);
nullptr /*read_callback*/, is_blob_index,
memtable_blob_fetcher_ptr);
if (!(s.ok() || s.IsNotFound() || s.IsMergeInProgress())) {
// unexpected error reading memtable.
@@ -6384,7 +6578,7 @@ Status DBImpl::GetLatestSequenceForKey(
sv->imm->GetFromHistory(lkey, /*value=*/nullptr, /*columns=*/nullptr,
timestamp, &s, &merge_context,
&max_covering_tombstone_seq, seq, read_options,
is_blob_index);
is_blob_index, memtable_blob_fetcher_ptr);
if (!(s.ok() || s.IsNotFound() || s.IsMergeInProgress())) {
// unexpected error reading memtable.
@@ -7386,6 +7580,7 @@ Status DBImpl::ReserveFileNumbersBeforeIngestion(
Status DBImpl::GetCreationTimeOfOldestFile(uint64_t* creation_time) {
if (mutable_db_options_.max_open_files == -1) {
uint64_t oldest_time = std::numeric_limits<uint64_t>::max();
std::once_flag waited_for_async_open;
for (auto cfd : *versions_->GetColumnFamilySet()) {
if (!cfd->IsDropped()) {
uint64_t ctime;
@@ -7393,6 +7588,17 @@ Status DBImpl::GetCreationTimeOfOldestFile(uint64_t* creation_time) {
SuperVersion* sv = GetAndRefSuperVersion(cfd);
Version* version = sv->current;
version->GetCreationTimeOfOldestFile(&ctime);
// For modern DBs, manifest carries file_creation_time and the
// first call returns the real value. We only need to wait for
// BGWorkAsyncFileOpen on legacy DBs whose manifest lacks
// file_creation_time -- those rely on the pinned reader, which is
// null until async file open completes.
if (ctime == 0 && immutable_db_options_.open_files_async) {
std::call_once(waited_for_async_open, [&]() {
WaitForAsyncFileOpen();
version->GetCreationTimeOfOldestFile(&ctime);
});
}
ReturnAndCleanupSuperVersion(cfd, sv);
}
+37 -1
View File
@@ -1096,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.
@@ -1371,7 +1372,6 @@ class DBImpl : public DB {
protected:
const std::string dbname_;
const bool read_only_;
// TODO(peterd): unify with VersionSet::db_id_
std::string db_id_;
// db_session_id_ is an identifier that gets reset
@@ -1855,6 +1855,11 @@ class DBImpl : public DB {
// 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
@@ -1870,6 +1875,7 @@ class DBImpl : public DB {
private:
friend class DB;
friend class DBImplReadOnly;
friend class DBImplSecondary;
friend class ErrorHandler;
friend class InternalStats;
@@ -2947,6 +2953,29 @@ class DBImpl : public DB {
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>
@@ -3351,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
+37 -1
View File
@@ -874,6 +874,24 @@ 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) {
@@ -3162,7 +3180,7 @@ void DBImpl::ResumeAllCompactions() {
void DBImpl::MaybeScheduleFlushOrCompaction() {
mutex_.AssertHeld();
TEST_SYNC_POINT("DBImpl::MaybeScheduleFlushOrCompaction:Start");
if (!opened_successfully_ || read_only_) {
if (!opened_successfully_) {
// Compaction may introduce data race to DB open
return;
}
@@ -5114,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
@@ -5147,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));
+1 -1
View File
@@ -1206,7 +1206,7 @@ Status DBImpl::MaybeUpdateNextFileNumber(RecoveryContext* recovery_ctx) {
normalized_fpath += kFilePathSeparator;
normalized_fpath.append(fname);
// Use >= so a crashed-mid-allocation file at exactly
// prev_next_file_number triggers the advance — otherwise the next
// prev_next_file_number triggers the advance -- otherwise the next
// NewFileNumber() would collide with it.
if (number >= prev_next_file_number) {
on_disk_file_advanced_counter = true;
+2 -1
View File
@@ -295,7 +295,8 @@ Status DB::OpenAsFollower(
impl->versions_.reset(new ReactiveVersionSet(
dbname, &impl->immutable_db_options_, impl->mutable_db_options_,
impl->file_options_, impl->table_cache_.get(),
impl->write_buffer_manager_, &impl->write_controller_, impl->io_tracer_));
impl->write_buffer_manager_, &impl->write_controller_, impl->io_tracer_,
impl->db_id_, impl->db_session_id_));
impl->column_family_memtables_.reset(
new ColumnFamilyMemTablesImpl(impl->versions_->GetColumnFamilySet()));
impl->wal_in_db_path_ = impl->immutable_db_options_.IsWalDirSameAsDBPath();
+35 -7
View File
@@ -5,7 +5,10 @@
#include "db/db_impl/db_impl_readonly.h"
#include <optional>
#include "db/arena_wrapped_db_iter.h"
#include "db/blob/blob_fetcher.h"
#include "db/db_impl/compacted_db_impl.h"
#include "db/db_impl/db_impl.h"
#include "db/manifest_ops.h"
@@ -63,13 +66,25 @@ Status DBImplReadOnly::GetImpl(const ReadOptions& read_options,
const Comparator* ucmp = get_impl_options.column_family->GetComparator();
assert(ucmp);
std::string* ts =
ucmp->timestamp_size() > 0 ? get_impl_options.timestamp : nullptr;
SequenceNumber snapshot = versions_->LastSequence();
GetWithTimestampReadCallback read_cb(snapshot);
auto cfh = static_cast_with_check<ColumnFamilyHandleImpl>(
get_impl_options.column_family);
auto cfd = cfh->cfd();
bool is_blob_index = false;
bool* is_blob_ptr = get_impl_options.is_blob_index;
std::string timestamp_storage;
std::string* ts = nullptr;
if (ucmp->timestamp_size() > 0) {
ts = get_impl_options.timestamp != nullptr
? get_impl_options.timestamp
: (get_impl_options.get_value ? &timestamp_storage : nullptr);
}
if (!is_blob_ptr && get_impl_options.get_value) {
is_blob_ptr = &is_blob_index;
}
const bool resolve_blob_backed_memtable_value =
get_impl_options.get_value && (is_blob_ptr == &is_blob_index);
if (tracer_) {
InstrumentedMutexLock lock(&trace_mutex_);
if (tracer_) {
@@ -95,6 +110,18 @@ Status DBImplReadOnly::GetImpl(const ReadOptions& read_options,
SequenceNumber max_covering_tombstone_seq = 0;
LookupKey lkey(key, snapshot, read_options.timestamp);
PERF_TIMER_STOP(get_snapshot_time);
std::optional<BlobFetcher> memtable_blob_fetcher;
if (cfd->ioptions().enable_blob_direct_write ||
cfd->GetLatestMutableCFOptions().enable_blob_files) {
// Recovered memtables can still contain older blob references after
// mutable blob-file settings change, so keep blob resolution available
// whenever either blob knob indicates it may be needed.
memtable_blob_fetcher.emplace(super_version->current, read_options,
cfd->blob_file_cache(),
/*allow_write_path_fallback=*/true);
}
const BlobFetcher* memtable_blob_fetcher_ptr =
memtable_blob_fetcher ? &*memtable_blob_fetcher : nullptr;
// Look up starts here
if (super_version->mem->Get(
@@ -102,11 +129,12 @@ Status DBImplReadOnly::GetImpl(const ReadOptions& read_options,
get_impl_options.value ? get_impl_options.value->GetSelf() : nullptr,
get_impl_options.columns, ts, &s, &merge_context,
&max_covering_tombstone_seq, read_options,
false /* immutable_memtable */, &read_cb,
/*is_blob_index=*/nullptr, /*do_merge=*/get_impl_options.get_value)) {
if (get_impl_options.value) {
get_impl_options.value->PinSelf();
}
false /* immutable_memtable */, &read_cb, is_blob_ptr,
/*do_merge=*/get_impl_options.get_value, memtable_blob_fetcher_ptr)) {
DBImpl::PostprocessMemtableValueRead(
key, ts, resolve_blob_backed_memtable_value, memtable_blob_fetcher_ptr,
get_impl_options.value, get_impl_options.columns, &s, &is_blob_index,
get_impl_options.value_found);
RecordTick(stats_, MEMTABLE_HIT);
} else {
PERF_TIMER_GUARD(get_from_output_files_time);
+51 -18
View File
@@ -6,8 +6,10 @@
#include "db/db_impl/db_impl_secondary.h"
#include <cinttypes>
#include <optional>
#include "db/arena_wrapped_db_iter.h"
#include "db/blob/blob_fetcher.h"
#include "db/log_reader.h"
#include "db/log_writer.h"
#include "db/merge_context.h"
@@ -363,13 +365,25 @@ Status DBImplSecondary::GetImpl(const ReadOptions& read_options,
const Comparator* ucmp = get_impl_options.column_family->GetComparator();
assert(ucmp);
std::string* ts =
ucmp->timestamp_size() > 0 ? get_impl_options.timestamp : nullptr;
SequenceNumber snapshot = versions_->LastSequence();
GetWithTimestampReadCallback read_cb(snapshot);
auto cfh = static_cast_with_check<ColumnFamilyHandleImpl>(
get_impl_options.column_family);
auto cfd = cfh->cfd();
bool is_blob_index = false;
bool* is_blob_ptr = get_impl_options.is_blob_index;
std::string timestamp_storage;
std::string* ts = nullptr;
if (ucmp->timestamp_size() > 0) {
ts = get_impl_options.timestamp != nullptr
? get_impl_options.timestamp
: (get_impl_options.get_value ? &timestamp_storage : nullptr);
}
if (!is_blob_ptr && get_impl_options.get_value) {
is_blob_ptr = &is_blob_index;
}
const bool resolve_blob_backed_memtable_value =
get_impl_options.get_value && (is_blob_ptr == &is_blob_index);
if (tracer_) {
InstrumentedMutexLock lock(&trace_mutex_);
if (tracer_) {
@@ -395,6 +409,18 @@ Status DBImplSecondary::GetImpl(const ReadOptions& read_options,
LookupKey lkey(key, snapshot, read_options.timestamp);
PERF_TIMER_STOP(get_snapshot_time);
bool done = false;
std::optional<BlobFetcher> memtable_blob_fetcher;
if (cfd->ioptions().enable_blob_direct_write ||
cfd->GetLatestMutableCFOptions().enable_blob_files) {
// Catch-up can rebuild older blob references into memtables after mutable
// blob-file settings change, so keep blob resolution available whenever
// either blob knob indicates it may be needed.
memtable_blob_fetcher.emplace(super_version->current, read_options,
cfd->blob_file_cache(),
/*allow_write_path_fallback=*/true);
}
const BlobFetcher* memtable_blob_fetcher_ptr =
memtable_blob_fetcher ? &*memtable_blob_fetcher : nullptr;
// Look up starts here
if (get_impl_options.get_value) {
@@ -404,12 +430,14 @@ Status DBImplSecondary::GetImpl(const ReadOptions& read_options,
: nullptr,
get_impl_options.columns, ts, &s, &merge_context,
&max_covering_tombstone_seq, read_options,
false /* immutable_memtable */, &read_cb,
/*is_blob_index=*/nullptr, /*do_merge=*/true)) {
false /* immutable_memtable */, &read_cb, is_blob_ptr,
/*do_merge=*/true, memtable_blob_fetcher_ptr)) {
done = true;
if (get_impl_options.value) {
get_impl_options.value->PinSelf();
}
DBImpl::PostprocessMemtableValueRead(
key, ts, resolve_blob_backed_memtable_value,
memtable_blob_fetcher_ptr, get_impl_options.value,
get_impl_options.columns, &s, &is_blob_index,
get_impl_options.value_found);
RecordTick(stats_, MEMTABLE_HIT);
} else if ((s.ok() || s.IsMergeInProgress()) &&
super_version->imm->Get(
@@ -417,11 +445,14 @@ Status DBImplSecondary::GetImpl(const ReadOptions& read_options,
get_impl_options.value ? get_impl_options.value->GetSelf()
: nullptr,
get_impl_options.columns, ts, &s, &merge_context,
&max_covering_tombstone_seq, read_options, &read_cb)) {
&max_covering_tombstone_seq, read_options, &read_cb,
is_blob_ptr, memtable_blob_fetcher_ptr)) {
done = true;
if (get_impl_options.value) {
get_impl_options.value->PinSelf();
}
DBImpl::PostprocessMemtableValueRead(
key, ts, resolve_blob_backed_memtable_value,
memtable_blob_fetcher_ptr, get_impl_options.value,
get_impl_options.columns, &s, &is_blob_index,
get_impl_options.value_found);
RecordTick(stats_, MEMTABLE_HIT);
}
} else {
@@ -433,13 +464,14 @@ Status DBImplSecondary::GetImpl(const ReadOptions& read_options,
get_impl_options.columns, ts, &s, &merge_context,
&max_covering_tombstone_seq, read_options,
false /* immutable_memtable */, &read_cb,
/*is_blob_index=*/nullptr, /*do_merge=*/false)) {
/*is_blob_index=*/nullptr, /*do_merge=*/false,
memtable_blob_fetcher_ptr)) {
done = true;
RecordTick(stats_, MEMTABLE_HIT);
} else if ((s.ok() || s.IsMergeInProgress()) &&
super_version->imm->GetMergeOperands(lkey, &s, &merge_context,
&max_covering_tombstone_seq,
read_options)) {
super_version->imm->GetMergeOperands(
lkey, &s, &merge_context, &max_covering_tombstone_seq,
read_options, memtable_blob_fetcher_ptr)) {
done = true;
RecordTick(stats_, MEMTABLE_HIT);
}
@@ -457,7 +489,7 @@ Status DBImplSecondary::GetImpl(const ReadOptions& read_options,
ts, &s, &merge_context, &max_covering_tombstone_seq, &pinned_iters_mgr,
/*value_found*/ nullptr,
/*key_exists*/ nullptr, /*seq*/ nullptr, &read_cb, /*is_blob*/ nullptr,
/*do_merge*/ true);
/*do_merge=*/get_impl_options.get_value);
RecordTick(stats_, MEMTABLE_MISS);
}
{
@@ -777,7 +809,8 @@ Status DBImplSecondary::OpenAsSecondaryImpl(
impl->versions_.reset(new ReactiveVersionSet(
dbname, &impl->immutable_db_options_, impl->mutable_db_options_,
impl->file_options_, impl->table_cache_.get(),
impl->write_buffer_manager_, &impl->write_controller_, impl->io_tracer_));
impl->write_buffer_manager_, &impl->write_controller_, impl->io_tracer_,
impl->db_id_, impl->db_session_id_));
impl->column_family_memtables_.reset(
new ColumnFamilyMemTablesImpl(impl->versions_->GetColumnFamilySet()));
impl->wal_in_db_path_ = impl->immutable_db_options_.IsWalDirSameAsDBPath();
@@ -1553,7 +1586,7 @@ Status DB::OpenAndCompact(
}
}
// 5. Open db As Secondary (skip WAL recovery — remote compaction only
// 5. Open db As Secondary (skip WAL recovery -- remote compaction only
// needs LSM state from MANIFEST, not memtable data from WAL replay)
std::unique_ptr<DB> db;
std::vector<ColumnFamilyHandle*> handles;
+5
View File
@@ -509,6 +509,7 @@ struct DBImpl::BlobDirectWriteContext {
sv->cfd->ioptions().enable_blob_direct_write;
settings.min_blob_size = sv->mutable_cf_options.min_blob_size;
settings.compression_type = sv->mutable_cf_options.blob_compression_type;
settings.compression_opts = sv->mutable_cf_options.blob_compression_opts;
settings.blob_cache = sv->cfd->ioptions().blob_cache.get();
settings.prepopulate_blob_cache =
sv->mutable_cf_options.prepopulate_blob_cache;
@@ -1545,6 +1546,10 @@ Status DBImpl::WriteImpl(
// Note: if we are to resume after non-OK statuses we need to revisit how
// we react to non-OK statuses here.
if (w.status.ok()) { // Don't publish a partial batch write
TEST_SYNC_POINT(
"DBImpl::WriteImpl:AfterWBWIIngestBeforeSetLastSequence:pause");
TEST_SYNC_POINT(
"DBImpl::WriteImpl:AfterWBWIIngestBeforeSetLastSequence:resume");
versions_->SetLastSequence(last_sequence);
}
}
+47 -52
View File
@@ -76,7 +76,7 @@ DBIter::DBIter(Env* _env, const ReadOptions& read_options,
user_comparator_(cmp),
merge_operator_(ioptions.merge_operator.get()),
iter_(iter),
blob_reader_(
blob_state_(
version, read_options.read_tier, read_options.verify_checksums,
read_options.fill_cache, read_options.io_activity,
cfh ? cfh->cfd()->blob_file_cache() : nullptr,
@@ -125,7 +125,6 @@ DBIter::DBIter(Env* _env, const ReadOptions& read_options,
: 0),
expose_blob_index_(expose_blob_index),
allow_unprepared_value_(read_options.allow_unprepared_value),
is_blob_(false),
arena_mode_(arena_mode) {
RecordTick(statistics_, NO_ITERATOR_CREATED);
if (pin_thru_lifetime_) {
@@ -178,7 +177,7 @@ Status DBIter::GetProperty(std::string prop_name, std::string* prop) {
} else if (prop_name == "rocksdb.iterator.is-value-pinned") {
if (valid_) {
*prop = (pin_thru_lifetime_ && iter_.Valid() &&
iter_.value().data() == value_columns_state_.value().data())
iter_.value().data() == value_columns_state_->value().data())
? "1"
: "0";
} else {
@@ -308,24 +307,24 @@ bool DBIter::SetValueAndColumnsFromBlobImpl(const Slice& user_key,
// has a write-path partition manager.
const bool allow_write_path_fallback =
cfh_ != nullptr && cfh_->cfd()->blob_partition_manager() != nullptr;
const Status s = blob_reader_.RetrieveAndSetBlobValue(
const Status s = blob_state_.mut()->reader.RetrieveAndSetBlobValue(
user_key, blob_index, allow_write_path_fallback);
if (!s.ok()) {
status_ = s;
valid_ = false;
is_blob_ = false;
blob_state_.mut()->is_blob = false;
return false;
}
SetValueAndColumnsFromPlain(blob_reader_.GetBlobValue());
SetValueAndColumnsFromPlain(blob_state_->reader.GetBlobValue());
return true;
}
bool DBIter::SetValueAndColumnsFromBlob(const Slice& user_key,
const Slice& blob_index) {
assert(!is_blob_);
is_blob_ = true;
assert(!blob_state_->is_blob);
blob_state_.mut()->is_blob = true;
if (expose_blob_index_) {
SetValueAndColumnsFromPlain(blob_index);
@@ -333,11 +332,11 @@ bool DBIter::SetValueAndColumnsFromBlob(const Slice& user_key,
}
if (allow_unprepared_value_) {
assert(value_columns_state_.value().empty());
assert(value_columns_state_.wide_columns().empty());
assert(value_columns_state_->value().empty());
assert(value_columns_state_->wide_columns().empty());
assert(lazy_blob_index_.empty());
lazy_blob_index_ = blob_index;
assert(blob_state_->lazy_blob_index.empty());
blob_state_.mut()->lazy_blob_index = blob_index;
return true;
}
@@ -346,7 +345,10 @@ bool DBIter::SetValueAndColumnsFromBlob(const Slice& user_key,
}
bool DBIter::SetValueAndColumnsFromEntity(Slice slice) {
auto& state = value_columns_state_;
// Auto-marks dirty via mut() up front since every successful path below
// populates wide_columns_ (and possibly the lazy entity/blob column
// vectors).
auto& state = *value_columns_state_.mut();
state.AssertReadyForEntity();
// Fast path: if no blob columns, use the simpler Deserialize
@@ -409,7 +411,7 @@ bool DBIter::SetValueAndColumnsFromEntity(Slice slice) {
}
bool DBIter::MaterializeLazyEntityColumns() const {
const auto& state = value_columns_state_;
const auto& state = *value_columns_state_;
if (state.lazy_entity_columns().empty() || !state.wide_columns().empty()) {
return true;
}
@@ -420,7 +422,7 @@ bool DBIter::MaterializeLazyEntityColumns() const {
}
DBIter* const mutable_this = const_cast<DBIter*>(this);
auto& mutable_state = mutable_this->value_columns_state_;
auto& mutable_state = *mutable_this->value_columns_state_.mut();
WideColumns materialized_columns;
materialized_columns.reserve(state.lazy_entity_columns().size());
for (const auto& col : state.lazy_entity_columns()) {
@@ -454,7 +456,7 @@ bool DBIter::SetValueAndColumnsFromMergeResult(const Status& merge_status,
}
if (result_type == kTypeWideColumnEntity) {
if (!SetValueAndColumnsFromEntity(value_columns_state_.saved_value())) {
if (!SetValueAndColumnsFromEntity(value_columns_state_->saved_value())) {
assert(!valid_);
return false;
}
@@ -466,7 +468,7 @@ bool DBIter::SetValueAndColumnsFromMergeResult(const Status& merge_status,
assert(result_type == kTypeValue);
SetValueAndColumnsFromPlain(pinned_value_.data()
? pinned_value_
: value_columns_state_.saved_value());
: value_columns_state_->saved_value());
valid_ = true;
return true;
}
@@ -474,17 +476,17 @@ bool DBIter::SetValueAndColumnsFromMergeResult(const Status& merge_status,
bool DBIter::PrepareValue() {
assert(valid_);
if (lazy_blob_index_.empty()) {
if (blob_state_->lazy_blob_index.empty()) {
return true;
}
assert(allow_unprepared_value_);
assert(is_blob_);
assert(blob_state_->is_blob);
const bool result =
SetValueAndColumnsFromBlobImpl(saved_key_.GetUserKey(), lazy_blob_index_);
const bool result = SetValueAndColumnsFromBlobImpl(
saved_key_.GetUserKey(), blob_state_->lazy_blob_index);
lazy_blob_index_.clear();
blob_state_.mut()->lazy_blob_index.clear();
return result;
}
@@ -621,7 +623,7 @@ bool DBIter::FindNextUserEntryInternal(bool skipping_saved_key) {
PERF_COUNTER_ADD(internal_delete_skipped_count, 1);
MarkMemtableForFlushForPerOpTrigger(mem_hidden_op_scanned);
// Track contiguous tombstones for range conversion.
// Skip if outside seek prefix — the top-of-loop check
// Skip if outside seek prefix -- the top-of-loop check
// flushed any pending run, but we must also avoid starting
// a new run outside the prefix.
if (min_tombstones_for_range_conversion_ > 0 &&
@@ -774,25 +776,18 @@ bool DBIter::FindNextUserEntryInternal(bool skipping_saved_key) {
}
} while (iter_.Valid());
// If we accumulated tombstones, insert the range tombstone. Use
// iterate_upper_bound_ if within the seek prefix, otherwise fall back to
// saved_key_ (the last tracked delete, covering n-1 deletes).
// If we accumulated tombstones, use the last tracked tombstone as the
// exclusive end key. It may be more optimal to use iterator upper bound if it
// exists, but the current iterator API makes that dangerous as upper bound
// points to user memory which is not guaranteed immutable.
if (contiguous_tombstone_count_ > 0 && iter_.status().ok()) {
if (iterate_upper_bound_ != nullptr && PrefixCheck(*iterate_upper_bound_)) {
if (timestamp_size_ == 0) {
MaybeInsertRangeTombstone(*iterate_upper_bound_);
} else {
// iterate_upper_bound_ is a plain user key without a timestamp
// suffix. Pad with min timestamp so it sorts after all entries with
// this user key, preserving the exclusive bound semantics.
std::string end_key_with_ts;
AppendKeyWithMinTimestamp(&end_key_with_ts, *iterate_upper_bound_,
timestamp_size_);
MaybeInsertRangeTombstone(end_key_with_ts);
}
} else if (prefix_.has_value()) {
MaybeInsertRangeTombstone(saved_key_.GetUserKey());
}
// It is unsafe to use iter_.key() here even when iter_.Valid() and the key
// is within the seek prefix. This is because memtable iterators are still
// valid past the upper bound, but sst iterators are not. So iter_.key() can
// point to a memtable entry that has skipped past real live entries in
// ssts.
assert(PrefixCheck(saved_key_.GetUserKey()));
MaybeInsertRangeTombstone(saved_key_.GetUserKey());
}
ResetContiguousTombstoneTracking();
@@ -1102,7 +1097,7 @@ void DBIter::PrevInternal() {
if (min_tombstones_for_range_conversion_ > 0 &&
range_tomb_end_key_.Size() > 0 && timestamp_lb_ == nullptr) {
if (!valid_ && found_visible && PrefixCheck(saved_key_without_ts)) {
// Key was deleted and is within the seek prefix — track it.
// Key was deleted and is within the seek prefix -- track it.
TrackContiguousTombstone(saved_key_.GetUserKey(),
/*always_update_first_key=*/true);
} else if (valid_) {
@@ -1146,7 +1141,7 @@ void DBIter::PrevInternal() {
// Sets ikey_ to the last visible entry's internal key. When found_visible
// is false, ikey_ is not updated and may contain stale data.
// Sets found_visible to true if at least one entry passed the IsVisible()
// check (seqno <= snapshot). When false, no entry was visible — the key
// check (seqno <= snapshot). When false, no entry was visible -- the key
// does not exist at this snapshot and should not be treated as a tombstone.
// Returns false if an error occurred, and !status().ok() and !valid_.
//
@@ -1201,7 +1196,7 @@ bool DBIter::FindValueForCurrentKey(bool& found_visible) {
break;
}
// Entry survived the visibility check — at least one visible version
// Entry survived the visibility check -- at least one visible version
// exists for this user key.
found_visible = true;
@@ -1592,7 +1587,7 @@ bool DBIter::MergeWithNoBaseValue(const Slice& user_key) {
merge_operator_, user_key, MergeHelper::kNoBaseValue,
merge_context_.GetOperands(), logger_, statistics_, clock_,
/* update_num_ops_stats */ true, /* op_failure_scope */ nullptr,
&value_columns_state_.saved_value(), &pinned_value_, &result_type);
&value_columns_state_.mut()->saved_value(), &pinned_value_, &result_type);
return SetValueAndColumnsFromMergeResult(s, result_type);
}
@@ -1605,13 +1600,13 @@ bool DBIter::MergeWithPlainBaseValue(const Slice& value,
merge_operator_, user_key, MergeHelper::kPlainBaseValue, value,
merge_context_.GetOperands(), logger_, statistics_, clock_,
/* update_num_ops_stats */ true, /* op_failure_scope */ nullptr,
&value_columns_state_.saved_value(), &pinned_value_, &result_type);
&value_columns_state_.mut()->saved_value(), &pinned_value_, &result_type);
return SetValueAndColumnsFromMergeResult(s, result_type);
}
bool DBIter::MergeWithBlobBaseValue(const Slice& blob_index,
const Slice& user_key) {
assert(!is_blob_);
assert(!blob_state_->is_blob);
if (expose_blob_index_) {
status_ =
@@ -1622,7 +1617,7 @@ bool DBIter::MergeWithBlobBaseValue(const Slice& blob_index,
const bool allow_write_path_fallback =
cfh_ != nullptr && cfh_->cfd()->blob_partition_manager() != nullptr;
const Status s = blob_reader_.RetrieveAndSetBlobValue(
const Status s = blob_state_.mut()->reader.RetrieveAndSetBlobValue(
user_key, blob_index, allow_write_path_fallback);
if (!s.ok()) {
status_ = s;
@@ -1632,11 +1627,11 @@ bool DBIter::MergeWithBlobBaseValue(const Slice& blob_index,
valid_ = true;
if (!MergeWithPlainBaseValue(blob_reader_.GetBlobValue(), user_key)) {
if (!MergeWithPlainBaseValue(blob_state_->reader.GetBlobValue(), user_key)) {
return false;
}
blob_reader_.ResetBlobValue();
blob_state_.Reset();
return true;
}
@@ -1645,7 +1640,7 @@ bool DBIter::MergeWithWideColumnBaseValue(const Slice& entity,
const Slice& user_key) {
// Resolve V2 entity blob columns if present, since TimedFullMerge only
// supports V1 format.
BlobFetcher blob_fetcher = blob_reader_.CreateBlobFetcher();
BlobFetcher blob_fetcher = blob_state_->reader.CreateBlobFetcher();
std::string resolved_entity;
Slice effective_entity;
Status s_resolve = WideColumnSerialization::ResolveEntityForMerge(
@@ -1664,7 +1659,7 @@ bool DBIter::MergeWithWideColumnBaseValue(const Slice& entity,
merge_operator_, user_key, MergeHelper::kWideBaseValue, effective_entity,
merge_context_.GetOperands(), logger_, statistics_, clock_,
/* update_num_ops_stats */ true, /* op_failure_scope */ nullptr,
&value_columns_state_.saved_value(), &pinned_value_, &result_type);
&value_columns_state_.mut()->saved_value(), &pinned_value_, &result_type);
return SetValueAndColumnsFromMergeResult(s, result_type);
}
+35 -18
View File
@@ -27,6 +27,7 @@
#include "rocksdb/wide_columns.h"
#include "table/iterator_wrapper.h"
#include "util/autovector.h"
#include "util/dirty_tracked.h"
namespace ROCKSDB_NAMESPACE {
class BlobFileCache;
@@ -195,14 +196,14 @@ class DBIter final : public Iterator {
Slice value() const override {
assert(valid_);
return value_columns_state_.value();
return value_columns_state_->value();
}
const WideColumns& columns() const override {
assert(valid_);
assert(!value_columns_state_.HasLazyEntityColumns() ||
value_columns_state_.HasMaterializedColumns());
return value_columns_state_.wide_columns();
assert(!value_columns_state_->HasLazyEntityColumns() ||
value_columns_state_->HasMaterializedColumns());
return value_columns_state_->wide_columns();
}
Status status() const override {
@@ -225,7 +226,7 @@ class DBIter final : public Iterator {
}
bool IsBlob() const {
assert(valid_);
return is_blob_;
return blob_state_->is_blob;
}
Status GetProperty(std::string prop_name, std::string* prop) override;
@@ -296,6 +297,20 @@ class DBIter final : public Iterator {
BlobFileCache* blob_file_cache_;
bool allow_write_path_fallback_;
};
struct BlobState {
BlobReader reader;
Slice lazy_blob_index;
bool is_blob = false;
template <typename... Args>
explicit BlobState(Args&&... args) : reader(std::forward<Args>(args)...) {}
void Reset() {
reader.ResetBlobValue();
lazy_blob_index.clear();
is_blob = false;
}
};
// Groups the current iterator result together with the backing storage and
// lazy entity-resolution metadata it depends on. Resetting this object drops
@@ -483,7 +498,9 @@ class DBIter final : public Iterator {
}
}
inline void ClearSavedValue() { value_columns_state_.ClearSavedValue(); }
inline void ClearSavedValue() {
value_columns_state_.mut()->ClearSavedValue();
}
inline void ResetInternalKeysSkippedCounter() {
local_stats_.skip_count_ += num_internal_keys_skipped_;
@@ -508,7 +525,7 @@ class DBIter final : public Iterator {
}
void SetValueAndColumnsFromPlain(const Slice& slice) {
value_columns_state_.SetFromPlain(slice);
value_columns_state_.mut()->SetFromPlain(slice);
}
bool SetValueAndColumnsFromBlobImpl(const Slice& user_key,
@@ -524,11 +541,7 @@ class DBIter final : public Iterator {
void ResetValueAndColumns() { value_columns_state_.Reset(); }
void ResetBlobData() {
blob_reader_.ResetBlobValue();
lazy_blob_index_.clear();
is_blob_ = false;
}
void ResetBlobData() { blob_state_.Reset(); }
// The following methods perform the actual merge operation for the
// no/plain/blob/wide-column base value cases.
@@ -539,6 +552,12 @@ class DBIter final : public Iterator {
bool MergeWithWideColumnBaseValue(const Slice& entity, const Slice& user_key);
bool PrepareValueInternal() {
// Capture this before PrepareValue(): PrepareValue() updates the wrapper
// state to "prepared" on success. We still call PrepareValue()
// unconditionally to preserve its contract/error handling, but only need
// to re-parse ikey_ when this call may have actually materialized the
// underlying iterator value/key.
const bool value_was_prepared = iter_.IsValuePrepared();
if (!iter_.PrepareValue()) {
assert(!iter_.status().ok());
valid_ = false;
@@ -548,7 +567,7 @@ class DBIter final : public Iterator {
// lookup and index_iter_ could point to different block resulting
// in ikey_ pointing to wrong key. So ikey_ needs to be updated in
// case of Seek/Next calls to point to right key again.
if (!ParseKey(&ikey_)) {
if (!value_was_prepared && !ParseKey(&ikey_)) {
return false;
}
return true;
@@ -570,7 +589,7 @@ class DBIter final : public Iterator {
// If enough contiguous tombstones have been tracked, insert a range
// tombstone [first_key, end_key) into the mutable memtable.
// end_key is the exclusive upper bound — typically the next live key.
// end_key is the exclusive upper bound -- typically the next live key.
void MaybeInsertRangeTombstone(const Slice& end_key);
void ResetContiguousTombstoneTracking() {
contiguous_tombstone_count_ = 0;
@@ -644,7 +663,7 @@ class DBIter final : public Iterator {
UserComparatorWrapper user_comparator_;
const MergeOperator* const merge_operator_;
IteratorWrapper iter_;
BlobReader blob_reader_;
DirtyTracked<BlobState> blob_state_;
ReadCallback* read_callback_;
// Max visible sequence number. It is normally the snapshot seq unless we have
// uncommitted data in db as in WriteUnCommitted.
@@ -661,7 +680,7 @@ class DBIter final : public Iterator {
// kTypeValuePreferredSeqno entry, this is the write time specified by the
// user.
uint64_t saved_write_unix_time_;
ValueColumnsState value_columns_state_;
DirtyTracked<ValueColumnsState> value_columns_state_;
Slice pinned_value_;
Statistics* statistics_;
uint64_t max_skip_;
@@ -682,7 +701,6 @@ class DBIter final : public Iterator {
std::optional<IterKey> prefix_;
Status status_;
Slice lazy_blob_index_;
// List of operands for merge operator.
MergeContext merge_context_;
@@ -721,7 +739,6 @@ class DBIter final : public Iterator {
// the stacked BlobDB implementation is used, false otherwise.
bool expose_blob_index_;
bool allow_unprepared_value_;
bool is_blob_;
bool arena_mode_;
IterKey range_tomb_first_key_;
+116 -40
View File
@@ -4151,7 +4151,7 @@ TEST_P(DBIteratorTest, PrefixSameAsStartSeekToNonInDomainKey) {
ASSERT_OK(Put("abc2", "v2"));
ASSERT_OK(Put("abc3", "v3"));
// Seek to "ab" (2 bytes) — out-of-domain for FixedPrefixTransform(3).
// Seek to "ab" (2 bytes) -- out-of-domain for FixedPrefixTransform(3).
// ShouldSetPrefix returns false for out-of-domain targets, so no prefix
// constraint is set. The seek should find "abc1" and iteration should
// proceed without prefix_same_as_start enforcement.
@@ -5820,11 +5820,13 @@ TEST_P(ReadPathRangeTombstoneTest, ExhaustedIteratorWithBounds) {
// Both directions encounter two tombstone runs (a-d and g-j).
ASSERT_EQ(attempted_insert_ranges_.size(), 2);
if (Forward()) {
// Forward: sees a-d tombstones first → [a, e), then g-j → [g, z).
// Forward: sees a-d tombstones first -> live e terminates -> [a, e).
// Then g-j -> exhaustion past j, saved_key_="j" fallback -> [g, j),
// covering g,h,i. j remains a point tombstone.
AssertRange(0, "a", "e");
AssertRange(1, "g", "z");
AssertRange(1, "g", "j");
} else {
// Reverse: sees j-g tombstones first, then f,e live, then d-a → exhausts.
// Reverse: sees j-g tombstones first, then f,e live, then d-a -> exhausts.
AssertRange(0, "g", "z");
AssertRange(1, "a", "e");
}
@@ -5860,13 +5862,24 @@ TEST_P(ReadPathRangeTombstoneTest, ExhaustedIteratorNoBounds) {
VerifyIteration({"e", "f"});
// Without bounds, only the a-d run (which has a live key boundary) gets
// inserted. The g-j run at the end has no upper bound → dropped.
ASSERT_EQ(attempted_insert_ranges_.size(), 1);
AssertRange(0, "a", "e");
ASSERT_EQ(
options.statistics->getTickerCount(READ_PATH_RANGE_TOMBSTONES_INSERTED),
1);
if (Forward()) {
// Forward: a-d run terminates at live e -> [a, e). g-j run exhausts
// past j -> saved_key_ fallback -> [g, j), covering g,h,i with j as a
// point tombstone.
ASSERT_EQ(attempted_insert_ranges_.size(), 2);
AssertRange(0, "a", "e");
AssertRange(1, "g", "j");
ASSERT_EQ(
options.statistics->getTickerCount(READ_PATH_RANGE_TOMBSTONES_INSERTED),
2);
} else {
// Reverse fallback path remains prefix-gated.
ASSERT_EQ(attempted_insert_ranges_.size(), 1);
AssertRange(0, "a", "e");
ASSERT_EQ(
options.statistics->getTickerCount(READ_PATH_RANGE_TOMBSTONES_INSERTED),
1);
}
}
TEST_P(ReadPathRangeTombstoneTest, DirectionChange) {
@@ -6048,7 +6061,7 @@ TEST_P(ReadPathRangeTombstoneTest, PrefixFilterDefaultReadOptions) {
}
TEST_P(ReadPathRangeTombstoneTest, PrefixFilterTotalOrderSeek) {
// total_order_seek=true disables prefix filtering — all files visible.
// total_order_seek=true disables prefix filtering -- all files visible.
// The delete run spans from prefix 'b' into prefix 'c', terminated by
// live key "cb" from L0. The tombstone [ba, cb) crosses prefix boundaries,
// which is safe because total_order_seek makes all files visible.
@@ -6090,7 +6103,7 @@ TEST_P(ReadPathRangeTombstoneTest, PrefixFilterTotalOrderSeek) {
ASSERT_OK(Flush());
// Memtable: contiguous deletes spanning prefixes 'b' and 'c'.
// No live key between "bb" and "cb" — the run crosses prefix boundaries.
// No live key between "bb" and "cb" -- the run crosses prefix boundaries.
ASSERT_OK(put("aa", "below"));
ASSERT_OK(del("ba"));
ASSERT_OK(del("bb"));
@@ -6218,6 +6231,55 @@ TEST_P(ReadPathRangeTombstoneTest, PrefixFilterPrefixSameAsStart) {
}
}
TEST_P(ReadPathRangeTombstoneTest,
PrefixFilterUpperBoundDoesNotCoverSkippedLiveKey) {
if (!Forward()) {
return;
}
Options options = CurrentOptions();
options.min_tombstones_for_range_conversion = 3;
options.statistics = CreateDBStatistics();
options.disable_auto_compactions = true;
options.prefix_extractor.reset(NewFixedPrefixTransform(1));
BlockBasedTableOptions table_options;
table_options.filter_policy.reset(NewBloomFilterPolicy(10));
table_options.whole_key_filtering = false;
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
DestroyAndReopen(options);
// Keep a live in-prefix key above the user upper bound in an SST so the
// bounded prefix scan cannot return it, then leave a later same-prefix
// memtable key available as the internal stop key. If cleanup uses that
// late memtable key as the synthetic range end, it will cover "be".
ASSERT_OK(Put("be", "live_b"));
ASSERT_OK(Flush());
ASSERT_OK(Delete("ba"));
ASSERT_OK(Delete("bb"));
ASSERT_OK(Delete("bc"));
ASSERT_OK(Put("bz", "live_z"));
attempted_insert_ranges_.clear();
ReadOptions ro;
ro.prefix_same_as_start = true;
std::string upper_str = "bd";
Slice upper(upper_str);
ro.iterate_upper_bound = &upper;
auto iter = std::unique_ptr<Iterator>(db_->NewIterator(ro));
std::vector<std::string> keys;
for (iter->Seek("ba"); iter->Valid(); iter->Next()) {
keys.push_back(iter->key().ToString());
}
ASSERT_OK(iter->status());
ASSERT_TRUE(keys.empty());
ASSERT_EQ(attempted_insert_ranges_.size(), 1u);
ASSERT_EQ(Get("be"), "live_b");
}
TEST_P(ReadPathRangeTombstoneTest, PrefixFilterOutOfDomainSeek) {
// With an out-of-domain seek target, prefix_same_as_start cannot establish
// a seek-prefix bound. The iterator therefore behaves like an unrestricted
@@ -6249,7 +6311,7 @@ TEST_P(ReadPathRangeTombstoneTest, PrefixFilterOutOfDomainSeek) {
ro.prefix_same_as_start = true;
auto it = std::unique_ptr<Iterator>(db_->NewIterator(ro));
if (Forward()) {
// Seek with a 1-byte key — out-of-domain for FixedPrefixTransform(4).
// Seek with a 1-byte key -- out-of-domain for FixedPrefixTransform(4).
// prefix_same_as_start cannot set a prefix bound for this target.
it->Seek("b");
while (it->Valid()) {
@@ -6612,8 +6674,8 @@ TEST_P(ReadPathRangeTombstoneTest, ExhaustedWithUDT) {
ASSERT_EQ(attempted_insert_ranges_.size(), 1);
if (Forward()) {
// Forward exhaustion: range is [e+ts, z+min_ts).
AssertRange(0, std::string("e") + ts, std::string("z") + min_ts);
// Forward exhaustion past h: saved_key_="h"+ts fallback -> [e+ts, h+ts).
AssertRange(0, std::string("e") + ts, std::string("h") + ts);
} else {
// Reverse exhaustion: range is [a+ts, e+ts).
AssertRange(0, std::string("a") + ts, std::string("e") + ts);
@@ -6624,9 +6686,10 @@ TEST_P(ReadPathRangeTombstoneTest, ExhaustedWithUDT) {
}
TEST_P(ReadPathRangeTombstoneTest, SeekForPrevTombstone) {
// SeekForPrev lands directly on tombstones. No upper bound, so forward
// exhaustion drops the trailing tombstone run (no end key available).
// Reverse: SeekForPrev("h") → Delete(h,g,f,e) → live "d" → range [e, h).
// SeekForPrev lands directly on tombstones. No upper bound. Forward
// exhaustion past h falls back to saved_key_="h" -> [e, h), covering
// e,f,g with h as a point tombstone.
// Reverse: SeekForPrev("h") -> Delete(h,g,f,e) -> live "d" -> range [e, h).
for (bool use_udt : {false, true}) {
SCOPED_TRACE(use_udt ? "with UDT" : "without UDT");
Options options = CurrentOptions();
@@ -6664,7 +6727,7 @@ TEST_P(ReadPathRangeTombstoneTest, SeekForPrevTombstone) {
keys.push_back(iter->key().ToString());
}
} else {
// SeekForPrev("h") lands on Delete(h), traverses g,f,e → finds "d".
// SeekForPrev("h") lands on Delete(h), traverses g,f,e -> finds "d".
for (iter->SeekForPrev("h"); iter->Valid(); iter->Prev()) {
keys.push_back(iter->key().ToString());
}
@@ -6673,11 +6736,16 @@ TEST_P(ReadPathRangeTombstoneTest, SeekForPrevTombstone) {
ASSERT_OK(iter->status());
ASSERT_EQ(keys, (std::vector<std::string>{"a", "b", "c", "d"}));
ASSERT_EQ(attempted_insert_ranges_.size(), 1);
if (Forward()) {
// No upper bound → trailing tombstone run has no end key → dropped.
ASSERT_EQ(attempted_insert_ranges_.size(), 0);
// Forward exhausts past h -> saved_key_="h" fallback -> [e, h),
// covering e,f,g with h as a point tombstone.
if (use_udt) {
AssertRange(0, std::string("e") + ts, std::string("h") + ts);
} else {
AssertRange(0, "e", "h");
}
} else {
ASSERT_EQ(attempted_insert_ranges_.size(), 1);
if (use_udt) {
std::string min_ts(sizeof(uint64_t), '\0');
AssertRange(0, std::string("e") + ts, std::string("h") + min_ts);
@@ -6687,15 +6755,18 @@ TEST_P(ReadPathRangeTombstoneTest, SeekForPrevTombstone) {
}
ASSERT_EQ(
options.statistics->getTickerCount(READ_PATH_RANGE_TOMBSTONES_INSERTED),
Forward() ? 0 : 1);
1);
}
}
TEST_P(ReadPathRangeTombstoneTest, UpperBoundTombstone) {
// iterate_upper_bound lands directly on tombstones. Both directions see
// tombstones e-h and use upper bound "i" as end key for the range.
// Forward: exhaustion at bound → [e, i). Reverse: SeekToLast delegates to
// SeekForPrev("i") → [e, i).
// iterate_upper_bound lands past the data. Both directions see tombstones
// e-h. Forward exhausts naturally past h (no key in DB >= upper) so the
// forward path falls back to saved_key_ ("h") as the exclusive end_key,
// covering n-1 of n deletes -- [e, h). The remaining tombstone for h
// stays as a point delete. Reverse captures the live key "d" before the
// run and uses it via range_tomb_end_key_ -- [e, i) when SeekToLast
// delegates to SeekForPrev("i").
for (bool use_udt : {false, true}) {
SCOPED_TRACE(use_udt ? "with UDT" : "without UDT");
Options options = CurrentOptions();
@@ -6746,13 +6817,17 @@ TEST_P(ReadPathRangeTombstoneTest, UpperBoundTombstone) {
ASSERT_EQ(attempted_insert_ranges_.size(), 1);
if (use_udt) {
// Forward end key: upper bound padded with min_ts.
// Reverse end key: upper bound padded with max_ts (via
// SetSavedKeyToSeekForPrevTarget).
std::string end_ts(sizeof(uint64_t), Forward() ? '\0' : '\xff');
AssertRange(0, std::string("e") + ts, std::string("i") + end_ts);
if (Forward()) {
// Forward end key: saved_key_ ("h") with the tombstone's own ts.
AssertRange(0, std::string("e") + ts, std::string("h") + ts);
} else {
// Reverse end key: upper bound padded with max_ts (via
// SetSavedKeyToSeekForPrevTarget).
std::string end_ts(sizeof(uint64_t), '\xff');
AssertRange(0, std::string("e") + ts, std::string("i") + end_ts);
}
} else {
AssertRange(0, "e", "i");
AssertRange(0, "e", Forward() ? "h" : "i");
}
ASSERT_EQ(
options.statistics->getTickerCount(READ_PATH_RANGE_TOMBSTONES_INSERTED),
@@ -6762,8 +6837,8 @@ TEST_P(ReadPathRangeTombstoneTest, UpperBoundTombstone) {
TEST_P(ReadPathRangeTombstoneTest, LowerBoundTruncatesReverse) {
// Keys a-j, delete a-h. Lower bound "e" truncates reverse iteration
// mid-tombstone-run. Forward: tombstones e-h ended by live key i → [e, i).
// Reverse: tombstones h,g,f,e then lower_bound hit → flush [e, i).
// mid-tombstone-run. Forward: tombstones e-h ended by live key i -> [e, i).
// Reverse: tombstones h,g,f,e then lower_bound hit -> flush [e, i).
for (bool use_udt : {false, true}) {
SCOPED_TRACE(use_udt ? "with UDT" : "without UDT");
Options options = CurrentOptions();
@@ -6900,14 +6975,14 @@ TEST_P(ReadPathRangeTombstoneTest, InvisibleKeysDontBreakTombstoneRun) {
ASSERT_OK(Put("f", "vf"));
ASSERT_OK(Flush());
// Delete b, d — visible tombstones.
// Delete b, d -- visible tombstones.
ASSERT_OK(Delete("b"));
ASSERT_OK(Delete("d"));
// Snapshot S. At S: a(live), b(del), d(del), f(live).
const Snapshot* snap = db_->GetSnapshot();
// Write c, e AFTER the snapshot — invisible at S.
// Write c, e AFTER the snapshot -- invisible at S.
// At S the iterator sees: a(live), b(del), c(invis), d(del),
// e(invis), f(live).
ASSERT_OK(Put("c", "vc"));
@@ -6919,7 +6994,7 @@ TEST_P(ReadPathRangeTombstoneTest, InvisibleKeysDontBreakTombstoneRun) {
VerifyIteration({"a", "f"}, ro);
// Invisible keys c and e should not break the tombstone run.
// 2 tombstones (b, d) ≥ threshold → range [b, f).
// 2 tombstones (b, d) >= threshold -> range [b, f).
ASSERT_EQ(attempted_insert_ranges_.size(), 1);
AssertRange(0, "b", "f");
@@ -7022,7 +7097,8 @@ TEST_P(ReadPathRangeTombstoneTest, SeekToLastStaleSavedKey) {
TEST_P(ReadPathRangeTombstoneTest, SeekToLastTombstones) {
if (Forward()) {
ROCKSDB_GTEST_SKIP("SeekToLast tombstone materialization is reverse-only.");
ROCKSDB_GTEST_BYPASS(
"SeekToLast tombstone materialization is reverse-only.");
}
Options options = CurrentOptions();
+195 -2
View File
@@ -7,9 +7,12 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#include "db/blob/blob_index.h"
#include "db/db_impl/db_impl_secondary.h"
#include "db/db_test_util.h"
#include "db/db_with_timestamp_test_util.h"
#include "db/wide/wide_column_test_util.h"
#include "db/write_batch_internal.h"
#include "port/stack_trace.h"
#include "rocksdb/utilities/transaction_db.h"
#include "test_util/sync_point.h"
@@ -357,8 +360,7 @@ TEST_F(DBSecondaryTest, GetMergeOperands) {
const Status s = db_secondary_->GetMergeOperands(
ReadOptions(), cfh, "k1", values.data(), &merge_operands_info,
&number_of_operands);
ASSERT_NOK(s);
ASSERT_TRUE(s.IsMergeInProgress());
ASSERT_OK(s);
ASSERT_EQ(number_of_operands, 4);
ASSERT_EQ(values[0].ToString(), "v1");
@@ -367,6 +369,197 @@ TEST_F(DBSecondaryTest, GetMergeOperands) {
ASSERT_EQ(values[3].ToString(), "v4");
}
TEST_F(DBSecondaryTest, GetMergeOperandsWithBlobBackedEntityDefaultColumn) {
// Goal: exercise the secondary read path with a blob-backed V2 entity base
// in SST and a newer merge operand applied through catch-up. The secondary
// DB must resolve the blob-backed default column for both Get() and
// GetMergeOperands() while combining the newer memtable merge with the older
// SST-backed base entity.
Options options = GetDefaultOptions();
options.create_if_missing = true;
options.enable_blob_files = true;
options.min_blob_size = 50;
options.disable_auto_compactions = true;
options.merge_operator = MergeOperators::CreateStringAppendOperator("|");
options.env = env_;
Reopen(options);
const std::string key = "secondary_blob_entity";
const std::string default_value(100, 'd');
const std::string large_value(120, 'l');
const std::string merge_operand = "suffix";
const std::string expected_merged = default_value + "|" + merge_operand;
WideColumns columns{{kDefaultWideColumnName, default_value},
{"col_large", large_value},
{"meta", "inline"}};
ASSERT_OK(
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
ASSERT_OK(Flush());
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
options.max_open_files = -1;
OpenSecondary(options);
ASSERT_OK(db_->Merge(WriteOptions(), db_->DefaultColumnFamily(), key,
merge_operand));
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
auto* cfh = db_secondary_->DefaultColumnFamily();
{
PinnableSlice result;
ASSERT_OK(db_secondary_->Get(ReadOptions(), cfh, key, &result));
ASSERT_EQ(result, expected_merged);
}
{
GetMergeOperandsOptions get_merge_opts;
get_merge_opts.expected_max_number_of_operands = 2;
std::array<PinnableSlice, 2> merge_operands;
int number_of_operands = 0;
ASSERT_OK(db_secondary_->GetMergeOperands(
ReadOptions(), cfh, key, merge_operands.data(), &get_merge_opts,
&number_of_operands));
ASSERT_EQ(number_of_operands, 2);
ASSERT_EQ(merge_operands[0], default_value);
ASSERT_EQ(merge_operands[1], merge_operand);
}
}
TEST_F(DBSecondaryTest, GetImplReturnsBlobIndexWhenRequested) {
// Goal: cover the internal secondary GetImpl contract when the caller
// explicitly asks for raw blob-index bytes via `is_blob_index`. Catch-up
// rebuilds the blob index into the secondary memtable, and this path must
// preserve the encoded index instead of eagerly resolving or rejecting it.
Options options = GetDefaultOptions();
options.create_if_missing = true;
options.env = env_;
Reopen(options);
options.max_open_files = -1;
OpenSecondary(options);
std::string blob_index;
BlobIndex::EncodeInlinedTTL(&blob_index, /*expiration=*/9876543210, "blob");
WriteBatch batch;
ASSERT_OK(WriteBatchInternal::PutBlobIndex(
&batch, db_->DefaultColumnFamily()->GetID(), "blob_key", blob_index));
ASSERT_OK(db_->Write(WriteOptions(), &batch));
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
PinnableSlice value;
bool is_blob_index = false;
DBImpl::GetImplOptions get_impl_options;
get_impl_options.column_family = db_secondary_->DefaultColumnFamily();
get_impl_options.value = &value;
get_impl_options.is_blob_index = &is_blob_index;
ASSERT_OK(db_secondary_full()->GetImpl(ReadOptions(), Slice("blob_key"),
get_impl_options));
ASSERT_TRUE(is_blob_index);
ASSERT_EQ(value, blob_index);
}
TEST_F(DBSecondaryTest,
GetAndGetEntityWithBlobBackedDefaultColumnDirectWriteMemtable) {
// Goal: cover the secondary memtable path after catch-up replays a blob
// direct-write entity from the primary WAL. The test checks both `Get()`,
// which must resolve the blob-backed default column, and `GetEntity()`,
// which must eagerly resolve all unresolved blob columns instead of exposing
// encoded blob indices to the caller.
Options options =
wide_column_test_util::GetDirectWriteOptions(GetDefaultOptions());
options.create_if_missing = true;
options.min_blob_size = 50;
options.env = env_;
Reopen(options);
options.max_open_files = -1;
OpenSecondary(options);
const std::string key = "secondary_direct_write_memtable_entity";
const std::string default_value =
wide_column_test_util::GenerateLargeValue(100, 'd');
const std::string large_value =
wide_column_test_util::GenerateLargeValue(120, 'l');
const std::string small_value = wide_column_test_util::GenerateSmallValue();
WideColumns columns{{kDefaultWideColumnName, default_value},
{"col_large", large_value},
{"col_small", small_value}};
ASSERT_OK(
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
auto* cfh = db_secondary_->DefaultColumnFamily();
{
PinnableSlice result;
ASSERT_OK(db_secondary_->Get(ReadOptions(), cfh, key, &result));
ASSERT_EQ(result, default_value);
}
{
PinnableWideColumns result;
ASSERT_OK(db_secondary_->GetEntity(ReadOptions(), cfh, key, &result));
ASSERT_EQ(result.columns(), columns);
}
}
TEST_F(DBSecondaryTest, SecondaryDirectWriteMemtableBlobBlockCacheTier) {
// Goal: cover the secondary memtable path under kBlockCacheTier. Catch-up
// rebuilds the blob-backed direct-write entity from WAL, so serving Get() or
// GetEntity() would require blob I/O through the memtable resolution path,
// which must return Incomplete instead of issuing that I/O.
Options options =
wide_column_test_util::GetDirectWriteOptions(GetDefaultOptions());
options.create_if_missing = true;
options.min_blob_size = 50;
options.env = env_;
Reopen(options);
options.max_open_files = -1;
OpenSecondary(options);
const std::string key = "secondary_direct_write_memtable_block_cache_tier";
const std::string default_value =
wide_column_test_util::GenerateLargeValue(100, 'd');
const std::string large_value =
wide_column_test_util::GenerateLargeValue(120, 'l');
const std::string small_value = wide_column_test_util::GenerateSmallValue();
WideColumns columns{{kDefaultWideColumnName, default_value},
{"col_large", large_value},
{"col_small", small_value}};
ASSERT_OK(
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
auto* cfh = db_secondary_->DefaultColumnFamily();
ReadOptions read_opts;
read_opts.read_tier = kBlockCacheTier;
{
PinnableSlice result;
const Status s = db_secondary_->Get(read_opts, cfh, key, &result);
ASSERT_TRUE(s.IsIncomplete()) << s.ToString();
ASSERT_TRUE(result.empty());
}
{
PinnableWideColumns result;
const Status s = db_secondary_->GetEntity(read_opts, cfh, key, &result);
ASSERT_TRUE(s.IsIncomplete()) << s.ToString();
ASSERT_TRUE(result.columns().empty());
}
}
TEST_F(DBSecondaryTest, InternalCompactionCompactedFiles) {
Options options;
options.env = env_;
+123
View File
@@ -8,14 +8,17 @@
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#include <memory>
#include <mutex>
#include <unordered_set>
#include <vector>
#include "db/db_test_util.h"
#include "port/port.h"
#include "port/stack_trace.h"
#include "rocksdb/advanced_compression.h"
#include "rocksdb/db.h"
#include "rocksdb/types.h"
#include "rocksdb/utilities/object_registry.h"
#include "rocksdb/utilities/table_properties_collectors.h"
#include "table/format.h"
#include "table/meta_blocks.h"
@@ -51,6 +54,42 @@ void VerifyTableProperties(DB* db, uint64_t expected_entries_size) {
VerifySstUniqueIds(props);
}
class ParseCompressionDisplayNameManager : public CompressionManagerWrapper {
public:
static constexpr const char* kCompatibilityName =
"ParseCompressionDisplayNameManager";
ParseCompressionDisplayNameManager()
: CompressionManagerWrapper(GetBuiltinV2CompressionManager()) {}
const char* Name() const override { return kCompatibilityName; }
const char* CompatibilityName() const override { return kCompatibilityName; }
std::string CompressionTypeToString(CompressionType type) const override {
if (type == kCustomCompression8A) {
return "CustomAlpha";
}
if (type == kCustomCompression8B) {
return "CustomBeta";
}
return CompressionManagerWrapper::CompressionTypeToString(type);
}
static void Register() {
static std::once_flag loaded;
std::call_once(loaded, []() {
auto& library = *ObjectLibrary::Default();
library.AddFactory<CompressionManager>(
kCompatibilityName, [](const std::string& /*uri*/,
std::unique_ptr<CompressionManager>* guard,
std::string* /*errmsg*/) {
*guard = std::make_unique<ParseCompressionDisplayNameManager>();
return guard->get();
});
});
}
};
} // anonymous namespace
class DBTablePropertiesTest : public DBTestBase,
@@ -812,6 +851,90 @@ TEST_F(DBTablePropertiesTest, KeyLargestSmallestSeqno) {
}
}
TEST_F(DBTablePropertiesTest, ParseCompressionNameForDisplay) {
auto custom_mgr = std::make_shared<ParseCompressionDisplayNameManager>();
// Test empty string
EXPECT_EQ("NoCompression", ParseCompressionNameForDisplay(""));
// Test old format (no semicolon)
EXPECT_EQ("ZSTD", ParseCompressionNameForDisplay("ZSTD"));
EXPECT_EQ("Snappy", ParseCompressionNameForDisplay("Snappy"));
EXPECT_EQ("LZ4", ParseCompressionNameForDisplay("LZ4"));
EXPECT_EQ("kZSTD", ParseCompressionNameForDisplay("kZSTD"));
EXPECT_EQ("kSnappyCompression",
ParseCompressionNameForDisplay("kSnappyCompression"));
// Test new format version 7 with ZSTD
EXPECT_EQ("ZSTD", ParseCompressionNameForDisplay("zstd;07;"));
EXPECT_EQ("ZSTD", ParseCompressionNameForDisplay("BuiltinV2;07;"));
EXPECT_EQ("ZSTD", ParseCompressionNameForDisplay("builtin_v2;07;"));
EXPECT_EQ("ZSTD", ParseCompressionNameForDisplay(";07;"));
EXPECT_EQ("ZSTD", ParseCompressionNameForDisplay("custom_mgr;07;extra;"));
// Test new format with LZ4
EXPECT_EQ("LZ4", ParseCompressionNameForDisplay("zstd;04;"));
EXPECT_EQ("LZ4", ParseCompressionNameForDisplay("lz4;04;"));
// Test lowercase hex for reserved/custom values
EXPECT_EQ("Reserved7F", ParseCompressionNameForDisplay("test;7f;"));
EXPECT_EQ("Custom8A", ParseCompressionNameForDisplay("test;8a;"));
// Test multiple compression types
EXPECT_EQ("LZ4,ZSTD", ParseCompressionNameForDisplay("test;0407;"));
EXPECT_EQ(
"ZSTD,LZ4",
ParseCompressionNameForDisplay("test;0704;")); // sorted by appearance
EXPECT_EQ("ZSTD", ParseCompressionNameForDisplay("test;0007;"));
// Test all standard compression types (01-07)
EXPECT_EQ("NoCompression", ParseCompressionNameForDisplay("test;00;"));
EXPECT_EQ("Snappy", ParseCompressionNameForDisplay("test;01;"));
EXPECT_EQ("Zlib", ParseCompressionNameForDisplay("test;02;"));
EXPECT_EQ("BZip2", ParseCompressionNameForDisplay("test;03;"));
EXPECT_EQ("LZ4", ParseCompressionNameForDisplay("test;04;"));
EXPECT_EQ("LZ4HC", ParseCompressionNameForDisplay("test;05;"));
EXPECT_EQ("Xpress", ParseCompressionNameForDisplay("test;06;"));
EXPECT_EQ("ZSTD", ParseCompressionNameForDisplay("test;07;"));
// Test custom/reserved types (>= 0x08)
// 0x08-0x7F are Reserved, 0x80-0xFE are Custom
EXPECT_EQ("Reserved08", ParseCompressionNameForDisplay("test;08;"));
EXPECT_EQ("Custom80", ParseCompressionNameForDisplay("test;80;"));
EXPECT_EQ("Reserved7F", ParseCompressionNameForDisplay("test;7F;"));
EXPECT_EQ("CustomFE", ParseCompressionNameForDisplay("test;FE;"));
EXPECT_EQ("CustomAlpha,CustomBeta",
ParseCompressionNameForDisplay(
"ParseCompressionDisplayNameManager;8A8B;", custom_mgr));
EXPECT_EQ("CustomAlpha,LZ4,CustomBeta",
ParseCompressionNameForDisplay(
"ParseCompressionDisplayNameManager;8A048B;", custom_mgr));
ParseCompressionDisplayNameManager::Register();
EXPECT_EQ("CustomAlpha", ParseCompressionNameForDisplay(
"ParseCompressionDisplayNameManager;8A;"));
// Test DisableOption (0xFF) - filtered out
EXPECT_EQ("NoCompression", ParseCompressionNameForDisplay("test;FF;"));
// Test NoCompression (empty hex field would be caught by validation, but test
// "00")
EXPECT_EQ("NoCompression", ParseCompressionNameForDisplay("test;00;"));
EXPECT_EQ("NoCompression", ParseCompressionNameForDisplay("BuiltinV2;;"));
EXPECT_EQ("NoCompression", ParseCompressionNameForDisplay("test;;"));
// Test three+ semicolons (future fields ignored)
EXPECT_EQ("ZSTD", ParseCompressionNameForDisplay("test;07;extra;fields;"));
// Test malformed inputs -> "Unknown"
EXPECT_EQ("Unknown", ParseCompressionNameForDisplay("only_one;"));
EXPECT_EQ("Unknown", ParseCompressionNameForDisplay("bad;0;")); // odd length
EXPECT_EQ("Unknown",
ParseCompressionNameForDisplay("bad;0G;")); // invalid hex
EXPECT_EQ("Unknown",
ParseCompressionNameForDisplay("bad;GG;")); // invalid hex
}
INSTANTIATE_TEST_CASE_P(DBTablePropertiesTest, DBTablePropertiesTest,
::testing::Values("kCompactionStyleLevel",
"kCompactionStyleUniversal"));
+92 -35
View File
@@ -8071,41 +8071,6 @@ INSTANTIATE_TEST_CASE_P(OpenFilesAsync, OpenFilesAsyncTest,
::testing::Values(-1, 10),
::testing::Bool()));
TEST_F(DBTest, ReadOnlyCloseDoesNotDeleteWriterFiles) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.disable_auto_compactions = true;
DestroyAndReopen(options);
ASSERT_OK(Put("before", "value"));
ASSERT_OK(Flush());
std::unique_ptr<DB> read_only_db;
ASSERT_OK(DB::OpenForReadOnly(options, dbname_, &read_only_db));
ASSERT_OK(Put("after", "value"));
ASSERT_OK(Flush());
std::vector<LiveFileMetaData> live_files;
db_->GetLiveFilesMetaData(&live_files);
ASSERT_GE(live_files.size(), 2);
std::string newest_sst_path;
uint64_t newest_file_number = 0;
for (const auto& live_file : live_files) {
if (live_file.file_number > newest_file_number) {
newest_file_number = live_file.file_number;
newest_sst_path = live_file.directory + "/" + live_file.relative_filename;
}
}
ASSERT_FALSE(newest_sst_path.empty());
ASSERT_OK(env_->FileExists(newest_sst_path));
read_only_db.reset();
ASSERT_OK(env_->FileExists(newest_sst_path));
}
// Test mix of races with async file open, reads, compactions
TEST_P(OpenFilesAsyncTest, ConcurrentFileAccess) {
Options options = CurrentOptions();
@@ -8238,6 +8203,98 @@ TEST_P(OpenFilesAsyncTest, BeforeRead) {
}
}
// For modern DBs (manifest carries file_creation_time),
// GetCreationTimeOfOldestFile returns the real value without waiting for async
// file open.
TEST_P(OpenFilesAsyncTest, GetCreationTimeOfOldestFileSkipsWaitForModernDB) {
if (max_open_files_ != -1) {
return;
}
Options options = CurrentOptions();
ASSERT_NO_FATAL_FAILURE(SetupData(options));
// If WaitForAsyncFileOpen is entered, the test fails -- modern DBs must
// never need the wait.
std::atomic<bool> waited{false};
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"DBImpl::WaitForAsyncFileOpen::BeforeWait",
[&](void* /*arg*/) { waited.store(true); });
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
OpenTestDB(options);
uint64_t creation_time = std::numeric_limits<uint64_t>::max();
ASSERT_OK(dbfull()->GetCreationTimeOfOldestFile(&creation_time));
EXPECT_GT(creation_time, 0);
EXPECT_LT(creation_time, std::numeric_limits<uint64_t>::max());
EXPECT_FALSE(waited.load());
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
Close();
}
// For legacy DBs (manifest lacks file_creation_time, value comes from the
// pinned reader), GetCreationTimeOfOldestFile must block until
// BGWorkAsyncFileOpen pins the readers; otherwise it returns 0 (the "info
// unavailable" sentinel) instead of the real value.
TEST_P(OpenFilesAsyncTest,
GetCreationTimeOfOldestFileBlocksOnAsyncOpenForLegacyDB) {
if (max_open_files_ != -1) {
return;
}
Options options = CurrentOptions();
ASSERT_NO_FATAL_FAILURE(SetupData(options));
// Simulate a legacy DB: force TryGetFileCreationTime to return
// kUnknownFileCreationTime until BGWorkAsyncFileOpen completes (i.e., what
// would happen if the manifest had no file_creation_time and the pinned
// reader was the only source).
std::atomic<bool> async_open_done{false};
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"DBImpl::BGWorkAsyncFileOpen:Done",
[&](void* /*arg*/) { async_open_done.store(true); });
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
"Version::GetCreationTimeOfOldestFile::FileCreationTime", [&](void* arg) {
if (!async_open_done.load()) {
*static_cast<uint64_t*>(arg) = kUnknownFileCreationTime;
}
});
// Dependency chain: caller's first pass returns 0, enters
// WaitForAsyncFileOpen -> main thread confirms wait -> main thread releases
// async open -> background worker completes -> caller wakes -> second pass
// sees the real value.
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency(
{{"DBImpl::WaitForAsyncFileOpen::BeforeWait",
"OpenFilesAsyncTest::CreationTime::CallerBlocked"},
{"OpenFilesAsyncTest::CreationTime::CallerBlocked",
"OpenFilesAsyncTest::CreationTime::ReleaseAsyncOpen"},
{"OpenFilesAsyncTest::CreationTime::ReleaseAsyncOpen",
"DBImpl::BGWorkAsyncFileOpen::Start"}});
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
OpenTestDB(options);
uint64_t creation_time = std::numeric_limits<uint64_t>::max();
port::Thread caller([&]() {
ASSERT_OK(dbfull()->GetCreationTimeOfOldestFile(&creation_time));
});
TEST_SYNC_POINT("OpenFilesAsyncTest::CreationTime::CallerBlocked");
TEST_SYNC_POINT("OpenFilesAsyncTest::CreationTime::ReleaseAsyncOpen");
caller.join();
EXPECT_GT(creation_time, 0);
EXPECT_LT(creation_time, std::numeric_limits<uint64_t>::max());
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
Close();
}
TEST_P(OpenFilesAsyncTest, Shutdown) {
Options options = CurrentOptions();
ASSERT_NO_FATAL_FAILURE(SetupData(options));
+62 -3
View File
@@ -107,7 +107,7 @@ TEST_F(DBTest2, ReadOnlyDBWalInDbPathInitialized) {
ASSERT_OK(Put("key2", "value2"));
Close();
// Reopen as read-only — wal_in_db_path_ must be properly initialized.
// Reopen as read-only -- wal_in_db_path_ must be properly initialized.
// Before the fix, closing this DB would read an uninitialized bool in
// DeleteObsoleteFileImpl, which UBSan catches as undefined behavior.
std::unique_ptr<DB> db_ptr;
@@ -115,7 +115,7 @@ TEST_F(DBTest2, ReadOnlyDBWalInDbPathInitialized) {
std::string value;
ASSERT_OK(db_ptr->Get(ReadOptions(), "key1", &value));
ASSERT_EQ("value1", value);
// Close the read-only DB — this triggers PurgeObsoleteFiles which reads
// Close the read-only DB -- this triggers PurgeObsoleteFiles which reads
// wal_in_db_path_. Under UBSan, an uninitialized bool here would fail.
db_ptr.reset();
@@ -7510,6 +7510,65 @@ TEST_F(DBTest2, GetLatestSeqAndTsForKey) {
ASSERT_EQ(0, options.statistics->getTickerCount(GET_HIT_L0));
}
TEST_F(DBTest2,
GetLatestSequenceForKeyFromHistoryWithBlobBackedWideColumnEntity) {
// Goal: exercise the memtable history lookup in GetLatestSequenceForKey()
// after blob direct write stores a blob-backed V2 entity in a flushed
// memtable. Using cache_only=true ensures the lookup succeeds only if the
// history path can resolve the wide-column base value under the newer merge.
Destroy(last_options_);
Options options = CurrentOptions();
options.create_if_missing = true;
options.enable_blob_files = true;
options.enable_blob_direct_write = true;
options.min_blob_size = 50;
options.allow_concurrent_memtable_write = false;
options.blob_direct_write_partitions = 1;
options.max_write_buffer_size_to_maintain = 64 << 10;
options.merge_operator = MergeOperators::CreateStringAppendOperator("|");
Reopen(options);
const std::string key = "history_blob_entity";
const std::string default_value(100, 'd');
const std::string merge_operand = "suffix";
WideColumns columns{{kDefaultWideColumnName, default_value},
{"meta", "inline"}};
ASSERT_OK(
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
ASSERT_OK(db_->Merge(WriteOptions(), db_->DefaultColumnFamily(), key,
merge_operand));
const SequenceNumber expected_seq = dbfull()->GetLatestSequenceNumber();
ASSERT_OK(Flush());
ASSERT_FALSE(GetBlobFileNumbers().empty());
uint64_t num_immutable_memtables = 0;
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kNumImmutableMemTable,
&num_immutable_memtables));
ASSERT_EQ(num_immutable_memtables, 0);
auto* cfhi = static_cast_with_check<ColumnFamilyHandleImpl>(
dbfull()->DefaultColumnFamily());
assert(cfhi);
assert(cfhi->cfd());
SuperVersion* sv = cfhi->cfd()->GetSuperVersion();
SequenceNumber seq = kMaxSequenceNumber;
bool found_record_for_key = false;
bool is_blob_index = false;
const Status s = dbfull()->GetLatestSequenceForKey(
sv, key, /*cache_only=*/true, /*lower_bound_seq=*/0, &seq,
/*timestamp=*/nullptr, &found_record_for_key, &is_blob_index);
ASSERT_OK(s);
ASSERT_TRUE(found_record_for_key);
ASSERT_EQ(expected_seq, seq);
ASSERT_FALSE(is_blob_index);
}
#if defined(ZSTD)
TEST_F(DBTest2, ZSTDChecksum) {
// Verify that corruption during decompression is caught.
@@ -8173,7 +8232,7 @@ TEST_F(DBTest2, FastSstOpenDisableAfterMetadataPersisted) {
ASSERT_EQ(1, test_fs->GetMetadataRetrievedCount());
db.reset();
// Reopen with fast_sst_open DISABLED — metadata is in the MANIFEST
// Reopen with fast_sst_open DISABLED -- metadata is in the MANIFEST
// but should NOT be passed to the filesystem
options.fast_sst_open = false;
test_fs->ResetCounters();
+2 -2
View File
@@ -218,7 +218,7 @@ TEST_P(IterKeySwapTest, SwapAndDestroy) {
b_use_secondary] = GetParam();
std::string expected_a, expected_b;
// Storage for pinned keys — must outlive the IterKeys.
// Storage for pinned keys -- must outlive the IterKeys.
std::string a_storage, b_storage;
IterKey a;
expected_a =
@@ -236,7 +236,7 @@ TEST_P(IterKeySwapTest, SwapAndDestroy) {
// After swap: a has b's old data, b has a's old data.
ASSERT_EQ(a.GetUserKey().ToString(), expected_b);
ASSERT_EQ(b.GetUserKey().ToString(), expected_a);
} // b destroyed here — must not corrupt a's data
} // b destroyed here -- must not corrupt a's data
// a must still hold valid data after b's destruction.
ASSERT_EQ(a.GetUserKey().ToString(), expected_b);
+3
View File
@@ -266,6 +266,9 @@ class FaultInjectionTest
CloseDB();
env_->ResetState();
Status s = DB::Open(options_, dbname_, &db_);
if (!s.ok()) {
return s;
}
assert(db_ != nullptr);
return s;
}
+3 -3
View File
@@ -1696,7 +1696,7 @@ TEST_F(EventListenerTest, BackgroundJobPressure) {
Env::Priority::LOW);
sleeping_task.WaitUntilSleeping();
// Phase 1: No pressure — 3 SST files (below slowdown trigger=4).
// Phase 1: No pressure -- 3 SST files (below slowdown trigger=4).
for (int i = 0; i < 3; i++) {
ASSERT_OK(Put("k" + std::to_string(i), std::string(100, 'x')));
ASSERT_OK(Flush());
@@ -1714,7 +1714,7 @@ TEST_F(EventListenerTest, BackgroundJobPressure) {
// MaybeScheduleFlushOrCompaction() runs before the pressure callback and
// may schedule new flush work, making flush counts non-deterministic.
// Phase 2: Build pressure — flush past slowdown trigger (4 L0 SST files).
// Phase 2: Build pressure -- flush past slowdown trigger (4 L0 SST files).
// Compaction is blocked, so L0 SST files pile up.
listener->Reset();
{
@@ -1750,7 +1750,7 @@ TEST_F(EventListenerTest, BackgroundJobPressure) {
ASSERT_TRUE(found_compaction_scheduled);
ASSERT_TRUE(found_high_proximity);
// Phase 3: Relieve pressure — unblock compaction, wait for completion.
// Phase 3: Relieve pressure -- unblock compaction, wait for completion.
listener->Reset();
sleeping_task.WakeUp();
sleeping_task.WaitUntilDone();
+1 -4
View File
@@ -48,15 +48,12 @@ Reader::Reader(std::shared_ptr<Logger> info_log,
first_record_read_(false),
compression_type_(kNoCompression),
compression_type_record_read_(false),
uncompress_(nullptr),
uncompress_(),
hash_state_(nullptr),
uncompress_hash_state_(nullptr) {}
Reader::~Reader() {
delete[] backing_store_;
if (uncompress_) {
delete uncompress_;
}
if (hash_state_) {
XXH3_freeState(hash_state_);
}
+1 -1
View File
@@ -175,7 +175,7 @@ class Reader {
CompressionType compression_type_;
// Track whether the compression type record has been read or not.
bool compression_type_record_read_;
StreamingUncompress* uncompress_;
std::unique_ptr<StreamingUncompress> uncompress_;
// Reusable uncompressed output buffer
std::unique_ptr<char[]> uncompressed_buffer_;
// Reusable uncompressed record
+3 -6
View File
@@ -1224,11 +1224,11 @@ TEST_P(StreamingCompressionTest, Basic) {
}
CompressionOptions opts;
constexpr uint32_t compression_format_version = 2;
StreamingCompress* compress = StreamingCompress::Create(
auto compress = StreamingCompress::Create(
compression_type, opts, compression_format_version, kBlockSize);
StreamingUncompress* uncompress = StreamingUncompress::Create(
auto uncompress = StreamingUncompress::Create(
compression_type, compression_format_version, kBlockSize);
MemoryAllocator* allocator = new DefaultMemoryAllocator();
std::unique_ptr<MemoryAllocator> allocator(new DefaultMemoryAllocator());
std::string input_buffer = BigString("abc", input_size);
std::vector<std::string> compressed_buffers;
size_t remaining;
@@ -1266,9 +1266,6 @@ TEST_P(StreamingCompressionTest, Basic) {
} while (ret_val > 0 || output_pos == kBlockSize);
}
allocator->Deallocate((void*)uncompressed_output_buffer);
delete allocator;
delete compress;
delete uncompress;
// The final return value from uncompress() should be 0.
ASSERT_EQ(ret_val, 0);
ASSERT_EQ(input_buffer, uncompressed_buffer);
+1 -4
View File
@@ -32,7 +32,7 @@ Writer::Writer(std::unique_ptr<WritableFileWriter>&& dest, uint64_t log_number,
header_size_(recycle_log_files ? kRecyclableHeaderSize : kHeaderSize),
manual_flush_(manual_flush),
compression_type_(compression_type),
compress_(nullptr),
compress_(),
track_and_verify_wals_(track_and_verify_wals),
last_seqno_recorded_(0) {
for (uint8_t i = 0; i <= kMaxRecordType; i++) {
@@ -48,9 +48,6 @@ Writer::~Writer() {
if (dest_) {
WriteBuffer(WriteOptions()).PermitUncheckedError();
}
if (compress_) {
delete compress_;
}
ThreadStatusUtil::SetThreadOperation(cur_op_type);
}
+1 -1
View File
@@ -156,7 +156,7 @@ class Writer {
// Compression Type
CompressionType compression_type_;
StreamingCompress* compress_;
std::unique_ptr<StreamingCompress> compress_;
// Reusable compressed output buffer
std::unique_ptr<char[]> compressed_buffer_;
+80 -22
View File
@@ -15,6 +15,7 @@
#include <memory>
#include <optional>
#include "db/blob/blob_fetcher.h"
#include "db/blob/blob_file_partition_manager.h"
#include "db/blob/blob_index.h"
#include "db/dbformat.h"
@@ -70,6 +71,67 @@ Status GetDefaultColumnBlobIndexSlice(Slice entity, Slice* blob_index_slice) {
return status;
}
Status PushWideColumnEntityDefaultOperand(const Slice& user_key,
const Slice& entity,
MergeContext* merge_context,
bool operand_pinned,
const BlobFetcher* blob_fetcher) {
assert(merge_context != nullptr);
Slice entity_ref = entity;
Slice value_of_default;
Status status = WideColumnSerialization::GetValueOfDefaultColumn(
entity_ref, value_of_default);
if (status.ok()) {
merge_context->PushOperand(value_of_default, operand_pinned);
return status;
}
if (!status.IsNotSupported()) {
return status;
}
if (blob_fetcher == nullptr) {
return Status::Corruption(
"Cannot resolve blob-backed default column without a blob fetcher");
}
PinnableSlice resolved_default;
bool resolved = false;
status = WideColumnSerialization::GetValueOfDefaultColumnResolvingBlobs(
entity, user_key, blob_fetcher, resolved_default, resolved);
if (status.ok()) {
// Resolved blob values are backed by this stack-local PinnableSlice, so
// copy them into MergeContext instead of pinning their storage.
merge_context->PushOperand(Slice(resolved_default), false);
}
return status;
}
Status MergeWithWideColumnEntityBaseValue(
const Slice& user_key, const Slice& entity,
const MergeOperator* merge_operator, MergeContext* merge_context,
Logger* logger, Statistics* statistics, SystemClock* clock,
std::string* value, PinnableWideColumns* columns,
const BlobFetcher* blob_fetcher) {
assert(merge_context != nullptr);
std::string resolved_entity;
Slice effective_entity;
Status status = WideColumnSerialization::ResolveEntityForMerge(
entity, user_key, blob_fetcher, nullptr /* prefetch_buffers */,
resolved_entity, effective_entity);
if (!status.ok()) {
return status;
}
// `op_failure_scope` (an output parameter) is not provided (set to nullptr)
// since a failure must be propagated regardless of its value.
return MergeHelper::TimedFullMerge(
merge_operator, user_key, MergeHelper::kWideBaseValue, effective_entity,
merge_context->GetOperands(), logger, statistics, clock,
/* update_num_ops_stats */ true, /* op_failure_scope */ nullptr, value,
columns);
}
} // namespace
ImmutableMemTableOptions::ImmutableMemTableOptions(
@@ -1254,6 +1316,7 @@ struct Saver {
bool inplace_update_support;
bool do_merge;
SystemClock* clock;
const BlobFetcher* blob_fetcher;
ReadCallback* callback_;
bool* is_blob_index;
@@ -1424,27 +1487,17 @@ static bool SaveValue(void* arg, const char* entry) {
// Preserve the value with the goal of returning it as part of
// raw merge operands to the user
Slice value_of_default;
*(s->status) = WideColumnSerialization::GetValueOfDefaultColumn(
v, value_of_default);
if (s->status->ok()) {
merge_context->PushOperand(
value_of_default,
s->inplace_update_support == false /* operand_pinned */);
}
*(s->status) = PushWideColumnEntityDefaultOperand(
s->key->user_key(), v, merge_context,
s->inplace_update_support == false /* operand_pinned */,
s->blob_fetcher);
} else if (*(s->merge_in_progress)) {
assert(s->do_merge);
if (s->value || s->columns) {
// `op_failure_scope` (an output parameter) is not provided (set
// to nullptr) since a failure must be propagated regardless of
// its value.
*(s->status) = MergeHelper::TimedFullMerge(
merge_operator, s->key->user_key(), MergeHelper::kWideBaseValue,
v, merge_context->GetOperands(), s->logger, s->statistics,
s->clock, /* update_num_ops_stats */ true,
/* op_failure_scope */ nullptr, s->value, s->columns);
*(s->status) = MergeWithWideColumnEntityBaseValue(
s->key->user_key(), v, merge_operator, merge_context, s->logger,
s->statistics, s->clock, s->value, s->columns, s->blob_fetcher);
}
} else if (s->value) {
Slice value_of_default;
@@ -1520,7 +1573,8 @@ bool MemTable::Get(const LookupKey& key, std::string* value,
SequenceNumber* max_covering_tombstone_seq,
SequenceNumber* seq, const ReadOptions& read_opts,
bool immutable_memtable, ReadCallback* callback,
bool* is_blob_index, bool do_merge) {
bool* is_blob_index, bool do_merge,
const BlobFetcher* blob_fetcher) {
// The sequence number is updated synchronously in version_set.h
if (IsEmpty()) {
// Avoiding recording stats for speed.
@@ -1578,7 +1632,7 @@ bool MemTable::Get(const LookupKey& key, std::string* value,
}
GetFromTable(key, *max_covering_tombstone_seq, do_merge, callback,
is_blob_index, value, columns, timestamp, s, merge_context,
seq, &found_final_value, &merge_in_progress);
seq, &found_final_value, &merge_in_progress, blob_fetcher);
}
// No change to value, since we have not yet found a Put/Delete
@@ -1601,7 +1655,8 @@ void MemTable::GetFromTable(const LookupKey& key,
PinnableWideColumns* columns,
std::string* timestamp, Status* s,
MergeContext* merge_context, SequenceNumber* seq,
bool* found_final_value, bool* merge_in_progress) {
bool* found_final_value, bool* merge_in_progress,
const BlobFetcher* blob_fetcher) {
Saver saver;
saver.status = s;
saver.found_final_value = found_final_value;
@@ -1619,6 +1674,7 @@ void MemTable::GetFromTable(const LookupKey& key,
saver.inplace_update_support = moptions_.inplace_update_support;
saver.statistics = moptions_.statistics;
saver.clock = clock_;
saver.blob_fetcher = blob_fetcher;
saver.callback_ = callback;
saver.is_blob_index = is_blob_index;
saver.do_merge = do_merge;
@@ -1648,7 +1704,8 @@ Status MemTable::ValidateKey(const char* key, bool allow_data_in_errors) {
}
void MemTable::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
ReadCallback* callback, bool immutable_memtable) {
ReadCallback* callback, bool immutable_memtable,
const BlobFetcher* blob_fetcher) {
// The sequence number is updated synchronously in version_set.h
if (IsEmpty()) {
// Avoiding recording stats for speed.
@@ -1740,6 +1797,7 @@ void MemTable::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
saver.inplace_update_support = moptions_.inplace_update_support;
saver.statistics = moptions_.statistics;
saver.clock = clock_;
saver.blob_fetcher = blob_fetcher;
saver.callback_ = callback;
saver.is_blob_index = &iter->is_blob_index;
saver.do_merge = true;
@@ -1831,7 +1889,7 @@ void MemTable::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
*(iter->lkey), iter->max_covering_tombstone_seq, true, callback,
&iter->is_blob_index, iter->value ? iter->value->GetSelf() : nullptr,
iter->columns, iter->timestamp, iter->s, &(iter->merge_context),
&dummy_seq, &found_final_value, &merge_in_progress);
&dummy_seq, &found_final_value, &merge_in_progress, blob_fetcher);
if (!found_final_value && merge_in_progress) {
if (iter->s->ok()) {
+14 -8
View File
@@ -39,6 +39,7 @@
namespace ROCKSDB_NAMESPACE {
class BlobFetcher;
class BlobFilePartitionManager;
struct FlushJobInfo;
class Mutex;
@@ -236,25 +237,27 @@ class ReadOnlyMemTable {
SequenceNumber* max_covering_tombstone_seq,
SequenceNumber* seq, const ReadOptions& read_opts,
bool immutable_memtable, ReadCallback* callback = nullptr,
bool* is_blob_index = nullptr, bool do_merge = true) = 0;
bool* is_blob_index = nullptr, bool do_merge = true,
const BlobFetcher* blob_fetcher = nullptr) = 0;
bool Get(const LookupKey& key, std::string* value,
PinnableWideColumns* columns, std::string* timestamp, Status* s,
MergeContext* merge_context,
SequenceNumber* max_covering_tombstone_seq,
const ReadOptions& read_opts, bool immutable_memtable,
ReadCallback* callback = nullptr, bool* is_blob_index = nullptr,
bool do_merge = true) {
bool do_merge = true, const BlobFetcher* blob_fetcher = nullptr) {
SequenceNumber seq;
return Get(key, value, columns, timestamp, s, merge_context,
max_covering_tombstone_seq, &seq, read_opts, immutable_memtable,
callback, is_blob_index, do_merge);
callback, is_blob_index, do_merge, blob_fetcher);
}
// @param immutable_memtable Whether this memtable is immutable. Used
// internally by NewRangeTombstoneIterator(). See comment above
// NewRangeTombstoneIterator() for more detail.
virtual void MultiGet(const ReadOptions& read_options, MultiGetRange* range,
ReadCallback* callback, bool immutable_memtable) = 0;
ReadCallback* callback, bool immutable_memtable,
const BlobFetcher* blob_fetcher = nullptr) = 0;
// Get total number of entries in the mem table.
// REQUIRES: external synchronization to prevent simultaneous
@@ -320,7 +323,7 @@ class ReadOnlyMemTable {
virtual uint64_t ApproximateOldestKeyTime() const = 0;
// Inserts a range tombstone [start_key, end_key) that is logically redundant
// — it is derived from existing point tombstones observed during iteration
// -- it is derived from existing point tombstones observed during iteration
// and does not delete any data that isn't already deleted. This is a
// best-effort optimization. It allows future reads to skip iterating over
// continuous single deletion tombstones.
@@ -688,10 +691,12 @@ class MemTable final : public ReadOnlyMemTable {
SequenceNumber* max_covering_tombstone_seq, SequenceNumber* seq,
const ReadOptions& read_opts, bool immutable_memtable,
ReadCallback* callback = nullptr, bool* is_blob_index = nullptr,
bool do_merge = true) override;
bool do_merge = true,
const BlobFetcher* blob_fetcher = nullptr) override;
void MultiGet(const ReadOptions& read_options, MultiGetRange* range,
ReadCallback* callback, bool immutable_memtable) override;
ReadCallback* callback, bool immutable_memtable,
const BlobFetcher* blob_fetcher = nullptr) override;
// If `key` exists in current memtable with type value_type and the existing
// value is at least as large as the new value, updates it in-place. Otherwise
@@ -991,7 +996,8 @@ class MemTable final : public ReadOnlyMemTable {
std::string* value, PinnableWideColumns* columns,
std::string* timestamp, Status* s,
MergeContext* merge_context, SequenceNumber* seq,
bool* found_final_value, bool* merge_in_progress);
bool* found_final_value, bool* merge_in_progress,
const BlobFetcher* blob_fetcher);
// Always returns non-null and assumes certain pre-checks (e.g.,
// is_range_del_table_empty_) are done. This is only valid during the lifetime
+18 -14
View File
@@ -108,18 +108,19 @@ bool MemTableListVersion::Get(const LookupKey& key, std::string* value,
MergeContext* merge_context,
SequenceNumber* max_covering_tombstone_seq,
SequenceNumber* seq, const ReadOptions& read_opts,
ReadCallback* callback, bool* is_blob_index) {
ReadCallback* callback, bool* is_blob_index,
const BlobFetcher* blob_fetcher) {
return GetFromList(&memlist_, key, value, columns, timestamp, s,
merge_context, max_covering_tombstone_seq, seq, read_opts,
callback, is_blob_index);
callback, is_blob_index, blob_fetcher);
}
void MemTableListVersion::MultiGet(const ReadOptions& read_options,
MultiGetRange* range,
ReadCallback* callback) {
MultiGetRange* range, ReadCallback* callback,
const BlobFetcher* blob_fetcher) {
for (auto memtable : memlist_) {
memtable->MultiGet(read_options, range, callback,
true /* immutable_memtable */);
true /* immutable_memtable */, blob_fetcher);
if (range->empty()) {
return;
}
@@ -128,12 +129,13 @@ void MemTableListVersion::MultiGet(const ReadOptions& read_options,
bool MemTableListVersion::GetMergeOperands(
const LookupKey& key, Status* s, MergeContext* merge_context,
SequenceNumber* max_covering_tombstone_seq, const ReadOptions& read_opts) {
SequenceNumber* max_covering_tombstone_seq, const ReadOptions& read_opts,
const BlobFetcher* blob_fetcher) {
for (ReadOnlyMemTable* memtable : memlist_) {
bool done = memtable->Get(
key, /*value=*/nullptr, /*columns=*/nullptr, /*timestamp=*/nullptr, s,
merge_context, max_covering_tombstone_seq, read_opts,
true /* immutable_memtable */, nullptr, nullptr, false);
true /* immutable_memtable */, nullptr, nullptr, false, blob_fetcher);
if (done) {
return true;
}
@@ -145,10 +147,11 @@ bool MemTableListVersion::GetFromHistory(
const LookupKey& key, std::string* value, PinnableWideColumns* columns,
std::string* timestamp, Status* s, MergeContext* merge_context,
SequenceNumber* max_covering_tombstone_seq, SequenceNumber* seq,
const ReadOptions& read_opts, bool* is_blob_index) {
const ReadOptions& read_opts, bool* is_blob_index,
const BlobFetcher* blob_fetcher) {
return GetFromList(&memlist_history_, key, value, columns, timestamp, s,
merge_context, max_covering_tombstone_seq, seq, read_opts,
nullptr /*read_callback*/, is_blob_index);
nullptr /*read_callback*/, is_blob_index, blob_fetcher);
}
bool MemTableListVersion::GetFromList(
@@ -156,17 +159,18 @@ bool MemTableListVersion::GetFromList(
std::string* value, PinnableWideColumns* columns, std::string* timestamp,
Status* s, MergeContext* merge_context,
SequenceNumber* max_covering_tombstone_seq, SequenceNumber* seq,
const ReadOptions& read_opts, ReadCallback* callback, bool* is_blob_index) {
const ReadOptions& read_opts, ReadCallback* callback, bool* is_blob_index,
const BlobFetcher* blob_fetcher) {
*seq = kMaxSequenceNumber;
for (auto& memtable : *list) {
assert(memtable->IsFragmentedRangeTombstonesConstructed());
SequenceNumber current_seq = kMaxSequenceNumber;
bool done =
memtable->Get(key, value, columns, timestamp, s, merge_context,
max_covering_tombstone_seq, &current_seq, read_opts,
true /* immutable_memtable */, callback, is_blob_index);
bool done = memtable->Get(key, value, columns, timestamp, s, merge_context,
max_covering_tombstone_seq, &current_seq,
read_opts, true /* immutable_memtable */,
callback, is_blob_index, true, blob_fetcher);
if (*seq == kMaxSequenceNumber) {
// Store the most recent sequence number of any operation on this key.
// Since we only care about the most recent change, we only need to
+16 -9
View File
@@ -61,29 +61,33 @@ class MemTableListVersion {
MergeContext* merge_context,
SequenceNumber* max_covering_tombstone_seq, SequenceNumber* seq,
const ReadOptions& read_opts, ReadCallback* callback = nullptr,
bool* is_blob_index = nullptr);
bool* is_blob_index = nullptr,
const BlobFetcher* blob_fetcher = nullptr);
bool Get(const LookupKey& key, std::string* value,
PinnableWideColumns* columns, std::string* timestamp, Status* s,
MergeContext* merge_context,
SequenceNumber* max_covering_tombstone_seq,
const ReadOptions& read_opts, ReadCallback* callback = nullptr,
bool* is_blob_index = nullptr) {
bool* is_blob_index = nullptr,
const BlobFetcher* blob_fetcher = nullptr) {
SequenceNumber seq;
return Get(key, value, columns, timestamp, s, merge_context,
max_covering_tombstone_seq, &seq, read_opts, callback,
is_blob_index);
is_blob_index, blob_fetcher);
}
void MultiGet(const ReadOptions& read_options, MultiGetRange* range,
ReadCallback* callback);
ReadCallback* callback,
const BlobFetcher* blob_fetcher = nullptr);
// Returns all the merge operands corresponding to the key by searching all
// memtables starting from the most recent one.
bool GetMergeOperands(const LookupKey& key, Status* s,
MergeContext* merge_context,
SequenceNumber* max_covering_tombstone_seq,
const ReadOptions& read_opts);
const ReadOptions& read_opts,
const BlobFetcher* blob_fetcher = nullptr);
// Similar to Get(), but searches the Memtable history of memtables that
// have already been flushed. Should only be used from in-memory only
@@ -94,17 +98,19 @@ class MemTableListVersion {
Status* s, MergeContext* merge_context,
SequenceNumber* max_covering_tombstone_seq,
SequenceNumber* seq, const ReadOptions& read_opts,
bool* is_blob_index = nullptr);
bool* is_blob_index = nullptr,
const BlobFetcher* blob_fetcher = nullptr);
bool GetFromHistory(const LookupKey& key, std::string* value,
PinnableWideColumns* columns, std::string* timestamp,
Status* s, MergeContext* merge_context,
SequenceNumber* max_covering_tombstone_seq,
const ReadOptions& read_opts,
bool* is_blob_index = nullptr) {
bool* is_blob_index = nullptr,
const BlobFetcher* blob_fetcher = nullptr) {
SequenceNumber seq;
return GetFromHistory(key, value, columns, timestamp, s, merge_context,
max_covering_tombstone_seq, &seq, read_opts,
is_blob_index);
is_blob_index, blob_fetcher);
}
Status AddRangeTombstoneIterators(const ReadOptions& read_opts, Arena* arena,
@@ -188,7 +194,8 @@ class MemTableListVersion {
SequenceNumber* max_covering_tombstone_seq,
SequenceNumber* seq, const ReadOptions& read_opts,
ReadCallback* callback = nullptr,
bool* is_blob_index = nullptr);
bool* is_blob_index = nullptr,
const BlobFetcher* blob_fetcher = nullptr);
void AddMemTable(ReadOnlyMemTable* m);
+4 -1
View File
@@ -777,7 +777,10 @@ void TableCache::ReleaseObsolete(Cache* cache, uint64_t file_number,
if (table_handle != nullptr) {
TableReader* table_reader = typed_cache.Value(table_handle);
table_reader->MarkObsolete(uncache_aggressiveness);
typed_cache.ReleaseAndEraseIfLastRef(table_handle);
// Mark the entry Invisible so that if concurrent readers hold references,
// the entry will be erased when the last reference is released.
cache->Erase(GetSliceForFileNumber(&file_number));
typed_cache.Release(table_handle);
}
}
+1 -1
View File
@@ -30,7 +30,7 @@ PinnedTableReader& PinnedTableReader::operator=(
TableReader* r = other.reader_.load(std::memory_order_acquire);
// Only read handle_ when reader_ is non-null. Pin() writes handle_ before
// reader_ (with release), so a non-null reader_ guarantees handle_ is stable.
// If reader_ is null, Pin() may be in progress — avoid reading handle_.
// If reader_ is null, Pin() may be in progress -- avoid reading handle_.
handle_ = (r != nullptr) ? other.handle_ : nullptr;
reader_.store(r, std::memory_order_release);
return *this;
+15 -10
View File
@@ -2243,8 +2243,10 @@ void Version::GetCreationTimeOfOldestFile(uint64_t* creation_time) {
uint64_t oldest_time = std::numeric_limits<uint64_t>::max();
for (int level = 0; level < storage_info_.num_non_empty_levels_; level++) {
for (FileMetaData* meta : storage_info_.LevelFiles(level)) {
assert(meta->fd.pinned_reader.Get() != nullptr);
uint64_t file_creation_time = meta->TryGetFileCreationTime();
TEST_SYNC_POINT_CALLBACK(
"Version::GetCreationTimeOfOldestFile::FileCreationTime",
&file_creation_time);
if (file_creation_time == kUnknownFileCreationTime) {
*creation_time = 0;
return;
@@ -4217,10 +4219,10 @@ void VersionStorageInfo::ComputeFilesMarkedForReadTriggeredCompaction(
return;
}
// Skip files at the last non-empty level — there is no lower level to
// Skip files at the last non-empty level -- there is no lower level to
// compact into. Exception: L0 files are allowed even when L0 is the last
// non-empty level, because in single-level universal or FIFO compaction, L0
// files can be compacted together (L0 → L0).
// files can be compacted together (L0 -> L0).
int last_non_empty = num_non_empty_levels_ - 1;
for (int level = 0; level < num_levels(); level++) {
@@ -5690,7 +5692,7 @@ Status VersionSet::Close(FSDirectory* db_dir, InstrumentedMutex* mu) {
content_manifest_name, fs_->OptimizeForManifestRead(file_options_),
&manifest_file, nullptr);
if (!content_io_s.ok()) {
// Surface I/O errors to the caller — users who call DB::Close() and
// Surface I/O errors to the caller -- users who call DB::Close() and
// check the status should know about filesystem problems.
s = content_io_s;
ROCKS_LOG_ERROR(db_options_->info_log,
@@ -5729,6 +5731,8 @@ Status VersionSet::Close(FSDirectory* db_dir, InstrumentedMutex* mu) {
// Manifest is healthy, no need to check again
break;
}
RecordTick(db_options_->statistics.get(),
MANIFEST_VALIDATION_FAILURE_COUNT);
IOStatus corrupt_io_s =
IOStatus::Corruption("MANIFEST content validation failed");
IOErrorInfo io_error_info(corrupt_io_s, FileOperationType::kVerify,
@@ -5740,7 +5744,7 @@ Status VersionSet::Close(FSDirectory* db_dir, InstrumentedMutex* mu) {
corrupt_io_s.PermitUncheckedError();
io_error_info.io_status.PermitUncheckedError();
if (content_check == 0) {
// First check failed — rewrite and verify again
// First check failed -- rewrite and verify again
ROCKS_LOG_ERROR(db_options_->info_log,
"MANIFEST content verification on Close failed, "
"filename %s, rewriting manifest\n",
@@ -5751,7 +5755,7 @@ Status VersionSet::Close(FSDirectory* db_dir, InstrumentedMutex* mu) {
s = LogAndApply(cfd, ReadOptions(), WriteOptions(), &recovery_edit, mu,
db_dir);
} else {
// Rewritten manifest is also corrupt — likely a recurring filesystem
// Rewritten manifest is also corrupt -- likely a recurring filesystem
// issue. Surface it so DB::Close() callers can detect the problem.
ROCKS_LOG_ERROR(db_options_->info_log,
"MANIFEST content verification on Close failed again "
@@ -6713,7 +6717,7 @@ Status VersionSet::ReopenManifestForAppend(const std::string& manifest_path) {
FileOptions opt_file_opts = GetFileOptionsForManifestWrite();
// Bail if the on-disk size diverges from what Recover's Reader
// consumed — likely a torn tail from a prior crash; mid-block append
// consumed -- likely a torn tail from a prior crash; mid-block append
// would mis-frame the record stream.
uint64_t physical_size = 0;
IOStatus stat_s = fs_->GetFileSize(manifest_path, IOOptions(), &physical_size,
@@ -8159,11 +8163,12 @@ ReactiveVersionSet::ReactiveVersionSet(
const MutableDBOptions& mutable_db_options,
const FileOptions& _file_options, Cache* table_cache,
WriteBufferManager* write_buffer_manager, WriteController* write_controller,
const std::shared_ptr<IOTracer>& io_tracer)
const std::shared_ptr<IOTracer>& io_tracer, const std::string& db_id,
const std::string& db_session_id)
: VersionSet(dbname, imm_db_options, mutable_db_options, _file_options,
table_cache, write_buffer_manager, write_controller,
/*block_cache_tracer=*/nullptr, io_tracer, /*db_id*/ "",
/*db_session_id*/ "", /*daily_offpeak_time_utc*/ "",
/*block_cache_tracer=*/nullptr, io_tracer, db_id,
db_session_id, /*daily_offpeak_time_utc*/ "",
/*error_handler=*/nullptr, /*unchanging=*/false) {}
ReactiveVersionSet::~ReactiveVersionSet() = default;
+4 -2
View File
@@ -1726,7 +1726,7 @@ class VersionSet {
// MANIFEST as before) if ReopenWritableFile fails.
Status ReopenManifestForAppend(const std::string& manifest_path);
// FileOptions for MANIFEST writes — applies the FS's
// FileOptions for MANIFEST writes -- applies the FS's
// OptimizeForManifestWrite tuning, then re-applies the user-configured
// temperature so a custom FS can't override it.
FileOptions GetFileOptionsForManifestWrite() const;
@@ -1898,7 +1898,9 @@ class ReactiveVersionSet : public VersionSet {
const FileOptions& _file_options, Cache* table_cache,
WriteBufferManager* write_buffer_manager,
WriteController* write_controller,
const std::shared_ptr<IOTracer>& io_tracer);
const std::shared_ptr<IOTracer>& io_tracer,
const std::string& db_id,
const std::string& db_session_id);
~ReactiveVersionSet() override;
+34 -2
View File
@@ -20,6 +20,7 @@
#include "rocksdb/advanced_options.h"
#include "rocksdb/convenience.h"
#include "rocksdb/file_system.h"
#include "rocksdb/statistics.h"
#include "table/block_based/block_based_table_factory.h"
#include "table/mock_table.h"
#include "table/unique_id_impl.h"
@@ -1202,7 +1203,7 @@ class VersionSetTestBase {
reactive_versions_ = std::make_shared<ReactiveVersionSet>(
dbname_, &imm_db_options_, mutable_db_options_, env_options_,
table_cache_.get(), &write_buffer_manager_, &write_controller_,
nullptr);
/*io_tracer=*/nullptr, /*db_id=*/"", /*db_session_id=*/"");
imm_db_options_.db_paths.emplace_back(dbname_,
std::numeric_limits<uint64_t>::max());
}
@@ -2412,6 +2413,8 @@ TEST_F(VersionSetTest, ManifestContentValidationOnCloseClean) {
// Enable content validation, perform normal operations, close.
// Verify no manifest rotation (file number unchanged).
NewDB();
auto stats = CreateDBStatistics();
imm_db_options_.statistics = stats;
mutable_db_options_.verify_manifest_content_on_close = true;
mutex_.Lock();
versions_->UpdatedMutableDbOptions(mutable_db_options_, &mutex_);
@@ -2437,6 +2440,9 @@ TEST_F(VersionSetTest, ManifestContentValidationOnCloseClean) {
// Verify content validation actually ran
ASSERT_TRUE(content_validation_ran);
// No corruption -- counter should be zero
ASSERT_EQ(0, stats->getTickerCount(MANIFEST_VALIDATION_FAILURE_COUNT));
// Manifest path should be unchanged (no rotation)
std::string manifest_path_after;
uint64_t manifest_file_number = 0;
@@ -2452,6 +2458,8 @@ TEST_F(VersionSetTest, ManifestContentValidationOnCloseCorruptRecord) {
// Enable content validation, corrupt the manifest after closing the writer,
// verify manifest rotation occurs and DB reopens cleanly.
NewDB();
auto stats = CreateDBStatistics();
imm_db_options_.statistics = stats;
mutable_db_options_.verify_manifest_content_on_close = true;
mutex_.Lock();
versions_->UpdatedMutableDbOptions(mutable_db_options_, &mutex_);
@@ -2488,6 +2496,9 @@ TEST_F(VersionSetTest, ManifestContentValidationOnCloseCorruptRecord) {
CloseDB();
SyncPoint::GetInstance()->DisableProcessing();
// Corruption detected once, rewrite succeeded
ASSERT_EQ(1, stats->getTickerCount(MANIFEST_VALIDATION_FAILURE_COUNT));
// Manifest should have been rotated (new file number)
std::string manifest_path_after;
uint64_t manifest_file_number = 0;
@@ -2502,8 +2513,10 @@ TEST_F(VersionSetTest, ManifestContentValidationOnCloseCorruptRecord) {
TEST_F(VersionSetTest, ManifestContentValidationOnCloseDisabled) {
// Default (option disabled), corrupt manifest after writer close,
// verify no rotation occurred — corrupt manifest persists.
// verify no rotation occurred -- corrupt manifest persists.
NewDB();
auto stats = CreateDBStatistics();
imm_db_options_.statistics = stats;
// verify_manifest_content_on_close defaults to false
VersionEdit edit;
@@ -2525,6 +2538,9 @@ TEST_F(VersionSetTest, ManifestContentValidationOnCloseDisabled) {
ASSERT_FALSE(content_validation_ran);
// Validation disabled -- counter should be zero
ASSERT_EQ(0, stats->getTickerCount(MANIFEST_VALIDATION_FAILURE_COUNT));
// Manifest path should be unchanged (no rotation since validation is off)
std::string manifest_path_after;
uint64_t manifest_file_number = 0;
@@ -2539,6 +2555,8 @@ TEST_F(VersionSetTest, ManifestContentValidationOnCloseSizeCheckFails) {
// Verify recovery happens via size-check path. Content validation still
// runs afterward on the freshly rewritten manifest.
NewDB();
auto stats = CreateDBStatistics();
imm_db_options_.statistics = stats;
mutable_db_options_.verify_manifest_content_on_close = true;
mutex_.Lock();
versions_->UpdatedMutableDbOptions(mutable_db_options_, &mutex_);
@@ -2566,6 +2584,10 @@ TEST_F(VersionSetTest, ManifestContentValidationOnCloseSizeCheckFails) {
ASSERT_NO_FATAL_FAILURE(CloseDB());
SyncPoint::GetInstance()->DisableProcessing();
// Size check caught the issue; content validation on rewritten manifest
// should pass -- no content validation failure recorded
ASSERT_EQ(0, stats->getTickerCount(MANIFEST_VALIDATION_FAILURE_COUNT));
// Size check should have triggered rotation
std::string manifest_path_after;
uint64_t manifest_file_number = 0;
@@ -2583,6 +2605,8 @@ TEST_F(VersionSetTest, ManifestContentValidationOnCloseCorruptAfterRewrite) {
// The loop should detect corruption twice: once triggering a rewrite, and
// once reporting that the rewritten manifest is also corrupt.
NewDB();
auto stats = CreateDBStatistics();
imm_db_options_.statistics = stats;
mutable_db_options_.verify_manifest_content_on_close = true;
mutex_.Lock();
versions_->UpdatedMutableDbOptions(mutable_db_options_, &mutex_);
@@ -2636,12 +2660,17 @@ TEST_F(VersionSetTest, ManifestContentValidationOnCloseCorruptAfterRewrite) {
// OnIOError should have fired twice (once per corrupt detection)
ASSERT_EQ(io_error_count, 2);
// Validation failure counter should match
ASSERT_EQ(2, stats->getTickerCount(MANIFEST_VALIDATION_FAILURE_COUNT));
}
TEST_F(VersionSetTest, ManifestContentValidationOnCloseOpenFails) {
// Delete the manifest before content validation so it can't be opened.
// Close() should surface the I/O error to the caller.
NewDB();
auto stats = CreateDBStatistics();
imm_db_options_.statistics = stats;
mutable_db_options_.verify_manifest_content_on_close = true;
mutex_.Lock();
versions_->UpdatedMutableDbOptions(mutable_db_options_, &mutex_);
@@ -2667,6 +2696,9 @@ TEST_F(VersionSetTest, ManifestContentValidationOnCloseOpenFails) {
SyncPoint::GetInstance()->ClearAllCallBacks();
ASSERT_TRUE(close_s.IsIOError()) << close_s.ToString();
// File couldn't be opened -- no content validation ran
ASSERT_EQ(0, stats->getTickerCount(MANIFEST_VALIDATION_FAILURE_COUNT));
}
TEST_F(VersionStorageInfoTest, AddRangeDeletionCompensatedFileSize) {
+398 -2
View File
@@ -30,6 +30,10 @@ class DBWideBasicTest : public DBTestBase {
return wide_column_test_util::GetOptionsForBlobTest(GetDefaultOptions());
}
Options GetDirectWriteOptions() {
return wide_column_test_util::GetDirectWriteOptions(GetDefaultOptions());
}
// Helper: runs the EntityBlobAfterFlush test logic with the given options.
void RunEntityBlobAfterFlush(const Options& options);
@@ -2159,6 +2163,57 @@ TEST_F(DBWideBasicTest, MultiGetEntityWithBlobResolution) {
}
}
TEST_F(DBWideBasicTest,
MultiGetBlobBackedEntityDirectWriteMemtableBatchLookup) {
// Goal: force the memtable batch MultiGet optimization to read a direct-write
// blob-backed entity from the active memtable. One key exercises merge
// resolution against a blob-backed entity base, which requires Saver to carry
// the BlobFetcher through the batched callback path. The second key proves
// the non-merge default-column path still resolves correctly in the same
// batched request.
Options options = GetDirectWriteOptions();
options.min_blob_size = 50;
options.memtable_batch_lookup_optimization = true;
options.merge_operator = MergeOperators::CreateStringAppendOperator("|");
DestroyAndReopen(options);
const std::string merge_key = "multiget_direct_write_merge_key";
const std::string plain_key = "multiget_direct_write_plain_key";
const std::string default_value = GenerateLargeValue(100, 'd');
const std::string other_default_value = GenerateLargeValue(110, 'p');
const std::string large_value = GenerateLargeValue(120, 'l');
const std::string small_value = GenerateSmallValue();
const std::string merge_operand = "tail";
WideColumns merge_columns{{kDefaultWideColumnName, default_value},
{"col_large", large_value},
{"col_small", small_value}};
WideColumns plain_columns{{kDefaultWideColumnName, other_default_value},
{"col_large", large_value},
{"col_small", small_value}};
ASSERT_OK(db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(),
merge_key, merge_columns));
ASSERT_OK(db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(),
plain_key, plain_columns));
ASSERT_OK(db_->Merge(WriteOptions(), db_->DefaultColumnFamily(), merge_key,
merge_operand));
std::array<Slice, 2> keys{{merge_key, plain_key}};
std::array<PinnableSlice, 2> values;
std::array<Status, 2> statuses;
db_->MultiGet(ReadOptions(), db_->DefaultColumnFamily(), keys.size(),
keys.data(), values.data(), statuses.data());
ASSERT_OK(statuses[0]);
ASSERT_EQ(values[0], default_value + "|" + merge_operand);
ASSERT_OK(statuses[1]);
ASSERT_EQ(values[1], other_default_value);
}
void DBWideBasicTest::RunEntityBlobAfterFlush(const Options& options) {
Reopen(options);
@@ -2776,7 +2831,7 @@ TEST_F(DBWideBasicTest, EntityBlobBlockCacheTierGet) {
// Reopen to clear any caches
Reopen(options);
// Read with kBlockCacheTier — blob is not in cache
// Read with kBlockCacheTier -- blob is not in cache
ReadOptions read_opts;
read_opts.read_tier = kBlockCacheTier;
PinnableSlice result;
@@ -2909,6 +2964,347 @@ TEST_F(DBWideBasicTest, MergeEntityWithBlobColumns) {
}
}
TEST_F(DBWideBasicTest, GetMergeOperandsWithBlobBackedEntityDefaultColumn) {
// Goal: cover both GetMergeOperands code paths that read a compacted V2
// wide-column entity whose default column was moved to a blob file. The
// first read exercises the base-value path directly, then a merge operand is
// added so the second read exercises the merge-plus-base path.
Options options = GetBlobTestOptions();
options.min_blob_size = 50;
options.merge_operator = MergeOperators::CreateStringAppendOperator("|");
DestroyAndReopen(options);
const std::string key = "merge_operands_blob_entity";
const std::string default_value = GenerateLargeValue(100, 'd');
const std::string large_value = GenerateLargeValue(120, 'l');
const std::string small_value = GenerateSmallValue();
const std::string merge_operand = "suffix";
WideColumns columns{{kDefaultWideColumnName, default_value},
{"col_large", large_value},
{"col_small", small_value}};
ASSERT_OK(
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
ASSERT_OK(Flush());
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
ASSERT_FALSE(GetBlobFileNumbers().empty());
{
// The compacted V2 entity becomes the first operand when there are no
// newer merge operands above it.
GetMergeOperandsOptions get_merge_opts;
get_merge_opts.expected_max_number_of_operands = 1;
std::array<PinnableSlice, 2> merge_operands;
int number_of_operands = 0;
ASSERT_OK(db_->GetMergeOperands(ReadOptions(), db_->DefaultColumnFamily(),
key, merge_operands.data(), &get_merge_opts,
&number_of_operands));
ASSERT_EQ(number_of_operands, 1);
ASSERT_EQ(merge_operands[0], default_value);
}
ASSERT_OK(db_->Merge(WriteOptions(), db_->DefaultColumnFamily(), key,
merge_operand));
{
// After a merge is added, GetMergeOperands must still resolve the blob
// backed base default column while traversing the older base entry below
// the newer merge operand.
GetMergeOperandsOptions get_merge_opts;
get_merge_opts.expected_max_number_of_operands = 2;
std::array<PinnableSlice, 2> merge_operands;
int number_of_operands = 0;
ASSERT_OK(db_->GetMergeOperands(ReadOptions(), db_->DefaultColumnFamily(),
key, merge_operands.data(), &get_merge_opts,
&number_of_operands));
ASSERT_EQ(number_of_operands, 2);
ASSERT_EQ(merge_operands[0], default_value);
ASSERT_EQ(merge_operands[1], merge_operand);
}
}
TEST_F(DBWideBasicTest,
GetMergeOperandsWithBlobBackedEntityDefaultColumnDirectWriteMemtable) {
// Goal: cover the pre-flush memtable path when blob direct write stores a
// blob-backed V2 entity in memory. The first GetMergeOperands call exercises
// the no-merge base-operand path. After adding a merge, DB::Get exercises
// merge resolution against the same memtable entity, and a second
// GetMergeOperands call exercises the merge-plus-base path.
Options options = GetDirectWriteOptions();
options.min_blob_size = 50;
options.merge_operator = MergeOperators::CreateStringAppendOperator("|");
DestroyAndReopen(options);
const std::string key = "direct_write_merge_operands_blob_entity";
const std::string default_value = GenerateLargeValue(100, 'd');
const std::string large_value = GenerateLargeValue(120, 'l');
const std::string small_value = GenerateSmallValue();
const std::string merge_operand = "suffix";
const std::string expected_merged = default_value + "|" + merge_operand;
WideColumns columns{{kDefaultWideColumnName, default_value},
{"col_large", large_value},
{"col_small", small_value}};
ASSERT_OK(
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
{
GetMergeOperandsOptions get_merge_opts;
get_merge_opts.expected_max_number_of_operands = 1;
std::array<PinnableSlice, 2> merge_operands;
int number_of_operands = 0;
ASSERT_OK(db_->GetMergeOperands(ReadOptions(), db_->DefaultColumnFamily(),
key, merge_operands.data(), &get_merge_opts,
&number_of_operands));
ASSERT_EQ(number_of_operands, 1);
ASSERT_EQ(merge_operands[0], default_value);
}
ASSERT_OK(db_->Merge(WriteOptions(), db_->DefaultColumnFamily(), key,
merge_operand));
{
PinnableSlice result;
ASSERT_OK(
db_->Get(ReadOptions(), db_->DefaultColumnFamily(), key, &result));
ASSERT_EQ(result, expected_merged);
}
{
GetMergeOperandsOptions get_merge_opts;
get_merge_opts.expected_max_number_of_operands = 2;
std::array<PinnableSlice, 2> merge_operands;
int number_of_operands = 0;
ASSERT_OK(db_->GetMergeOperands(ReadOptions(), db_->DefaultColumnFamily(),
key, merge_operands.data(), &get_merge_opts,
&number_of_operands));
ASSERT_EQ(number_of_operands, 2);
ASSERT_EQ(merge_operands[0], default_value);
ASSERT_EQ(merge_operands[1], merge_operand);
}
}
TEST_F(DBWideBasicTest,
GetAndGetEntityWithBlobBackedDefaultColumnDirectWriteMemtableReadOnly) {
// Goal: cover the read-only reopen path after writing a blob-backed
// direct-write entity without manually flushing it first. The test checks
// both `Get()`, which must resolve the default-column blob reference, and
// `GetEntity()`, which must eagerly resolve the entity's unresolved blob
// columns before returning them to the caller.
Options options = GetDirectWriteOptions();
options.min_blob_size = 50;
DestroyAndReopen(options);
const std::string key = "readonly_direct_write_memtable_entity";
const std::string default_value = GenerateLargeValue(100, 'd');
const std::string large_value = GenerateLargeValue(120, 'l');
const std::string small_value = GenerateSmallValue();
WideColumns columns{{kDefaultWideColumnName, default_value},
{"col_large", large_value},
{"col_small", small_value}};
ASSERT_OK(
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
Close();
options.avoid_flush_during_recovery = true;
ASSERT_OK(ReadOnlyReopen(options));
{
PinnableSlice result;
ASSERT_OK(
db_->Get(ReadOptions(), db_->DefaultColumnFamily(), key, &result));
ASSERT_EQ(result, default_value);
}
{
PinnableWideColumns result;
ASSERT_OK(db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(), key,
&result));
ASSERT_EQ(result.columns(), columns);
}
}
TEST_F(DBWideBasicTest, ReadOnlyDirectWriteMemtableBlobBlockCacheTier) {
// Goal: cover the read-only memtable path under kBlockCacheTier. The test
// keeps a blob-backed direct-write entity in WAL-backed recovery state so
// resolving either Get() or GetEntity() would require blob I/O, which must
// surface as Incomplete rather than silently reading the blob file.
Options options = GetDirectWriteOptions();
options.min_blob_size = 50;
DestroyAndReopen(options);
const std::string key = "readonly_direct_write_memtable_block_cache_tier";
const std::string default_value = GenerateLargeValue(100, 'd');
const std::string large_value = GenerateLargeValue(120, 'l');
const std::string small_value = GenerateSmallValue();
WideColumns columns{{kDefaultWideColumnName, default_value},
{"col_large", large_value},
{"col_small", small_value}};
ASSERT_OK(
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
Close();
options.avoid_flush_during_recovery = true;
ASSERT_OK(ReadOnlyReopen(options));
ReadOptions read_opts;
read_opts.read_tier = kBlockCacheTier;
{
PinnableSlice result;
const Status s =
db_->Get(read_opts, db_->DefaultColumnFamily(), key, &result);
ASSERT_TRUE(s.IsIncomplete()) << s.ToString();
ASSERT_TRUE(result.empty());
}
{
PinnableWideColumns result;
const Status s =
db_->GetEntity(read_opts, db_->DefaultColumnFamily(), key, &result);
ASSERT_TRUE(s.IsIncomplete()) << s.ToString();
ASSERT_TRUE(result.columns().empty());
}
}
TEST_F(DBWideBasicTest,
GetMergeOperandsWithBlobBackedEntityDefaultColumnReadOnly) {
// Goal: exercise OpenForReadOnly on a blob-backed V2 entity base in SST with
// a newer merge operand in a separate SST. The read-only DB must resolve the
// blob-backed default column for both Get() and GetMergeOperands() without
// relying on mutable memtable state.
Options options = GetBlobTestOptions();
options.min_blob_size = 50;
options.merge_operator = MergeOperators::CreateStringAppendOperator("|");
DestroyAndReopen(options);
const std::string key = "readonly_merge_operands_blob_entity";
const std::string default_value = GenerateLargeValue(100, 'd');
const std::string large_value = GenerateLargeValue(120, 'l');
const std::string small_value = GenerateSmallValue();
const std::string merge_operand = "suffix";
const std::string expected_merged = default_value + "|" + merge_operand;
WideColumns columns{{kDefaultWideColumnName, default_value},
{"col_large", large_value},
{"col_small", small_value}};
ASSERT_OK(
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
ASSERT_OK(Flush());
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
ASSERT_FALSE(GetBlobFileNumbers().empty());
ASSERT_OK(db_->Merge(WriteOptions(), db_->DefaultColumnFamily(), key,
merge_operand));
ASSERT_OK(Flush());
Close();
ASSERT_OK(ReadOnlyReopen(options));
{
PinnableSlice result;
ASSERT_OK(
db_->Get(ReadOptions(), db_->DefaultColumnFamily(), key, &result));
ASSERT_EQ(result, expected_merged);
}
{
GetMergeOperandsOptions get_merge_opts;
get_merge_opts.expected_max_number_of_operands = 2;
std::array<PinnableSlice, 2> merge_operands;
int number_of_operands = 0;
ASSERT_OK(db_->GetMergeOperands(ReadOptions(), db_->DefaultColumnFamily(),
key, merge_operands.data(), &get_merge_opts,
&number_of_operands));
ASSERT_EQ(number_of_operands, 2);
ASSERT_EQ(merge_operands[0], default_value);
ASSERT_EQ(merge_operands[1], merge_operand);
}
}
TEST_F(DBWideBasicTest,
GetMergeOperandsWithBlobBackedEntityDefaultColumnBlockCacheTier) {
// Goal: prove the SST-backed GetMergeOperands path propagates Incomplete
// when resolving a blob-backed default column would require I/O. One key
// covers the direct base-entity path, and the other covers the merge-plus-
// base path with the merge operand flushed to a newer SST.
Options options = GetBlobTestOptions();
options.min_blob_size = 50;
options.merge_operator = MergeOperators::CreateStringAppendOperator("|");
DestroyAndReopen(options);
const std::string base_key = "merge_operands_blob_entity_cache_base";
const std::string merge_key = "merge_operands_blob_entity_cache_merge";
const std::string default_value = GenerateLargeValue(100, 'd');
const std::string large_value = GenerateLargeValue(120, 'l');
const std::string small_value = GenerateSmallValue();
WideColumns columns{{kDefaultWideColumnName, default_value},
{"col_large", large_value},
{"col_small", small_value}};
ASSERT_OK(db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), base_key,
columns));
ASSERT_OK(db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(),
merge_key, columns));
ASSERT_OK(Flush());
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
ASSERT_OK(db_->Merge(WriteOptions(), db_->DefaultColumnFamily(), merge_key,
"tail"));
ASSERT_OK(Flush());
// Reopen to clear any cached blob readers/values so kBlockCacheTier must
// report Incomplete instead of succeeding through a warm cache.
Reopen(options);
ReadOptions read_opts;
read_opts.read_tier = kBlockCacheTier;
{
GetMergeOperandsOptions get_merge_opts;
get_merge_opts.expected_max_number_of_operands = 1;
std::array<PinnableSlice, 2> merge_operands;
int number_of_operands = 0;
const Status s = db_->GetMergeOperands(
read_opts, db_->DefaultColumnFamily(), base_key, merge_operands.data(),
&get_merge_opts, &number_of_operands);
ASSERT_TRUE(s.IsIncomplete()) << s.ToString();
}
{
GetMergeOperandsOptions get_merge_opts;
get_merge_opts.expected_max_number_of_operands = 2;
std::array<PinnableSlice, 2> merge_operands;
int number_of_operands = 0;
const Status s = db_->GetMergeOperands(
read_opts, db_->DefaultColumnFamily(), merge_key, merge_operands.data(),
&get_merge_opts, &number_of_operands);
ASSERT_TRUE(s.IsIncomplete()) << s.ToString();
}
}
TEST_F(DBWideBasicTest, MergeEntityWithBlobColumnsNoDefault) {
// Test: Merge on a V2 entity without a default column. The merge result
// should produce a valid entity with all original columns plus a default
@@ -2998,7 +3394,7 @@ TEST_F(DBWideBasicTest, MergeEntityWithBlobColumnsBlockCacheTier) {
// Reopen to clear caches
Reopen(options);
// Read with kBlockCacheTier — blob not in cache, merge needs blob resolution
// Read with kBlockCacheTier -- blob not in cache, merge needs blob resolution
ReadOptions read_opts;
read_opts.read_tier = kBlockCacheTier;
PinnableSlice result;
+1 -1
View File
@@ -59,7 +59,7 @@ Status ReadPathBlobResolver::ResolveColumn(size_t column_index,
blob_resolver_util::FindBlobColumn(blob_columns_, column_index);
if (blob_index_ptr == nullptr) {
// Inline column — return the value directly
// Inline column -- return the value directly
*resolved_value = (*columns_)[column_index].value();
} else {
// Check if already resolved
+4
View File
@@ -153,6 +153,8 @@ class WideColumnSerialization {
// value() is initially the serialized BlobIndex bytes from the entity.
// blob_columns: receives (column_index, blob_index) pairs identifying which
// entries in `columns` are blob references and their decoded BlobIndex data.
// For valid input, this must agree with HasBlobColumns() about whether blob
// column references are present.
static Status DeserializeV2(
Slice& input, std::vector<WideColumn>& columns,
std::vector<std::pair<size_t, BlobIndex>>& blob_columns);
@@ -160,6 +162,8 @@ class WideColumnSerialization {
// Check if the serialized entity has any blob column references.
// Sets *has_blob_columns to true if version >= 2 and at least one column
// has blob type; false otherwise.
// For valid input, this must agree with DeserializeV2() about whether blob
// column references are present.
// Returns Status::Corruption on decode errors.
static Status HasBlobColumns(const Slice& input, bool& has_blob_columns);
+2 -2
View File
@@ -391,7 +391,7 @@ static BlobIndex MakeBlobIndex(uint64_t file_number, uint64_t offset,
return bi;
}
// Helper: V2 serialize → DeserializeV2 round-trip, returning
// Helper: V2 serialize -> DeserializeV2 round-trip, returning
// deserialized columns and blob column info.
static void V2SerializeAndDeserialize(
const std::vector<std::pair<std::string, std::string>>& columns,
@@ -798,7 +798,7 @@ TEST_F(WideColumnSerializationTest, RandomizedSerializeDeserializeRoundTrip) {
}
}
// V2 serialize → DeserializeV2 round-trip
// V2 serialize -> DeserializeV2 round-trip
std::string serialized;
std::vector<WideColumn> deserialized;
std::vector<std::pair<size_t, BlobIndex>> blob_out;
+2 -2
View File
@@ -197,10 +197,10 @@ class DbStressFSWrapper : public FileSystemWrapper {
assert(!file_opts.file_checksum_func_name.empty());
if (file_opts.file_checksum_func_name == kUnknownFileChecksumFuncName ||
file_opts.file_checksum_func_name == kNoFileChecksumFuncName) {
// No checksum available — checksum value must be empty
// No checksum available -- checksum value must be empty
assert(file_opts.file_checksum.empty());
} else {
// A real checksum function — checksum value must be present
// A real checksum function -- checksum value must be present
assert(!file_opts.file_checksum.empty());
}
}
+1 -1
View File
@@ -2900,7 +2900,7 @@ class NonBatchedOpsStressTest : public StressTest {
op_logs += "N";
}
// backward scan — skip when backward iteration is not supported
// backward scan -- skip when backward iteration is not supported
if (FLAGS_test_backward_scan) {
key_str = Key(ub - 1);
iter->SeekForPrev(key_str);
+5 -1
View File
@@ -88,7 +88,7 @@ hx235:
pdillinger:
full_name: Peter Dillinger
fbid: 513108
fbid: 593944283
alanpaxton:
full_name: Alan Paxton
@@ -101,3 +101,7 @@ anand1976:
poojam23:
full_name: Pooja Malik
joshkang97:
full_name: Josh Kang
fbid: 180489168460795
@@ -0,0 +1,118 @@
---
title: Interpolation search for SST index blocks
layout: post
author: joshkang97
category: blog
---
For workloads with uniformly distributed keys, RocksDB now supports **interpolation search** for SST index blocks as an alternative to the default binary search.
## The idea
Binary search always splits the remaining range in half:
```
mid = low + (high - low) / 2
```
That's Θ(log n) probes regardless of the data. Interpolation search instead estimates where the target should land based on its value relative to the current boundaries:
```
probe = low + (target - key[low]) * (high - low) / (key[high] - key[low])
```
On uniformly distributed keys, that's expected O(log log n) probes. The canonical example: for an index block with restart keys `0, 1, 2, ..., 1023` and a seek for `900`, binary search needs about 10 hops; interpolation search lands on it in 1.
The catch is that pure interpolation search degrades to O(n) on badly skewed data.
## Turning a key into a number
The interpolation formula needs numeric values, but index keys are variable-length byte slices. RocksDB extracts a `uint64_t` per key by reading the first 8 bytes after the common prefix shared by the block's boundary keys, in big-endian, and zero-pads to the right if the remaining bytes are too short.
```cpp
inline uint64_t ReadBe64FromKey(Slice s, bool is_user_key, size_t offset) {
// ... strip internal seq/type bytes if needed ...
if (s.size() - offset >= 8) {
uint64_t val;
memcpy(&val, s.data() + offset, sizeof(val));
return port::kLittleEndian ? EndianSwapValue(val) : val;
}
// pad short tails with zeros on the right (preserves bytewise order)
}
```
Big-endian + zero-pad preserves bytewise ordering, so the linear interpolation formula stays consistent with the comparator. This is also why the feature requires `BytewiseComparator`.
Two distinct keys can still collapse to the same `uint64_t` once you go past the first 8 non-shared bytes. To avoid a divide-by-zero, we simply fall back to binary search in that case.
## How to enable it
To force interpolation search on every index block:
```cpp
rocksdb::BlockBasedTableOptions table_options;
table_options.index_block_search_type =
rocksdb::BlockBasedTableOptions::kInterpolation;
```
## kAuto: per-block selection
`kAuto` is the recommended way to use the feature. It chooses the search algorithm for each index block automatically, based on a uniformity hint written into the block footer at SST construction time:
```cpp
table_options.index_block_search_type =
rocksdb::BlockBasedTableOptions::kAuto;
table_options.uniform_cv_threshold = 0.2;
```
When `uniform_cv_threshold >= 0`, the SST writer scans each index block's restart keys and computes the **coefficient of variation (CV)** of the gaps between consecutive numeric key values:
```
gap[i] = key_value[i + 1] - key_value[i]
CV = stddev(gap) / mean(gap)
```
Lower CV means the gaps are more uniform — and the more likely interpolation search will outperform binary search. The CV is computed incrementally with Welford's online algorithm, so the scan is one pass over the restart points.
If `CV < uniform_cv_threshold`, RocksDB sets an `is_uniform` bit in the block footer. At read time, `kAuto` resolves to `kInterpolation` only when that bit is set *and* the comparator is bytewise; otherwise it uses `kBinary`.
### Write overhead
Computing the `is_uniform` bit is a cheap operation as it is only computed for the index blocks in a SST file. CPU profiling of `db_bench -benchmarks=fillseq,compact -compression_type=none -disable_wal=1` attributes only ~0.08% of write-path CPU to `ScanForUniformity`.
After a few more releases, we plan to enable kAuto and uniform_cv_threshold by default.
## Benchmarks
Setup — populate a DB and force a single-level shape so all reads hit the same index structure, then measure point-read throughput:
```
# Build a release binary
make clean && DEBUG_LEVEL=0 make db_bench
# Load + compact, varying the index_shortening_mode
./db_bench -benchmarks=fillrandom,compact \
-index_shortening_mode=1
# Then point-read against the populated DB
./db_bench -use_existing_db=true -benchmarks=readrandom \
-index_block_search_type=binary_search # or interpolation_search / auto_search
```
`index_shortening_mode=1` (`kShortenSeparators`) keeps the file's last index key intact, which preserves a roughly uniform numeric distribution for the benchmark.
Results, averaged over multiple runs:
| Mode | ops/s | vs `binary_search` |
|---|---|---|
| `binary_search` | 335,749 | baseline |
| `interpolation_search` | 366,598 | **+9.2%** |
| `auto_search` | 366,832 | **+9.2%** |
## Compatibility
The `is_uniform` bit reuses a previously-reserved bit in the data block footer. SSTs written by older RocksDB never set it and decode as `is_uniform = false`, so they read with binary search under `kAuto`. However, after the bit is set, if read by an older RocksDB version < 11.0.0, it will read it as a corruption error.
## Future work
Some future opportunities can involve extending interpolation search to data blocks, as well as supporting other comparators such as `ReverseBytewiseComparator`.
+2 -2
View File
@@ -1892,7 +1892,7 @@ TEST_F(EnvPosixTest, SupportedOpsNoAsyncIOOnIOUringInitFailure) {
int64_t thread_supported_ops2 = 0;
std::thread t([&]() {
fs->SupportedOps(thread_supported_ops);
// Second call on the same thread — cached failure, no retry.
// Second call on the same thread -- cached failure, no retry.
fs->SupportedOps(thread_supported_ops2);
});
t.join();
@@ -1903,7 +1903,7 @@ TEST_F(EnvPosixTest, SupportedOpsNoAsyncIOOnIOUringInitFailure) {
// Second call should also lack kAsyncIO.
ASSERT_EQ(thread_supported_ops2 & (1 << FSSupportedOps::kAsyncIO), 0);
ASSERT_NE(thread_supported_ops2 & (1 << FSSupportedOps::kFSPrefetch), 0);
// CreateIOUring must have been called exactly once — not retried.
// CreateIOUring must have been called exactly once -- not retried.
ASSERT_EQ(create_count, 1);
SyncPoint::GetInstance()->DisableProcessing();
+1 -1
View File
@@ -221,7 +221,7 @@ IOStatus GenerateOneFileChecksum(
std::unique_ptr<FSRandomAccessFile> r_file;
FileOptions fopts = file_options;
if (fopts.file_checksum.empty()) {
// No expected checksum is known — this is a from-scratch computation.
// No expected checksum is known -- this is a from-scratch computation.
fopts.file_checksum_func_name = kNoFileChecksumFuncName;
}
io_s = fs->NewRandomAccessFile(file_path, fopts, &r_file, nullptr);
+1 -1
View File
@@ -3899,7 +3899,7 @@ TEST_P(FSBufferPrefetchTest, FSBufferPrefetchStatsInternals) {
&result, &s, for_compaction);
// Platforms that don't have IO uring may not support async IO.
// With the ReadAsync sync fallback, s will be OK even when async IO is
// unavailable — detect by checking if the second buffer has an async read
// unavailable -- detect by checking if the second buffer has an async read
// in progress.
if (use_async_prefetch && s.IsNotSupported()) {
return;
+39 -67
View File
@@ -1,89 +1,61 @@
#!/usr/bin/env bash
# pre-push hook: auto-format changed code before pushing.
# pre-push hook: block pushing on common mistakes.
#
# Runs `make format-auto` to apply clang-format to files changed relative to
# the upstream merge base. If any files are reformatted, they are committed as
# a new "format" commit and the push is retried automatically.
# 1. Checks for untracked source files in tracked directories, which likely
# need to be committed or removed before the pushed code will build.
# 2. Runs `make check-format` to verify formatting.
#
# Install with:
# make install-hooks
# Automatically active via `make all` / `make check` (sets core.hooksPath).
#
# Skip with:
# git push --no-verify
set -e
REMOTE="$1"
URL="$2"
# Only act when we have a terminal (skip in CI / automated pushes)
if [ ! -t 1 ] && [ -z "$ROCKSDB_FORMAT_HOOK" ]; then
exit 0
fi
# Read the refs being pushed from stdin
PUSH_REFS=()
while read local_ref local_oid remote_ref remote_oid; do
PUSH_REFS+=("$local_ref:$remote_ref")
done
FAILED=0
# --- Check for untracked source files in tracked directories ---
SUSPECT_FILES=$(git ls-files --others --exclude-standard \
| grep -E '\.(cc|cpp|c|h|hpp|java|py|mk|sh)$' \
| grep -v '^third-party/' \
| while IFS= read -r f; do
top_dir="${f%%/*}"
if [ "$top_dir" = "$f" ] || [ -n "$(git ls-files "$top_dir/" | head -1)" ]; then
echo "$f"
fi
done)
if [ -n "$SUSPECT_FILES" ]; then
echo ""
echo "==========================================================="
echo " pre-push: untracked source files in tracked directories."
echo " Push blocked. These may need to be committed, or the"
echo " pushed code could fail to build:"
echo ""
echo "$SUSPECT_FILES" | sed 's/^/ /'
echo ""
echo " Commit them, add to .gitignore, delete, or --no-verify."
echo "==========================================================="
FAILED=1
fi
# --- Check formatting ---
echo "pre-push: running format check..."
# Save any uncommitted work so format-auto only touches committed code
NEED_STASH=0
if ! git diff --quiet || ! git diff --cached --quiet; then
git stash push -q -m "pre-push-hook-$(date +%s)"
NEED_STASH=1
fi
cleanup() {
if [ "$NEED_STASH" -eq 1 ]; then
git stash pop -q 2>/dev/null || true
fi
}
trap cleanup EXIT
# Run format check (check-only mode first to see if anything needs fixing)
if make check-format 2>/dev/null; then
echo "pre-push: formatting is clean."
exit 0
fi
echo "pre-push: formatting issues detected, auto-fixing..."
make format-auto
# Check if format-auto actually changed anything
if git diff --quiet; then
echo "pre-push: formatting is clean after auto-fix."
exit 0
fi
# Create a new format commit
git add -A
git commit -m "format: auto-format code" --no-verify -q
# Build the refspecs using the current (post-format) branch HEAD
REFSPECS=()
for ref_pair in "${PUSH_REFS[@]}"; do
remote_ref="${ref_pair#*:}"
REFSPECS+=("HEAD:$remote_ref")
done
# Retry push, skipping the hook to avoid recursion
if git push --no-verify "$REMOTE" "${REFSPECS[@]}" 2>&1; then
if ! make check-format; then
echo ""
echo "==========================================================="
echo " pre-push: automatically formatted with a new commit and"
echo " pushed. You can ignore the 'failed to push' message below"
echo " — that is git cancelling the stale push; your code is"
echo " already on the remote."
echo " pre-push: formatting issues detected. Push blocked."
echo " Run 'make format-auto' then amend your commit and retry."
echo "==========================================================="
exit 1
FAILED=1
else
echo ""
echo "==========================================================="
echo " pre-push: format commit was added but push failed."
echo " Please run 'git push' again manually."
echo "==========================================================="
exit 1
echo "pre-push: formatting is clean."
fi
exit $FAILED
+6 -1
View File
@@ -152,7 +152,12 @@ class Cache : public Customizable {
// For helpers without SecondaryCache support
explicit CacheItemHelper(CacheEntryRole _role, DeleterFn _del_cb = nullptr)
: CacheItemHelper(_role, _del_cb, nullptr, nullptr, nullptr, this) {}
: del_cb(_del_cb),
size_cb(nullptr),
saveto_cb(nullptr),
create_cb(nullptr),
role(_role),
without_secondary_compat(this) {}
// For helpers with SecondaryCache support
explicit CacheItemHelper(CacheEntryRole _role, DeleterFn _del_cb,
+11
View File
@@ -456,6 +456,13 @@ class CompressionManager
// supported)
virtual bool SupportsCompressionType(CompressionType type) const = 0;
// Return a human-readable name for the given compression type within this
// CompressionManager's schema. The default implementation returns the
// generic built-in name such as "Snappy", "ZSTD", "Reserved4F", or
// "Custom8A". Override to provide more specific names for custom
// compression types.
virtual std::string CompressionTypeToString(CompressionType type) const;
// TODO: function to check compatibility with or sanitize CompressionOptions
// ************************* Compressor creation *********************** //
@@ -656,6 +663,10 @@ class CompressionManagerWrapper : public CompressionManager {
return wrapped_->SupportsCompressionType(type);
}
std::string CompressionTypeToString(CompressionType type) const override {
return wrapped_->CompressionTypeToString(type);
}
std::unique_ptr<Compressor> GetCompressorForSST(
const FilterBuildingContext& context, const CompressionOptions& opts,
CompressionType preferred) override {
+6 -6
View File
@@ -946,7 +946,7 @@ struct AdvancedColumnFamilyOptions {
// (estimated_reads / file_size) exceeds this threshold. This helps reduce
// read amplification for hot keys by compacting frequently-read files.
//
// Only "collapsible" reads are counted — lookups that return NotFound
// Only "collapsible" reads are counted -- lookups that return NotFound
// (bloom filter false positive), Delete/SingleDeletion (tombstone), or
// Merge (partial result). These are reads where the file contributed no
// final value and compaction would eliminate the wasted work.
@@ -972,15 +972,15 @@ struct AdvancedColumnFamilyOptions {
// r * S * B = 2 * (1 + F) * S
// r = 2 * (1 + F) / B
//
// With F = 10, B = 4096: r = 22 / 4096 ≈ 0.005.
// With F = 10, B = 4096: r = 22 / 4096 ~= 0.005.
//
// With a block-cache hit rate h (0 ≤ h < 1), each collapsible read
// With a block-cache hit rate h (0 <= h < 1), each collapsible read
// only costs (1 - h) * B bytes of actual disk IO, so:
// r = 2 * (1 + F) / ((1 - h) * B)
//
// h = 0 → r ≈ 0.005
// h = 0.5 → r ≈ 0.01
// h = 0.9 → r ≈ 0.05
// h = 0 -> r ~= 0.005
// h = 0.5 -> r ~= 0.01
// h = 0.9 -> r ~= 0.05
//
// A recommended starting point is 0.01, which avoids triggering
// compactions that cost more IO than they save for most cache-friendly
+5 -1
View File
@@ -287,6 +287,10 @@ class CompactionFilter : public Customizable {
// may not realize there is a write conflict and may allow a Transaction to
// Commit that should have failed. Instead, it is better to implement any
// Merge filtering inside the MergeOperator.
//
// Note: Under WritePrepared, WriteUnprepared, and WriteCommitted
// TransactionDB, this method is skipped for operands at or below the latest
// published sequence, typically every operand in the input.
virtual bool FilterMergeOperand(int /*level*/, const Slice& /*key*/,
const Slice& /*operand*/) const {
return false;
@@ -416,7 +420,7 @@ class CompactionFilter : public Customizable {
// Returns true if the filter overrides FilterV4 and can handle
// WideColumnBlobResolver (lazy blob loading). When false (default),
// FilterV4 delegates to FilterV3 with fully resolved column values —
// FilterV4 delegates to FilterV3 with fully resolved column values --
// blob columns are eagerly fetched before the filter is called, ensuring
// backward compatibility with FilterV3-only filters.
//
+7
View File
@@ -1868,6 +1868,13 @@ class DB {
// and may not have file_creation_time property. In both the cases
// file_creation_time is considered 0 which means this API will return
// creation_time = 0 as there wouldn't be a timestamp lower than 0.
//
// NOTE: When the DB was opened with open_files_async = true, this API may
// block to wait for background SST file loading to complete, but only when
// necessary (i.e., when a file's manifest does not carry file_creation_time
// and the value must come from the SST reader). Modern DBs return the real
// value immediately. It is possible to have an incomplete result if the DB
// is closed while files are still being opened in the background.
virtual Status GetCreationTimeOfOldestFile(uint64_t* creation_time) = 0;
// Note: this API is not yet consistent with WritePrepared transactions.
+5 -2
View File
@@ -235,18 +235,21 @@ struct ExternalTableBuilderOptions {
const std::string db_id;
const std::string db_session_id;
const TableFileCreationReason reason;
const std::shared_ptr<FileSystem>& fs;
ExternalTableBuilderOptions(
const ReadOptions& _read_options, const WriteOptions& _write_options,
const std::shared_ptr<const SliceTransform>& _prefix_extractor,
const Comparator* _comparator, const std::string& _column_family_name,
const TableFileCreationReason _reason)
const TableFileCreationReason _reason,
const std::shared_ptr<FileSystem>& _fs)
: read_options(_read_options),
write_options(_write_options),
prefix_extractor(_prefix_extractor),
comparator(_comparator),
column_family_name(_column_family_name),
reason(_reason) {}
reason(_reason),
fs(_fs) {}
};
class ExternalTableFactory : public Customizable {
+1 -1
View File
@@ -556,7 +556,7 @@ struct IOErrorInfo {
uint64_t offset;
};
// EXPERIMENTAL — under active development, fields may change.
// EXPERIMENTAL -- under active development, fields may change.
// Point-in-time snapshot of background job pressure for one DB: how busy
// compaction and flush are, and how close the DB is to write-stalling.
struct BackgroundJobPressure {
+1 -1
View File
@@ -2952,7 +2952,7 @@ struct SizeApproximationOptions {
// blob file data in the key range. When enabled, the total blob file size
// is prorated by the ratio of SST data in the range to the total SST data:
//
// blob_size_in_range ≈ total_blob_size * (sst_in_range / total_sst)
// blob_size_in_range ~= total_blob_size * (sst_in_range / total_sst)
//
// Limitations of this approximation:
// - Assumes blob data is distributed proportionally to SST data, which
+7
View File
@@ -9,6 +9,7 @@
#include "rocksdb/options.h"
#include "rocksdb/slice.h"
#include "rocksdb/table_properties.h"
#include "rocksdb/types.h"
namespace ROCKSDB_NAMESPACE {
@@ -56,6 +57,12 @@ class SstFileReader {
// capabilities like filling cache, read ahead are disabled.
std::unique_ptr<Iterator> NewTableIterator();
// Parses a raw key returned by NewTableIterator().
// The Slice fields in parsed_key point into raw_table_key and are valid only
// while raw_table_key remains valid.
Status ParseTableIteratorKey(const Slice& raw_table_key,
ParsedEntryInfo* parsed_key) const;
std::shared_ptr<const TableProperties> GetTableProperties() const;
// Verifies whether there is corruption in this table.
+3
View File
@@ -600,6 +600,9 @@ enum Tickers : uint32_t {
// via FileOptions::file_metadata (on DB open / table cache miss)
FILE_OPEN_METADATA_PASSED,
// # of times MANIFEST content validation detected corruption on DB close
MANIFEST_VALIDATION_FAILURE_COUNT,
TICKER_ENUM_MAX
};
+23
View File
@@ -17,6 +17,7 @@
namespace ROCKSDB_NAMESPACE {
class InternalTblPropColl;
class CompressionManager;
// -- Table Properties
// Other than basic table properties, each table may also have the user
@@ -445,6 +446,28 @@ struct TableProperties {
std::string* mismatch) const;
};
// Parse TableProperties::compression_name into human-readable format.
// Thread-safe, but not purely local: the single-argument overload may consult
// globally registered CompressionManagers via ObjectLibrary using the stored
// compatibility name, and the two-argument overload consults the supplied
// CompressionManager first before falling back to that lookup.
// For format_version >= 7: "<compatibility_name>;<hex_codes>;" -> "ZSTD",
// "LZ4", etc. For older versions (no semicolon): returns as-is (e.g., "ZSTD",
// "Snappy", "kZSTD" are preserved with their original names). This means the
// single-argument overload can now return manager-specific names for custom
// compression types when a compatible CompressionManager is registered
// globally; otherwise, reserved/custom values fall back to generic names such
// as "Reserved7F" or "Custom8A". Returns "NoCompression" for an empty input,
// an empty hex field such as "BuiltinV2;;", or when all parsed entries are
// filtered out as NoCompression/DisableOption. Returns "Unknown" for malformed
// format_version >= 7 metadata, including a missing second semicolon,
// odd-length hex payload, or invalid hex characters. If multiple compression
// types are present, returns a comma-separated list in input order.
std::string ParseCompressionNameForDisplay(const std::string& compression_name);
std::string ParseCompressionNameForDisplay(
const std::string& compression_name,
std::shared_ptr<CompressionManager> compression_manager);
// Extra properties
// Below is a list of non-basic properties that are collected by database
// itself. Especially some properties regarding to the internal keys (which
+2 -2
View File
@@ -43,7 +43,7 @@ class UserDefinedIndexBuilder {
kOther = 3, // Other types (e.g., blob reference, wide-column entity).
// The value format is type-specific and may not be the
// actual user data.
kTypeMax, // Sentinel — must be last. Value may change across releases.
kTypeMax, // Sentinel -- must be last. Value may change across releases.
};
// File offset and size of the data block
@@ -106,7 +106,7 @@ class UserDefinedIndexBuilder {
// (e.g., trie-based indexes) can leave this as a no-op.
//
// @key: The user key (without sequence number or type suffix).
// @type: The entry type — kValue (Put), kDelete, kMerge, or kOther.
// @type: The entry type -- kValue (Put), kDelete, kMerge, or kOther.
// For kDelete entries, the value may be empty. For kOther, the
// value format is type-specific and may not be actual user data.
// @value: The associated value (may be empty for deletions).
+2 -2
View File
@@ -12,8 +12,8 @@
// NOTE: in 'main' development branch, this should be the *next*
// minor or major version number planned for release.
#define ROCKSDB_MAJOR 11
#define ROCKSDB_MINOR 2
#define ROCKSDB_PATCH 4
#define ROCKSDB_MINOR 3
#define ROCKSDB_PATCH 0
// Make it easy to do conditional compilation based on version checks, i.e.
// #if ROCKSDB_VERSION_GE(4, 5, 6)
+4
View File
@@ -5311,6 +5311,8 @@ class TickerTypeJni {
return -0x69;
case ROCKSDB_NAMESPACE::Tickers::READ_PATH_RANGE_TOMBSTONES_DISCARDED:
return -0x6A;
case ROCKSDB_NAMESPACE::Tickers::MANIFEST_VALIDATION_FAILURE_COUNT:
return -0x6B;
case ROCKSDB_NAMESPACE::Tickers::TICKER_ENUM_MAX:
// -0x54 is the max value at this time. Since these values are exposed
// directly to Java clients, we'll keep the value the same till the next
@@ -5812,6 +5814,8 @@ class TickerTypeJni {
return ROCKSDB_NAMESPACE::Tickers::READ_PATH_RANGE_TOMBSTONES_INSERTED;
case -0x6A:
return ROCKSDB_NAMESPACE::Tickers::READ_PATH_RANGE_TOMBSTONES_DISCARDED;
case -0x6B:
return ROCKSDB_NAMESPACE::Tickers::MANIFEST_VALIDATION_FAILURE_COUNT;
case -0x54:
// -0x54 is the max value at this time. Since these values are exposed
// directly to Java clients, we'll keep the value the same till the next
@@ -967,6 +967,11 @@ public enum TickerType {
*/
READ_PATH_RANGE_TOMBSTONES_DISCARDED((byte) -0x6A),
/**
* # of times MANIFEST content validation detected corruption on DB close
*/
MANIFEST_VALIDATION_FAILURE_COUNT((byte) -0x6B),
TICKER_ENUM_MAX((byte) -0x54);
private final byte value;
+3 -3
View File
@@ -559,7 +559,7 @@ TEST_F(InlineSkipTest, MultiGetDuplicateKeysWithCallbackWalk) {
}
// Callback that walks forward through multiple entries before stopping.
// This simulates the Merge operand accumulation in SaveValue — the callback
// This simulates the Merge operand accumulation in SaveValue -- the callback
// returns true for entries until it reaches one >= stop_at, simulating
// walking through merge chain entries.
struct WalkingCallbackArg {
@@ -1068,11 +1068,11 @@ static void ConcurrentMultiGetWorker(void* arg) {
// Validate all results: each found result must equal its query key
// (since all queried keys were inserted, an exact match is expected).
// However, due to concurrent inserts, a key sampled from the ring
// might not yet be visible — so we only check that if a result is
// might not yet be visible -- so we only check that if a result is
// found, it equals the query key (i.e., no wrong key returned).
for (size_t j = 0; j < batch_size; j++) {
if (results[j] != 0 && results[j] != query_keys[j]) {
// Got a result that doesn't match the query — either a bug or
// Got a result that doesn't match the query -- either a bug or
// the exact key wasn't inserted and we got the next one. Since
// all our query keys are inserted, this means the result should
// be >= query. For the just-inserted key we already checked
+12 -2
View File
@@ -48,13 +48,18 @@ bool WBWIMemTable::Get(const LookupKey& key, std::string* value,
SequenceNumber* max_covering_tombstone_seq,
SequenceNumber* out_seq, const ReadOptions&,
bool immutable_memtable, ReadCallback* callback,
bool* is_blob_index, bool do_merge) {
bool* is_blob_index, bool do_merge,
const BlobFetcher* blob_fetcher) {
assert(s->ok() || s->IsMergeInProgress());
(void)immutable_memtable;
(void)timestamp;
(void)columns;
(void)blob_fetcher;
assert(immutable_memtable);
assert(!timestamp); // TODO: support UDT
// IngestWriteBatchWithIndex() is rejected while any live column family has
// blob direct write enabled, so WBWI should never need blob resolution.
assert(blob_fetcher == nullptr);
assert(assigned_seqno_.upper_bound != kMaxSequenceNumber);
assert(assigned_seqno_.lower_bound != kMaxSequenceNumber);
// WBWI does not support DeleteRange yet.
@@ -154,10 +159,15 @@ bool WBWIMemTable::Get(const LookupKey& key, std::string* value,
void WBWIMemTable::MultiGet(const ReadOptions& read_options,
MultiGetRange* range, ReadCallback* callback,
bool immutable_memtable) {
bool immutable_memtable,
const BlobFetcher* blob_fetcher) {
(void)immutable_memtable;
(void)blob_fetcher;
// Should only be used as immutable memtable.
assert(immutable_memtable);
// IngestWriteBatchWithIndex() is rejected while any live column family has
// blob direct write enabled, so WBWI should never need blob resolution.
assert(blob_fetcher == nullptr);
// TODO: reuse the InternalIterator created in Get().
for (auto iter = range->begin(); iter != range->end(); ++iter) {
SequenceNumber dummy_seq = 0;
+4 -2
View File
@@ -134,10 +134,12 @@ class WBWIMemTable final : public ReadOnlyMemTable {
SequenceNumber* max_covering_tombstone_seq, SequenceNumber* seq,
const ReadOptions& read_opts, bool immutable_memtable,
ReadCallback* callback = nullptr, bool* is_blob_index = nullptr,
bool do_merge = true) override;
bool do_merge = true,
const BlobFetcher* blob_fetcher = nullptr) override;
void MultiGet(const ReadOptions& read_options, MultiGetRange* range,
ReadCallback* callback, bool immutable_memtable) override;
ReadCallback* callback, bool immutable_memtable,
const BlobFetcher* blob_fetcher = nullptr) override;
uint64_t NumEntries() const override { return num_entries_; }
+2
View File
@@ -302,6 +302,8 @@ const std::vector<std::pair<Tickers, std::string>> TickersNameMap = {
"rocksdb.read.path.range.tombstones.discarded"},
{FILE_OPEN_METADATA_RETRIEVED, "rocksdb.file.open.metadata.retrieved"},
{FILE_OPEN_METADATA_PASSED, "rocksdb.file.open.metadata.passed"},
{MANIFEST_VALIDATION_FAILURE_COUNT,
"rocksdb.manifest.validation.failure.count"},
};
const std::vector<std::pair<Histograms, std::string>> HistogramsNameMap = {
+2 -2
View File
@@ -434,14 +434,14 @@ void InstallStackTraceHandler() {
// Ignore SIGPIPE so that broken-pipe writes (e.g. to a closed stdout)
// return EPIPE instead of killing the process.
signal(SIGPIPE, SIG_IGN);
// Crash signals — invoke full stack trace + ring buffer
// Crash signals -- invoke full stack trace + ring buffer
signal(SIGILL, StackTraceHandler);
signal(SIGSEGV, StackTraceHandler);
signal(SIGBUS, StackTraceHandler);
signal(SIGABRT, StackTraceHandler);
signal(SIGFPE, StackTraceHandler);
signal(SIGQUIT, StackTraceHandler);
// Termination signals — print ring buffer only, no stack trace
// Termination signals -- print ring buffer only, no stack trace
signal(SIGTERM, TerminationHandler);
signal(SIGINT, TerminationHandler);
atexit(AtExit);
@@ -41,7 +41,7 @@ void BlockBasedTableIterator::SeekImpl(const Slice* target,
return;
}
// MultiScan requires an explicit seek key — SeekToFirst() is not supported
// MultiScan requires an explicit seek key -- SeekToFirst() is not supported
if (multi_scan_read_set_ && !target) {
multi_scan_status_ = Status::InvalidArgument("No seek key for MultiScan");
RecordTick(table_->GetStatistics(), MULTISCAN_SEEK_ERRORS);
@@ -683,7 +683,7 @@ void BlockBasedTableIterator::FindBlockForward() {
if (multi_scan_index_iter_ &&
multi_scan_index_iter_->IsScanRangeExhausted()) {
if (multi_scan_index_iter_->HasMoreScanRanges()) {
// More ranges remain — signal out-of-bound so DBIter/LevelIter
// More ranges remain -- signal out-of-bound so DBIter/LevelIter
// will trigger the next Seek for the next scan range.
is_out_of_bound_ = true;
}
@@ -1087,13 +1087,17 @@ class BlockBasedTableReaderMultiScanAsyncIOTest
: public BlockBasedTableReaderMultiScanTest {};
// TODO: test no block cache case
TEST_P(BlockBasedTableReaderMultiScanAsyncIOTest, MultiScanPrepare) {
TEST_P(BlockBasedTableReaderMultiScanAsyncIOTest, DISABLED_MultiScanPrepare) {
auto param = GetParam();
auto fill_cache = param.fill_cache;
auto use_async_io = param.use_async_io;
options_.statistics = CreateDBStatistics();
std::shared_ptr<FileSystem> fs = options_.env->GetFileSystem();
int64_t supported_ops = 0;
fs->SupportedOps(supported_ops);
const bool async_supported =
use_async_io && ((supported_ops & (1 << FSSupportedOps::kAsyncIO)) != 0);
ReadOptions read_opts;
read_opts.fill_cache = fill_cache;
size_t ts_sz = options_.comparator->timestamp_size();
@@ -1209,17 +1213,12 @@ TEST_P(BlockBasedTableReaderMultiScanAsyncIOTest, MultiScanPrepare) {
iter->Prepare(&scan_options);
read_count_after =
options_.statistics->getTickerCount(NON_LAST_LEVEL_READ_COUNT);
if (!use_async_io) {
if (!fill_cache) {
ASSERT_EQ(read_count_before + 1, read_count_after);
} else {
ASSERT_EQ(read_count_before + 2, read_count_after);
}
} else {
// stat is recorded in async callback which happens in Poll(), and
// Poll() happens during scanning.
ASSERT_EQ(read_count_before, read_count_after);
}
// When async IO is available on this thread, Prepare() only queues the reads
// and stats are updated later during Poll(). Otherwise it falls back to the
// synchronous path and records the reads immediately.
const uint64_t expected_prepare_reads =
async_supported ? 0 : (fill_cache ? 2 : 1);
ASSERT_EQ(read_count_before + expected_prepare_reads, read_count_after);
iter->Seek(kv[40 * kEntriesPerBlock].first);
for (size_t i = 40 * kEntriesPerBlock; i < 80 * kEntriesPerBlock; ++i) {
Loaded 100 of 148 files, more files were not shown because too many files have changed in this diff. Show more