Compare commits

...
66 Commits
Author SHA1 Message Date
anand76 a19ac636e7 Revert "Fix Windows build issue in trie_index_factory.h"
This reverts commit c62e9a0d19.
2026-10-15 10:39:38 -07:00
Anand Ananthabhotla 4cc5e15c15 Make InjectedErrorLog::Entry 256 bytes on 32-bit targets (#15388)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15388

`InjectedErrorLog::Entry` in `utilities/fault_injection_fs.h` is written to the injected error log in its native layout, and a `static_assert` requires `sizeof(Entry) == 256` so the record size stays stable. That assert fails on i386 builds.

Root cause: the fields add up to only 252 bytes. On targets where `alignof(uint64_t)` is 8 (x86_64, aarch64, armv7), the compiler adds 4 bytes of implicit tail padding, so the size is 256. On i386, `uint64_t` members are 4-byte aligned inside a struct, so there is no tail padding and the size is 252.

Fix: add an explicit trailing `uint8_t padding[4]` field so the size no longer depends on `uint64_t` alignment.

Compatibility: on 64-bit targets the new field takes the place of the old implicit tail padding, so every field offset and the 256-byte record layout are unchanged. `Record()` value-initializes the entry (`Entry entry{}`), so the padding bytes are always written as zero. No decoder in the repo depends on the padding bytes.

___

landed-with-radar-review

Differential Revision: D124873369

fbshipit-source-id: f5c6f43ca85013b0e769853ad13b8d4f3f89b958
2026-10-13 10:50:04 -07:00
xingbowang b3063c1db0 Fix big-endian builds for the fault-injection filesystem (#15297)
Summary:
The persistent injected-error log writes `InjectedErrorLog::Entry` objects using their native in-memory layout, while the log format's consumer expects little-endian multi-byte fields. The existing `static_assert(port::kLittleEndian)` protected that format by rejecting every big-endian build that included the fault-injection filesystem, even though the log is an optional diagnostic.

This change keeps RocksDB and the fault-injection filesystem available on supported big-endian platforms while disabling only the incompatible persistent log. Log-file creation now returns an inactive descriptor when `port::kLittleEndian` is false, which makes `Record()` and `Flush()` remain safe no-ops. The fixed 256-byte entry-size assertion remains in place, and format-dependent tests are bypassed on big-endian targets.

The change also adds a release note and documents big-endian support plus the preferred capability-check pattern for optional architecture-specific features.

## Compatibility and limitations

- Little-endian behavior and the on-disk log layout are unchanged.
- On big-endian platforms, configuring an injected-error log path does not create or populate a log file; the rest of fault injection remains available.
- Windows already leaves this POSIX-backed diagnostic disabled.
- This does not introduce a portable log encoding. Supporting persistent injected-error logs on big-endian platforms would require a format change.

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

Test Plan:
- `build_tools/rockstest.sh fault_injection_fs_test '--gtest_filter=InjectedErrorLogTest.*'` (little-endian macOS): all 5 tests passed.
- `make check-sources`
- `git diff --check upstream/main...2026_10_01_endian_fix`

Native big-endian build and test execution were not available locally; that path relies on the compile-time `port::kLittleEndian` capability and should be covered by big-endian CI.

Reviewed By: jaykorean

Differential Revision: D122880760

Pulled By: xingbowang

fbshipit-source-id: 59dae9661a47608aa12f41078ad1b3ef0fa7231e
2026-10-13 10:49:57 -07:00
anand76 c62e9a0d19 Fix Windows build issue in trie_index_factory.h 2026-10-13 10:49:35 -07:00
anand76 abeebd9630 Additional HISTORY.md update for 11.8.1 2026-08-04 15:22:01 -07:00
Anand Ananthabhotla 3730e34363 Defer obsolete OPTIONS file deletion on open (#15004)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15004

This change reduces DB open latency on remote storage by avoiding foreground deletion of obsolete `OPTIONS-*` files during open when `avoid_unnecessary_blocking_io` is enabled. DB open now avoids foreground deletion of obsolete `OPTIONS-*` files on the pre-open `WriteOptionsFile()` path. `RenameTempFileToOptionsFile()` records the new OPTIONS file and chooses an explicit cleanup mode: skip while open is still in progress, schedule deletion when background work is available, or delete immediately when blocking IO is allowed or background jobs are rejected.

`DBImpl::Open()` makes the deferred cleanup decision once final open status is known. A successful open schedules obsolete OPTIONS files on the purge queue, while a failed open deletes them synchronously before tearing down the partially opened DB. The default synchronous behavior remains unchanged when `avoid_unnecessary_blocking_io` is disabled, and the latest-two OPTIONS retention policy is preserved.

Reviewed By: pdillinger

Differential Revision: D113593680

fbshipit-source-id: 8aee99b3f38a729647d0bcb644d212181fe86895
2026-08-04 15:13:23 -07:00
anand1976 d4a9fdc01b Update HISTORY.md for 11.8.1 release 2026-08-04 11:45:22 -07:00
Josh Kang 9657aad4ec Fix IoUringBackend crash in ReadIOExecutor worker threads (#15020)
Summary:

Each read executor worker creates its own `IoUringBackend`, and Folly's I/O thread pool creates its worker threads and EventBases lazily. A successful calling-thread probe or `DB::Open()` therefore does not validate every worker. A later read can start a new worker; previously, an `IoUringBackend` initialization exception on that worker could escape startup and terminate the process. The previous 512-submit setup also allowed Folly's capacity retry to reduce the completion queue to 512 while retaining a 1024-entry submission queue, which the kernel rejects with `EINVAL`.

Use a 256-submit limit so the 512-entry retry remains valid. Configure the shared read executor during `DB::Open()` through `DBOptions::read_io_executor_threads`, defaulting to one thread. The backend factory catches per-worker io_uring initialization failures and uses Folly's default backend. Each worker's thread-local availability marker starts false and is set true only after `IoUringBackend` construction succeeds. When construction fails, the marker remains false and `SubmitReadAsync()` performs the read synchronously. The executor's maximum thread capacity can be raised or lowered; `db_stress` requests eight threads for async DB APIs.

To expose this degradation, `FSRandomAccessFile::SubmitReadAsync()` now reports whether the asynchronous path was used. `RandomAccessFileReader` increments the `FILE_SUBMIT_ASYNC_READ_FALLBACK` ticker for every submission that falls back to synchronous I/O, making the fallback visible through the DB's `Statistics`.

Reviewed By: xingbowang

Differential Revision: D114031018
2026-08-04 11:37:29 -07:00
Anand Ananthabhotla b4fd1daa68 Avoid deletefile test purge/flush deadlock (#15027)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15027

`BackgroundPurgeCFDropTest` blocks `DBImpl::BGWorkPurge:start` to verify dropped-CF files remain until background purge is released. Runtime OPTIONS-file cleanup can also schedule a purge on the HIGH pool when `avoid_unnecessary_blocking_io` is enabled, so installing the sync-point dependency before test setup can park that OPTIONS purge before the foreground `Flush()` has a chance to run.

This does not need a broader product change. Runtime OPTIONS-file writes are rare, and the hard deadlock is a test artifact caused by the sync-point dependency. Outside the test, the observable impact would be limited to a foreground `Flush()` API call waiting longer if its flush is queued behind the OPTIONS-file purge.

Scope the sync-point dependency to the behavior under test by installing it only for the background-purge case, after the setup flush has produced the SST and just before `DropColumnFamily()`. This keeps setup-side OPTIONS-file purge work outside the blocked window while still verifying that dropped-CF file deletion waits for background purge.

Reviewed By: joshkang97

Differential Revision: D114241457

fbshipit-source-id: 21c5f4242a50590e161b3720077a9dfe5e77aa08
2026-07-30 12:20:29 -07:00
anand1976 3a75b13f6a Update HISTORY.md for 11.8.0 release 2026-07-29 16:58:07 -07:00
Josh Kang 7247edbf0a Add opt-in stats for async RocksDB reads (#15017)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15017

## Summary

RocksDB async `GetAsync` and `MultiGetAsync` callbacks now default to a completion-only path. Callbacks can override `EnableStats()` when they need request-specific `PerfContext` or `IOStatsContext`; otherwise RocksDB avoids creating a coroutine stats context and returns null stats pointers. Callbacks must not initiate another async read from `OnComplete()`.

RocksDB stats normally use thread-local storage, which does not compose with interleaved coroutine reads. A stats-enabled request captures only the caller's configuration: perf level, per-level perf enablement, and whether io stats are disabled. It then starts with empty request contexts. `onSet()` loads that request's contexts into TLS, while `onUnset()` saves the accumulated contexts back into request data. On resume, the same contexts are loaded again. This isolates request counters without preserving or restoring pre-existing executor TLS state. The synchronous fallback similarly resets TLS before executing a stats-enabled read.

`PerfContext` and `IOStatsContext` gain `Merge()` support for caller-side aggregation. `PerfContext` move construction and assignment now transfer ownership of per-level state rather than deep-copying it, avoiding allocations during coroutine context handoffs. A fbcode-wide audit found no caller that reads a moved-from `PerfContext`.

`db_bench` opts in only for coroutine jobs that print per-job perf context and merges every completed request's callback stats into the job aggregate.

## Example: Two Async Gets on One Thread

Both callbacks enable stats. Each request starts with empty counters and carries its contexts across suspensions.

```text
Shared executor thread
|
|  onSet(A): load A's empty contexts into TLS
|  A records 3 reads
|  onUnset(A): save TLS contexts back into A
|
|  onSet(B): load B's empty contexts into TLS
|  B records 4 reads
|  onUnset(B): save TLS contexts back into B
|
|  onSet(A): load A's contexts containing 3 reads
|  A records 2 more reads
|  callback A receives block_read_count = 5
|
|  onSet(B): load B's contexts containing 4 reads
|  callback B receives block_read_count = 4
```

Isolation comes from replacing TLS with the active request's saved contexts on every `onSet()`.

### Timers

Existing wall-clock timers remain in coroutine frames, so timers spanning `co_await` include suspended time and measure end-to-end latency.

Request-level `get_cpu_nanos` is suspension-aware: it starts in `onSet()` and stops in `onUnset()` or `Finalize()`, charging only time while the request is active.

Nested CPU timers remain disabled because normal RAII CPU timers would also charge executor work performed while their coroutine is suspended. `MultiGetAsync` fanout additionally requires per-child timer state to avoid double counting.

## Benchmarks

Benchmarked to confirm no performance regressions when perf is disabled, and also to see the overhead of enabling perf.

```bash
db_bench --benchmarks=<readrandomcoroutine|multireadrandomcoroutine> --db=<db_path> --coro_jobs_per_thread=10 --use_existing_db=true --num=2000000 --value_size=1024 --compression_type=none --threads=8 --batch_size=16 --multiread_batched=true --use_direct_reads=true --cache_size=1428076626 --reads=320000 --perf_level=<1|3> --seed=<seed>
```

| Workload | Before change | Perf level 1 | Before -> 1 | Perf level 3 | 1 -> 3 |
| -------- | ------------- | ------------ | ----------- | ------------ | ------ |
| Async Get | 475.6K ops/s | 476.7K ops/s | +0.2% | 470.5K ops/s | -1.3% |
| Async MultiGet | 567.3K ops/s | 567.4K ops/s | +0.0% | 552.2K ops/s | -2.7% |

Reviewed By: xingbowang

Differential Revision: D112634874

fbshipit-source-id: 94e240aeb1496fa12dac995b5aa5a9c3543cc5fc
2026-07-28 09:32:26 -07:00
Ranjan Banerjee f5c0c3e274 Record CheckpointEngine copy bytes under checkpoint tickers (#14997)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14997

`CheckpointEngine`'s shared `CopyEngine` recorded copied bytes under the
`BACKUP_READ_BYTES` / `BACKUP_WRITE_BYTES` tickers (the `CopyEngineOptions`
defaults), conflating checkpoint I/O with backup I/O in statistics. This adds
dedicated `CHECKPOINT_READ_BYTES` / `CHECKPOINT_WRITE_BYTES` tickers and points
`CheckpointEngineImpl`'s `CopyEngineOptions` at them, so checkpoint copies report
under their own metric names. `BackupEngine` is unchanged (still uses the
backup-ticker defaults).

Addresses review feedback that the shared `CopyEngine` should not hardcode
backup metric names for non-backup callers.

The new tickers are intentionally not mirrored in the Java bindings
(`TickerType.java` / `portal.h`): the signed-`jbyte` id space there is exhausted,
so they remain C++-only, documented inline in `statistics.h`.

Reviewed By: xingbowang

Differential Revision: D113469742

fbshipit-source-id: 2a6a7ef04c939a1240f1c48c7bcaba8b2e7a1c10
2026-07-28 09:23:58 -07:00
Xingbo Wang 2fc58a678b Fix MultiScanPrepare fallback after io_uring init failure (#15016)
Summary:
- Gate the `MultiScanPrepare` test's async I/O mode on the filesystem's reported `kAsyncIO` support.
- Use the same gated async-I/O flag for `MultiScanArgs::use_async_io` and read-count expectations.
- Match production DB-level MultiScan behavior so the test falls back consistently after io_uring initialization fails.

## Background
- Recent Sandcastle failures in T265255062 all hit the MultiScanPrepare read-count assertion after PosixFileSystem::SupportedOps() logged "CreateIOUring failed: Cannot allocate memory (errno=12)" and returned without kAsyncIO support.
- The test already probed SupportedOps(), but it only used that result when computing expected reads. It still copied the raw use_async_io test parameter into MultiScanArgs. That let the lower-level table-reader path call ReadAsync() and retry CreateIOUring() after the support probe had failed. If the retry succeeded, Prepare queued async reads and recorded zero sync reads while the assertion expected the sync fallback count.

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

Test Plan:
- `make format-auto`
- `AUTO_CLEAN=1 make -j$(nproc) block_based_table_reader_test`
- `timeout 60s ./block_based_table_reader_test --gtest_filter="*MultiScanPrepare*"`
- `make check-sources`

Reviewed By: archang19

Differential Revision: D113830233

Pulled By: xingbowang

fbshipit-source-id: 38460f24fd44b33eb139a8f59bd87c637983e503
2026-07-27 16:15:50 -07:00
Josh Kang a4645e864d Implement Get/MultiGet coroutine (#15014)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15014

Implements coroutine-backed execution behind the callback-based `DB::GetAsync` and `DB::MultiGetAsync` APIs that issue SST block reads asynchronously, so a single application thread can keep many point-lookup reads in flight at once instead of paying one blocking read per thread. The motivation is point-lookup throughput on disk-bound workloads (working set larger than the block cache), where the win comes from queue depth rather than per-read latency.

The async capability rests on a small, generic FileSystem API that a `FileSystem` must implement to participate. Every method is virtual with a working synchronous default, so existing and custom `FileSystem` implementations compile and run unchanged (they simply stay synchronous):

- `FSRandomAccessFile::SubmitReadAsync(req, opts, cb, dbg)` -- a fire-and-forget read: it starts the read and returns without blocking, then invokes `cb` once `req.result`/`req.status` are filled in (the callback may run on a different thread). The default implementation is synchronous -- a blocking `Read` followed immediately by `cb`.
- `FileSystem::GetReadExecutor()` -- returns the `folly::Executor*` that async queries and read completions run on. The default returns `nullptr`, which the query layer treats as "async reads unavailable" and falls back to the synchronous path. It is exposed to users, so they may add additional application logic to run on the same thread pool, thus avoiding expensive context switches.
- `FileSystem::SetReadIOExecutorThreads(int)` -- sizes that executor. The default is a no-op.

`RandomAccessFileReader::Read`/`MultiRead` are `DEFINE_SYNC_AND_ASYNC` coroutines (new `file/random_access_file_reader_sync_and_async.h`), and `DBImpl::GetAsync`/`MultiGetAsync` bind the query to `GetReadExecutor()` with `co_withExecutor`. For a MultiRead, `SubmitMultiReadAsync` issues all of the block reads through `SubmitReadAsync` back-to-back and then `co_await`s a `folly::coro::Baton`: awaiting the baton suspends the query and releases the worker thread while the reads are in flight, and the callback for the last outstanding read posts the baton to resume it. A MultiGet's per-file `MultiGetFromSST` reads additionally fan out across the current eventbase so overlapping-level lookups run in parallel. This layer works against any executor and any `SubmitReadAsync` implementation.

The Posix `FileSystem` is the concrete async implementation, and is where io_uring enters. `GetReadExecutor()` is a `folly::IOThreadPoolExecutor` whose workers are EventBases that each drive an io_uring ring (sized by `SetReadIOExecutorThreads`). `PosixRandomAccessFile::SubmitReadAsync` -- which must be invoked on one of those EventBase workers -- places the read on that worker's submission queue via `IoUringBackend::queueRead` and returns immediately: no blocking `pread`, no parked thread. When the kernel posts the completion the backend runs the callback on that same worker, and a short read re-queues its remainder from inside the completion callback, so even multi-syscall reads stay on the non-blocking path. The net effect is that one worker thread keeps many reads outstanding at once, bounded by io_uring queue depth rather than thread count.

## Thread-local state handling

A coroutine suspends at every `co_await` and can resume on a different read-executor worker, and another coroutine can run on that worker in between. Any thread-local state held across a suspension point is therefore unsafe -- it can be observed or mutated by an interleaved coroutine, restored onto the wrong thread, or trip an in-use assertion. The read path handles each thread-local it touches explicitly, and introduces no new thread-local storage of its own:

- SuperVersion cache: the thread-local SuperVersion slot is marked `kSVInUse` for the duration of a read, so a second coroutine resuming on the same worker would trip that assertion. The coroutine path instead takes an independent heap reference that releases the slot before any suspension -- `cfd->GetReferencedSuperVersion()` for single Get, and `kExtraSvRef = true` threaded through `MultiCFSnapshot` for MultiGet -- and releases it with `CleanupSuperVersion` (via a `Defer` for single Get). The synchronous path is unchanged and keeps using the thread-local slot (`GetAndRefSuperVersion` / `ReturnAndCleanupSuperVersion`).
- perf_context / iostats_context / perf level: the timing guards that save-and-restore or accumulate into these thread-locals across an IO -- `GetPerfLevel`/`SetPerfLevel`, `IOSTATS_TIMER_GUARD(read_nanos)`, `IOSTATS_CPU_TIMER_GUARD(cpu_read_nanos)` -- are compiled out on the coroutine path (`#if defined(WITHOUT_COROUTINES)`), so none of that thread-local timing state spans a `co_await`. The tradeoff is that these perf/IO counters are not populated for coroutine reads (a known limitation of this path).
- TLS-free helpers are left as-is: `StopWatch`, for instance, holds only clock/statistics pointers and a start time, so it is safe across a suspension (it can over-count wall time under oversubscription, which is a metrics-accuracy issue, not corruption).

## Benchmarks

Benchmarked using 8 threads for both multiget (with/without async_io enabled), and single get on both direct-io enabled and disabled on a ~11 GB DB. Non direct-io benchmarks are not a good representation because my dev machine is capable of paging the entire DB into memory. But, it is still useful to compare against async_io=true, since that feature only benefits non direct-io.

Jobs-per-thread (jpt) is the multiplier for the number of tasks being submitted to the thread pool. E.g. a threadpool of 8 threads with jpt = 4 has 32 running tasks on the thread pool. It is important to oversubscribe so that when a task suspends, another one can resume on the thread pool.

MultiGet (batch 16):

| IO       | cache hit % | sync | async_io | coro jpt=1 | coro jpt=2 | coro jpt=5 | coro jpt=10 |
| -------- | --- | ---- | -------- | ---------- | ---------- | ---------- | ----------- |
| direct   | 10% | 65K  | 64K      | 145K       | 151K       | 161K       | 166K        |
| direct   | 50% | 88K  | 80K      | 239K       | 287K       | 327K       | 340K        |
| direct   | 90% | 435K | 399K     | 732K       | 991K       | 1,093K     | 1,119K      |
| buffered | 10% | 449K | 561K     | 555K       | 593K       | 572K       | 566K        |
| buffered | 50% | 495K | 682K     | 656K       | 724K       | 700K       | 701K        |
| buffered | 90% | 600K | 878K     | 828K       | 1,021K     | 1,048K     | 1,060K      |

single Get:

| IO       | cache hit % | sync | coro jpt=1 | coro jpt=2 | coro jpt=5 | coro jpt=10 |
| -------- | --- | ---- | ---------- | ---------- | ---------- | ----------- |
| direct   | 10% | 41K  | 37K        | 69K        | 125K       | 144K        |
| direct   | 50% | 74K  | 66K        | 127K       | 238K       | 281K        |
| direct   | 90% | 390K | 330K       | 540K       | 767K       | 849K        |
| buffered | 10% | 403K | 291K       | 369K       | 447K       | 447K        |
| buffered | 50% | 441K | 337K       | 443K       | 554K       | 527K        |
| buffered | 90% | 544K | 433K       | 606K       | 753K       | 810K        |
Example command (MultiGet coroutine, ~50% hit):

```
db_bench -benchmarks=multireadrandomcoroutine -coro_jobs_per_thread=10 -use_existing_db=true -num=2000000 -value_size=1024 -compression_type=none -threads=8 -batch_size=16 -use_direct_reads=true -cache_size=402653184 -reads=320000 -statistics
```

### Coroutine vs context switch overhead

Coroutines are not zero overhead, so we also compare it against the context switching of using many sync threads. Both variants were pinned to the same 8 CPUs and ran 80 concurrent MultiGet operations.

| variant                                  | throughput   |
| ---------------------------------------- | ------------ |
| sync, 80 threads                         | 330.6K ops/s |
| coroutine, 8 executor threads / 80 jobs | 446.0K ops/s |

Reviewed By: xingbowang

Differential Revision: D111116635

fbshipit-source-id: bd0ac052efe79261a552e9e66137e5e70f7cd8a1
2026-07-27 15:09:34 -07:00
Josh Kang 039c45075e Add db_stress coroutine read flag (#15013)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15013

Add `--use_async_db_api` to db_stress and route direct `DB::Get` and value `DB::MultiGet` reads through the callback async APIs (`DB::GetAsync` / `DB::MultiGetAsync`) when coroutine support is available. The db_stress helpers block on callback completion and notify waiters while holding the callback lock, so existing stress-test control flow and callback lifetime are preserved. Builds without coroutine support silently keep the existing synchronous path.

Centralize the dispatch in db_stress helpers and use them for workload, verification, secondary, and consistency/debug reads while leaving transaction and entity APIs on their existing paths.

Randomize `use_async_db_api` in `tools/db_crashtest.py` one run in six.

Reviewed By: jaykorean

Differential Revision: D111132477

fbshipit-source-id: 2aeac267867d8501131161c9fdbaf1da2b0c51d5
2026-07-27 13:22:07 -07:00
Xingbo Wang fbd446cebb Disable open_read_only_one_in when mmap_read is enabled (#15011)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15011

Root cause: `open_read_only_one_in` can create a read-only DB instance on the same live directory as the primary DB. When the stress run also enables `mmap_read=1`, that read-only instance can hold mmap-backed slices into SST files while the primary continues compacting and deleting obsolete files. If `SstFileManager` uses chunked truncation (`sst_file_manager_bytes_per_truncate > 0`), Linux can raise SIGBUS when the read-only path later touches a page whose backing SST was truncated or removed. The range tombstone fragmenter/comparator is where the invalid mmap-backed key slice was touched, not necessarily the root cause.

Evidence from T281873950 / Sandcastle job `63050395314610170`: the crashing command had `--mmap_read=1`, `--open_read_only_one_in=16`, and `--sst_file_manager_bytes_per_truncate=1048576`. The crash stack was `BlockBasedTable::ReadRangeDelBlock()` -> `FragmentedRangeTombstoneList::FragmentTombstones()` -> `std::set<ParsedInternalKey>::emplace()` -> comparator -> `memcmp`, ending in signal 7/SIGBUS. The crash DB LOG shows `allow_mmap_reads: 1`; table `104662.sst` was created cleanly at size `6257069` with range tombstones and was later deleted by the primary. The artifact does not show persistent SST corruption or checksum failure, and injected read faults would normally surface as RocksDB statuses rather than an OS SIGBUS.

Why this started surfacing: D111995911 / `rFBS0f7a2ef93e63cfa6677b6fa32c1bd34eb818c978` added randomized read-only-on-primary coverage via `open_read_only_one_in` on 2026-07-20. Before that change, blackbox crash test did not randomly keep a separate read-only DB open on the primary live directory, so the existing `mmap_read + SstFileManager truncation` hazard lacked the second reader needed to trigger SIGBUS. The failure is therefore a probabilistic exposure of the new stress mode when it combines with mmap reads and chunked SST truncation.

Fix: disable `open_read_only_one_in` in the existing `mmap_read=1` sanitizer path. This preserves read-only-on-primary stress coverage for non-mmap runs while avoiding an OS-level mmap/truncation hazard that RocksDB cannot convert into a normal `Status`.

Reviewed By: jaykorean

Differential Revision: D113774892

fbshipit-source-id: f82ec2924c12f4512aeeb7a913659dc9a72a4c2f
2026-07-27 11:00:39 -07:00
Xingbo Wang 6f5587ae2d Fix GetCorruptionStatus() crash with separated KV blocks (#15006)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15006

Block::GetCorruptionStatus() uses restart_offset_ in error state to decide whether it can re-decode the data block footer for a more specific corruption status: nonzero means the footer decode failed and restart_offset_ stores the original block size; zero means the corruption was detected after the footer decoded.

For separated-KV data blocks, Block::Block() first sets restart_offset_ to the restart-array offset, then validates footer.values_section_offset. If that offset is past the restart array, the block is marked corrupt with size = 0, but restart_offset_ was left nonzero. NewDataIterator() then called GetCorruptionStatus(), which treated the stale restart-array offset as an original block size, re-decoded a syntactically valid footer, and hit DEBUG_FAIL("ok status on presumed bad block contents").

Reset restart_offset_ to 0 on this semantic-corruption path so GetCorruptionStatus() reports generic block corruption instead of trying to re-decode a footer that already decoded successfully.

The regression test builds a separated-KV hash-index data block, corrupts only the encoded values_section_offset to point past the block, and verifies NewDataIterator() returns "Corruption: bad block contents". Removing the fix line makes the test reproduce the original assertion.

This update also applies the clang-format layout required by check-format for the new regression test.

Reviewed By: joshkang97

Differential Revision: D113561303

fbshipit-source-id: 8d989a38761063c62694837976aad98f1868f884
2026-07-27 10:14:36 -07:00
Peter Dillinger c73841572a Simplify ReadOptions plumbing for read-path blob fetchers (#15003)
Summary:
Follow-up cleanup to the embedded (same-file) blob wide-column work (PR https://github.com/facebook/rocksdb/issues/14990): the "ReadOptions usage cleanup/optimization" item.

Split the Version-backed blob fetcher into two variants sharing a common VersionBlobFetcherBase (which implements FetchBlob via a virtual read_options()):
- VersionBlobFetcher now holds a `const ReadOptions&` for transient/stack fetchers (the common case: point Get/MultiGet, DBIter, version_set), so the growing ReadOptions is no longer copied per fetcher on the hot path. A deleted rvalue-ref constructor rejects binding to a temporary.
- OwningVersionBlobFetcher takes `ReadOptions&&` and owns a copy, for fetchers whose lifetime is independent of the caller (the read-path lazy resolver and compaction), making ownership transfer explicit.

DBIter::BlobReader now holds a single blob fetcher instead of decomposed scalar fields (read_tier/verify_checksums/fill_cache/io_activity). This removes the decompose-then-recompose that the old CreateBlobFetcher and RetrieveAndSetBlobValue performed, and closes the field-loss gap: all ReadOptions fields now flow through to blob reads rather than just those four. CreateBlobFetcher is gone; the merge path uses the reader's owned fetcher directly. A CanResolve() predicate on the fetcher preserves the prior "unexpected blob index" Corruption instead of dereferencing a null Version, and the now-redundant allow_blob_write_path_fallback_ member/ parameter (equal to the fetcher's own flag) was dropped.

ReadPathBlobResolver keeps owning its fetcher (now OwningVersionBlobFetcher) so it stays self-contained for the planned lazy entity read path, where the result outlives the read call. A TODO in db_iter.h records the remaining duplication (BlobReader and ReadPathBlobResolver each own a fetcher with the same {version, read_options, blob_file_cache, allow_write_path_fallback}), deferred to that work.

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

Test Plan:
Pure refactoring with no intended behavior change; Version::GetBlob's Slice overload just decodes and calls the BlobIndex overload, so routing whole-value blob reads through the fetcher is equivalent, and the direct-write fallback flag/read options are unchanged. Covered by existing tests:
- db/blob/db_blob_index_test.cc: embedded/entity blob GetEntity/MultiGetEntity zero-copy, plain Get of an embedded default column, merge over an embedded-blob entity, read_tier == kBlockCacheTier Incomplete, corrupt record Corruption.
- db/blob/db_blob_basic_test.cc and db/wide/db_wide_blob_direct_write_test.cc: separate-file and direct-write blob resolution (the fallback branch).
- db/db_iterator_test.cc (blob) and db/compaction/compaction_iterator_test.cc: DBIter blob/merge paths and the compaction (owning) fetcher.

Reviewed By: xingbowang

Differential Revision: D113582247

Pulled By: pdillinger

fbshipit-source-id: 231c9364e266b826d12a8479cbb7f026a0d26bf6
2026-07-26 21:05:16 -07:00
Xingbo Wang 14a7646fb5 Fix PinnableSlice move construction from pinned slices (#14998)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14998

T280925646 reported an ASAN double-free in db_stress' batched prefix scan. The visible free was in VerifyWideColumns(), but WideColumn values are non-owning Slice views, so that destructor should only free the vector element storage. The stack points to prior ownership corruption rather than an intentional verifier free.

The concrete bug is in PinnableSlice's move constructor: it delegates to move assignment without first initializing the target object. When moving from a pinned slice, move assignment does not set buf_ because the target is still pinned. If that moved-to slice is later Reset() and reused with PinSelf(), it writes through an uninitialized buf_ pointer.

This became reachable after D112212266 changed PinnableWideColumns to keep backing buffers in std::forward_list<PinnableSlice> and emplace moved PinnableSlice values into backing nodes.

Fix the move constructor by delegating to the default constructor first, so buf_ is initialized before move assignment. Add a regression that poisons placement-new storage, move-constructs from a pinned slice, resets it, and reuses it with PinSelf(); this crashed before the fix.

Reviewed By: pdillinger

Differential Revision: D113480016

fbshipit-source-id: 38ce9030f392ff9f0438d38d53d19e356e2d2fbf
2026-07-25 14:18:27 -07:00
Xingbo Wang 03c3c090fb Measure IODispatcher async read latency (#15001)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/15001

Add IODispatcher-specific histograms for async read observed completion latency, `FileSystem::Poll` wait time, and submit-to-consumer-poll prefetch lead time. Record submit timestamps on async read state, report completion latency from the callback, and report poll wait/prefetch lead when the block consumer polls the handle.

Keep Java histogram byte mappings stable by assigning the new histogram IDs after the existing range. Add a controlled async filesystem test that verifies one deferred async read records all three histograms.

Address Infer feedback by iterating `async_state->block_indices` directly when removing async IO map entries. Keep `PollAndProcessAsyncIO()` holding an owning `shared_ptr<AsyncIOState>` while erasing those entries so the coalesced request state cannot be destroyed mid-loop.

Format `util/io_dispatcher_imp.cc` and `util/io_dispatcher_test.cc` with clang-format 21.1.2 to fix the format signal.

Reviewed By: pdillinger, anand1976

Differential Revision: D113549401

fbshipit-source-id: dafc272c9252e1baccf404f681d33f76890ce0b2
2026-07-24 13:51:09 -07:00
Jay Huh 71d3bd1773 Fix heap-buffer-overflow(read) parsing a corrupted data-block hash index (#14993)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14993

A corrupted RocksDB SST data block whose footer sets the hash-index bit
(`kDataBlockBinaryAndHash`) could drive an out-of-bounds heap read while the
block is being parsed. `DataBlockHashIndex::Initialize` guarded its
`num_buckets` / `map_offset` arithmetic only with `assert()`, which is compiled
out in release and ASAN builds. When the encoded `num_buckets` exceeds the
block, the `uint16_t` `map_offset` computation underflows; that value then
flows into `Block`'s ctor at `input.remove_suffix(input.size() - map_offset)`,
inflating `restart_offset_` far past the real allocation. If `size` is later
zeroed as an error marker (e.g. a corrupted separated-KV footer),
`Block::GetCorruptionStatus` builds a `Slice(data(), restart_offset_)` and
`DataBlockFooter::DecodeFrom` reads `data() + restart_offset_ - 4`, over-reading
the block's heap buffer (reported by agentic fuzzing, T279933950).

Fix:
- `DataBlockHashIndex::Initialize` now returns `bool` and rejects a
  `num_buckets` of 0 or one whose bucket array plus the `NUM_BUCKETS` field does
  not fit within `size`, instead of relying on debug-only asserts.
- `Block`'s ctor checks that return value and also rejects blocks too large for
  the 64KiB `uint16_t`-offset hash-index format, marking the block corrupt
  (`size = 0`) rather than under-flowing `map_offset` and over-reading.

Reviewed By: pdillinger

Differential Revision: D113429299

fbshipit-source-id: 3167106e793252c09fd6a558e3ac96de7a2169c0
2026-07-24 10:42:15 -07:00
Peter Dillinger bb77d17072 Make Flush() optionally wait for OnFlushCompleted (FlushOptions::listener_wait) (#14992)
Summary:
DB::Flush() with wait=true only waits until the flush result is committed (memtables removed from the immutable list). The EventListener::OnFlushCompleted callbacks run afterward, on the background flush thread, with the DB mutex released, and the waiter is only woken by a shared condition variable that many unrelated events also signal. As a result a Flush(wait=true) caller can return before -- or in the middle of -- its OnFlushCompleted callbacks. This surfaced as a flaky failure in the c_test table_filter_listener_metadata case, which asserts listener_state.flush_count == 3 immediately after Flush() returns (https://github.com/facebook/rocksdb/actions/runs/29976431449). It is also long-standing behavior that other tests have worked around with TEST_WaitForBackgroundWork().

This adds an opt-in FlushOptions::listener_wait (default false). When set together with wait=true, Flush() does not return until the OnFlushCompleted callbacks for the flushed memtables have finished. Default behavior is unchanged.

Implementation: MemTableList gains a per-column-family num_pending_flush_notifications_ counter (guarded by the DB mutex). The background flush thread reserves a pending notification at flush-commit time -- before the mutex is released to run the callbacks -- and releases it after they return, in both the single-CF (FlushMemTableToOutputFile) and atomic (AtomicFlushMemTablesToOutputFiles) paths. WaitForFlushMemTables treats a CF as finished only when its normal predicate holds and the counter is zero. Because the memtables are removed and the counter incremented under one continuous mutex hold (and, for atomic flush, all CFs are reserved before the per-CF notify loop starts releasing the mutex), a listener_wait waiter can never observe the flush as finished before the callbacks complete.

The counter is per-CF rather than per-flush, so a listener_wait waiter can wait somewhat longer than strictly necessary: under rapid flushing it may block until an unrelated, later flush's OnFlushCompleted on the same CF also drains (e.g. a waiter satisfied by GetEarliestMemTableID() advancing still waits for the whole CF's pending notifications). This can only over-wait, never return early, and is tolerated in favor of implementation simplicity; a per-memtable-id watermark could tighten it if it ever matters.

To keep the counter balanced against future edits, the release uses an RAII Defer (runs on every exit path) and ~MemTableList asserts the counter has drained to zero; SubPendingFlushNotifications asserts against underflow. A reservation is taken only when a callback will actually fire (committed flush info non-empty, not a mempurge, CF not dropped, listeners registered).

Also adds the C API rocksdb_flushoptions_set/get_listener_wait (regenerated via tools/c_api_gen) and a DBImpl::TEST_FlushMemTableWithListenerWait() helper.

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

Test Plan:
New db_flush_test cases ListenerWaitWaitsForOnFlushCompleted and ListenerWaitAtomicFlushWaitsForAllOnFlushCompleted use a listener that blocks inside OnFlushCompleted plus spurious bg_cv_ wakeups to deterministically verify that Flush(wait=true, listener_wait=true) does not return until the callback (all CFs, for atomic flush) has finished; the atomic case guards the per-CF mutex-release gap in the notify loop.

Converted existing racy/worked-around cases to the new option: c_test table_filter_listener_metadata, listener_test OnSingleDBFlushTest and MultiDBMultiListeners, and db_flush_test FireOnFlushCompletedAfterCommittedResult (the last previously relied on TEST_WaitForBackgroundWork()). The auto-generated C API roundtrip test covers the new flushoptions field.

db_stress StressTest::TestFlush now sets listener_wait on a hard-coded 1 in 4 of its manual flushes (both the atomic and non-atomic branches), so the crash test exercises the new path with the DbStressListener registered.

Reviewed By: jaykorean

Differential Revision: D113425407

Pulled By: pdillinger

fbshipit-source-id: 6fd0f5bc93fabaccd8010c2fead1d93c4c44ccfb
2026-07-24 09:36:04 -07:00
Peter Dillinger 1eb4a6e3e1 Rocksdb Secondary Cache Test failed: assertion failed - corruption: corrupted atomic group The file (#15000)
Summary: Pull Request resolved: https://github.com/facebook/rocksdb/pull/15000

Reviewed By: xingbowang

Differential Revision: D113225538

Pulled By: pdillinger

fbshipit-source-id: fea9a671e9291ed030c5396264734195b2fb78bf
2026-07-24 08:30:27 -07:00
Ranjan Banerjee 19482988e3 Add parallel CheckpointEngine [4/4] (#14948)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14948

Adds a reusable `CheckpointEngine` (opened once, works for any DB) that creates checkpoints via the shared `CopyEngine` pool, copying and hard-linking data files across `max_background_operations` threads. `CheckpointImpl::CreateCheckpoint` is refactored into a shared `CreateCheckpointImpl`, with the serial/parallel split behind a `CheckpointFileMover`; files the FileSystem cannot hard-link fall back to copies. The legacy `Checkpoint` API stays serial and unchanged.

Reviewed By: xingbowang

Differential Revision: D111387993

fbshipit-source-id: 036d375da9c9b8f051cfc3da0551b035e29d80c4
2026-07-23 20:58:25 -07:00
Ranjan Banerjee 0c0bfbe8be Add hard-link support to CopyEngine [3/4] (#14947)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14947

Teaches the shared `CopyEngine` to hard-link files through its background pool: adds `WorkItemType::Link` and `CopyEngine::LinkFile(src, dst, dst_env)`, which returns the FileSystem's `NotSupported` when links are unavailable so callers can fall back to copying. This is what CheckpointEngine (next diff) needs for warm storage, where linking is supported but benefits from parallelism.

No existing caller links through the engine yet (BackupEngine never hard-links), so no behavior change; the path is exercised by a new `copy_engine_test.cc` (parallel copy, parallel link, single-thread copy, per-item error reporting).

Reviewed By: xingbowang

Differential Revision: D111387972

fbshipit-source-id: ec0b0e9b80749a3f51e62066ceef2df21a1811d2
2026-07-23 19:27:36 -07:00
Ranjan Banerjee fac47c3072 Extract shared CopyEngine [2/4] (#14946) (#14946)
Summary:
Extracts BackupEngine's parallel file-copy/checksum thread pool into a standalone `CopyEngine` (`utilities/copy_engine/copy_engine.{h,cc}`) so `CheckpointEngine` (later in the stack) can reuse it. Behavior-preserving; all `TEST_SYNC_POINT` names are unchanged.

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

Reviewed By: xingbowang

Differential Revision: D111387950

Pulled By: rban1

fbshipit-source-id: b5046adb98735a606fd1ba471731fd316ce6f036
2026-07-23 19:07:17 -07:00
Anusha Mukka f56112abb7 Plumb Env::IOActivity into DBImpl::ResumeImpl (#14952)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14952

`DBImpl::ResumeImpl()` constructed its local `ReadOptions`/`WriteOptions`
with defaults and carried a `// TODO: plumb Env::IOActivity, Env::IOPriority`
comment. Those options flow into `VersionSet::LogAndApply()` (the MANIFEST
write during error recovery), so the rate limiter could not attribute or
prioritize that I/O correctly.

This resolves the TODO for `ResumeImpl` (1 of 91 identical TODOs across the
codebase) by accepting an `Env::IOActivity` parameter and constructing the
options with it, following the pattern already established in `Get()`,
`MultiGet()`, and `NewIterator()`, and in the `kDBOpen` paths in
`db_impl_open.cc`.

Error recovery is driven by flush operations (as the existing comment in
`ErrorHandler::RecoverFromRetryableBGIOError` notes), so both production
callers and the `TEST_ResumeImpl` hook pass `Env::IOActivity::kFlush`. Setting
`io_activity` on these options is safe: `LogAndApply` does not assert
`io_activity == kUnknown`.

Reviewed By: pdillinger

Differential Revision: D112537885

fbshipit-source-id: 59045fd2f0a6e5bbacef6e0381e03ff95cd28c51
2026-07-23 18:43:39 -07:00
Peter Dillinger 744f120b12 Zero-copy reads of embedded (same-file) blob wide-column entities (#14990)
Summary:
This is a follow-up to D112212266 /
https://github.com/facebook/rocksdb/pull/14943 (multi-buffer PinnableWideColumns), which added the zero-copy read path for separate-blob-file references and explicitly left extending it to SST embedded-blob references as a follow-up.

Point GetEntity/MultiGetEntity of a wide-column entity whose columns are stored as embedded (same-file) blob records previously deserialized the entity, copied every column name/value, fetched each embedded blob (copying it), and re-serialized the whole entity before GetContext ever saw it (BlockBasedTable::MaybeResolveEmbeddedValue's wide-column branch). This extends the existing multi-buffer PinnableWideColumns zero-copy machinery (previously only for separate blob files / direct-write) to embedded blob columns.

Changes:
- New SameFileBlobReader interface (db/blob/same_file_blob_reader.h), a narrow seam for resolving a same-file BlobIndex against the current SST. BlockBasedTable implements it (GetSameFileBlob), delegating to the existing blob-cache-backed / pinned embedded reads.
- BlobFetcher is now an abstract interface; the Version-backed implementation moved to VersionBlobFetcher. Added an EmbeddedAwareBlobFetcher decorator that routes same-file references to a SameFileBlobReader and delegates all others to a base fetcher (EffectiveFetcher() selects the decorator only when a same-file reader is present).
- The point-Get and MultiGet loops now hand the raw entity to GetContext with the table as SameFileBlobReader, instead of pre-resolving/re-serializing. This is gated on not building a row-cache replay log, so a row-cache entry still captures a fully resolved entity (a later row-cache hit never reopens the SST to resolve same-file references).
- GetContext composes the decorator and resolves same-file and separate-file blob columns zero-copy across the columns / default-value / merge consumers.
- The iterator (DBIter) path is unchanged (still re-serializes via MaybeResolveEmbeddedValue).
- Also fixes latent method hiding in the test-only MockBlobFetcher.

Intended follow-ups:
- Lazy/deferred embedded resolution: return a LazyWideColumns result whose blob columns stay unresolved and are resolved on demand (whole or by byte range). The SameFileBlobReader seam and the polymorphic BlobFetcher introduced here are the mechanism for embedded support there.
- ReadOptions usage cleanup/optimization: have DBIter/BlobReader hold a single ReadOptions instead of decomposed scalar fields (removing the decompose-then-recompose in CreateBlobFetcher and closing the field-loss gap), and split VersionBlobFetcher into a ReadOptions-referencing variant for transient/stack fetchers plus an owning variant for lifetime-independent ones (the lazy resolver and compaction), to avoid copying the (growing) ReadOptions on the hot path.

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

Test Plan:
Added embedded wide-column tests to db/blob/db_blob_index_test.cc:
- GetEntity/MultiGetEntity of an embedded-blob wide-column entity return the correct columns and resolve them zero-copy (blob columns served from the blob cache: identical data() across reads and in separate backing buffers; pointer-stable across a PinnableWideColumns move).
- Plain Get of an entity whose default column is an embedded blob.
- Merge base and GetMergeOperands over an embedded-blob entity.
- read_tier == kBlockCacheTier yields Incomplete for an unresolved same-file reference.
- A corrupt embedded record surfaces Corruption, not a raw BlobIndex.

# Performance validation with db_bench

Measured with db_bench benchmarks added in https://github.com/facebook/rocksdb/issues/14995 / D113455538, temporarily stacked
on this diff for A/B testing.

## Setup

- **Baseline**: this diff's parent + db_bench benchmark commit.
- **Treatment**: this diff, incl. its test/lint fixups + the same
  db_bench benchmark commit.
- Both are release builds (`DEBUG_LEVEL=0 make db_bench`). The diff touches only
  the read path, so both binaries produce byte-identical SSTs; the db_bench commit
  is additive (`tools/db_bench_tool.cc` only) and identical in both.
- Host: AMD EPYC-Genoa, 166 vCPU, 224 GiB RAM. Single reader thread.

## Method

- Datasets are created **once** and read by **both** binaries via
  `--use_existing_db`, so the A/B compares identical on-disk data.
- Each read runs twice in-process (warmup + measured); the blob cache (1 GiB) and
  block cache (512 MiB) hold the whole ~400 MiB dataset, so the reported (2nd) run
  is fully warm — isolating the column-resolution cost the diff changes rather than
  I/O.
- 100k keys, `value_size=1024`, `embedded_blob_min_size=256`,
  `compression_type=none`, `format_version=7`, `--duration=10` per measured run.
- Datasets:
  - **A** — wide-column entity, `num_short_wide_columns=1` + `num_wide_columns=4`
    (small inline default column + 4 embedded-blob columns). Read via
    `readrandomentity` (GetEntity) and `multireadentity` (MultiGetEntity, batch 32).
  - **B** — wide-column entity, `num_short_wide_columns=0` + `num_wide_columns=4`
    (the default column itself is an embedded blob). Read via plain `readrandom`
    (Get returns the resolved default column).
  - **C** — whole-value embedded blobs (`fillembeddedblob`). Read via plain
    `readrandom`. This path is **not** changed by the diff — a regression check.

Example commands:

```
# create (once)
db_bench --benchmarks=fillembeddedentity --db=/tmp/perfA --use_existing_db=0 \
  --num=100000 --value_size=1024 --embedded_blob_min_size=256 --format_version=7 \
  --num_wide_columns=4 --num_short_wide_columns=1 \
  --use_blob_cache=true --blob_cache_size=1073741824 --cache_size=536870912 \
  --compression_type=none
# read (each binary; warmup + measured)
db_bench --benchmarks=readrandomentity,readrandomentity --db=/tmp/perfA \
  --use_existing_db=1 --threads=1 --duration=10 <same options>
```

## Results (warm, 2 runs each)

| Benchmark (read path) | Baseline µs/op | Treatment µs/op | Latency | Baseline Kops/s | Treatment Kops/s | Throughput |
|---|---|---|---|---|---|---|
| `readrandomentity` — GetEntity, inline default + 4 embedded cols | 7.16 | 5.00 | **-30%** | 140 | 200 | **1.43×** |
| `multireadentity` — MultiGetEntity (batch 32) | 6.46 | 4.54 | **-30%** | 155 | 220 | **1.42×** |
| `readrandom` — Get of embedded default column (dataset B) | 6.55 | 2.12 | **-68%** | 153 | 471 | **3.09×** |
| `readrandom` — whole-value embedded blob (dataset C, unchanged path) | 1.92 | 1.91 | ~0% | 521 | 524 | neutral |

Both runs were within ~1–3% of each other (raw numbers below).

## Interpretation

- **GetEntity / MultiGetEntity of embedded-blob wide-column entities improve ~30%
  in latency (~1.4× throughput)** — the expected win from resolving embedded blob
  columns zero-copy into the `PinnableWideColumns` backing buffers instead of the
  old deserialize + per-column copy + re-serialize round trip.
- **Plain `Get` of an entity whose default column is an embedded blob improves
  ~3×** — the largest win, because the old path re-serialized the entire entity
  even when only the default column is returned, whereas the new path resolves just
  that column.
- **The whole-value embedded blob `Get` path is unchanged (~0% delta), confirming
  no regression** on the pre-existing zero-copy path.

No unexpected regressions observed; all deltas match the diff's intended scope.

## Raw measured numbers (both runs)

```
readrandomentity   BASELINE  7.183 / 7.137 µs/op   139225 / 140118 ops/s
readrandomentity   TREATMENT 4.992 / 5.008 µs/op   200333 / 199681 ops/s
multireadentity    BASELINE  6.458 / 6.458 µs/op   154837 / 154843 ops/s
multireadentity    TREATMENT 4.509 / 4.577 µs/op   221798 / 218463 ops/s
readrandom (Bdef)  BASELINE  6.497 / 6.609 µs/op   153920 / 151307 ops/s
readrandom (Bdef)  TREATMENT 2.126 / 2.120 µs/op   470317 / 471805 ops/s
readrandom (Cblob) BASELINE  1.891 / 1.947 µs/op   528760 / 513554 ops/s
readrandom (Cblob) TREATMENT 1.895 / 1.920 µs/op   527767 / 520807 ops/s
```

Reviewed By: xingbowang

Differential Revision: D113347622

Pulled By: pdillinger

fbshipit-source-id: c51bc187085695d559411d9c0555bb52c8e0a61a
2026-07-23 16:15:49 -07:00
Peter Dillinger 0347b4df61 Add db_bench benchmarks for embedded-blob and wide-column entity reads (#14995)
Summary:
db_bench had no way to exercise SST embedded (same-file) blob reads or wide-column entity reads (GetEntity/MultiGetEntity), which are needed to measure the zero-copy embedded-blob wide-column entity read path.

This adds:
- Two setup benchmarks, fillembeddedentity and fillembeddedblob, that build an SST via SstFileWriter::OpenWithEmbeddedBlobs and ingest it (embedded blobs can only be produced this way, not by fillrandom/flush/compaction). Entity mode writes wide-column entities whose large columns are embedded blobs; blob mode writes whole-value embedded blobs. Both require format_version >= 7.
- readrandomentity (GetEntity) and multireadentity (MultiGetEntity) read benchmarks; plain readrandom is reused for the whole-value embedded blob baseline and for entity default-column Get.
- Flags embedded_blob_min_size, num_wide_columns, and num_short_wide_columns to control the embedded threshold and the inline/embedded column mix per entity.

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

Test Plan: Manual db_bench runs of the new benchmarks, used to validate D113347622.

Reviewed By: xingbowang

Differential Revision: D113455538

Pulled By: pdillinger

fbshipit-source-id: c6b62f5a2d6a61ae0b7cc32ac1b445b566ea8842
2026-07-23 16:01:07 -07:00
Peter Dillinger 755c104bca Unify clang detection for the C API codegen and add make regen-c-api (#14994)
Summary:
`USE_CLANG=1 make check-c-api-gen` failed with "clang++ not found" because the staleness check and the Python generator detected clang inconsistently: the Makefile inspected the `$(CXX)` make variable (the fbcode clang path) while the generator read the `CXX` environment variable, which make never exports. More broadly, clang++ was detected in several ad hoc places with fragile heuristics (grepping the compiler path, duplicated fallback lists in shell and Python).

Centralize clang++ detection in build_detect_platform as a single `CLANG_CXX`:
- `CLANG_CXX` honors `$CXX` when it is itself clang (so CI's `CXX=clang++-21` is used), else takes the first clang++ on PATH (bare, versioned, or the Meta-internal clang++.par). `CXX` (when building with clang) and `CLANG_ANALYZER` now derive from `CLANG_CXX` instead of re-detecting.
- Infer `USE_CLANG=1` when the compiler is clang, detected via the compiler's own `__clang__` predefined macro rather than matching the binary path (which false-positives, e.g. a home dir containing "clang"), and emit it so clang-specific Makefile logic is consistent.
- Emit `CLANG_CXX` to make_config.mk and pass it explicitly to the codegen, so `make check-c-api-gen` works without requiring `USE_CLANG=1` and the generator never guesses.

Add a first-class `make regen-c-api` target -- now the preferred regenerate command referenced by the generated banners and user-facing messages. The Python generators no longer detect clang themselves; `--clang` defaults to `clang++` for manual runs and is overridden by the Makefile with `CLANG_CXX`. Regeneration verifies the clang runs before writing any file, so a missing/broken toolchain aborts cleanly instead of leaving a partially regenerated tree.

Make field discovery clang-version-independent: older clang (e.g. 15) spells field types fully qualified (`rocksdb::ReadTier`,
`rocksdb::BlockBasedTableOptions::IndexType`) while the fbcode/CI clang (21) uses the bare `ReadTier` / `IndexType` the family tables expect. Strip the ROCKSDB namespace and enclosing-struct qualifiers so both agree; this is a no-op on newer clang, so checked-in output and CI are unaffected.

Also escape the literal "generated" in the generator .py sources (they emit the marker but are not themselves generated) so tooling stops treating them as read-only generated files.

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

Test Plan:
Manual verification (not covered by CI):
- `make check-c-api-gen` now runs the staleness sub-check (instead of skipping) and passes in a default gcc build as well as with `USE_CLANG=1` on an fbcode host, since `CLANG_CXX` is always populated.
- `ROCKSDB_NO_FBCODE=1 make regen-c-api` using a system clang 15 (clang++.par) produces output byte-identical to the checked-in files (generated with clang 21), and `check-c-api-gen` reports "up to date" -- confirming the namespace/struct stripping is version-robust and a no-op on clang 21.
- `regen_all.py --clang /nonexistent` exits non-zero with a clear message and leaves the generated files untouched.
- `make regen-c-api` is idempotent (no diff on a clean tree).
- Confirmed the escaped `\x40generated` still emits `// generated` byte-for-byte in the regenerated c.h/c.cc/.inc.
- Reviewed .github/workflows/pr-jobs.yml: the build-linux-clang-21-no_test_run job's `CXX=clang++-21 make check-c-api-gen` resolves `CLANG_CXX` to clang++-21 (honoring `$CXX`) rather than the container's stray bare clang++.

Reviewed By: xingbowang

Differential Revision: D113447462

Pulled By: pdillinger

fbshipit-source-id: 91ca92d21a71a8b92f9ff57500a8cec91a884078
2026-07-23 15:50:16 -07:00
Ranjan Banerjee 0a2ba2a164 Add shared backup/checkpoint option bases [1/4] (#14945)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14945

First diff in a stack that lets `Checkpoint` and `BackupEngine` share one parallel copy/link infrastructure. Pure options refactor, no behavior change.

Adds two shared option bases in `rocksdb/utilities/checkpoint.h`: `CheckpointOrBackupEngineOptions` (engine-level config) and `CreateCheckpointOrBackupOptions` (per-operation options). `CheckpointEngineOptions` / `CreateCheckpointOptions` derive from them for checkpoint; `BackupEngineOptions` / `CreateBackupOptions` derive from them for backup and keep their backup-only fields.

Reviewed By: xingbowang

Differential Revision: D111387916

fbshipit-source-id: d7efa528ac91fb00090130c9d2a671b13e934a82
2026-07-23 14:41:33 -07:00
Jay Huh dcb5c2bf53 Restore MultiScan prefetch stats dropped in the IODispatcher refactor (#14991)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14991

D91705195 ("Replace Prefetch Logic in BlockBasedTableIterator with IODispatcher") moved MultiScan block prefetching out of BlockBasedTableIterator and into the IODispatcher. The IO-level RecordTick/RecordInHistogram calls lived in the deleted prefetch code, and the IODispatcher was not given equivalent instrumentation, so these tickers were still defined but never populated (they published as absent):

- MULTISCAN_BLOCKS_PREFETCHED
- MULTISCAN_BLOCKS_FROM_CACHE
- MULTISCAN_PREFETCH_BYTES
- MULTISCAN_IO_REQUESTS
- MULTISCAN_IO_COALESCED_NONADJACENT
- MULTISCAN_BLOCKS_PER_PREPARE (histogram)

The PREPARE_*, SEEK_ERRORS, and PREFETCH_BLOCKS_WASTED tickers were unaffected (recorded on the prepare/teardown path, which was not replaced).

Fix:
- ReadSet now exposes generic, caller-agnostic prefetch counters (GetNumBlocksPrefetched, GetNumIORequests, GetPrefetchBytes, GetNumCoalescedNonadjacent), incremented once per block in DispatchPrefetch -- the single choke point for both immediate and memory-deferred dispatch. Counting at dispatch time (not completion) keeps async and deferred prefetches accounted for without double counting the async->sync fallback.
- PrepareIORequests now outputs prefetch_bytes (sum of request len) and num_coalesced_nonadjacent (within-threshold gap count) while building coalesced requests, per review feedback to avoid the extra pass over read_reqs/coalesced_block_indices at former line 836. DispatchPrefetch now gets stats directly from PrepareIORequests.
- The MULTISCAN_* ticker names stay at the caller: MultiScanIndexIterator's destructor records the aggregate counters at end-of-scan (alongside the existing PREFETCH_BLOCKS_WASTED), so async/deferred reads that complete during iteration are included. MULTISCAN_BLOCKS_PER_PREPARE is recorded in BlockBasedTableIterator::Prepare. This keeps the IODispatcher a generic mechanism rather than leaking a caller's metric names into it.
- Also plumb the same metrics into PerfContext for per-operation visibility.

Reviewed By: xingbowang

Differential Revision: D113356436

fbshipit-source-id: 660e9755d6b1b9aecccd112df94eb6838061b298
2026-07-23 14:09:07 -07:00
Jay Huh 7c385f4441 Add license headers to generated C API fragments (#14985)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14985

The Open Source Automated Checkup (T280493780) flagged the 27 generated
`c_api_gen/c_generated_*.inc` fragments for missing the required copyright
header ("Meta Platforms, Inc. and affiliates"). The fragments are emitted by
the C API code generators, none of which wrote a header, so every
regeneration reproduced the violation.

Root-cause fix in the generators so regenerated output is compliant:
- `tools/c_api_gen/generate_c_api.py` (`HEADER_BANNER`)
- `tools/c_api_gen/auto_simple_bindings.py` (`auto_header_banner`)
- `tools/c_api_gen/gen_roundtrip_tests.py`

Each now emits the standard Meta dual-license block (matching `c_base.h` /
`include/rocksdb/c.h`) ahead of the `// generated` marker, and the 27
checked-in `.inc` fragments are regenerated accordingly.

The assembled `include/rocksdb/c.h` and `db/c.cc` already carry a top-level
header, so `regen_all.py`'s inliner now strips each fragment's license block
when inlining to avoid repeating it once per fragment; those two files are
therefore unchanged.

Also adds the header to `tools/gtest_parallel_repro.py`, the one non-`.inc`
source file flagged by the checkup.

Reviewed By: pdillinger

Differential Revision: D113242908

fbshipit-source-id: 4d81d1355c8d8a35cbc4377dd9b21df901ad3cb7
2026-07-23 12:19:02 -07:00
Josh Kang 2c77c32ac9 Add Get and MultiGet sync/async plumbing (#14934)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14934

First diff in the async read stack: pure plumbing, no behavior change. Both the point `Get` path and the batched `MultiGet` path gain synchronous/coroutine twins generated by `DEFINE_SYNC_AND_ASYNC`; `DB::GetAsync` and `DB::MultiGetAsync` become the public callback-based async entry points with synchronous default implementations. For `Get`, the existing `DBImpl::Get` / `GetImpl` bodies are relocated into the sync/async wrapper. For `MultiGet`, the same wiring continues through the DBImpl, version, and table-cache layers. The generated coroutine variants still run the existing synchronous logic.

This keeps the refactor mechanical while preserving dispatch semantics: the coroutine side remains virtual at the same `GetImpl` boundary as the synchronous path, read-only and secondary DBs use generated sync/coroutine overrides instead of falling back to base `DBImpl`, and `StackableDB` leaves async `Get` / `MultiGet` to the base wrappers so wrapper-specific `Get` / `MultiGet` behavior is preserved.

For ABI compatibility and avoiding MACRO blocks in public headers, the asynchronous APIs are callback based instead of coroutine based. Internally it will be developed using coroutines, and applications can also develop their own coroutine wrappers.

This is meant to make the later changes to actually enable coroutine-backed async queries much easier to review.

Benchmarks:

Compared `db_bench` with and without this diff using:

`db_bench --benchmarks=fillseq,readrandom --num=1000000 --reads=5000000 --key_size=16 --value_size=100 --seed=12345 --db=<tmp>`

| Benchmark | Without diff avg | With diff avg | Delta |
| `fillseq` | 4.076 us/op | 4.165 us/op | +2.2% |
| `readrandom` | 3.939 us/op | 3.947 us/op | +0.2% |

No meaningful local `db_bench` regression was observed.

Reviewed By: xingbowang

Differential Revision: D109346179

fbshipit-source-id: 0dee1239f49099b67c11c63426204466e963f903
2026-07-23 09:56:30 -07:00
Andrew Chang 2b628ccfe5 Preserve fast_sst_open metadata across MANIFEST rotation (#14987)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14987

The `fast_sst_open` optimization speeds up DB open times by persisting file metadata information in the MANIFEST.

When MANIFESTs are rotated however, this metadata is currently getting dropped, since it is not fed through `VersionEdit::AddFile`. There were also a few additional missing `AddFile` call sites.

Fix: Add an optional `file_open_metadata` to `AddFile`. Feed this through wherever we call `AddFile`.

Reviewed By: anand1976

Differential Revision: D113265655

fbshipit-source-id: c8371d66f6555bb9c372eea095bfa30a5f56a88a
2026-07-22 16:01:44 -07:00
Andrew Chang 0745290ca6 Add OpenAndCompact DB open latency histogram (#14986)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14986

Why: We want to measure the DB open time during offloaded compactions.

What: Add a histogram for the DB open time in `DB::OpenAndCompact()`.

Reviewed By: anand1976

Differential Revision: D113136652

fbshipit-source-id: f8b2d74bcda3506f7b9ac80fa3e50a172deafc5e
2026-07-22 15:57:50 -07:00
Peter Dillinger ffa95117b9 Bug fix and clean up error/status handling for wide-column + blob support (#14980)
Summary:
An audit of the wide-column blob-reference read paths (from the preceding "multiple backing buffers" change, D112212266) surfaced several inconsistencies in how Status is used: control-flow-via-Status, a status classification that diverged from the rest of the codebase, the same "no blob fetcher" Corruption repeated at many call sites, a swallowed non-OK status (bug), and read statistics advanced on failed reads. This change cleans those up. It is internal only -- no public API or on-disk format change -- and stacks on that diff.

Concretely, this resolves two of the intended follow-ups called out in D112212266:
 * "Resolve FIXME about pre-existing swallowed non-OK status in db_impl.cc"
 * the error-status/API cleanup (classify an unrecognized wide-column version as Corruption, not NotSupported, at all entry points; replace the NotSupported sentinel from GetValueOfDefaultColumn with an is_blob_reference out-param, removing GetValueOfDefaultColumnResolvingBlobs; and centralize the "blob-backed value without a blob fetcher" Corruption into the resolvers, resolving the ResolveEntityForMerge FIXME, rather than repeating it at each caller). The other two follow-ups from that diff -- the value_size_soft_limit TODO in db_impl.cc and extending the zero-copy optimization to SST embedded-blob references -- are deferred to separate diffs.

Unrecognized version -> Corruption. A wide-column version newer than kVersion2 now returns Corruption instead of NotSupported, consistent with how the rest of the codebase classifies an unrecognized serialized version/type (BlobIndex, the blob log header, the table footer). The guard is applied at every entry point (Deserialize, GetValueOfDefaultColumn, HasBlobColumns, ForEachBlobFileNumber); the latter two previously lacked it and would misparse a future/corrupt version.

Reject trailing data after an empty entity. Deserialize() previously returned OK early for a zero-column entity, silently ignoring any trailing bytes; it now dispatches zero-column entities to the V1/V2 parser like the non-empty case, so the "exactly one serialized entity" trailing-data check the preceding diff added for non-empty entities now covers the empty case too.

Default-column extraction no longer signals via Status. now returns (value, is_blob_reference): for a blob-referenced default column it returns OK with the raw serialized BlobIndex bytes and the flag set, instead of a NotSupported sentinel that every caller had to detect and pair with a second, redundant full deserialization. GetValueOfDefaultColumnResolvingBlobs and the memtable GetDefaultColumnBlobIndexSlice helper are removed; a single ResolveDefaultColumnBlobReference() resolves the raw BlobIndex (handling an inlined index with no fetch). This also fixes a case where a non-default column 0 that happened to be a blob reference was misreported as a blob-backed default. The one remaining NotSupported is the honest, explicit base-DB case: reading a blob-index default column when is_blob_index was not requested.

Null-blob-fetcher handling centralized. "A blob-backed value with no fetcher to read it" (reachable via a reader without blob support such as SstFileReader, or corrupt/legacy data) was checked -- and reported as Corruption -- at ~6 call sites with three slightly different messages, forced by the resolvers asserting the fetcher was non-null. The asserts are gone; each resolver now owns the check in one commented place: ResolveEntityBlobColumnsMultiBuffer (whole entity), ResolveDefaultColumnBlobReference (default column), and the MaybeResolveMemtableBlobValue plain-value path (which open-codes its fetch). Callers just propagate the status. A side benefit: an entity/column whose only blob references are inlined now resolves without a fetcher instead of being wrongly rejected.

Return Status from the memtable resolve helpers, fixing a swallowed error. MaybeResolveMemtableBlobValue (was bool + Status*) and PostprocessMemtableValue- Read (was void + Status*) now return Status, matching codebase convention; MaybeResolveMemtableBlobValue reports "there was a reference to resolve" via a bool* did_resolve out-param (true means it has finalized value/columns). This resolves a pre-existing bug: on the blob-resolve path a non-OK status was returned early without the output cleanup the plain path performed, leaving a half-populated result behind an error. PostprocessMemtableValueRead now has a single success/error finalization block reached by every path (self-pin on success, clear outputs on any error), and returns the finalized status. Also switched the memtable "no blob fetcher" case from NotSupported to Corruption (an internal helper whose sole caller treats all non-OK statuses identically).

Read-only/secondary read stats mirror the primary path on error. DBImpl::GetImpl gates byte/throughput accounting (BYTES_READ, BYTES_PER_READ, get_read_bytes) and merge-operand output on s.ok(); the read-only and secondary Get paths did so unconditionally, logging spurious samples (e.g. a 0-byte BYTES_PER_READ sample) on NotFound/error. They now gate on s.ok() too, while keeping NUMBER_KEYS_READ, MEMTABLE_HIT, and super-version cleanup unconditional.

Header docs updated to match (Deserialize/DeserializeSimple return Corruption for an unexpected blob reference; unknown per-column value types are rejected as Corruption).

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

Test Plan:
Extended db/wide/wide_column_serialization_test.cc: DeserializeUnsupportedVersion now expects Corruption; new FutureVersionRejectedConsistently verifies that Deserialize, GetValueOfDefaultColumn, HasBlobColumns, and ForEachBlobFileNumber all reject a future version as Corruption; V2GetValueOfDefaultColumnBlobRef now checks the is_blob_reference flag and raw-BlobIndex bytes and that resolving without a fetcher is Corruption; DeserializeRejectsTrailingData now also covers empty (zero-column) V1 and V2 entities.

db/blob/db_blob_index_test.cc: MaybeResolveMemtableBlobValueWithoutFetcherFails- Closed updated to the new signature (asserts did_resolve + Corruption); new PostprocessMemtableValueReadClearsOutputsOnError pins the swallowed-status fix, covering both an already-non-OK incoming status and (the actual FIXME scenario) an OK incoming status whose blob-resolve path fails -- both clear outputs and return the error. The StackableDB-style DBBlobIndexTest.Get contract is unchanged (its NotSupported comes from the base-DB blob-index read path, not these helpers).

Reviewed By: xingbowang

Differential Revision: D113099114

Pulled By: pdillinger

fbshipit-source-id: 4bc374dd74f8ba83bdf49bd8753c1a8d88529cde
2026-07-22 14:29:35 -07:00
Peter Dillinger 30ba4908ba Enforce value_size_soft_limit for direct-write blob values in MultiGet (#14983)
Summary:
Resolves the value_size_soft_limit TODO left in db_impl.cc by the PinnableWideColumns multi-buffer change (D112212266 / PR https://github.com/facebook/rocksdb/issues/14943).

ReadOptions::value_size_soft_limit is meant to bound how much a MultiGet buffers, but it was enforced in the batch loop on pre-resolution sizes. Pre-flush blob direct write references (a plain value encoded as a BlobIndex, or a wide-column entity with unresolved blob-valued columns) are only resolved later, in MultiGetImpl's post-processing loop, so their (possibly large) blob values were fetched with no limit applied -- read unbounded. Separate-file blob columns do not have this gap because they are resolved eagerly in GetContext and thus already counted.

Now the post-processing loop enforces the limit on direct-write resolution using the "always make progress" soft-limit semantics: a key is aborted only once the bytes already returned by this MultiGet exceed the limit, so at least one key is always read even if its value alone exceeds the limit. Once the returned total is over the limit, subsequent keys that need fetching get Status::Aborted. This bounds the reads while ensuring a caller retrying the aborted keys cannot loop forever on a single oversized value.

Pieces that benefited from human input and refinement:
* Enforcement semantics: settled on "always make progress" -- checking the bytes already returned (rather than a projected pre-fetch size, which in foreseeable user integrations could starve a lone key that by itself exceeds the limit) -- matching the existing separate-file/batch soft-limit behavior.

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

Test Plan:
Added to db/wide/db_wide_blob_direct_write_test.cc (memtable / pre-flush direct-write reads, using the ResolveDirectWriteWideColumns:BlobFetched sync point to confirm which blobs are fetched):
* MultiGetEntitySoftLimitBoundsDirectWriteBlobResolution -- two over-limit entities: first read (progress), second aborted; one blob fetch.
* MultiGetEntitySoftLimitAlwaysReadsOversizedKey -- a single entity whose value alone far exceeds the limit is returned OK (no starvation).
* MultiGetSoftLimitBoundsDirectWritePlainValue -- same for plain values stored as direct-write blob references: first read, second aborted.

Reviewed By: xingbowang

Differential Revision: D113159249

Pulled By: pdillinger

fbshipit-source-id: 5af443b86bdfc3ecd0112d3c5af541a77f0d3d67
2026-07-22 09:52:19 -07:00
Jay Huh 144deb248a Harden MaybeOpenReadOnlyOnPrimary stress test: fix TSAN lock-limit crash + drop false-positive read-only Get (#14979)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14979

`MaybeOpenReadOnlyOnPrimary` (added in D111995911) opens a read-only DB on the primary's live directory while the primary keeps mutating it. Two separate stress-test false positives were observed there; this diff fixes both and consolidates the earlier one-off fixes into a single diff.

1) TSAN deadlock-detector crash in the CF-name snapshot (`fbcode_tsan_blackbox_crash_test`):

    ThreadSanitizer: CHECK failed: sanitizer_deadlock_detector.h:67
    "((n_all_locks_)) < (...)" (0x80, 0x80)

`n_all_locks_` is the number of mutexes a thread holds *simultaneously*; TSAN caps it at 128 (0x80). The snapshot loop called `LockColumnFamily(cf)` to read `column_family_names_[cf]`, and that helper `.Lock()`s ALL per-key mutexes for the CF at once (`ceil(max_key / 2^log2_keys_per_lock)` -- e.g. 25,000 in blackbox with max_key=100000, log2_keys_per_lock=2) without releasing any, so a single call blows past 128. The writer (`MaybeClearOneColumnFamily`) holds all key locks while renaming a CF, so a single key lock is enough to synchronize the read. Fix: take one `MutexLock` on key-lock index 0 per CF instead of `LockColumnFamily`, keeping the simultaneously-held count at 1.

(`LockColumnFamily` is a latent hazard for any caller; the pre-existing caller `MaybeClearOneColumnFamily` never runs in the crash-test config -- `clear_column_family_one_in=0` -- so only this new, ungated caller exposed it.)

2) False-positive read-only Get (folded in from D112987468). After flushing the primary, the test performed a `Get(Key(0))` on each CF of the read-only instance. That Get races against concurrent primary compaction, which can delete SST files the read-only's frozen MANIFEST still references, surfacing as IOError/PathNotFound/Corruption ("truncated block read"). The Get adds no unique coverage -- the scenario under test (the read-only close wrongly deleting primary live files) is detected on the primary's own verification paths -- so remove the Get block entirely instead of widening a tolerated-status list.

Note on D112936376: that diff tried to fix crash (1) by setting `table_cache_numshardbits=0` on the read-only instance, on the theory that the primary's and reader's table caches contribute "128 unique lock objects." That is a misdiagnosis: the failing CHECK counts locks held *simultaneously*, and sharded-cache shard mutexes are held transiently (one at a time; read-path nesting is a constant depth of 2), so shard count does not affect `n_all_locks_`. The real cause is `LockColumnFamily`, fixed in (1), so `table_cache_numshardbits=0` is unnecessary and is not included here.

This diff subsumes and replaces D112936376 and D112987468.

Reviewed By: joshkang97

Differential Revision: D112980148

fbshipit-source-id: 012ff852109223f3d19d8dbbc065fde9340f9c66
2026-07-21 16:57:11 -07:00
Jay Huh 990827e916 db_bench: tolerate remote IO errors by skipping ops and continuing (#14971)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14971

Follow-up to https://github.com/facebook/rocksdb/pull/14942. That PR made db_bench read ops end the run with an INVALID result (`SetFatal` -> exit 1) on a tolerable remote IO error and left write ops on unconditional `SetFatal`. In practice that still failed the remote benchmark TW jobs (exit 1) on transient WS infra errors, and did not cover write ops or `DB::Open`.

Change the toleration to skip-and-continue so a transient remote IO error no longer fails the run:
- `HandleBenchmarkIOError` now just logs and returns on a tolerable error (was: `SetFatal`); the caller skips the failed op and keeps going. Non-tolerable, data-loss, and local-DB errors still `abort()` (core dump for debugging).
- Applied uniformly to all worker op error paths: `Get` / `MultiGet`, put / merge, delete / delete-range, `DoDelete`, `RandomWithVerify` put/delete-many, and the `BGScan` iterator (stops the scan and continues).
- `DB::Open`: on a tolerable remote IO error, exit cleanly (0) -- the benchmark cannot run, but it is not a db_bench / RocksDB failure.

Gated by `--tolerate_io_errors_for_remote_dbs` plus a remote `--env_uri` / `--fs_uri` backend, so local runs and non-IO / data-loss errors are unaffected. The result is best-effort (skipped ops are not counted); the goal is to keep remote benchmark jobs green through transient WS infrastructure errors instead of failing or crashing.

db_stress is intentionally NOT changed: it is a correctness (crash-recovery) test and must keep failing on real errors.

Differential Revision: D112749712

fbshipit-source-id: a8ccd46ef66e6c3c97a1baffd35d27830879d428
2026-07-21 08:15:35 -07:00
Hui Xiao 7c4bf11044 CPU corruption injector: robust seed handling, per-run command logging, and doc fixes (#14940)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14940

A few improvements to the CPU corruption injector:

- Seed handling: `--seed` is now validated against db_stress's accepted range and no longer silently wraps, so an out-of-range seed fails fast instead of producing misleading NO_INJECTION runs. Omit `--seed` for a fresh random base seed.
- Per-run reproducibility: each run's full db_stress command is recorded at the top of its `gdb.log`, so any run can be reproduced by copy-pasting one line.
- README: clarified the `--seed` semantics and examples.

**Test plan:**

All db_stress runs use a debug build: `make DEBUG_LEVEL=0 EXTRA_CXXFLAGS="-g -fno-inline" db_stress`.

## Seed handling: out-of-range seeds fail fast

Command:

```
python3 tools/cpu_corruption_injector/runner.py --op write --runs 2 --stress_cmd ./db_stress --report_dir /tmp/tp_badseed --seed 4294967295; echo "exit=$?"
python3 tools/cpu_corruption_injector/runner.py --op write --runs 1 --stress_cmd ./db_stress --report_dir /tmp/tp_badseed0 --seed 0; echo "exit=$?"
```

Output:

```
2026-07-15 01:26:14,408 ERROR --seed range [4294967295, 4294967296] is outside db_stress's accepted [1, 4294967295]; pass a smaller --seed
exit=2
2026-07-15 01:26:14,558 ERROR --seed range [0, 0] is outside db_stress's accepted [1, 4294967295]; pass a smaller --seed
exit=2
```

Both are rejected before any run starts (previously the seed silently wrapped and every run came back a misleading NO_INJECTION).

## Per-run command logged (full db_stress command in gdb.log)

Command:

```
python3 tools/cpu_corruption_injector/runner.py --op write --runs 2 --stress_cmd ./db_stress --report_dir /tmp/tp_seed --seed 424242
head -6 /tmp/tp_seed/run_00000/gdb.log
```

Output (the first line is the full command; the run's gdb/db_stress output follows it):

```
# /usr/bin/gdb --batch --nx -iex 'py import sys; sys.argv=["injector.py", "--op", "write", "--op_index", "259", "--entry_fn", "rocksdb::MemTable::Add", "--target_fn", "rocksdb::MemTable::Add", "--seed", "424242", "--dir", "/tmp/tp_seed/run_00000"]' -x /data/users/huixiao/fbsource/fbcode/internal_repo_rocksdb/repo/tools/cpu_corruption_injector/injector.py --args /data/users/huixiao/fbsource/fbcode/internal_repo_rocksdb/repo/db_stress --db=/tmp/tp_seed/run_00000/db --expected_values_dir=/tmp/tp_seed/run_00000/exp --seed=424242 --verify_cpu_corruption_dir=/tmp/tp_seed/run_00000 --ops_per_thread=1000 --max_key=1000 --value_size_mult=8 --column_families=1 --nooverwritepercent=0 --writepercent=50 --delpercent=40 --delrangepercent=10 --readpercent=0 --iterpercent=0 --prefixpercent=0 --flush_one_in=20 --compact_range_one_in=10 --compact_files_one_in=10 --target_file_size_base=65536 --reopen=0 --destroy_db_initially=1 --clear_column_family_one_in=0 --disable_wal=0 --threads=1 --num_dbs=1 --disable_auto_compactions=1 --write_buffer_size=1073741824 --remote_compaction_worker_threads=0 --subcompactions=1 --batch_protection_bytes_per_key=8 --memtable_protection_bytes_per_key=8 --block_protection_bytes_per_key=8 --paranoid_file_checks=1 --paranoid_memory_checks=1 --memtable_verify_per_key_checksum_on_seek=1 --detect_filter_construct_corruption=true --verify_checksum=1 --compression_checksum=1 --file_checksum_impl=crc32c --verify_checksum_one_in=1 --verify_file_checksums_one_in=1000 --read_fault_one_in=0 --write_fault_one_in=0 --metadata_read_fault_one_in=0 --metadata_write_fault_one_in=0 --open_read_fault_one_in=0 --open_write_fault_one_in=0 --open_metadata_read_fault_one_in=0 --open_metadata_write_fault_one_in=0 --secondary_cache_fault_one_in=0 --sync_fault_injection=0

This GDB supports auto-downloading debuginfo from the following URLs:
  <https://debuginfod.centos.org/>
Debuginfod has been disabled.
```

The first line of `gdb.log` is the complete `gdb` + `db_stress` command, including `--seed=424242` and `--max_key=1000`; the run's own output follows below it.

## The logged command reflects real flag values

Temporarily set `max_key` to `12345` in `runner_build.py`, then:

Command:

```
python3 tools/cpu_corruption_injector/runner.py --op write --runs 1 --stress_cmd ./db_stress --report_dir /tmp/tp_maxkey --seed 424242
head -1 /tmp/tp_maxkey/run_00000/gdb.log | tr ' ' '
' | grep -- --max_key
```

Output:

```
--max_key=12345
```

The logged command tracked the changed preset value. Reverted the change afterward.

Reviewed By: archang19

Differential Revision: D112075800

fbshipit-source-id: 0d2be1741ede806a84a1a0f9ca6b2a32147c4e9c
2026-07-21 07:37:03 -07:00
Peter Dillinger 81ea677072 PinnableWideColumns support for >1 backing buffer to avoid re-serialization (#14943)
Summary:
Wide-column read paths that materialize blob-referenced columns used to re-serialize the entire entity into a fresh contiguous buffer, copying the (potentially large) resolved column values byte-for-byte. This was forced by PinnableWideColumns holding a single backing buffer with all column Slices pointing into it.

PinnableWideColumns now holds its columns as zero-copy Slices into one or more stable backing buffers, kept in a std::forward_list<PinnableSlice> whose nodes never relocate on move, move-assignment, or splice. Resolved blob columns can be spliced in as additional backing buffers instead of re-serializing the entity.

Using address-stable nodes also cleans up the old Slice-invalidation handling. The previous single-buffer representation had to cope with a self-pinned PinnableSlice whose small (SSO) std::string relocates when the PinnableSlice is moved: on every move it detected the relocated backing pointer and re-derived the column Slices by re-parsing. With forward_list nodes that never move, the column Slices stay valid across move / move-assignment / splice, so that handling is gone and the move operations are now the defaulted member-wise moves.

The multi-buffer plumbing is internal only: a new PinnableWideColumnsHelper (a friend) performs the splice and the little bit of private-member access the read paths need, so there is no new public API surface and the previous `friend class DBImpl` is gone. The public view is unchanged: columns() still returns a single, in-order set.

Read paths converted to resolve without re-serializing:
- GetContext::SaveWideColumnEntityToColumns (eager GetEntity SST reads, separate-blob-file references)
- DBImpl::ResolveDirectWriteWideColumns (direct-write blob references)
- DBImpl::MaybeResolveMemtableBlobValue (blob-backed memtable reads)

Merge output still serializes (the MergeOperator API consumes/produces a serialized entity). Same-file ("embedded") blob columns are resolved -- and still re-serialized -- earlier in the block-based table reader (MaybeResolveEmbeddedValue / ResolveEmbeddedValueForGet) before the entity reaches GetContext, so the GetEntity path above only avoids re-serialization for separate-blob-file references. Compaction's separate entity machinery is unchanged.

API/accounting: deprecated PinnableWideColumns::serialized_size() (comment-only, per repo convention) in favor of the new payload_size() (sum of column name + value sizes; equals the value size for a plain value). Read statistics for entities (BYTES_READ, BYTES_PER_READ, per-key GetEntity/MultiGetEntity byte counts) and the ReadOptions::value_size_soft_limit threshold now measure entities by payload_size() instead of the serialized size. This drops framing bytes (entity measurements are slightly smaller), thus making plain-value and wide-column accounting consistent and reducing unnecessary exposure to implementation details.

Serialization cleanup enabled by the above:
- Consolidated Deserialize/DeserializeV2 into a single Deserialize(const Slice&, WideColumns&, blob_columns*) plus a DeserializeSimple alias for the no-blob-references case; both take const Slice&, require the input to be exactly one serialized entity, and reject any trailing data uniformly (V1 previously ignored trailing bytes). An unexpected blob reference where a fully-resolved entity is required now returns Corruption.
- ResolveEntityBlobColumns (serialized output, used only by merge) is now a thin wrapper over the new multi-buffer resolver plus Serialize, removing the duplicated fetch loop and the SerializeResolvedEntity helper.
- Consistently use `const Slice& input` rather than `Slice& input` for wide column functions expecting an input slice with no trailing data.

Design/polish from human review:
* The initial plan called for more complex handling around a vector<PinnableSlice> backing store (unique_ptr indirection and re-deriving the column Slices on move to cope with reallocation and SSO relocation); switching the backing store to a std::forward_list of address-stable nodes was a human suggestion that removed that complexity and the move-time Slice re-derivation entirely.
* Keep resolve plumbing off the public API via PinnableWideColumnsHelper and removing the DBImpl friend
* Various pieces of cleanup and polish (more left to do)
* The eager-GetEntity optimization
* The memtable inline-copy NOTE/TODO.

Intended follow-up:
* Resolve FIXME about pre-existing swallowed non-OK status in db_impl.cc
* Resolve TODO about value_size_soft_limit in db_impl.cc
* Extend this optimization to SST embedded-blob references
* Error-status/API cleanup: classify an unrecognized wide-column version as Corruption (not NotSupported) at all entry points; replace the NotSupported sentinel from GetValueOfDefaultColumn with an explicit is_blob_reference out-param (removing GetValueOfDefaultColumnResolvingBlobs); and centralize the "blob-backed value without a blob fetcher" Corruption into the resolvers (resolving the ResolveEntityForMerge FIXME) rather than repeating it at each caller

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

Test Plan:
Extended db/wide/wide_column_serialization_test.cc with PinnableWideColumns unit tests: set/payload_size for plain, V1, and resolved entities; move and move-assign of a multi-buffer result preserve column data pointers (the node-stability guarantee); Reset; ResolveColumns splices blob buffers in zero-copy (resolved values' data() point into the spliced-in buffers, not a re-serialized entity); serialized_size() == SerializedSizeV1(columns()); and Deserialize rejects trailing data after a serialized entity for both V1 and V2 layouts.

Built and ran (debug): wide_column_serialization_test, db_wide_basic_test, db_wide_blob_direct_write_test, db_blob_basic_test, compaction_iterator_test, db_merge_operator_test, db_iterator_test, udt_util_test, write_batch_with_index_test -- all pass. librocksdb and db_stress build. make format-auto and build_tools/check-sources.sh are clean.

Reviewed By: xingbowang

Differential Revision: D112212266

Pulled By: pdillinger

fbshipit-source-id: 13b89e2bb7d8371f4c1ff18b234a549bea9c9e77
2026-07-20 16:32:42 -07:00
Jay Huh 04cad97e7e Stress test concurrent primary and read-only DB on the same directory (#14938)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14938

Adds db_stress coverage for the topology where a read-only DB (`DB::OpenForReadOnly`) is opened on the same directory as a live read-write primary. This interaction was previously untested: `FLAGS_read_only` runs a DB by itself in read-only mode (no live writer), and `FLAGS_test_secondary` co-locates a secondary that tails the MANIFEST and never owns/deletes files, so neither exercises a read-only instance holding a stale live-file snapshot while the primary keeps creating files. That gap allowed a bug where a read-only DB's close ran obsolete-file cleanup against its stale snapshot and deleted the primary's live SST files.

New flag `open_read_only_one_in=N`: on thread 0, with probability 1/N per operation, open a read-only instance on the primary's live directory, flush the primary (so new SSTs exist that are absent from the reader's frozen snapshot), do a few reads, then close the reader. If the read-only close wrongly purges obsolete files, the primary's live files are deleted and surface through db_stress's existing missing-file/corruption detection (deterministically on the next reopen).

The read-only reader is opened with the primary's options (so it inherits the table factory, e.g. UDI) but with `compaction_service` and `listeners` cleared, since a read-only reader never compacts and must not share the primary's background-coordination services -- otherwise the reader's open/close would abort the primary's in-flight remote compactions.

`db_crashtest.py` enables the mode randomly and excludes configurations it does not support (transactions, BlobDB, TTL, and the blob-direct-write profile). Startup validation in `db_stress_tool.cc` rejects those same combinations when the flag is set directly.

Reviewed By: joshkang97

Differential Revision: D111995911

fbshipit-source-id: 0f7a2ef93e63cfa6677b6fa32c1bd34eb818c978
2026-07-20 15:23:12 -07:00
anand1976 865568398a Update version to 11.8.0 for 11.7 release (#14950)
Summary:
- Bump version.h from 11.7.0 to 11.8.0
- Add `11.7.fb` to `check_format_compatible.sh`

Part of 11.7 release workflow.

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

Reviewed By: rban1

Differential Revision: D112627463

Pulled By: anand1976

fbshipit-source-id: 0685a7a7a45c157c2a7e392420e3e16d5a364727
2026-07-20 11:02:10 -07:00
Jay Huh 2809ce9d0f Tolerate remote IO errors in db_stress crash test and db_bench (#14942)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14942

Remote storage reached over `--env_uri` / `--fs_uri` (e.g. Warm Storage) can surface transient infrastructure IO errors that are unrelated to any RocksDB bug. Previously such an error would take down the test/benchmark process, making remote runs unusable. This diff lets both `db_stress` and `db_bench` tolerate non-data-loss IO errors, behind default-off flags that only take effect against a remote backend, so local behavior is unchanged and local IO errors are never masked.

**db_stress:** Add a default-off flag `--tolerate_non_injected_io_errors_for_remote_dbs` that treats non-injected, non-data-loss IO errors as retryable, so remote infrastructure errors do not fail the stress harness through `SharedState::SafeTerminate()`. `IsErrorInjectedAndRetryable` enforces at the binary level that this only applies when a remote backend (`--env_uri` / `--fs_uri`) is in use; `db_crashtest.py` additionally force-disables it for local DBs. Remote crash-test jobs opt in by passing the flag on the command line (see the companion Tupperware config change).

**db_bench:** Add a default-off flag `--tolerate_io_errors_for_remote_dbs`. When set and a remote backend (`--env_uri` / `--fs_uri`) is in use, a non-data-loss IO error surfaced by a benchmark operation ends the run gracefully with an INVALID result through the existing cooperative shutdown path (`SharedState::SetFatal`) instead of calling `abort()` (which previously produced a `Received signal 6 (Aborted)` process crash). All read `Get` / `MultiGet` abort sites and the `BGScan` iterator-error site are routed through a shared `HandleBenchmarkIOError` helper; IO errors on local DBs and data-loss errors are never tolerated. With the flag off (default), the original `abort()` behavior is preserved so local runs still produce a core dump for debugging.

Reviewed By: anand1976

Differential Revision: D112136438

fbshipit-source-id: 510dbc644da78c4315a476ec766e26b2711dc060
2026-07-17 18:57:46 -07:00
Josh Kang 223b5ee13d Fix data race in VerifyPkSkFast between virtual call and TransactionDB destruction (#14930)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14930

Fix a TSAN-detected data race on the vptr between `MultiOpsTxnsStressTest::VerifyPkSkFast` making virtual calls through `db_` and `WritePreparedTxnDB::~WritePreparedTxnDB` updating the vptr during destruction.

The race occurs because `VerifyPkSkFast` (called from a compaction notification callback on a background thread) accesses `db_` which is a non-atomic raw pointer. Meanwhile, the main thread destroys the TransactionDB wrapper via `db_owner_.reset()` in `StressTest::CleanUp()`.

The previous fix (commit 768838ddcd99, `OnDBShutdownBegin`) reduced the race window by adding a `shutting_down_` check in the listener, but did not fully eliminate it because a background thread can pass the check before shutdown begins and then proceed to use `db_` while the destructor runs.

This fix addresses the root cause in two ways:
1. In `VerifyPkSkFast`: Use the atomically loaded `db` local variable (from `db_aptr_.load(memory_order_acquire)`) for ALL virtual calls instead of the non-atomic `db_` member. This ensures the pointer used for virtual dispatch was safely acquired before any shutdown signaling.
2. In `StressTest::CleanUp()`: Store nullptr to `db_aptr_` before calling `Close()`. This prevents any NEW entries into `VerifyPkSkFast` from background threads that have not yet loaded the pointer. `Close()` then waits for all in-flight background work (including any `VerifyPkSkFast` that already loaded a non-null pointer) to complete before proceeding to destroy the DB.

Together, these changes ensure that no background thread performs a virtual call through the TransactionDB pointer while the destructor is modifying the vptr.

Reviewed By: xingbowang

Differential Revision: D111480308

fbshipit-source-id: 0c998888d07712236c28eee60d04c6f169949a80
2026-07-17 11:52:34 -07:00
Jay Huh 6d8370ec56 cmake: discover gtest tests at ctest time to fix macOS CMake CI build (#14951)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14951

The `build-macos-cmake` CI job intermittently but persistently fails during `make` with:

  GoogleTestAddTests.cmake:... (parse_tests_from_json)
  * Line 1, Column 1
  Syntax error: value, object or array expected.
  make: *** [all] Error 2

This is `gtest_discover_tests` running in its default POST_BUILD mode: it executes each freshly-built test binary during the parallel `-j` build to enumerate test cases as JSON. On the resource-constrained arm64 macOS runners, that discovery run competes with the in-flight compile jobs and can produce empty output, which CMake's JSON parser then rejects, aborting the whole build. The failing targets (e.g. `cache_reservation_manager_test`, `lru_cache_test`) are unrelated to whatever change is under test -- other test binaries in the same run discover fine -- confirming it is a build-phase discovery race, not a code defect.

Switch discovery to `DISCOVERY_MODE PRE_TEST` so enumeration happens at ctest time (serially, after the build) instead of during the parallel build. This keeps the build green while preserving full test discovery. `PRE_TEST` requires CMake >= 3.18, so it is guarded by a version check; on older CMake (the project's declared minimum is 3.12) behavior is unchanged.

Reviewed By: xingbowang

Differential Revision: D112478854

fbshipit-source-id: 0d5edea416610f9cc1e9ae806d7d521165abe656
2026-07-17 09:09:51 -07:00
Laurynas Biveinis d461606f0f Make range-lock waits interruptible with SetIsKilledCallback (#14927)
Summary:
Add RangeLockManagerHandle::SetIsKilledCallback, a std::function<bool()> predicate mirroring SetEscalationBarrierFunc. When installed, RangeTreeLockManager wakes the toku lock_request::wait loop every 100ms to poll it; if it fires, the wait completes with a new DB_LOCK_INTERRUPTED result mapped to Status::Aborted(), so a blocked GetRangeLock() returns promptly instead of waiting out the lock timeout. Range-lock users that install no callback keep the original wait-until-grant-or-timeout behavior (no extra wakeups).

Also remove the unused toku kill_waiter abort path (lock_request and locktree_manager), which had no caller.

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

Test Plan:
`build_tools/rocksptest.sh range_locking_test` passes, including the two new tests. KilledCallbackAbortsWait completes in ~90ms despite a 100s lock timeout, confirming the kill callback aborts the wait promptly; KilledCallbackFalseStillTimesOut confirms an installed-but-false callback still times out.

Tested end-to-end together with the MyRocks side in D110890436: with rocksdb_use_range_locking=ON, a KILL QUERY parked on a conflicting range-lock wait is interrupted promptly (rocksdb.range_locking MTR suite) instead of waiting out rocksdb_lock_wait_timeout.

Reviewed By: pdillinger

Differential Revision: D111663405

Pulled By: laurynas-biveinis

fbshipit-source-id: 4819e09bde66f4e932aef74f6adfd14789bb1425
2026-07-15 21:21:45 -07:00
Peter Dillinger c9536aaef7 Fix quadratic point-in-time MANIFEST recovery in VersionBuilder (#14929)
Summary:
Point-in-time MANIFEST replay -- best-efforts recovery (VersionSet::TryRecoverFromOneManifest) and secondary/follower catch-up (ManifestTailer) -- was quadratic in the number of MANIFEST records.

d458331ee (D61171320, "Move file tracking in VersionEditHandlerPointInTime to VersionBuilder") took a clean approach: it consolidated applying a VersionEdit and checking Version completeness into a single VersionBuilder implementation, so those checks were no longer duplicated between the handler and the builder. To let VersionEditHandlerPointInTime still build a Version reflecting the state before a valid->invalid edit, it added a save point -- but that save point proved inefficient: it was implemented by deep-copying the entire VersionBuilder::Rep before every edit (all of levels_[].added_files with a ref bump per file, table_file_levels_, found_files_, mutable_blob_file_metas_, ...). With N edits accumulating O(F) live files that is O(N*F) time and memory, and in the common healthy case the copy is pure waste since no Version is built during replay.

D110842287 (66bccb467, "Store BySmallestKey by value to avoid shared_ptr atomic copy in Rep") addressed a CPU-overhead symptom that had been misattributed to the shared_ptr atomic copies in that per-edit Rep copy. The real expense was copying Rep's containers -- notably table_file_levels_ -- on every edit. This change keeps d458331ee's clean single-implementation design but makes the save point cheap, removing the copy entirely:

- Replace the whole-Rep save point with a single-edit undo. Apply() records a compact, self-inverse undo of just the current edit (the pre-edit value of each touched key), and the PIT handler builds the pre-edit Version by rolling that one edit back, then redoing it to continue replay. This is O(edit) per edit, single-copy memory, and adds no extra file-verification I/O except on the rare valid->invalid negative edge. Also removes the Rep copy constructor.

- Store the undo registries in UnorderedMap (hash map) where ordering is not needed, which reduces the remaining per-edit CPU further. added_files and deleted_files keep an ordered map keyed by (level, file number), which trivial moves (same file number at two levels in one edit) require.

- Group the scalar/flag state mutated during Apply into a MutableScalars base so the undo can snapshot/restore it with a single copy/swap without duplicating the field list. Add util/cast_util.h up_cast<Base> for the explicit, compile-time-checked upcasts this uses instead of raw static_cast.

- Generalize D110842287: also store the stateless L0 comparators level_zero_cmp_by_epochno_ and level_zero_cmp_by_seqno_ by value rather than shared_ptr, dropping their make_shared allocation and atomic refcounting.

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

Test Plan: Debug build (asserts + force_consistency_checks on). The single-edit rollback path is exercised by the existing recovery coverage, all passing: version_builder_test, version_set_test, db_secondary_test (ManifestTailer), db_follower_test (PartialVersionRecovery = the negative-edge rebuild), and the db_basic_test best-efforts suite -- including BestEffortsRecoverIncompleteVersion with blob-file variants (blob addition rolled back/redone) and BestEffortRecoveryFailureWithTableCacheUseAfterFree (FileMetaData/table-cache lifecycle) -- plus corruption_test.

Reviewed By: joshkang97

Differential Revision: D111753669

Pulled By: pdillinger

fbshipit-source-id: 7e6b5238f55cd906b1d4ff0044aeb0a02ec1cdcd
2026-07-15 13:34:03 -07:00
Jay Huh cf22ee8f5b Harden RocksDB PR comment artifact downloads (#14941)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14941

Download GitHub Actions artifacts into `_artifact` before reading comment result files so untrusted artifacts cannot overwrite checked-out workflow helper scripts. Keep `post-pr-comment.js` loaded only from `.github/scripts` and remove unnecessary inherited secrets from the review-comment wrapper workflows.

Reviewed By: pdillinger

Differential Revision: D112129548

fbshipit-source-id: c807eab2197018b0672a5e1d9710e4c6bfcfd0ac
2026-07-15 12:54:46 -07:00
Peter Dillinger c7f7b1b2ae Skip redundant DeleteWalsBefore during recovery when markers already advanced (#14918)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14918

When `optimize_manifest_for_recovery=true` and `track_and_verify_wals_in_manifest=true`, the close-time code (`MaybeWriteWalMarkersToManifestOnClose`) already writes `DeleteWalsBefore(max_wal+1)` to the MANIFEST. On the subsequent clean reopen, recovery was unconditionally emitting the same `DeleteWalsBefore` edit, causing 43 bytes of unnecessary MANIFEST growth. This violated the zero-growth guarantee that the stress test STRICT mode verifies, triggering `exit(1)` which then caused a secondary `property_info != nullptr` assertion in `DumpStats()` (from the Timer thread running during static destructor teardown).

The fix adds the same monotonicity guard (`new_min_log > GetMinWalNumberToKeep()`) that is already used at all four other `DeleteWalsBefore` call sites (db_impl.cc:748, db_impl_files.cc:906, db_impl_write.cc:3331, memtable_list.cc:1015). This skips the redundant write when the WalSet min was already advanced at close time, while still correctly emitting it after a crash where the close-time markers were never written.

Reviewed By: xingbowang

Differential Revision: D110713771

fbshipit-source-id: 3592d11b20a9e660f1fea58a7a72565b2030c6d8
2026-07-15 12:12:10 -07:00
Josh Kang dc72a64a5b Flush no-WAL live-file data with LockWAL (#14931)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14931

The failing crash-test scenario combines `disable_wal=1`, `LockWAL()`, and backup/checkpoint live-file-copy verification. A WAL-disabled write can leave a key only in the memtable. The old live-file path skipped `FlushForGetLiveFiles()` whenever `LockWAL()` was active, so the copied SST/WAL file set could omit that memtable-only key even though the original DB still served it. Restore verification could then report the key as present in the original DB but missing from the restored backup/checkpoint.

Fix live-file capture to flush unpersisted WAL-disabled data even while `LockWAL()` is active. If all data is already persisted, the path still skips the flush while WAL is locked. If unpersisted data exists, backup/checkpoint can now block behind `LockWAL()` until another thread calls `UnlockWAL()` and the flush can safely proceed.

To avoid self-deadlock, track `LockWAL()` ownership by thread with recursive counts. `UnlockWAL()` now has to be called by the same thread that successfully called `LockWAL()`, and wait=true flushes called by that same owner return `Aborted("Likely deadlock as the same thread called LockWAL()")` instead of waiting forever. Cross-thread flushes remain allowed and wait for all `LockWAL()` owners to unlock.

Also audited MyRocks and can confirm they already conform to the Lock/Unlock on same thread semantics.

Reviewed By: pdillinger

Differential Revision: D111793089

fbshipit-source-id: 1bbb03b25d00d47440126cc6bb4ce7410302e25b
2026-07-15 12:07:38 -07:00
Peter Dillinger 74aba599ab Fix WriteUnprepared transaction rollback after commit write failure (#14920)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14920

When a WriteUnprepared transaction's commit write fails while two_write_queues
is enabled (e.g. due to an I/O error), CommitInternal() unconditionally cleared
unprep_seqs_ and the flushed/unflushed save points before returning the error.
That left the transaction with no record of its prepared sequence numbers, so a
subsequent Rollback() -- the natural way for a caller to recover from a failed
commit -- constructed a WriteUnpreparedCommitEntryPreReleaseCallback over an
empty unprep_seqs_ and tripped assert(unprep_seqs.size() > 0) in its
constructor. In release builds this would instead corrupt commit-cache
accounting rather than aborting.

Fix: only clear unprep_seqs_ and the save points on the commit success path. On
the two-write-queues commit-write failure path we still remove the prepared
entry that AddPreparedCallback may have added for commit_batch_seq (that seq is
never stored in unprep_seqs_, so a later Rollback cannot clean it up), but
unprep_seqs_ is otherwise preserved so the transaction can still be rolled back
once the caller resumes the DB from the write error.

Originally authored by Devmate trigger

Reviewed By: xingbowang

Differential Revision: D110271443

fbshipit-source-id: 7d991e01ddc74ccd170cbc25f6af160c5f3d5663
2026-07-14 16:12:13 -07:00
xingbowang 329d9e7adf Add blog post on Blob Direct Write and partitioned blob files (#14873)
Summary:
- Add a RocksDB blog post explaining Blob Direct Write and partitioned blob files.
- Cover the write-path transformation to `BlobIndex`, partition selection, blob-file lifecycle, read fallback for in-flight direct-write files, and the current v1 scope.
- Describe policy-driven grouping use cases such as TTL bucketing with Universal Compaction and wide-column metadata.

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

Test Plan: {F1991646296}

Reviewed By: joshkang97

Differential Revision: D109564817

Pulled By: xingbowang

fbshipit-source-id: 712fc0b6a8ffe68e2068af756dceb421c65a9af8
2026-07-14 07:31:34 -07:00
Josh Kang 5b1c3b37ac Disable codex reviews on PRs (#14933)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14933

Codex reviews currently have some issues with the API Key. Will disable them from reviews to prevent noise until it is properly fixed.

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

Differential Revision: D111798073

Pulled By: joshkang97

fbshipit-source-id: 9340b89371185979e34465325c20d02a494990e5
2026-07-13 15:10:40 -07:00
Jay Huh eca1a114e0 Count unreleased MultiScan prefetch blocks as wasted (#14926)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14926

This fixes a gap in the MultiScan prefetch waste accounting introduced by https://github.com/facebook/rocksdb/pull/14876. That change added reverse MultiScan support and introduced `unreleased_prefetch_blocks_` to track prefetched blocks still owned by the `ReadSet`, because reverse scans can prefetch a suffix of the collected block list and release blocks in non-prefix order. However, those unreleased prefetched blocks were only released during `MultiScanIndexIterator` destruction; they were not included in `MULTISCAN_PREFETCH_BLOCKS_WASTED`. As a result, scans that stopped before consuming all prefetched blocks underreported wasted prefetch blocks.

This change counts unreleased prefetched blocks as wasted before destructor cleanup, while avoiding counting the currently positioned block as wasted when it has been reached.

Reviewed By: xingbowang

Differential Revision: D111531409

fbshipit-source-id: 5e605ce686037256bf6847ab7ca363c3f7701401
2026-07-11 08:20:42 -07:00
Hui Xiao 7c80a5ac85 Size-tolerant deserialization for compaction service response's counts array (#14807)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14807

The "counts" array in CompactionServiceResult (one compaction count per CompactionReason) is non-critical information. During cross-process remote compaction the primary and the remote worker can run different RocksDB versions and therefore disagree on kNumOfReasons, so the serialized counts array can have a different length than the reader expects. The plain OptionTypeInfo::Array parser rejects any element-count mismatch, which fails deserialization of the entire result over this non-critical field.

This makes deserialization of "counts" size-tolerant via SizeTolerantDeserializeArray, which overrides only the parse callback: extra trailing elements are dropped and missing trailing elements are zero-filled, so a length mismatch no longer fails deserialization. Serialization and comparison are inherited from Array<T, kSize> unchanged.

The serialized width is intentionally kept at kNumOfReasons - 1 here, so this change does not alter what is written -- only the read path becomes tolerant. Widening the serialized width to the full kNumOfReasons is done as a separate follow-up change, which must not land until this tolerant read has been deployed to every primary (including rollback targets), so that no reader still rejects a wider result.

== Test plan ==
**Legend**

CompactionReason is an enum whose last entry is kNumOfReasons (currently 21). CompactionStats::counts is an int[kNumOfReasons] holding one count per reason; it is serialized into the remote compaction result and read back by the primary.
- "narrow" = kNumOfReasons - 1 counts (omits the newest reason); what current code writes.
- "wide" = kNumOfReasons counts (includes the newest reason).
- strict reader: accepts only its exact expected element count; any other count fails deserialization (the plain OptionTypeInfo::Array behavior).
- tolerant reader: accepts any count -- more elements than expected are dropped, fewer are zero-filled; never fails on a count mismatch (this change, SizeTolerantDeserializeArray).

**Unit test**

CompactionServiceTest.CompatCheckCountsWidthTolerance -- verifies that deserializing "counts" tolerates a serialized element count equal to, one greater than, or one less than the reader's expected width (extras dropped, missing zero-filled), and that the binary serializes exactly the expected number of elements.

**Manual cross-version verification (ldb remote_compaction)**

Test 1: baseline (unchanged behavior) -- expect-narrow strict-read primary reads narrow counts written by a worker
```
$ strict_narrow remote_compaction_worker  --db=$DB --job_dir=$JOB &
$ strict_narrow remote_compaction_primary --db=$DB --job_dir=$JOB
remote_compaction_worker: OK (4913 bytes result)
remote_compaction_primary: OK
$ grep 'remote compaction result' $DB/LOG
[default] [JOB 3] Received remote compaction result, output path: /tmp/cs_run/output, files: 000022.sst
```
=> PASS: the primary parsed and installed the remote result.

Test 2: expect-narrow tolerant-read primary reads the narrow counts a worker writes
```
$ strict_narrow remote_compaction_worker  --db=$DB --job_dir=$JOB &
$ tolerant_narrow remote_compaction_primary --db=$DB --job_dir=$JOB
remote_compaction_worker: OK (4912 bytes result)
remote_compaction_primary: OK
$ grep 'remote compaction result' $DB/LOG
[default] [JOB 3] Received remote compaction result, output path: /tmp/cs_run/output, files: 000022.sst
```
=> PASS: the primary parsed and installed the remote result.

Test 3: expect-narrow tolerant-read primary reads the wide counts a worker writes (extra reason dropped)
```
$ tolerant_wide remote_compaction_worker  --db=$DB --job_dir=$JOB &
$ tolerant_narrow remote_compaction_primary --db=$DB --job_dir=$JOB
remote_compaction_worker: OK (4915 bytes result)
remote_compaction_primary: OK
$ grep 'remote compaction result' $DB/LOG
[default] [JOB 3] Received remote compaction result, output path: /tmp/cs_run/output, files: 000022.sst
```
=> PASS: the primary parsed and installed the remote result.

Test 4: expect-wide, tolerant primary reads the narrow counts a worker writes (missing reason zero-filled)
```
$ strict_narrow remote_compaction_worker  --db=$DB --job_dir=$JOB &
$ tolerant_wide remote_compaction_primary --db=$DB --job_dir=$JOB
remote_compaction_worker: OK (4911 bytes result)
remote_compaction_primary: OK
$ grep 'remote compaction result' $DB/LOG
[default] [JOB 3] Received remote compaction result, output path: /tmp/cs_run/output, files: 000022.sst
```
=> PASS: the primary parsed and installed the remote result.

Test 5: expect-wide, tolerant primary reads a wide counts a worker write
```
$ tolerant_wide remote_compaction_worker  --db=$DB --job_dir=$JOB &
$ tolerant_wide remote_compaction_primary --db=$DB --job_dir=$JOB
remote_compaction_worker: OK (4917 bytes result)
remote_compaction_primary: OK
$ grep 'remote compaction result' $DB/LOG
[default] [JOB 3] Received remote compaction result, output path: /tmp/cs_run/output, files: 000022.sst
```
=> PASS: the primary parsed and installed the remote result.

Reviewed By: jaykorean

Differential Revision: D106321320

fbshipit-source-id: fc1bd66a4d11ac6bf38dff6aa214ebafab75f3d0
2026-07-09 10:37:35 -07:00
Josh Kang 053a9de468 Fix broken github codex review (#14923)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14923

Fix the GitHub Actions Codex review job by running generated review prompts through generic `codex exec` instead of the specialized `codex exec review` subcommand. The workflow already embeds PR metadata, reviewer instructions, and the diff in `/tmp/prompt.txt`, while `codex exec review --base` rejects a positional/stdin prompt. This removes the invalid `--base`/stdin combination from both automatic and manual Codex review paths while leaving `/codex-query` behavior unchanged.

Reviewed By: xingbowang

Differential Revision: D111135454

fbshipit-source-id: 16e86adda41d3e784c59466658cb769278c909c7
2026-07-09 09:55:44 -07:00
Jay Huh f32f747125 Address reverse MultiScan follow-up comments (#14922)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14922

- Preserve source compatibility for `Scan::Reset` by restoring the default `reverse` argument.
- Make `HasMoreScanRanges()` defensive for reverse prepared scans instead of asserting.
- Restore broader forward coverage in `MultiScanAsyncIOTest` while keeping reverse coverage on the representative async IO matrix.
- Let `--multiscan_reverse` drive reverse MultiScan stress coverage independently of `--test_backward_scan`.
- Add the reverse MultiScan public API release note.

Reviewed By: xingbowang

Differential Revision: D111127403

fbshipit-source-id: 1a17d0a36aaf66e8cbc51f445d0f85e145fd653c
2026-07-08 20:56:36 -07:00
generatedunixname1395027625275998 c3f6e4d625 Cache GetKeysEndOffset in BlockIter to speed up Next inner loop (#14917)
Summary: Pull Request resolved: https://github.com/facebook/rocksdb/pull/14917

Reviewed By: kunalspathak

Differential Revision: D110209012

fbshipit-source-id: 8ce3b94ea9617977d956f3856d92d50f2b547ddc
2026-07-08 14:03:21 -07:00
Jay Huh e1ea04d9ef Add reverse order support to MultiScan (#14876)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14876

Add reverse scan support to `MultiScanArgs`/`MultiScan` so callers can iterate bounded scan ranges in descending order. Plumb reverse scan state through `DBIter`, `BlockBasedTableIterator`, and `MultiScanIndexIterator`, including reverse prefetch windowing and async I/O support. Update `db_bench` `multiscanrandom` to honor `--reverse_iterator` and report rows scanned so reverse MultiScan and iterator baselines can be compared on row throughput.

Add `db_stress --multiscan_reverse` coverage so the prepared MultiScan path can be stressed with `SeekForPrev()`/`Prev()` against a control iterator.

Also fix the table child iterator path to accept reverse `SeekForPrev()` targets above a file's prepared ranges. `MergingIterator` and `LevelIterator` can legally seek every child to the current range limit; a child file may only have lower prepared ranges, so treating that as invalid caused intermittent `SeekForPrev target is outside prepared ranges` exceptions in the benchmark.

Reviewed By: xingbowang

Differential Revision: D109357730

fbshipit-source-id: eccece5ca7d8f5d6760d51ebe76801a332a49061
2026-07-08 13:58:00 -07:00
anand1976 27b48646cd Update folly hash for 11.6 release (#14912)
Summary:
- Update `folly.mk` FOLLY_COMMIT_HASH to 2a68075b77d9

This is folly's `main` at release-cut time. Review and merge to take the bump, or close to skip folly for this release.

Part of 11.6 release workflow.

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

Reviewed By: jaykorean

Differential Revision: D110524765

Pulled By: pdillinger

fbshipit-source-id: 3361fdc6277b5f02828f23942b9301237b1ed272
2026-07-07 18:44:23 -07:00
generatedunixname1395027625275998 66bccb467d Store BySmallestKey by value to avoid shared_ptr atomic copy in Rep (#14919)
Summary: Pull Request resolved: https://github.com/facebook/rocksdb/pull/14919

Reviewed By: DenisYaroshevskiy

Differential Revision: D110842287

fbshipit-source-id: df84b4b319aefd9b21abdb7db20989ba1efd7eba
2026-07-07 07:42:34 -07:00
generatedunixname1395027625275998 0f15da1c2d Reserve key_context capacity in MultiGetCommon to avoid autovector reallocs (#14909)
Summary: Pull Request resolved: https://github.com/facebook/rocksdb/pull/14909

Reviewed By: kunalspathak

Differential Revision: D110380492

fbshipit-source-id: d1d3214c52a65326de237adcbfe6ea5d23d94960
2026-07-06 19:48:44 -07:00
anand1976 13d55eba73 Update version to 11.7.0 for 11.6 release (#14913)
Summary:
- Bump version.h from 11.6.0 to 11.7.0
- Add `11.6.fb` to `check_format_compatible.sh`
- Sync HISTORY.md `## 11.6.` section from `11.6.fb`
- Delete 5 consumed `unreleased_history/` note file(s) from main

Part of 11.6 release workflow.

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

Reviewed By: anand1976

Differential Revision: D110520723

Pulled By: pdillinger

fbshipit-source-id: 20f2f9b4be03c08d78d6f2c4e6b70812a408d9f0
2026-07-02 16:16:22 -07:00
Peter Dillinger 1192897aad Fix table cache entry leak in VersionBuilder::UnrefFile on failed LogAndApply (#14914)
Summary:
Pull Request resolved: https://github.com/facebook/rocksdb/pull/14914

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

### Context

`VersionSet::LogAndApply` loads table handlers before the `MANIFEST` update is durable. If that `MANIFEST` update later fails, newly added files can be discarded by `VersionBuilder` without ever being installed in a `Version`. `VersionBuilder::UnrefFile` released the `FileMetaData` `pinned_reader` handle, but releasing that handle only dropped the reference; it did not erase the cache key, so an orphaned table-cache entry could survive for a file that is neither live nor quarantined. When metadata read fault injection prevents the obsolete-file scan from cleaning up the orphan, DB close can trip `TEST_VerifyNoObsoleteFilesCached` with File N is not live nor quarantined.

### Fix
After releasing a pinned_reader for a `FileMetaData` whose refcount reaches zero, explicitly evict that file number from the table cache. The new `VersionBuilderDBTest.FailedLogAndApplyEvictsTableCacheEntry` regression test exercises the realistic production path: it creates a real SST, calls `LogAndApply` through the DB's own `VersionSet`, injects a MANIFEST sync failure via the `AfterSyncManifest` sync point, and asserts that the table cache no longer contains the file after `ProcessManifestWrites` destroys the `VersionBuilder`. With the eviction removed, the test fails deterministically with `leaked_cache_entry=true`.

Reviewed By: hx235

Differential Revision: D99759696

fbshipit-source-id: 0ebb32b1544ad1982d23a6c06569984be97a0a88
2026-07-02 16:08:21 -07:00
252 changed files with 15290 additions and 6266 deletions

No files matched your search

+2 -10
View File
@@ -714,8 +714,6 @@ jobs:
CODEX_HOME: /tmp/codex-home
CODEX_MODEL: ${{ inputs.default_model }}
CODEX_REASONING_EFFORT: ${{ steps.budget_codex.outputs.effort }}
BASE_SHA: ${{ steps.pr_info.outputs.base_sha }}
PR_TITLE: ${{ steps.pr_info.outputs.pr_title }}
run: |
if [ -z "${OPENAI_API_KEY}" ]; then
echo "OPENAI_API_KEY not configured, skipping Codex review"
@@ -723,9 +721,7 @@ jobs:
fi
mkdir -p "${CODEX_HOME}"
set +e
codex exec review \
--base "${BASE_SHA}" \
--title "${PR_TITLE}" \
codex exec \
-m "${CODEX_MODEL}" \
-c model_reasoning_effort="${CODEX_REASONING_EFFORT}" \
--dangerously-bypass-approvals-and-sandbox \
@@ -1340,8 +1336,6 @@ jobs:
CODEX_HOME: /tmp/codex-home
CODEX_MODEL: ${{ inputs.selected_model != '' && inputs.selected_model || inputs.default_model }}
CODEX_REASONING_EFFORT: ${{ steps.manual_budget_codex.outputs.effort }}
BASE_SHA: ${{ steps.pr_info.outputs.base_sha }}
PR_TITLE: ${{ steps.pr_info.outputs.pr_title }}
COMMAND_TYPE: ${{ steps.request.outputs.type }}
run: |
mkdir -p "${CODEX_HOME}"
@@ -1354,9 +1348,7 @@ jobs:
--output-last-message codex-review-output.md \
- < /tmp/prompt.txt > codex-review-execution.log 2>&1
else
codex exec review \
--base "${BASE_SHA}" \
--title "${PR_TITLE}" \
codex exec \
-m "${CODEX_MODEL}" \
-c model_reasoning_effort="${CODEX_REASONING_EFFORT}" \
--dangerously-bypass-approvals-and-sandbox \
+32 -14
View File
@@ -40,6 +40,7 @@ jobs:
name: ${{ inputs.result_artifact_name }}
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
path: _artifact
continue-on-error: true
- name: Post or update PR comment
@@ -52,8 +53,18 @@ jobs:
with:
script: |
const fs = require('fs');
if (!fs.existsSync(process.env.COMMENT_FILE) ||
!fs.existsSync('pr_number.txt')) {
const path = require('path');
const artifactDir = '_artifact';
const commentFile = path.join(
artifactDir,
process.env.COMMENT_FILE
);
const prNumberFile = path.join(artifactDir, 'pr_number.txt');
const triggerTypeFile = path.join(artifactDir, 'trigger_type.txt');
const headShaFile = path.join(artifactDir, 'head_sha.txt');
if (!fs.existsSync(commentFile) ||
!fs.existsSync(prNumberFile)) {
core.info('No review results found; skipping.');
return;
}
@@ -61,16 +72,16 @@ jobs:
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 body = fs.readFileSync(commentFile, 'utf8');
const prNumber = parseInt(
fs.readFileSync('pr_number.txt', 'utf8').trim(),
fs.readFileSync(prNumberFile, 'utf8').trim(),
10
);
const trigger = fs.existsSync('trigger_type.txt') ?
fs.readFileSync('trigger_type.txt', 'utf8').trim() :
const trigger = fs.existsSync(triggerTypeFile) ?
fs.readFileSync(triggerTypeFile, 'utf8').trim() :
'auto';
const headSha = fs.existsSync('head_sha.txt') ?
fs.readFileSync('head_sha.txt', 'utf8').trim() :
const headSha = fs.existsSync(headShaFile) ?
fs.readFileSync(headShaFile, 'utf8').trim() :
'';
const shortSha = headSha ? headSha.substring(0, 7) : '';
const runId = process.env.RUN_ID;
@@ -119,12 +130,16 @@ jobs:
with:
script: |
const fs = require('fs');
if (!fs.existsSync('trigger_type.txt')) return;
const trigger = fs.readFileSync('trigger_type.txt', 'utf8').trim();
const path = require('path');
const artifactDir = '_artifact';
const triggerTypeFile = path.join(artifactDir, 'trigger_type.txt');
const commentIdFile = path.join(artifactDir, 'comment_id.txt');
if (!fs.existsSync(triggerTypeFile)) return;
const trigger = fs.readFileSync(triggerTypeFile, 'utf8').trim();
if (trigger !== 'manual') return;
if (!fs.existsSync('comment_id.txt')) return;
if (!fs.existsSync(commentIdFile)) return;
const commentId = parseInt(
fs.readFileSync('comment_id.txt', 'utf8').trim(),
fs.readFileSync(commentIdFile, 'utf8').trim(),
10
);
if (!commentId) return;
@@ -147,6 +162,7 @@ jobs:
name: ${{ inputs.result_artifact_name }}
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
path: _artifact
continue-on-error: true
- name: React with failure
@@ -155,9 +171,11 @@ jobs:
with:
script: |
const fs = require('fs');
if (!fs.existsSync('comment_id.txt')) return;
const path = require('path');
const commentIdFile = path.join('_artifact', 'comment_id.txt');
if (!fs.existsSync(commentIdFile)) return;
const commentId = parseInt(
fs.readFileSync('comment_id.txt', 'utf8').trim(),
fs.readFileSync(commentIdFile, 'utf8').trim(),
10
);
if (!commentId) return;
+11 -3
View File
@@ -25,6 +25,7 @@ jobs:
name: clang-tidy-result
run-id: ${{ github.event.workflow_run.id }}
github-token: ${{ secrets.GITHUB_TOKEN }}
path: _artifact
continue-on-error: true
- name: Post or update PR comment
@@ -33,12 +34,19 @@ jobs:
with:
script: |
const fs = require('fs');
if (!fs.existsSync('clang-tidy-comment.md') || !fs.existsSync('pr_number.txt')) {
if (!fs.existsSync('_artifact/clang-tidy-comment.md') ||
!fs.existsSync('_artifact/pr_number.txt')) {
core.info('No clang-tidy results found; skipping.');
return;
}
const body = fs.readFileSync('clang-tidy-comment.md', 'utf8');
const prNumber = parseInt(fs.readFileSync('pr_number.txt', 'utf8').trim());
const body = fs.readFileSync(
'_artifact/clang-tidy-comment.md',
'utf8'
);
const prNumber = parseInt(
fs.readFileSync('_artifact/pr_number.txt', 'utf8').trim(),
10
);
const post = require('./.github/scripts/post-pr-comment.js');
await post({
@@ -22,4 +22,3 @@ jobs:
provider: claude
result_artifact_name: claude-review-result
comment_file: claude-review-comment.md
secrets: inherit
@@ -22,4 +22,3 @@ jobs:
provider: codex
result_artifact_name: codex-review-result
comment_file: codex-review-comment.md
secrets: inherit
+3 -8
View File
@@ -7,14 +7,9 @@
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]
# Automatic PR reviews are temporarily disabled while Codex authentication is
# repaired. Manual review requests remain available through issue comments
# and workflow_dispatch.
issue_comment:
types: [created]
+18
View File
@@ -268,6 +268,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
"util/compression.cc",
"util/compression_context_cache.cc",
"util/concurrent_task_limiter_impl.cc",
"util/coro_stats_util.cc",
"util/crc32c.cc",
"util/crc32c_arm64.cc",
"util/data_structure.cc",
@@ -306,6 +307,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
"utilities/compaction_filters.cc",
"utilities/compaction_filters/remove_emptyvalue_compactionfilter.cc",
"utilities/convenience/info_log_finder.cc",
"utilities/copy_engine/copy_engine.cc",
"utilities/counted_fs.cc",
"utilities/debug.cc",
"utilities/env_mirror.cc",
@@ -379,11 +381,21 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
"utilities/write_batch_with_index/write_batch_with_index_internal.cc",
], deps=[
"//folly/container:f14_hash",
"//folly/coro:baton",
"//folly/coro:blocking_wait",
"//folly/coro:collect",
"//folly/coro:coroutine",
"//folly/coro:nothrow",
"//folly/coro:task",
"//folly/executors/thread_factory:named_thread_factory",
"//folly/executors:io_executor",
"//folly/executors:io_thread_pool_executor",
"//folly/io/async:async_base",
"//folly/io/async:event_base_manager",
"//folly/io/async:io_uring_backend",
"//folly/io/async:request_context",
"//folly/synchronization:distributed_mutex",
"//folly:executor",
], headers=glob(["**/*.h"]), link_whole=False, extra_test_libs=False)
cpp_library_wrapper(name="rocksdb_whole_archive_lib", srcs=[], deps=[":rocksdb_lib"], headers=[], link_whole=True, extra_test_libs=False)
@@ -4743,6 +4755,12 @@ cpp_unittest_wrapper(name="configurable_test",
extra_compiler_flags=[])
cpp_unittest_wrapper(name="copy_engine_test",
srcs=["utilities/copy_engine/copy_engine_test.cc"],
deps=[":rocksdb_test_lib"],
extra_compiler_flags=[])
cpp_unittest_wrapper(name="corruption_test",
srcs=["db/corruption_test.cc"],
deps=[":rocksdb_test_lib"],
+18 -1
View File
@@ -8,7 +8,7 @@ This document provides guidance for generating and reviewing code in the RocksDB
### Code Quality and Maintainability
**Clarity and Readability:** Write clear, self-documenting code. Use meaningful variable names, add comments for complex logic, and structure code to minimize cognitive load. Avoid clever tricks that sacrifice readability for marginal performance gains unless absolutely necessary.
**Clarity and Readability:** Write clear, self-documenting code. Use meaningful variable names, add comments for complex logic, and structure code to minimize cognitive load. Avoid clever tricks that sacrifice readability for marginal performance gains unless absolutely necessary. Avoid static_cast, reinterpret_cast, and C-style casts; static_cast_with_check, up_cast, and lossless_cast from cast_util.h are preferred.
**Consistent Style:** Follow existing code style conventions. RocksDB uses `.clang-format` for formatting, specific naming conventions, and structural patterns. Deviations from these patterns are frequently flagged in reviews.
@@ -243,6 +243,11 @@ from an implementation detail instead of an explicit option.
* Don't manually edit BUCK file, after updating src.mk, run
/usr/local/bin/python3 buckifier/buckify_rocksdb.py to update it
* For -j in make command, use the number of CPU cores to decide it.
* When searching for references to something (a symbol, library, etc.), do not
restrict or truncate your search based on presumed relevance or scope. It is
important and time-saving to keep the repo reasonably consistent across
different build systems, programming languages, and even between
documentation and implementation.
### Avoiding mixed build modes with Make (use `AUTO_CLEAN=1`)
@@ -293,6 +298,7 @@ build (and where noted, run tests) across:
| Axis | Must support |
|------|--------------|
| OS | Linux (x86_64 + ARM), macOS, Windows |
| Architecture | Little-endian and big-endian targets, including Linux on s390x |
| Compiler | GCC, Clang (libstdc++ **and** libc++), AppleClang, **MSVC (VS2022)**, MinGW (Linux cross-compile, build-only, no gflags) |
| Build system | Make, CMake, and BUCK (internal) -- keep all in sync (see "Build system" above) |
| Config | release (`-fno-rtti`), `ASSERT_STATUS_CHECKED`, ASAN/UBSAN/TSAN, folly, unity build, JNI/Java |
@@ -307,6 +313,12 @@ abstractions; otherwise guard with `#ifdef OS_WIN` (POSIX `<unistd.h>` ->
Windows `<process.h>`). Because libc++ is also tested, include what you use
rather than relying on libstdc++ transitive includes.
Do not use `static_assert` to reject a supported architecture for an optional
feature. Check an existing `port::` capability such as `port::kLittleEndian`
and disable only the unsupported feature so the rest of RocksDB still builds.
Use a portable representation when the feature itself must work across
architectures.
### Unit Test
* After all of the unit tests are added, review them and try to extract common
reusable utility functions to reduce code duplication due to copy past between
@@ -334,6 +346,9 @@ rather than relying on libstdc++ transitive includes.
* For CI-style flaky tests that do not reproduce with `gtest_parallel.py`,
`--gtest_repeat`, or normal coerce-mode runs, inspect
`tools/gtest_parallel_repro.py --help`.
* Each unit test file has overheads, so avoid creating new unit test files
for random minor features. Consider adding to slice_test, db_etc3_test, or
others.
### Unit test dedup guidelines
* Extract helper functions for repeated patterns such as object
@@ -381,6 +396,8 @@ rather than relying on libstdc++ transitive includes.
### Adding release note
* Release note should be kept short at high level for external user consumption.
* If more than single markdown line, consider how their formatting will be
integrated into HISTORY.md.
### Blog posts (docs/_posts)
* Blog post authors must be defined in `docs/_data/authors.yml` to be displayed
+29 -3
View File
@@ -698,10 +698,19 @@ if(USE_FOLLY)
include(${FOLLY_INST_PATH}/lib/cmake/folly/folly-config.cmake)
# Fix gflags library name for debug builds
# Fix gflags library name for debug builds. The getdeps gflags shared
# library is versioned (e.g. libgflags_debug.so.2.3), so resolve the
# actual file by glob rather than hard-coding a version.
if(CMAKE_BUILD_TYPE STREQUAL "Debug")
file(GLOB GFLAGS_DEBUG_SHARED_LIBS
"${GFLAGS_INST_PATH}/lib/libgflags_debug.so.*")
if(NOT GFLAGS_DEBUG_SHARED_LIBS)
message(FATAL_ERROR
"Could not find getdeps libgflags_debug.so under ${GFLAGS_INST_PATH}/lib")
endif()
list(GET GFLAGS_DEBUG_SHARED_LIBS 0 GFLAGS_DEBUG_SHARED_LIB)
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,-rpath=${GFLAGS_INST_PATH}/lib")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${GFLAGS_INST_PATH}/lib/libgflags_debug.so.2.2")
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} ${GFLAGS_DEBUG_SHARED_LIB}")
endif()
endif()
@@ -993,6 +1002,7 @@ set(SOURCES
util/simple_mixed_compressor.cc
util/compression_context_cache.cc
util/concurrent_task_limiter_impl.cc
util/coro_stats_util.cc
util/crc32c.cc
util/data_structure.cc
util/dynamic_bloom.cc
@@ -1014,6 +1024,7 @@ set(SOURCES
util/xxhash.cc
utilities/agg_merge/agg_merge.cc
utilities/backup/backup_engine.cc
utilities/copy_engine/copy_engine.cc
utilities/blob_db/blob_compaction_filter.cc
utilities/blob_db/blob_db.cc
utilities/blob_db/blob_db_impl.cc
@@ -1631,6 +1642,7 @@ if(WITH_TESTS)
utilities/cassandra/cassandra_row_merge_test.cc
utilities/cassandra/cassandra_serialize_test.cc
utilities/checkpoint/checkpoint_test.cc
utilities/copy_engine/copy_engine_test.cc
utilities/sorted_run_builder/sorted_run_builder_test.cc
utilities/env_timed_test.cc
utilities/fault_injection_fs_test.cc
@@ -1698,7 +1710,21 @@ if(WITH_TESTS)
)
target_link_libraries(${exename}${ARTIFACT_SUFFIX} testutillib${ARTIFACT_SUFFIX} testharness gtest ${THIRDPARTY_LIBS} ${ROCKSDB_LIB})
if(NOT "${exename}" MATCHES "db_sanity_test")
gtest_discover_tests(${exename} DISCOVERY_TIMEOUT 120)
if(CMAKE_VERSION VERSION_GREATER_EQUAL 3.18)
# Discover tests at ctest time instead of during the parallel build.
# POST_BUILD discovery (the default) runs each freshly-built test
# binary while the -j build is still compiling; on resource-constrained
# CI (notably the arm64 macOS runners) that discovery run can yield
# empty output, and CMake's JSON parser then aborts the whole build
# with "Syntax error: value, object or array expected". PRE_TEST moves
# discovery out of the build phase so a slow/empty enumeration no
# longer fails the build.
gtest_discover_tests(${exename}
DISCOVERY_TIMEOUT 120
DISCOVERY_MODE PRE_TEST)
else()
gtest_discover_tests(${exename} DISCOVERY_TIMEOUT 120)
endif()
add_dependencies(rocksdb_check ${exename}${ARTIFACT_SUFFIX})
endif()
endforeach(sourcefile ${TESTS})
+42
View File
@@ -1,6 +1,48 @@
# Rocksdb Change Log
> NOTE: Entries for next release do not go here. Follow instructions in `unreleased_history/README.txt`
## 11.8.1 (07/28/2026)
### Public API Changes
* Add `DBOptions::read_io_executor_threads` to increase the maximum thread count of the shared filesystem read I/O executor used for asynchronous reads when opening a DB. Opening a DB never reduces the shared executor thread count. Change the experimental `FSRandomAccessFile::SubmitReadAsync()` API to return whether the asynchronous path was used, and add the `FILE_SUBMIT_ASYNC_READ_FALLBACK` ticker to count calls that fall back to synchronous reads.
### Performance Improvements
* When `DBOptions::avoid_unnecessary_blocking_io` is true, obsolete `OPTIONS-*` files found during DB open are deleted by background purge instead of synchronously on the opening thread.
## 11.8.0 (07/28/2026)
### Public API Changes
* Add callback-based asynchronous read APIs, `DB::GetAsync()` and `DB::MultiGetAsync()`. The idea is that when IO is required, RocksDB can suspend its internal coroutine read path, allowing the read executor thread to do other work. When the IO is complete, RocksDB invokes the user callback. This requires filesystem support for full performance benefits. A new filesystem API `FSRandomAccessFile::SubmitReadAsync()` is introduced for this. Unlike `ReadAsync`, the filesystem is responsible for eventually calling the callback. `FileSystem::GetReadExecutor()` returns the IO executor whose EventBases run coroutine read processing. In the Posix filesystem, an IO-uring based event loop is used to complete the IO.
* Added `FlushOptions::listener_wait` (default `false`). When set together with `FlushOptions::wait == true`, `DB::Flush()` will not return until the registered `EventListener::OnFlushCompleted` callbacks for the flushed memtables have finished running. By default (`false`), `Flush(wait=true)` may return as soon as the flush result is committed, which can be before (or while) the `OnFlushCompleted` callbacks execute on the background flush thread. Also added the corresponding C API `rocksdb_flushoptions_set_listener_wait()` / `rocksdb_flushoptions_get_listener_wait()`.
* Deprecated `PinnableWideColumns::serialized_size()` in favor of the new `PinnableWideColumns::payload_size()`, which returns the total size of the columns' names and values (for a plain value this equals the value size). Relatedly, wide-column read accounting changed subtly: read statistics for entities (`BYTES_READ`, `BYTES_PER_READ`, and the per-key byte counts for `GetEntity`/`MultiGetEntity`) and the `ReadOptions::value_size_soft_limit` threshold now measure entities by this payload size instead of the serialized entity size. This drops serialization framing bytes from those measurements (so reported/limited sizes for entity reads are slightly smaller than before) and makes plain-value and wide-column accounting consistent.
### Behavior Changes
* `ReadOptions::value_size_soft_limit` now also bounds the blob values resolved from pre-flush blob direct write references during `MultiGet`/`MultiGetEntity` (previously these were resolved after the limit check and read without bound). Enforcement is "always make progress": at least one key is read even if its value alone exceeds the limit, and only once the returned size exceeds the limit do subsequent keys get `Status::Aborted` -- so a caller retrying the aborted keys cannot loop forever on a single value that by itself exceeds the limit.
### Performance Improvements
* Reduced copying when reading wide-column entities that have blob-referenced columns, whether stored in separate blob files or embedded (same-file) as blob records in the same SST. On the point-lookup (`GetEntity`/`MultiGetEntity`) and blob-backed memtable/direct-write read paths, resolving these columns no longer re-serializes the whole entity into a fresh buffer: inline columns are referenced in place in the pinned entity and each resolved blob value is referenced in place from its fetched buffer. This is enabled by an internal change to `PinnableWideColumns` that lets its columns reference multiple backing buffers.
## 11.7.0 (07/16/2026)
### Public API Changes
* Added `RangeLockManagerHandle::SetIsKilledCallback()`, a `std::function<bool()>` predicate polled during range-lock waits. When it returns true, a blocked `GetRangeLock()` wait returns promptly with `Status::Aborted()` instead of waiting for the lock timeout. Range-lock users that install no callback keep the original wait-until-grant-or-timeout behavior.
* Added `MultiScanArgs::reverse` to support reverse MultiScan reads over bounded scan ranges.
### Behavior Changes
* When using `LockWAL()`, live-file capture APIs such as checkpoint and backup now flush WAL-disabled unpersisted data; they can block until `UnlockWAL()` or return `Aborted` when called from the same thread that holds `LockWAL()` to avoid deadlock. `UnlockWAL()` must now also be called from the same thread that called `LockWAL()`.
### Performance Improvements
* Fixed slow DB open with `best_efforts_recovery`, and slow catch-up of secondary and follower (read-only) instances, in which time and memory grew quadratically with the number of files in the DB (so the slowdown was worst for large DBs with long history).
## 11.6.0 (07/02/2026)
### New Features
* Added EXPERIMENTAL embedded blob SST support through `SstFileWriter::OpenWithEmbeddedBlobs()`, storing eligible large values as same-file blob records in block-based SST files and resolving them transparently for reads. This niche feature currently supports uncompressed embedded blobs only; compression options are placeholders and compression support is deferred to follow-up work.
### Public API Changes
* Expanded the C API (`include/rocksdb/c.h`) with a large set of new `rocksdb_*` functions, mostly option getters/setters plus table-properties, job/event-listener, and metadata accessors, a WAL filter, a ReadOptions table filter, and a backup exclude-files callback. Many are now produced by a new semi-automated generator (`tools/c_api_gen/`) from the C++ headers; `include/rocksdb/c.h` remains a single self-contained header and the signatures of pre-existing functions are unchanged.
### Bug Fixes
* Reverted PR14831 that made range_lock_manager aware of reverse-order CF
* Fixed a bug in `RandomAccessFileReader::ReadAsync` where an already-aligned direct-IO read request with a null `scratch` and a caller-provided `aligned_buf` would take the "already aligned" fast path and submit the null buffer to the underlying async read (e.g. a null iovec base to io_uring, failing with EFAULT). This could surface as spurious iterator failures during async prefetch (MultiScan with async IO) on direct-IO databases. The async path now allocates a backing buffer in this case, matching the synchronous `Read` path.
* 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.5.0 (06/16/2026)
### New Features
* External table readers that open files through `ExternalTableOptions::fs` now update RocksDB SST/file-read statistics and file IO listener callbacks, making external file IO activity visible in existing read metrics.
+28 -12
View File
@@ -1174,30 +1174,43 @@ check-c-api-gen:
echo "Skipping C API backward-compatibility check ($(API_COMPAT_REF) not found; set API_COMPAT_REF)"; \
fi
# Staleness: regenerate and confirm the checked-in output is current. Needs a
# clang++ (the generator parses C++ ASTs); detect one the way the generator
# does (a clang in $(CXX) -- which may be ccache-prefixed/versioned -- else a
# bare/versioned clang++ on PATH) rather than testing $(CXX) verbatim.
# clang++ (the generator parses C++ ASTs). Use CLANG_CXX, which
# build_tools/build_detect_platform resolves once (the fbcode clang++ in
# fbcode builds, else a versioned clang++ on PATH) and passes explicitly, so
# the check and the generator agree on the same binary without heuristics.
@strict=""; case "$(CHECK_C_API_GEN_STRICT)" in ""|0|no|NO|false|FALSE) ;; *) strict=1 ;; esac; \
cf_arg=""; \
if [ -n "$(CLANG_FORMAT_BINARY)" ]; then cf_arg="--clang-format $(CLANG_FORMAT_BINARY)"; fi; \
have_clang=""; \
for c in $$(printf '%s\n' $(CXX) | grep -i clang) clang++ clang++-21 clang++-20 clang++-19 clang++-18 clang++-17 clang++-16 clang++-15 clang++-14 clang++-13; do \
if command -v "$$c" >/dev/null 2>&1; then have_clang=1; break; fi; \
done; \
if [ -n "$$have_clang" ]; then \
$(PYTHON) tools/c_api_gen/verify_generated_up_to_date.py $$cf_arg; \
if [ -n "$(CLANG_CXX)" ] && command -v "$(CLANG_CXX)" >/dev/null 2>&1; then \
$(PYTHON) tools/c_api_gen/verify_generated_up_to_date.py --clang "$(CLANG_CXX)" $$cf_arg; \
elif [ -n "$$strict" ]; then \
echo "ERROR: no clang++ found and CHECK_C_API_GEN_STRICT is set; cannot run the C API staleness check" >&2; exit 1; \
echo "ERROR: no clang++ found (CLANG_CXX unset) and CHECK_C_API_GEN_STRICT is set; cannot run the C API staleness check" >&2; exit 1; \
else \
echo "Skipping C API codegen staleness check (no clang++ found; install clang++ or set CXX to a clang to enable)"; \
echo "Skipping C API codegen staleness check (no clang++ found; CLANG_CXX unset, install clang++ to enable)"; \
fi
# Regenerate the checked-in C API generated files (fragments under c_api_gen/,
# the inlined include/rocksdb/c.h and db/c.cc, and the round-trip tests). Uses
# CLANG_CXX (resolved by build_tools/build_detect_platform) so it works in a gcc
# build too. Aborts up front if no usable clang++ is available, so a bad
# toolchain never leaves the tree in a partially regenerated state. Pin the
# formatter to match CI with CLANG_FORMAT_BINARY, e.g.:
# make regen-c-api CLANG_FORMAT_BINARY=clang-format-21
.PHONY: regen-c-api
regen-c-api:
@if [ -z "$(CLANG_CXX)" ] || ! command -v "$(CLANG_CXX)" >/dev/null 2>&1; then \
echo "ERROR: no clang++ found (CLANG_CXX unset); build on a host with clang++ or the fbcode toolchain to regenerate the C API" >&2; exit 1; \
fi
@cf_arg=""; \
if [ -n "$(CLANG_FORMAT_BINARY)" ]; then cf_arg="--clang-format $(CLANG_FORMAT_BINARY)"; fi; \
$(PYTHON) tools/c_api_gen/regen_all.py --clang "$(CLANG_CXX)" $$cf_arg
# Quick local validation for C API generation plus the focused C API test.
# This verifies the checked-in generated fragments as well as the inlined
# include/rocksdb/c.h and db/c.cc outputs, then runs c_test in an isolated
# TEST_TMPDIR to avoid stale-state failures.
check-c-api-c_test:
$(PYTHON) tools/c_api_gen/verify_generated_up_to_date.py
$(PYTHON) tools/c_api_gen/verify_generated_up_to_date.py --clang "$(CLANG_CXX)"
$(MAKE) c_test
@tmpdir=$$(mktemp -d); \
trap 'rm -rf "$$tmpdir"' EXIT; \
@@ -1816,6 +1829,9 @@ backup_engine_test: $(OBJ_DIR)/utilities/backup/backup_engine_test.o $(TEST_LIBR
checkpoint_test: $(OBJ_DIR)/utilities/checkpoint/checkpoint_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
copy_engine_test: $(OBJ_DIR)/utilities/copy_engine/copy_engine_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
sorted_run_builder_test: $(OBJ_DIR)/utilities/sorted_run_builder/sorted_run_builder_test.o $(TEST_LIBRARY) $(LIBRARY)
$(AM_LINK)
+10
View File
@@ -145,11 +145,21 @@ def generate_buck(repo_path, deps_map):
# always add range_tree, it's only excluded on ppc64, which we don't use internally
src_mk["RANGE_TREE_SOURCES"] + src_mk["TOOL_LIB_SOURCES"],
deps=[
"//folly:executor",
"//folly/container:f14_hash",
"//folly/coro:baton",
"//folly/coro:blocking_wait",
"//folly/coro:collect",
"//folly/coro:coroutine",
"//folly/coro:nothrow",
"//folly/coro:task",
"//folly/executors:io_executor",
"//folly/executors:io_thread_pool_executor",
"//folly/executors/thread_factory:named_thread_factory",
"//folly/io/async:async_base",
"//folly/io/async:event_base_manager",
"//folly/io/async:io_uring_backend",
"//folly/io/async:request_context",
"//folly/synchronization:distributed_mutex",
],
headers=LiteralValue("glob([\"**/*.h\"])")
+43 -5
View File
@@ -89,13 +89,36 @@ if test -z "$CC"; then
fi
fi
# CLANG_CXX is the single clang++ detection point for the whole script: it is
# used for CXX when building with clang, for CLANG_ANALYZER (scan-build), and by
# tooling that needs clang even in a gcc build (e.g. the C API codegen, which
# parses C++ ASTs). The fbcode config above already sets CLANG_CXX; otherwise
# resolve it here.
if test -z "$CLANG_CXX"; then
if [ -n "$CXX" ] && echo | $CXX -dM -E -x c++ - 2>/dev/null | grep -q '\b__clang__\b'; then
# Honor an already-configured clang compiler (e.g. CI sets
# CXX=clang++-21) so codegen parses headers with the same clang the
# build uses, not an unrelated clang++ that merely happens to come first
# on PATH (an image may ship both a bare clang++ and clang++-NN).
CLANG_CXX="$CXX"
else
# Otherwise take the first clang++ on PATH. Candidates cover a bare
# clang++, versioned names, and clang++.par (a Meta-internal wrapper
# often present on dev machines).
CLANG_CXX=$(command -v clang++ || command -v clang++-21 || command -v clang++-20 \
|| command -v clang++-19 || command -v clang++-18 || command -v clang++-17 \
|| command -v clang++-16 || command -v clang++-15 || command -v clang++-14 \
|| command -v clang++-13 || command -v clang++.par || true)
fi
fi
if test -z "$CXX"; then
if [ "$USE_CLANG" -a -x "$(command -v clang++)" ]; then
CXX=clang++
if [ "$USE_CLANG" ] && [ -n "$CLANG_CXX" ]; then
CXX="$CLANG_CXX"
elif [ -x "$(command -v g++)" ]; then
CXX=g++
elif [ -x "$(command -v clang++)" ]; then
CXX=clang++
elif [ -n "$CLANG_CXX" ]; then
CXX="$CLANG_CXX"
else
CXX=g++
fi
@@ -113,6 +136,17 @@ if test -z "$AR"; then
fi
fi
# If the selected C++ compiler is actually clang (e.g. clang was the only
# compiler available), reflect that as USE_CLANG so clang-specific logic in the
# Makefile applies consistently. Detect it via the compiler's own __clang__
# predefined macro rather than matching the binary path, which can false-positive
# (e.g. a home directory containing "clang").
if test -z "$USE_CLANG"; then
if echo | $CXX -dM -E -x c++ - 2>/dev/null | grep -q '\b__clang__\b'; then
USE_CLANG=1
fi
fi
# Detect OS
if test -z "$TARGET_OS"; then
TARGET_OS=`uname -s`
@@ -127,7 +161,7 @@ if test -z "$CLANG_SCAN_BUILD"; then
fi
if test -z "$CLANG_ANALYZER"; then
CLANG_ANALYZER=$(command -v clang++ 2> /dev/null)
CLANG_ANALYZER="$CLANG_CXX"
fi
if test -z "$FIND"; then
@@ -818,6 +852,10 @@ echo "ROCKSDB_MINOR=$ROCKSDB_MINOR" >> "$OUTPUT"
echo "ROCKSDB_PATCH=$ROCKSDB_PATCH" >> "$OUTPUT"
echo "CLANG_SCAN_BUILD=$CLANG_SCAN_BUILD" >> "$OUTPUT"
echo "CLANG_ANALYZER=$CLANG_ANALYZER" >> "$OUTPUT"
echo "CLANG_CXX=$CLANG_CXX" >> "$OUTPUT"
if test -n "$USE_CLANG"; then
echo "USE_CLANG=$USE_CLANG" >> "$OUTPUT"
fi
echo "PROFILING_FLAGS=$PROFILING_FLAGS" >> "$OUTPUT"
echo "FIND=$FIND" >> "$OUTPUT"
echo "WATCH=$WATCH" >> "$OUTPUT"
+5 -1
View File
@@ -104,6 +104,10 @@ CLANG_BIN="$CLANG_BASE/bin"
CLANG_LIB="$CLANG_BASE/lib"
CLANG_SRC="$CLANG_BASE/../../src"
# A clang++ is always available in the fbcode toolchain regardless of whether
# the build itself uses gcc or clang. CLANG_CXX exposes it for tooling (e.g. the
# C API codegen, which parses C++ ASTs) that needs clang even in a gcc build.
CLANG_CXX="$CLANG_BIN/clang++"
CLANG_ANALYZER="$CLANG_BIN/clang++"
CLANG_SCAN_BUILD="$CLANG_BIN/scan-build"
@@ -167,4 +171,4 @@ EXEC_LDFLAGS_SHARED="$SNAPPY_LIBS $ZLIB_LIBS $BZIP_LIBS $LZ4_LIBS $ZSTD_LIBS $GF
VALGRIND_VER="$VALGRIND_BASE/bin/"
export CC CXX AR AS CFLAGS CXXFLAGS EXEC_LDFLAGS EXEC_LDFLAGS_SHARED VALGRIND_VER JEMALLOC_LIB JEMALLOC_INCLUDE CLANG_ANALYZER CLANG_SCAN_BUILD
export CC CXX AR AS CFLAGS CXXFLAGS EXEC_LDFLAGS EXEC_LDFLAGS_SHARED VALGRIND_VER JEMALLOC_LIB JEMALLOC_INCLUDE CLANG_CXX CLANG_ANALYZER CLANG_SCAN_BUILD
@@ -1,3 +1,7 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
// @generated
// -----------------------------------------------------------------------------
// Auto-generated by tools/c_api_gen/generate_c_api.py.
@@ -1,3 +1,7 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
// @generated
// -----------------------------------------------------------------------------
// Auto-generated by tools/c_api_gen/generate_c_api.py.
@@ -1,3 +1,7 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
// @generated
// -----------------------------------------------------------------------------
// Auto-generated by tools/c_api_gen/generate_c_api.py.
@@ -1,3 +1,7 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
// @generated
// -----------------------------------------------------------------------------
// Auto-generated by tools/c_api_gen/generate_c_api.py.
@@ -1,3 +1,7 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
// @generated
// -----------------------------------------------------------------------------
// Auto-generated by tools/c_api_gen/generate_c_api.py.
@@ -1,3 +1,7 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
// @generated
// -----------------------------------------------------------------------------
// Auto-generated by tools/c_api_gen/generate_c_api.py.
@@ -1,3 +1,7 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
// @generated
// -----------------------------------------------------------------------------
// Auto-generated by tools/c_api_gen/auto_simple_bindings.py.
+4
View File
@@ -1,3 +1,7 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
// @generated
// -----------------------------------------------------------------------------
// Auto-generated by tools/c_api_gen/auto_simple_bindings.py.
@@ -1,3 +1,7 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
// @generated
// -----------------------------------------------------------------------------
// Auto-generated by tools/c_api_gen/generate_c_api.py.
@@ -1,3 +1,7 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
// @generated
// -----------------------------------------------------------------------------
// Auto-generated by tools/c_api_gen/generate_c_api.py.
@@ -1,3 +1,7 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
// @generated
// -----------------------------------------------------------------------------
// Auto-generated by tools/c_api_gen/auto_simple_bindings.py.
@@ -478,6 +482,17 @@ uint64_t rocksdb_compaction_file_info_oldest_blob_file_number(
/* BlobFileAdditionInfo */
const char* rocksdb_blob_file_addition_info_blob_file_path(
const rocksdb_blob_file_addition_info_t* info, size_t* size) {
*size = info->rep.blob_file_path.size();
return info->rep.blob_file_path.data();
}
uint64_t rocksdb_blob_file_addition_info_blob_file_number(
const rocksdb_blob_file_addition_info_t* info) {
return info->rep.blob_file_number;
}
uint64_t rocksdb_blob_file_addition_info_total_blob_count(
const rocksdb_blob_file_addition_info_t* info) {
return info->rep.total_blob_count;
@@ -490,6 +505,17 @@ uint64_t rocksdb_blob_file_addition_info_total_blob_bytes(
/* BlobFileGarbageInfo */
const char* rocksdb_blob_file_garbage_info_blob_file_path(
const rocksdb_blob_file_garbage_info_t* info, size_t* size) {
*size = info->rep.blob_file_path.size();
return info->rep.blob_file_path.data();
}
uint64_t rocksdb_blob_file_garbage_info_blob_file_number(
const rocksdb_blob_file_garbage_info_t* info) {
return info->rep.blob_file_number;
}
uint64_t rocksdb_blob_file_garbage_info_garbage_blob_count(
const rocksdb_blob_file_garbage_info_t* info) {
return info->rep.garbage_blob_count;
@@ -1,3 +1,7 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
// @generated
// -----------------------------------------------------------------------------
// Auto-generated by tools/c_api_gen/auto_simple_bindings.py.
@@ -342,6 +346,14 @@ rocksdb_compaction_file_info_oldest_blob_file_number(
/* BlobFileAdditionInfo */
extern ROCKSDB_LIBRARY_API const char*
rocksdb_blob_file_addition_info_blob_file_path(
const rocksdb_blob_file_addition_info_t* info, size_t* size);
extern ROCKSDB_LIBRARY_API uint64_t
rocksdb_blob_file_addition_info_blob_file_number(
const rocksdb_blob_file_addition_info_t* info);
extern ROCKSDB_LIBRARY_API uint64_t
rocksdb_blob_file_addition_info_total_blob_count(
const rocksdb_blob_file_addition_info_t* info);
@@ -352,6 +364,14 @@ rocksdb_blob_file_addition_info_total_blob_bytes(
/* BlobFileGarbageInfo */
extern ROCKSDB_LIBRARY_API const char*
rocksdb_blob_file_garbage_info_blob_file_path(
const rocksdb_blob_file_garbage_info_t* info, size_t* size);
extern ROCKSDB_LIBRARY_API uint64_t
rocksdb_blob_file_garbage_info_blob_file_number(
const rocksdb_blob_file_garbage_info_t* info);
extern ROCKSDB_LIBRARY_API uint64_t
rocksdb_blob_file_garbage_info_garbage_blob_count(
const rocksdb_blob_file_garbage_info_t* info);
@@ -1,3 +1,7 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
// @generated
// -----------------------------------------------------------------------------
// Auto-generated by tools/c_api_gen/auto_simple_bindings.py.
@@ -137,6 +141,15 @@ int rocksdb_options_get_max_file_opening_threads(rocksdb_options_t* opt) {
return opt->rep.max_file_opening_threads;
}
void rocksdb_options_set_read_io_executor_threads(rocksdb_options_t* opt,
int v) {
opt->rep.read_io_executor_threads = v;
}
int rocksdb_options_get_read_io_executor_threads(rocksdb_options_t* opt) {
return opt->rep.read_io_executor_threads;
}
void rocksdb_options_set_open_files_async(rocksdb_options_t* opt,
unsigned char v) {
opt->rep.open_files_async = v;
@@ -1674,6 +1687,16 @@ unsigned char rocksdb_flushoptions_get_force_atomic_flush(
return opt->rep.force_atomic_flush;
}
void rocksdb_flushoptions_set_listener_wait(rocksdb_flushoptions_t* opt,
unsigned char v) {
opt->rep.listener_wait = v;
}
unsigned char rocksdb_flushoptions_get_listener_wait(
rocksdb_flushoptions_t* opt) {
return opt->rep.listener_wait;
}
/* FlushWALOptions */
void rocksdb_flushwaloptions_set_sync(rocksdb_flushwaloptions_t* opt,
@@ -2703,6 +2726,36 @@ uint64_t rocksdb_fifo_compaction_options_get_trivial_copy_buffer_size(
/* BackupEngineOptions */
void rocksdb_backup_engine_options_set_io_buffer_size(
rocksdb_backup_engine_options_t* opt, uint64_t v) {
opt->rep.io_buffer_size = v;
}
uint64_t rocksdb_backup_engine_options_get_io_buffer_size(
rocksdb_backup_engine_options_t* opt) {
return opt->rep.io_buffer_size;
}
void rocksdb_backup_engine_options_set_backup_rate_limit(
rocksdb_backup_engine_options_t* opt, uint64_t v) {
opt->rep.backup_rate_limit = v;
}
uint64_t rocksdb_backup_engine_options_get_backup_rate_limit(
rocksdb_backup_engine_options_t* opt) {
return opt->rep.backup_rate_limit;
}
void rocksdb_backup_engine_options_set_max_background_operations(
rocksdb_backup_engine_options_t* opt, int v) {
opt->rep.max_background_operations = v;
}
int rocksdb_backup_engine_options_get_max_background_operations(
rocksdb_backup_engine_options_t* opt) {
return opt->rep.max_background_operations;
}
void rocksdb_backup_engine_options_set_backup_dir(
rocksdb_backup_engine_options_t* opt, const char* v) {
opt->rep.backup_dir = v;
@@ -2754,26 +2807,6 @@ unsigned char rocksdb_backup_engine_options_get_backup_log_files(
return opt->rep.backup_log_files;
}
void rocksdb_backup_engine_options_set_io_buffer_size(
rocksdb_backup_engine_options_t* opt, uint64_t v) {
opt->rep.io_buffer_size = v;
}
uint64_t rocksdb_backup_engine_options_get_io_buffer_size(
rocksdb_backup_engine_options_t* opt) {
return opt->rep.io_buffer_size;
}
void rocksdb_backup_engine_options_set_backup_rate_limit(
rocksdb_backup_engine_options_t* opt, uint64_t v) {
opt->rep.backup_rate_limit = v;
}
uint64_t rocksdb_backup_engine_options_get_backup_rate_limit(
rocksdb_backup_engine_options_t* opt) {
return opt->rep.backup_rate_limit;
}
void rocksdb_backup_engine_options_set_restore_rate_limit(
rocksdb_backup_engine_options_t* opt, uint64_t v) {
opt->rep.restore_rate_limit = v;
@@ -2794,16 +2827,6 @@ unsigned char rocksdb_backup_engine_options_get_share_files_with_checksum(
return opt->rep.share_files_with_checksum;
}
void rocksdb_backup_engine_options_set_max_background_operations(
rocksdb_backup_engine_options_t* opt, int v) {
opt->rep.max_background_operations = v;
}
int rocksdb_backup_engine_options_get_max_background_operations(
rocksdb_backup_engine_options_t* opt) {
return opt->rep.max_background_operations;
}
void rocksdb_backup_engine_options_set_callback_trigger_interval_size(
rocksdb_backup_engine_options_t* opt, uint64_t v) {
opt->rep.callback_trigger_interval_size = v;
@@ -2858,16 +2881,6 @@ rocksdb_backup_engine_options_get_current_temperatures_override_manifest(
/* CreateBackupOptions */
void rocksdb_create_backup_options_set_flush_before_backup(
rocksdb_create_backup_options_t* opt, unsigned char v) {
opt->rep.flush_before_backup = v;
}
unsigned char rocksdb_create_backup_options_get_flush_before_backup(
rocksdb_create_backup_options_t* opt) {
return opt->rep.flush_before_backup;
}
void rocksdb_create_backup_options_set_decrease_background_thread_cpu_priority(
rocksdb_create_backup_options_t* opt, unsigned char v) {
opt->rep.decrease_background_thread_cpu_priority = v;
@@ -2890,6 +2903,16 @@ int rocksdb_create_backup_options_get_background_thread_cpu_priority(
return static_cast<int>(opt->rep.background_thread_cpu_priority);
}
void rocksdb_create_backup_options_set_flush_before_backup(
rocksdb_create_backup_options_t* opt, unsigned char v) {
opt->rep.flush_before_backup = v;
}
unsigned char rocksdb_create_backup_options_get_flush_before_backup(
rocksdb_create_backup_options_t* opt) {
return opt->rep.flush_before_backup;
}
void rocksdb_create_backup_options_set_atomic_flush(
rocksdb_create_backup_options_t* opt, unsigned char v) {
opt->rep.atomic_flush = v;
+48 -32
View File
@@ -1,3 +1,7 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
// @generated
// -----------------------------------------------------------------------------
// Auto-generated by tools/c_api_gen/auto_simple_bindings.py.
@@ -51,6 +55,12 @@ rocksdb_options_set_verify_sst_unique_id_in_manifest(rocksdb_options_t* opt,
extern ROCKSDB_LIBRARY_API unsigned char
rocksdb_options_get_verify_sst_unique_id_in_manifest(rocksdb_options_t* opt);
extern ROCKSDB_LIBRARY_API void rocksdb_options_set_read_io_executor_threads(
rocksdb_options_t* opt, int v);
extern ROCKSDB_LIBRARY_API int rocksdb_options_get_read_io_executor_threads(
rocksdb_options_t* opt);
extern ROCKSDB_LIBRARY_API const char* rocksdb_options_get_db_log_dir(
rocksdb_options_t* opt, size_t* size);
@@ -541,6 +551,12 @@ extern ROCKSDB_LIBRARY_API void rocksdb_flushoptions_set_force_atomic_flush(
extern ROCKSDB_LIBRARY_API unsigned char
rocksdb_flushoptions_get_force_atomic_flush(rocksdb_flushoptions_t* opt);
extern ROCKSDB_LIBRARY_API void rocksdb_flushoptions_set_listener_wait(
rocksdb_flushoptions_t* opt, unsigned char v);
extern ROCKSDB_LIBRARY_API unsigned char rocksdb_flushoptions_get_listener_wait(
rocksdb_flushoptions_t* opt);
/* FlushWALOptions */
extern ROCKSDB_LIBRARY_API void rocksdb_flushwaloptions_set_sync(
@@ -1313,6 +1329,30 @@ rocksdb_fifo_compaction_options_get_trivial_copy_buffer_size(
/* BackupEngineOptions */
extern ROCKSDB_LIBRARY_API void
rocksdb_backup_engine_options_set_io_buffer_size(
rocksdb_backup_engine_options_t* opt, uint64_t v);
extern ROCKSDB_LIBRARY_API uint64_t
rocksdb_backup_engine_options_get_io_buffer_size(
rocksdb_backup_engine_options_t* opt);
extern ROCKSDB_LIBRARY_API void
rocksdb_backup_engine_options_set_backup_rate_limit(
rocksdb_backup_engine_options_t* opt, uint64_t v);
extern ROCKSDB_LIBRARY_API uint64_t
rocksdb_backup_engine_options_get_backup_rate_limit(
rocksdb_backup_engine_options_t* opt);
extern ROCKSDB_LIBRARY_API void
rocksdb_backup_engine_options_set_max_background_operations(
rocksdb_backup_engine_options_t* opt, int v);
extern ROCKSDB_LIBRARY_API int
rocksdb_backup_engine_options_get_max_background_operations(
rocksdb_backup_engine_options_t* opt);
extern ROCKSDB_LIBRARY_API void rocksdb_backup_engine_options_set_backup_dir(
rocksdb_backup_engine_options_t* opt, const char* v);
@@ -1350,22 +1390,6 @@ extern ROCKSDB_LIBRARY_API unsigned char
rocksdb_backup_engine_options_get_backup_log_files(
rocksdb_backup_engine_options_t* opt);
extern ROCKSDB_LIBRARY_API void
rocksdb_backup_engine_options_set_io_buffer_size(
rocksdb_backup_engine_options_t* opt, uint64_t v);
extern ROCKSDB_LIBRARY_API uint64_t
rocksdb_backup_engine_options_get_io_buffer_size(
rocksdb_backup_engine_options_t* opt);
extern ROCKSDB_LIBRARY_API void
rocksdb_backup_engine_options_set_backup_rate_limit(
rocksdb_backup_engine_options_t* opt, uint64_t v);
extern ROCKSDB_LIBRARY_API uint64_t
rocksdb_backup_engine_options_get_backup_rate_limit(
rocksdb_backup_engine_options_t* opt);
extern ROCKSDB_LIBRARY_API void
rocksdb_backup_engine_options_set_restore_rate_limit(
rocksdb_backup_engine_options_t* opt, uint64_t v);
@@ -1382,14 +1406,6 @@ extern ROCKSDB_LIBRARY_API unsigned char
rocksdb_backup_engine_options_get_share_files_with_checksum(
rocksdb_backup_engine_options_t* opt);
extern ROCKSDB_LIBRARY_API void
rocksdb_backup_engine_options_set_max_background_operations(
rocksdb_backup_engine_options_t* opt, int v);
extern ROCKSDB_LIBRARY_API int
rocksdb_backup_engine_options_get_max_background_operations(
rocksdb_backup_engine_options_t* opt);
extern ROCKSDB_LIBRARY_API void
rocksdb_backup_engine_options_set_callback_trigger_interval_size(
rocksdb_backup_engine_options_t* opt, uint64_t v);
@@ -1431,14 +1447,6 @@ rocksdb_backup_engine_options_get_current_temperatures_override_manifest(
/* CreateBackupOptions */
extern ROCKSDB_LIBRARY_API void
rocksdb_create_backup_options_set_flush_before_backup(
rocksdb_create_backup_options_t* opt, unsigned char v);
extern ROCKSDB_LIBRARY_API unsigned char
rocksdb_create_backup_options_get_flush_before_backup(
rocksdb_create_backup_options_t* opt);
extern ROCKSDB_LIBRARY_API void
rocksdb_create_backup_options_set_decrease_background_thread_cpu_priority(
rocksdb_create_backup_options_t* opt, unsigned char v);
@@ -1455,6 +1463,14 @@ extern ROCKSDB_LIBRARY_API int
rocksdb_create_backup_options_get_background_thread_cpu_priority(
rocksdb_create_backup_options_t* opt);
extern ROCKSDB_LIBRARY_API void
rocksdb_create_backup_options_set_flush_before_backup(
rocksdb_create_backup_options_t* opt, unsigned char v);
extern ROCKSDB_LIBRARY_API unsigned char
rocksdb_create_backup_options_get_flush_before_backup(
rocksdb_create_backup_options_t* opt);
extern ROCKSDB_LIBRARY_API void rocksdb_create_backup_options_set_atomic_flush(
rocksdb_create_backup_options_t* opt, unsigned char v);
@@ -1,3 +1,7 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
// @generated
// -----------------------------------------------------------------------------
// Auto-generated by tools/c_api_gen/generate_c_api.py.
@@ -1,3 +1,7 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
// @generated
// -----------------------------------------------------------------------------
// Auto-generated by tools/c_api_gen/generate_c_api.py.
@@ -1,3 +1,7 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
// @generated
// -----------------------------------------------------------------------------
// Auto-generated by tools/c_api_gen/auto_simple_bindings.py.
@@ -1,3 +1,7 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
// @generated
// -----------------------------------------------------------------------------
// Auto-generated by tools/c_api_gen/auto_simple_bindings.py.
+13 -1
View File
@@ -1,9 +1,13 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
// @generated
// -----------------------------------------------------------------------------
// Auto-generated by tools/c_api_gen/gen_roundtrip_tests.py.
// DO NOT EDIT THIS FILE DIRECTLY.
// Round-trip (set -> get -> assert) coverage for generated option wrappers.
// Rerun: python3 tools/c_api_gen/regen_all.py
// Rerun `make regen-c-api`
// -----------------------------------------------------------------------------
static void rocksdb_roundtrip_test_rocksdb_block_based_options(void) {
@@ -459,6 +463,10 @@ static void rocksdb_roundtrip_test_rocksdb_flushoptions(void) {
CheckCondition(rocksdb_flushoptions_get_force_atomic_flush(obj) == 1);
rocksdb_flushoptions_set_force_atomic_flush(obj, 0);
CheckCondition(rocksdb_flushoptions_get_force_atomic_flush(obj) == 0);
rocksdb_flushoptions_set_listener_wait(obj, 1);
CheckCondition(rocksdb_flushoptions_get_listener_wait(obj) == 1);
rocksdb_flushoptions_set_listener_wait(obj, 0);
CheckCondition(rocksdb_flushoptions_get_listener_wait(obj) == 0);
rocksdb_flushoptions_set_wait(obj, 1);
CheckCondition(rocksdb_flushoptions_get_wait(obj) == 1);
rocksdb_flushoptions_set_wait(obj, 0);
@@ -841,6 +849,10 @@ static void rocksdb_roundtrip_test_rocksdb_options(void) {
CheckCondition(rocksdb_options_get_preserve_internal_time_seconds(obj) == 1);
rocksdb_options_set_preserve_internal_time_seconds(obj, 0);
CheckCondition(rocksdb_options_get_preserve_internal_time_seconds(obj) == 0);
rocksdb_options_set_read_io_executor_threads(obj, 1);
CheckCondition(rocksdb_options_get_read_io_executor_threads(obj) == 1);
rocksdb_options_set_read_io_executor_threads(obj, 0);
CheckCondition(rocksdb_options_get_read_io_executor_threads(obj) == 0);
rocksdb_options_set_reuse_manifest_on_open(obj, 1);
CheckCondition(rocksdb_options_get_reuse_manifest_on_open(obj) == 1);
rocksdb_options_set_reuse_manifest_on_open(obj, 0);
+4
View File
@@ -1,3 +1,7 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
// @generated
// -----------------------------------------------------------------------------
// Auto-generated by tools/c_api_gen/generate_c_api.py.
+4
View File
@@ -1,3 +1,7 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
// @generated
// -----------------------------------------------------------------------------
// Auto-generated by tools/c_api_gen/generate_c_api.py.
@@ -1,3 +1,7 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
// @generated
// -----------------------------------------------------------------------------
// Auto-generated by tools/c_api_gen/generate_c_api.py.
@@ -1,3 +1,7 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
// @generated
// -----------------------------------------------------------------------------
// Auto-generated by tools/c_api_gen/generate_c_api.py.
@@ -1,3 +1,7 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
// @generated
// -----------------------------------------------------------------------------
// Auto-generated by tools/c_api_gen/generate_c_api.py.
@@ -1,3 +1,7 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
// @generated
// -----------------------------------------------------------------------------
// Auto-generated by tools/c_api_gen/generate_c_api.py.
@@ -1,3 +1,7 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
// @generated
// -----------------------------------------------------------------------------
// Auto-generated by tools/c_api_gen/generate_c_api.py.
@@ -1,3 +1,7 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
// @generated
// -----------------------------------------------------------------------------
// Auto-generated by tools/c_api_gen/generate_c_api.py.
+34 -7
View File
@@ -7,6 +7,7 @@
#include "db/blob/blob_file_partition_manager.h"
#include "db/blob/blob_index.h"
#include "db/blob/same_file_blob_reader.h"
#include "db/version_set.h"
namespace ROCKSDB_NAMESPACE {
@@ -25,21 +26,47 @@ Status BlobFetcher::FetchBlob(const Slice& user_key,
return status;
}
Status BlobFetcher::FetchBlob(const Slice& user_key,
const BlobIndex& blob_index,
FilePrefetchBuffer* prefetch_buffer,
PinnableSlice* blob_value,
uint64_t* bytes_read) const {
Status VersionBlobFetcherBase::FetchBlob(const Slice& user_key,
const BlobIndex& blob_index,
FilePrefetchBuffer* prefetch_buffer,
PinnableSlice* blob_value,
uint64_t* bytes_read) const {
const ReadOptions& read_options = this->read_options();
if (!allow_write_path_fallback_) {
assert(version_);
return version_->GetBlob(read_options_, user_key, blob_index,
return version_->GetBlob(read_options, user_key, blob_index,
prefetch_buffer, blob_value, bytes_read);
}
return BlobFilePartitionManager::ResolveBlobDirectWriteIndex(
read_options_, user_key, blob_index, version_, blob_file_cache_,
read_options, user_key, blob_index, version_, blob_file_cache_,
prefetch_buffer, blob_value, bytes_read);
}
Status EmbeddedAwareBlobFetcher::FetchBlob(const Slice& user_key,
const BlobIndex& blob_index,
FilePrefetchBuffer* prefetch_buffer,
PinnableSlice* blob_value,
uint64_t* bytes_read) const {
// Only an enabled decorator is ever routed to (see EffectiveFetcher()).
assert(same_file_reader_ != nullptr);
if (blob_index.IsSameFile()) {
// Same-file ("embedded") blob records live in the current SST and are read
// by the SameFileBlobReader (the BlockBasedTable). A separate blob-file
// prefetch buffer and bytes_read counter do not apply here (embedded reads
// account their own BLOB_DB_* stats via BlobSource).
return same_file_reader_->GetSameFileBlob(read_options(), blob_index,
blob_value);
}
if (base_ == nullptr) {
return Status::Corruption(
"Cannot resolve non-same-file blob reference without a base blob "
"fetcher");
}
return base_->FetchBlob(user_key, blob_index, prefetch_buffer, blob_value,
bytes_read);
}
} // namespace ROCKSDB_NAMESPACE
+157 -13
View File
@@ -5,6 +5,8 @@
#pragma once
#include <utility>
#include "rocksdb/options.h"
#include "rocksdb/status.h"
@@ -16,32 +18,174 @@ class Slice;
class FilePrefetchBuffer;
class PinnableSlice;
class BlobIndex;
class SameFileBlobReader;
// A thin wrapper around blob retrieval. By default it reads through Version,
// and it can optionally fall back to direct-write blob files that are not yet
// manifest-visible.
// Abstract interface for blob retrieval on the read path. An implementation
// resolves a BlobIndex to its value, pinning it into a PinnableSlice. Callers
// hold a plain const BlobFetcher* so resolution can be intercepted by a
// decorator (e.g. EmbeddedAwareBlobFetcher) without their knowledge.
class BlobFetcher {
public:
BlobFetcher(const Version* version, const ReadOptions& read_options,
BlobFileCache* blob_file_cache = nullptr,
bool allow_write_path_fallback = false)
: version_(version),
read_options_(read_options),
blob_file_cache_(blob_file_cache),
allow_write_path_fallback_(allow_write_path_fallback) {}
virtual ~BlobFetcher() = default;
// Convenience overload: decodes `blob_index_slice` into a BlobIndex and
// dispatches to the virtual overload below. Non-virtual; shared by all
// implementations.
Status FetchBlob(const Slice& user_key, const Slice& blob_index_slice,
FilePrefetchBuffer* prefetch_buffer,
PinnableSlice* blob_value, uint64_t* bytes_read) const;
virtual Status FetchBlob(const Slice& user_key, const BlobIndex& blob_index,
FilePrefetchBuffer* prefetch_buffer,
PinnableSlice* blob_value,
uint64_t* bytes_read) const = 0;
// The read options this fetcher operates under. Used (via an
// EmbeddedAwareBlobFetcher decorator) to resolve same-file blob references,
// which need the current read_tier.
virtual const ReadOptions& read_options() const = 0;
};
// Shared implementation of the two Version-backed BlobFetchers below. Reads
// through a Version, optionally falling back to direct-write blob files that
// are not yet manifest-visible. Everything except the ReadOptions is stored
// here; the concrete subclass decides whether it references a caller-owned
// ReadOptions (VersionBlobFetcher) or owns its own copy
// (OwningVersionBlobFetcher). FetchBlob reads the options via the virtual
// read_options() so both storage strategies share one implementation.
class VersionBlobFetcherBase : public BlobFetcher {
public:
// Un-hide the base's Slice-based convenience overload.
using BlobFetcher::FetchBlob;
Status FetchBlob(const Slice& user_key, const BlobIndex& blob_index,
FilePrefetchBuffer* prefetch_buffer,
PinnableSlice* blob_value, uint64_t* bytes_read) const;
PinnableSlice* blob_value,
uint64_t* bytes_read) const override;
// True if this fetcher has enough context to resolve a (non-inlined,
// non-same-file) blob reference: either a Version to read through, or the
// direct-write fallback (a blob file cache) enabled. Callers that may hold a
// null Version (e.g. DBIter) can check this to surface a clean Corruption on
// an unexpected blob index instead of dereferencing null in FetchBlob.
bool CanResolve() const {
return version_ != nullptr ||
(allow_write_path_fallback_ && blob_file_cache_ != nullptr);
}
protected:
VersionBlobFetcherBase(const Version* version, BlobFileCache* blob_file_cache,
bool allow_write_path_fallback)
: version_(version),
blob_file_cache_(blob_file_cache),
allow_write_path_fallback_(allow_write_path_fallback) {}
private:
const Version* version_;
ReadOptions read_options_;
BlobFileCache* blob_file_cache_;
bool allow_write_path_fallback_;
};
// The default BlobFetcher for transient/stack use: holds a reference to a
// caller-owned ReadOptions rather than a copy, avoiding a per-fetcher copy of
// the (growing) ReadOptions on the hot path. The referenced ReadOptions must
// outlive the fetcher, so this is only appropriate for fetchers whose lifetime
// is bounded by the caller's (e.g. a stack local per Get / per resolution).
// Use OwningVersionBlobFetcher for lifetime-independent fetchers.
class VersionBlobFetcher : public VersionBlobFetcherBase {
public:
VersionBlobFetcher(const Version* version, const ReadOptions& read_options,
BlobFileCache* blob_file_cache = nullptr,
bool allow_write_path_fallback = false)
: VersionBlobFetcherBase(version, blob_file_cache,
allow_write_path_fallback),
read_options_(read_options) {}
// Reject binding to a temporary ReadOptions, which would leave a dangling
// reference. Callers that cannot guarantee the ReadOptions outlives the
// fetcher must use OwningVersionBlobFetcher instead.
VersionBlobFetcher(const Version* version, ReadOptions&& read_options,
BlobFileCache* blob_file_cache = nullptr,
bool allow_write_path_fallback = false) = delete;
const ReadOptions& read_options() const override { return read_options_; }
private:
const ReadOptions& read_options_;
};
// A BlobFetcher that owns its ReadOptions, for fetchers whose lifetime is
// independent of the caller that created them (the lazy read-path resolver and
// compaction). Takes ownership of the ReadOptions by rvalue so ownership
// transfer is explicit and no hidden copy is made on the way in; later reads
// never touch caller state. Callers holding a borrowed ReadOptions they cannot
// move must make the copy explicit (e.g. ReadOptions(read_options)).
class OwningVersionBlobFetcher : public VersionBlobFetcherBase {
public:
OwningVersionBlobFetcher(const Version* version, ReadOptions&& read_options,
BlobFileCache* blob_file_cache = nullptr,
bool allow_write_path_fallback = false)
: VersionBlobFetcherBase(version, blob_file_cache,
allow_write_path_fallback),
read_options_(std::move(read_options)) {}
const ReadOptions& read_options() const override { return read_options_; }
private:
ReadOptions read_options_;
};
// A BlobFetcher decorator that resolves same-file ("embedded") blob references
// against a SameFileBlobReader (the current SST) and delegates all other
// references to a base BlobFetcher. It is cheap to construct as a stack local
// per resolution: the same-file target is per-SST while the base fetcher is one
// per-Get object shared across SSTs, so an immutable per-SST decorator avoids
// shared-mutable-state hazards under async MultiGet.
//
// A decorator is "enabled" when it has a same-file reader and "disabled"
// (unusable) when constructed with a null `same_file_reader` (e.g. via the
// default constructor). A disabled decorator must not be routed through
// directly; callers construct one unconditionally and use EffectiveFetcher(),
// which returns this decorator when enabled or the undecorated base fetcher
// otherwise. This avoids std::optional / branching at call sites while adding
// no indirection on the common (non-embedded) path.
//
// The base fetcher may be null for an entity whose blob columns are all
// same-file (no separate-file blob support wired, e.g. SstFileReader); a
// non-same-file reference then surfaces as a Corruption rather than silently.
class EmbeddedAwareBlobFetcher : public BlobFetcher {
public:
explicit EmbeddedAwareBlobFetcher(
const BlobFetcher* base = nullptr,
const SameFileBlobReader* same_file_reader = nullptr)
: base_(base), same_file_reader_(same_file_reader) {}
// The fetcher to route resolution through: this decorator when it has a
// same-file reader, otherwise the (undecorated) base fetcher. Only an enabled
// decorator's FetchBlob is ever invoked.
const BlobFetcher* EffectiveFetcher() const {
return same_file_reader_ != nullptr ? this : base_;
}
// Un-hide the base's Slice-based convenience overload.
using BlobFetcher::FetchBlob;
Status FetchBlob(const Slice& user_key, const BlobIndex& blob_index,
FilePrefetchBuffer* prefetch_buffer,
PinnableSlice* blob_value,
uint64_t* bytes_read) const override;
const ReadOptions& read_options() const override {
// Same-file resolution uses the base fetcher's read options; when there is
// no base (e.g. SstFileReader) a default (kReadAllTier) suffices.
static const ReadOptions kDefaultReadOptions;
return base_ != nullptr ? base_->read_options() : kDefaultReadOptions;
}
private:
const BlobFetcher* base_;
// Null iff this decorator is disabled (see class comment); otherwise the
// same-file blob reader for the current SST.
const SameFileBlobReader* same_file_reader_;
};
} // namespace ROCKSDB_NAMESPACE
+2 -3
View File
@@ -215,11 +215,10 @@ Status BlobWriteBatchTransformer::PutEntityCF(uint32_t column_family_id,
output_batch_, column_family_id, key, entity);
}
Slice entity_ref = entity;
std::vector<WideColumn> columns;
std::vector<std::pair<size_t, BlobIndex>> existing_blob_columns;
Status status = WideColumnSerialization::DeserializeV2(entity_ref, columns,
existing_blob_columns);
Status status = WideColumnSerialization::Deserialize(entity, columns,
&existing_blob_columns);
if (status.ok()) {
// Reject pre-serialized entities that already contain blob references.
// Passing them through would preserve references to blob files outside this
+306 -13
View File
@@ -143,14 +143,24 @@ 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);
Status MaybeResolveMemtableBlobValueForTest(
const Slice& key, const BlobFetcher* blob_fetcher, PinnableSlice* value,
PinnableWideColumns* columns, bool* did_resolve, bool* is_blob_index,
bool* value_found = nullptr) {
return DBImpl::MaybeResolveMemtableBlobValue(key, blob_fetcher, value,
columns, did_resolve,
is_blob_index, value_found);
}
Status PostprocessMemtableValueReadForTest(
const Slice& key, bool resolve_blob_backed_memtable_value,
const BlobFetcher* blob_fetcher, PinnableSlice* value,
PinnableWideColumns* columns, Status memtable_read_status,
bool* is_blob_index, bool* value_found = nullptr) {
return DBImpl::PostprocessMemtableValueRead(
key, /*timestamp=*/nullptr, resolve_blob_backed_memtable_value,
blob_fetcher, value, columns, std::move(memtable_read_status),
is_blob_index, value_found);
}
Options GetTestOptions() {
@@ -309,17 +319,65 @@ TEST_F(DBBlobIndexTest,
value.GetSelf()->assign(blob_index.data(), blob_index.size());
value.PinSelf();
Status s = Status::OK();
bool did_resolve = false;
bool is_blob_index = true;
ASSERT_TRUE(MaybeResolveMemtableBlobValueForTest(
Slice("key"), /*blob_fetcher=*/nullptr, &value, /*columns=*/nullptr, &s,
&is_blob_index));
Status s = MaybeResolveMemtableBlobValueForTest(
Slice("key"), /*blob_fetcher=*/nullptr, &value, /*columns=*/nullptr,
&did_resolve, &is_blob_index);
ASSERT_TRUE(s.IsNotSupported()) << s.ToString();
ASSERT_TRUE(did_resolve);
ASSERT_TRUE(s.IsCorruption()) << s.ToString();
ASSERT_TRUE(value.empty());
ASSERT_FALSE(is_blob_index);
}
TEST_F(DBBlobIndexTest, PostprocessMemtableValueReadClearsOutputsOnError) {
// Regression test for the "swallowed status" FIXME: on any error,
// PostprocessMemtableValueRead must clear the outputs and return the status,
// never leaving a half-populated result behind a non-OK status.
// Case 1: the incoming memtable read status is already non-OK. The resolve
// path is skipped; the finalization block clears outputs and returns it.
{
PinnableSlice value;
value.GetSelf()->assign("stale", 5);
value.PinSelf();
bool is_blob_index = false;
Status s = PostprocessMemtableValueReadForTest(
Slice("key"), /*resolve_blob_backed_memtable_value=*/true,
/*blob_fetcher=*/nullptr, &value, /*columns=*/nullptr,
Status::Corruption("read failed"), &is_blob_index);
ASSERT_TRUE(s.IsCorruption()) << s.ToString();
ASSERT_TRUE(value.empty());
}
// Case 2: the incoming status is OK but the blob-resolve path itself fails (a
// blob-backed value with no fetcher). This is the actual scenario the FIXME
// referred to -- previously the non-OK status from the resolve path was
// dropped. The failure must propagate and the raw blob-index bytes must be
// cleared rather than handed back 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();
bool is_blob_index = true;
Status s = PostprocessMemtableValueReadForTest(
Slice("key"), /*resolve_blob_backed_memtable_value=*/true,
/*blob_fetcher=*/nullptr, &value, /*columns=*/nullptr, Status::OK(),
&is_blob_index);
ASSERT_TRUE(s.IsCorruption()) << s.ToString();
ASSERT_TRUE(value.empty());
}
}
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
@@ -775,6 +833,241 @@ TEST_F(DBBlobIndexTest, EmbeddedBlobSharedBlockAndBlobCache) {
ASSERT_OK(iter->status());
}
// Point GetEntity / MultiGetEntity of a wide-column entity whose non-default
// columns are embedded (same-file) blobs must return the correct columns and
// resolve them zero-copy: each resolved blob column value points straight at
// its pinned blob-cache buffer (identical across reads, and in a separate
// buffer per column), rather than being copied into one re-serialized entity.
TEST_F(DBBlobIndexTest, EmbeddedBlobWideColumnGetEntityZeroCopy) {
Options options = GetTestOptions();
options.create_if_missing = true;
options.statistics = CreateDBStatistics();
LRUCacheOptions co;
co.capacity = 8 << 20;
options.blob_cache = NewLRUCache(co);
DestroyAndReopen(options);
const std::string sst_path = dbname_ + "/embedded_wc_zerocopy.sst";
SstFileWriterEmbeddedBlobOptions embedded_blob_options;
embedded_blob_options.min_blob_size = 8;
const std::string small_default = "d"; // inline default column
const std::string big1(1024, 'a'); // same-file blob column
const std::string big2(2048, 'b'); // same-file blob column
const WideColumns columns{
{kDefaultWideColumnName, small_default}, {"big1", big1}, {"big2", big2}};
SstFileWriter writer(EnvOptions(), options);
ASSERT_OK(writer.OpenWithEmbeddedBlobs(sst_path, embedded_blob_options));
ASSERT_OK(writer.PutEntity("k", columns));
ASSERT_OK(writer.Finish());
ASSERT_OK(db_->IngestExternalFile({sst_path}, IngestExternalFileOptions()));
const char* big1_ptr = nullptr;
const char* big2_ptr = nullptr;
{
PinnableWideColumns result;
ASSERT_OK(db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(), "k",
&result));
ASSERT_EQ(result.columns(), columns);
// columns() is sorted by name -> [default(""), big1, big2].
big1_ptr = result.columns()[1].value().data();
big2_ptr = result.columns()[2].value().data();
// Multi-buffer: each blob column lives in its own backing buffer, so the
// two values are not laid out contiguously (as they would be in a single
// re-serialized V1 entity).
ASSERT_NE(big1_ptr + big1.size(), big2_ptr);
// Pointer stability across move (address-stable backing nodes).
PinnableWideColumns moved(std::move(result));
ASSERT_EQ(moved.columns(), columns);
ASSERT_EQ(moved.columns()[1].value().data(), big1_ptr);
ASSERT_EQ(moved.columns()[2].value().data(), big2_ptr);
}
// A second read serves the same pinned blob-cache buffers (zero-copy): the
// resolved column values point at the very same addresses, which a per-read
// re-serialization would not.
{
PinnableWideColumns result;
ASSERT_OK(db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(), "k",
&result));
ASSERT_EQ(result.columns(), columns);
ASSERT_EQ(result.columns()[1].value().data(), big1_ptr);
ASSERT_EQ(result.columns()[2].value().data(), big2_ptr);
}
// MultiGetEntity: same correctness and zero-copy (cache-hit) guarantee.
{
const std::array<Slice, 1> keys{Slice("k")};
std::array<PinnableWideColumns, 1> results;
std::array<Status, 1> statuses;
db_->MultiGetEntity(ReadOptions(), db_->DefaultColumnFamily(), keys.size(),
keys.data(), results.data(), statuses.data());
ASSERT_OK(statuses[0]);
ASSERT_EQ(results[0].columns(), columns);
ASSERT_EQ(results[0].columns()[1].value().data(), big1_ptr);
ASSERT_EQ(results[0].columns()[2].value().data(), big2_ptr);
}
}
// A wide-column entity whose default column is itself an embedded (same-file)
// blob must resolve correctly on the plain Get path (default-column
// extraction) as well as GetEntity, matching the values written.
TEST_F(DBBlobIndexTest, EmbeddedBlobWideColumnGetDefaultColumn) {
Options options = GetTestOptions();
options.create_if_missing = true;
DestroyAndReopen(options);
const std::string sst_path = dbname_ + "/embedded_wc_default.sst";
SstFileWriterEmbeddedBlobOptions embedded_blob_options;
embedded_blob_options.min_blob_size = 8;
const std::string big_default(1024, 'x'); // same-file blob default column
const std::string small_attr = "s"; // inline non-default column
const WideColumns columns{{kDefaultWideColumnName, big_default},
{"attr", small_attr}};
SstFileWriter writer(EnvOptions(), options);
ASSERT_OK(writer.OpenWithEmbeddedBlobs(sst_path, embedded_blob_options));
ASSERT_OK(writer.PutEntity("k", columns));
ASSERT_OK(writer.Finish());
ASSERT_OK(db_->IngestExternalFile({sst_path}, IngestExternalFileOptions()));
// Plain Get returns the (blob-backed) default column value.
PinnableSlice value;
ASSERT_OK(db_->Get(ReadOptions(), db_->DefaultColumnFamily(), "k", &value));
ASSERT_EQ(value, big_default);
// GetEntity returns all columns.
PinnableWideColumns result;
ASSERT_OK(
db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(), "k", &result));
ASSERT_EQ(result.columns(), columns);
}
// Merge over an embedded-blob wide-column base entity must resolve the base
// entity's same-file default column before merging, and GetMergeOperands must
// return the resolved default column as the base operand.
TEST_F(DBBlobIndexTest, EmbeddedBlobWideColumnMergeBase) {
Options options = GetTestOptions(); // merge_operator = StringAppend(',')
options.create_if_missing = true;
DestroyAndReopen(options);
const std::string sst_path = dbname_ + "/embedded_wc_merge.sst";
SstFileWriterEmbeddedBlobOptions embedded_blob_options;
embedded_blob_options.min_blob_size = 8;
const std::string big_default(1024, 'D'); // same-file blob default column
const WideColumns columns{{kDefaultWideColumnName, big_default},
{"attr", "s"}};
SstFileWriter writer(EnvOptions(), options);
ASSERT_OK(writer.OpenWithEmbeddedBlobs(sst_path, embedded_blob_options));
ASSERT_OK(writer.PutEntity("k", columns));
ASSERT_OK(writer.Finish());
ASSERT_OK(db_->IngestExternalFile({sst_path}, IngestExternalFileOptions()));
// Newer merge operand lives in the memtable; the embedded entity base lives
// in the ingested SST, so the merge resolves the base via the same-file path.
ASSERT_OK(db_->Merge(WriteOptions(), db_->DefaultColumnFamily(), "k", "op1"));
PinnableSlice value;
ASSERT_OK(db_->Get(ReadOptions(), db_->DefaultColumnFamily(), "k", &value));
ASSERT_EQ(value, big_default + "," + "op1");
// GetMergeOperands returns the resolved base default column plus the operand.
std::array<PinnableSlice, 8> operands;
GetMergeOperandsOptions get_merge_opts;
get_merge_opts.expected_max_number_of_operands =
static_cast<int>(operands.size());
int num_operands = 0;
ASSERT_OK(db_->GetMergeOperands(ReadOptions(), db_->DefaultColumnFamily(),
"k", operands.data(), &get_merge_opts,
&num_operands));
ASSERT_EQ(num_operands, 2);
ASSERT_EQ(operands[0], big_default);
ASSERT_EQ(operands[1], "op1");
}
// With read_tier == kBlockCacheTier, an unresolved same-file blob column of a
// wide-column entity must surface Incomplete (never a raw same-file BlobIndex).
TEST_F(DBBlobIndexTest, EmbeddedBlobWideColumnGetEntityBlockCacheTier) {
Options options = GetTestOptions();
options.create_if_missing = true;
DestroyAndReopen(options);
const std::string sst_path = dbname_ + "/embedded_wc_blockcache.sst";
SstFileWriterEmbeddedBlobOptions embedded_blob_options;
embedded_blob_options.min_blob_size = 8;
const std::string big1(1024, 'a');
const WideColumns columns{{kDefaultWideColumnName, "d"}, {"big", big1}};
SstFileWriter writer(EnvOptions(), options);
ASSERT_OK(writer.OpenWithEmbeddedBlobs(sst_path, embedded_blob_options));
ASSERT_OK(writer.PutEntity("k", columns));
ASSERT_OK(writer.Finish());
ASSERT_OK(db_->IngestExternalFile({sst_path}, IngestExternalFileOptions()));
// Warm the block cache so the data block read succeeds under kBlockCacheTier
// and the lookup actually reaches same-file blob column resolution.
{
PinnableWideColumns result;
ASSERT_OK(db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(), "k",
&result));
ASSERT_EQ(result.columns(), columns);
}
ReadOptions read_options;
read_options.read_tier = kBlockCacheTier;
PinnableWideColumns result;
Status s =
db_->GetEntity(read_options, db_->DefaultColumnFamily(), "k", &result);
ASSERT_TRUE(s.IsIncomplete()) << s.ToString();
}
// A corrupt embedded blob record backing a wide-column column must surface an
// error on GetEntity, not expose the raw same-file BlobIndex.
// A failure resolving an embedded blob record backing a wide-column column
// (e.g. a bad record or blob-region read fault) must surface on GetEntity, not
// expose the raw same-file BlobIndex. The failure is injected via a sync point
// so the test is independent of the on-disk byte layout (e.g. encrypted env).
TEST_F(DBBlobIndexTest, EmbeddedBlobWideColumnGetEntityCorruptionSurfaced) {
Options options = GetTestOptions();
options.create_if_missing = true;
DestroyAndReopen(options);
const std::string sst_path = dbname_ + "/embedded_wc_corrupt.sst";
SstFileWriterEmbeddedBlobOptions embedded_blob_options;
embedded_blob_options.min_blob_size = 8;
const std::string big_col(1024, 'Z');
const WideColumns columns{{kDefaultWideColumnName, "d"}, {"big", big_col}};
SstFileWriter writer(EnvOptions(), options);
ASSERT_OK(writer.OpenWithEmbeddedBlobs(sst_path, embedded_blob_options));
ASSERT_OK(writer.PutEntity("k", columns));
ASSERT_OK(writer.Finish());
ASSERT_OK(db_->IngestExternalFile({sst_path}, IngestExternalFileOptions()));
const Status kInjected = Status::Corruption("injected embedded blob error");
SyncPoint::GetInstance()->SetCallBack(
"BlockBasedTable::MaybeResolveEmbeddedValue:InjectError",
[&](void* arg) { *static_cast<Status*>(arg) = kInjected; });
SyncPoint::GetInstance()->EnableProcessing();
PinnableWideColumns result;
const Status s =
db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(), "k", &result);
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
ASSERT_TRUE(s.IsCorruption()) << s.ToString();
}
class PlainBlobValueFilterV3 : public CompactionFilter {
public:
PlainBlobValueFilterV3(std::atomic<int>* filter_call_count,
+39
View File
@@ -0,0 +1,39 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#pragma once
#include "rocksdb/rocksdb_namespace.h"
#include "rocksdb/status.h"
namespace ROCKSDB_NAMESPACE {
class BlobIndex;
class PinnableSlice;
struct ReadOptions;
// Narrow interface for resolving a same-file ("embedded") blob reference
// against the physical file that currently holds the table entry. Implemented
// by BlockBasedTable, which owns the reader context (file, footer, blob cache)
// needed to read an embedded blob record.
//
// Kept in its own low-level header, free of any table-layer dependency, so that
// db/blob (EmbeddedAwareBlobFetcher) and table/get_context can depend on just
// this interface without pulling in BlockBasedTable. ReadPathBlobResolver (the
// lazy-resolution read path) can likewise hold a const SameFileBlobReader*.
class SameFileBlobReader {
public:
virtual ~SameFileBlobReader() = default;
// Resolves a same-file blob reference (one for which blob_index.IsSameFile()
// is true) into `value`, pinning the payload zero-copy (from the blob cache
// or an owned buffer). read_tier == kBlockCacheTier without a cached record
// yields Status::Incomplete; a bad record yields Status::Corruption.
virtual Status GetSameFileBlob(const ReadOptions& read_options,
const BlobIndex& blob_index,
PinnableSlice* value) const = 0;
};
} // namespace ROCKSDB_NAMESPACE
+82 -41
View File
@@ -7,7 +7,7 @@
// Auto-generated by tools/c_api_gen/regen_all.py.
// DO NOT EDIT THIS FILE DIRECTLY.
// Edit tools/c_api_gen/c_base.cc or the inputs under tools/c_api_gen/,
// then rerun: python3 tools/c_api_gen/regen_all.py
// then rerun `make regen-c-api`
// -----------------------------------------------------------------------------
#include "rocksdb/c.h"
@@ -5289,6 +5289,17 @@ uint64_t rocksdb_compaction_file_info_oldest_blob_file_number(
/* BlobFileAdditionInfo */
const char* rocksdb_blob_file_addition_info_blob_file_path(
const rocksdb_blob_file_addition_info_t* info, size_t* size) {
*size = info->rep.blob_file_path.size();
return info->rep.blob_file_path.data();
}
uint64_t rocksdb_blob_file_addition_info_blob_file_number(
const rocksdb_blob_file_addition_info_t* info) {
return info->rep.blob_file_number;
}
uint64_t rocksdb_blob_file_addition_info_total_blob_count(
const rocksdb_blob_file_addition_info_t* info) {
return info->rep.total_blob_count;
@@ -5301,6 +5312,17 @@ uint64_t rocksdb_blob_file_addition_info_total_blob_bytes(
/* BlobFileGarbageInfo */
const char* rocksdb_blob_file_garbage_info_blob_file_path(
const rocksdb_blob_file_garbage_info_t* info, size_t* size) {
*size = info->rep.blob_file_path.size();
return info->rep.blob_file_path.data();
}
uint64_t rocksdb_blob_file_garbage_info_blob_file_number(
const rocksdb_blob_file_garbage_info_t* info) {
return info->rep.blob_file_number;
}
uint64_t rocksdb_blob_file_garbage_info_garbage_blob_count(
const rocksdb_blob_file_garbage_info_t* info) {
return info->rep.garbage_blob_count;
@@ -9085,6 +9107,15 @@ int rocksdb_options_get_max_file_opening_threads(rocksdb_options_t* opt) {
return opt->rep.max_file_opening_threads;
}
void rocksdb_options_set_read_io_executor_threads(rocksdb_options_t* opt,
int v) {
opt->rep.read_io_executor_threads = v;
}
int rocksdb_options_get_read_io_executor_threads(rocksdb_options_t* opt) {
return opt->rep.read_io_executor_threads;
}
void rocksdb_options_set_open_files_async(rocksdb_options_t* opt,
unsigned char v) {
opt->rep.open_files_async = v;
@@ -10622,6 +10653,16 @@ unsigned char rocksdb_flushoptions_get_force_atomic_flush(
return opt->rep.force_atomic_flush;
}
void rocksdb_flushoptions_set_listener_wait(rocksdb_flushoptions_t* opt,
unsigned char v) {
opt->rep.listener_wait = v;
}
unsigned char rocksdb_flushoptions_get_listener_wait(
rocksdb_flushoptions_t* opt) {
return opt->rep.listener_wait;
}
/* FlushWALOptions */
void rocksdb_flushwaloptions_set_sync(rocksdb_flushwaloptions_t* opt,
@@ -11651,6 +11692,36 @@ uint64_t rocksdb_fifo_compaction_options_get_trivial_copy_buffer_size(
/* BackupEngineOptions */
void rocksdb_backup_engine_options_set_io_buffer_size(
rocksdb_backup_engine_options_t* opt, uint64_t v) {
opt->rep.io_buffer_size = v;
}
uint64_t rocksdb_backup_engine_options_get_io_buffer_size(
rocksdb_backup_engine_options_t* opt) {
return opt->rep.io_buffer_size;
}
void rocksdb_backup_engine_options_set_backup_rate_limit(
rocksdb_backup_engine_options_t* opt, uint64_t v) {
opt->rep.backup_rate_limit = v;
}
uint64_t rocksdb_backup_engine_options_get_backup_rate_limit(
rocksdb_backup_engine_options_t* opt) {
return opt->rep.backup_rate_limit;
}
void rocksdb_backup_engine_options_set_max_background_operations(
rocksdb_backup_engine_options_t* opt, int v) {
opt->rep.max_background_operations = v;
}
int rocksdb_backup_engine_options_get_max_background_operations(
rocksdb_backup_engine_options_t* opt) {
return opt->rep.max_background_operations;
}
void rocksdb_backup_engine_options_set_backup_dir(
rocksdb_backup_engine_options_t* opt, const char* v) {
opt->rep.backup_dir = v;
@@ -11702,26 +11773,6 @@ unsigned char rocksdb_backup_engine_options_get_backup_log_files(
return opt->rep.backup_log_files;
}
void rocksdb_backup_engine_options_set_io_buffer_size(
rocksdb_backup_engine_options_t* opt, uint64_t v) {
opt->rep.io_buffer_size = v;
}
uint64_t rocksdb_backup_engine_options_get_io_buffer_size(
rocksdb_backup_engine_options_t* opt) {
return opt->rep.io_buffer_size;
}
void rocksdb_backup_engine_options_set_backup_rate_limit(
rocksdb_backup_engine_options_t* opt, uint64_t v) {
opt->rep.backup_rate_limit = v;
}
uint64_t rocksdb_backup_engine_options_get_backup_rate_limit(
rocksdb_backup_engine_options_t* opt) {
return opt->rep.backup_rate_limit;
}
void rocksdb_backup_engine_options_set_restore_rate_limit(
rocksdb_backup_engine_options_t* opt, uint64_t v) {
opt->rep.restore_rate_limit = v;
@@ -11742,16 +11793,6 @@ unsigned char rocksdb_backup_engine_options_get_share_files_with_checksum(
return opt->rep.share_files_with_checksum;
}
void rocksdb_backup_engine_options_set_max_background_operations(
rocksdb_backup_engine_options_t* opt, int v) {
opt->rep.max_background_operations = v;
}
int rocksdb_backup_engine_options_get_max_background_operations(
rocksdb_backup_engine_options_t* opt) {
return opt->rep.max_background_operations;
}
void rocksdb_backup_engine_options_set_callback_trigger_interval_size(
rocksdb_backup_engine_options_t* opt, uint64_t v) {
opt->rep.callback_trigger_interval_size = v;
@@ -11806,16 +11847,6 @@ rocksdb_backup_engine_options_get_current_temperatures_override_manifest(
/* CreateBackupOptions */
void rocksdb_create_backup_options_set_flush_before_backup(
rocksdb_create_backup_options_t* opt, unsigned char v) {
opt->rep.flush_before_backup = v;
}
unsigned char rocksdb_create_backup_options_get_flush_before_backup(
rocksdb_create_backup_options_t* opt) {
return opt->rep.flush_before_backup;
}
void rocksdb_create_backup_options_set_decrease_background_thread_cpu_priority(
rocksdb_create_backup_options_t* opt, unsigned char v) {
opt->rep.decrease_background_thread_cpu_priority = v;
@@ -11838,6 +11869,16 @@ int rocksdb_create_backup_options_get_background_thread_cpu_priority(
return static_cast<int>(opt->rep.background_thread_cpu_priority);
}
void rocksdb_create_backup_options_set_flush_before_backup(
rocksdb_create_backup_options_t* opt, unsigned char v) {
opt->rep.flush_before_backup = v;
}
unsigned char rocksdb_create_backup_options_get_flush_before_backup(
rocksdb_create_backup_options_t* opt) {
return opt->rep.flush_before_backup;
}
void rocksdb_create_backup_options_set_atomic_flush(
rocksdb_create_backup_options_t* opt, unsigned char v) {
opt->rep.atomic_flush = v;
+5
View File
@@ -4531,6 +4531,11 @@ int main(int argc, char** argv) {
CheckNoError(err);
rocksdb_flushoptions_set_wait(flush_options, 1);
// Ensure the OnFlushCompleted callbacks (which update listener_state) have
// finished by the time rocksdb_flush() returns, so the flush_count /
// saw_flush_entries_* assertions below are not racy.
rocksdb_flushoptions_set_listener_wait(flush_options, 1);
CheckCondition(1 == rocksdb_flushoptions_get_listener_wait(flush_options));
rocksdb_put(local_db, local_woptions, "a", 1, "1", 1, &err);
CheckNoError(err);
+18 -16
View File
@@ -251,8 +251,9 @@ void CompactionIterator::SetBlobFetcher(const Version* version,
read_options.io_activity = io_activity;
read_options.fill_cache = false;
blob_fetcher_ = std::make_unique<BlobFetcher>(
version, read_options, blob_file_cache, allow_write_path_fallback);
blob_fetcher_ = std::make_unique<OwningVersionBlobFetcher>(
version, std::move(read_options), blob_file_cache,
allow_write_path_fallback);
blob_resolver_.Init(blob_fetcher_.get(), prefetch_buffers_.get(),
&iter_stats_);
}
@@ -467,14 +468,14 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
}
}
if (UNLIKELY(has_blob_columns)) {
// Entity has blob columns. DeserializeV2() populates
// Entity has blob columns. Deserialize() populates
// entity_columns_ with the full column set; blob-backed entries still
// carry the serialized BlobIndex bytes in value(). The companion
// entity_blob_columns_ side list identifies which column indexes are
// blob references and provides the decoded BlobIndex objects.
Slice input_copy = value_;
Status s = WideColumnSerialization::DeserializeV2(
input_copy, entity_columns_, entity_blob_columns_);
Status s = WideColumnSerialization::Deserialize(
input_copy, entity_columns_, &entity_blob_columns_);
if (!s.ok()) {
status_ = s;
validity_info_.Invalidate();
@@ -541,7 +542,7 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
} else {
// No blob columns, use fast path
Slice value_copy = value_;
const Status s = WideColumnSerialization::Deserialize(
const Status s = WideColumnSerialization::DeserializeSimple(
value_copy, filter_existing_columns_);
if (!s.ok()) {
@@ -1550,8 +1551,8 @@ void CompactionIterator::ExtractLargeColumnValuesIfNeeded() {
Slice entity_slice = value_;
entity_columns_.clear();
entity_blob_columns_.clear();
Status s = WideColumnSerialization::DeserializeV2(
entity_slice, entity_columns_, entity_blob_columns_);
Status s = WideColumnSerialization::Deserialize(
entity_slice, entity_columns_, &entity_blob_columns_);
if (!s.ok()) {
status_ = s;
validity_info_.Invalidate();
@@ -1669,7 +1670,7 @@ bool CompactionIterator::FetchBlobsNeedingGC(
Status s = blob_fetcher_->FetchBlob(user_key(), blob_index, prefetch_buffer,
&blob_value, &bytes_read);
if (!s.ok()) {
status_ = s;
status_ = std::move(s);
validity_info_.Invalidate();
return false;
}
@@ -1817,8 +1818,8 @@ void CompactionIterator::GarbageCollectEntityBlobsIfNeeded() {
Slice entity_slice = value_;
entity_columns_.clear();
entity_blob_columns_.clear();
Status s = WideColumnSerialization::DeserializeV2(
entity_slice, entity_columns_, entity_blob_columns_);
Status s = WideColumnSerialization::Deserialize(
entity_slice, entity_columns_, &entity_blob_columns_);
if (!s.ok()) {
status_ = s;
validity_info_.Invalidate();
@@ -1869,13 +1870,13 @@ void CompactionIterator::PrepareOutput() {
"CompactionIterator::PrepareOutput:DeserializeEntity");
// Deserialize entity once into member variables, then decide between
// blob GC and extraction based on whether blob columns exist.
// This avoids the double parse of HasBlobColumns() + DeserializeV2().
// This avoids the double parse of HasBlobColumns() + Deserialize().
entity_columns_.clear();
entity_blob_columns_.clear();
Slice entity_slice = value_;
{
Status s_deser = WideColumnSerialization::DeserializeV2(
entity_slice, entity_columns_, entity_blob_columns_);
Status s_deser = WideColumnSerialization::Deserialize(
entity_slice, entity_columns_, &entity_blob_columns_);
if (!s_deser.ok()) {
status_ = s_deser;
validity_info_.Invalidate();
@@ -2064,8 +2065,9 @@ std::unique_ptr<BlobFetcher> CompactionIterator::CreateBlobFetcherIfNeeded(
}
}
return std::unique_ptr<BlobFetcher>(new BlobFetcher(
version, read_options, blob_file_cache, allow_write_path_fallback));
return std::unique_ptr<BlobFetcher>(
new OwningVersionBlobFetcher(version, std::move(read_options),
blob_file_cache, allow_write_path_fallback));
}
std::unique_ptr<PrefetchBufferCollection>
+26 -20
View File
@@ -1896,8 +1896,8 @@ TEST_F(WideColumnEntityBlobExtractionTest,
Slice value_slice(output_values[0]);
std::vector<WideColumn> output_columns;
std::vector<std::pair<size_t, BlobIndex>> blob_columns;
ASSERT_OK(WideColumnSerialization::DeserializeV2(value_slice, output_columns,
blob_columns));
ASSERT_OK(WideColumnSerialization::Deserialize(value_slice, output_columns,
&blob_columns));
// The large column should now be a blob reference
ASSERT_EQ(1, blob_columns.size());
@@ -1941,8 +1941,8 @@ TEST_F(WideColumnEntityBlobExtractionTest,
Slice value_slice(output_values[0]);
std::vector<WideColumn> output_columns;
std::vector<std::pair<size_t, BlobIndex>> blob_columns;
ASSERT_OK(WideColumnSerialization::DeserializeV2(value_slice, output_columns,
blob_columns));
ASSERT_OK(WideColumnSerialization::Deserialize(value_slice, output_columns,
&blob_columns));
// All three columns should be blob references
ASSERT_EQ(3, blob_columns.size());
@@ -2014,8 +2014,8 @@ TEST_F(WideColumnEntityBlobExtractionTest, WideColumnEntityMixedSizes) {
Slice value_slice(output_values[0]);
std::vector<WideColumn> output_columns;
std::vector<std::pair<size_t, BlobIndex>> blob_columns;
ASSERT_OK(WideColumnSerialization::DeserializeV2(value_slice, output_columns,
blob_columns));
ASSERT_OK(WideColumnSerialization::Deserialize(value_slice, output_columns,
&blob_columns));
// Only the large column should be a blob reference
ASSERT_EQ(1, blob_columns.size());
@@ -2058,8 +2058,8 @@ TEST_F(WideColumnEntityBlobExtractionTest,
Slice value_slice(output_values[0]);
std::vector<WideColumn> output_columns;
std::vector<std::pair<size_t, BlobIndex>> blob_columns;
ASSERT_OK(WideColumnSerialization::DeserializeV2(value_slice, output_columns,
blob_columns));
ASSERT_OK(WideColumnSerialization::Deserialize(value_slice, output_columns,
&blob_columns));
// The default column should be a blob reference
ASSERT_EQ(1, blob_columns.size());
@@ -2078,11 +2078,14 @@ TEST_F(WideColumnEntityBlobExtractionTest,
// Mock BlobFetcher for testing blob GC without a real Version
class MockBlobFetcher : public BlobFetcher {
public:
MockBlobFetcher() : BlobFetcher(nullptr /* version */, ReadOptions()) {}
MockBlobFetcher() = default;
using BlobFetcher::FetchBlob;
Status FetchBlob(const Slice& /* user_key */, const BlobIndex& blob_index,
FilePrefetchBuffer* /* prefetch_buffer */,
PinnableSlice* blob_value, uint64_t* bytes_read) const {
PinnableSlice* blob_value,
uint64_t* bytes_read) const override {
auto it = blob_data_.find(
std::make_pair(blob_index.file_number(), blob_index.offset()));
if (it == blob_data_.end()) {
@@ -2096,6 +2099,8 @@ class MockBlobFetcher : public BlobFetcher {
return Status::OK();
}
const ReadOptions& read_options() const override { return read_options_; }
void AddBlob(uint64_t file_number, uint64_t offset,
const std::string& value) {
blob_data_[std::make_pair(file_number, offset)] = value;
@@ -2103,6 +2108,7 @@ class MockBlobFetcher : public BlobFetcher {
private:
std::map<std::pair<uint64_t, uint64_t>, std::string> blob_data_;
ReadOptions read_options_;
};
// FakeCompaction that supports blob garbage collection
@@ -2372,8 +2378,8 @@ TEST_F(WideColumnEntityBlobGCTest, WideColumnEntityBlobGCRelocate) {
Slice value_slice(output_values[0]);
std::vector<WideColumn> output_columns;
std::vector<std::pair<size_t, BlobIndex>> blob_columns_out;
ASSERT_OK(WideColumnSerialization::DeserializeV2(value_slice, output_columns,
blob_columns_out));
ASSERT_OK(WideColumnSerialization::Deserialize(value_slice, output_columns,
&blob_columns_out));
// Entity structure should be preserved
ASSERT_EQ(2, output_columns.size());
@@ -2431,8 +2437,8 @@ TEST_F(WideColumnEntityBlobGCTest, WideColumnEntityBlobGCNoRelocateNewerFile) {
Slice value_slice(output_values[0]);
std::vector<WideColumn> output_columns;
std::vector<std::pair<size_t, BlobIndex>> blob_columns_out;
ASSERT_OK(WideColumnSerialization::DeserializeV2(value_slice, output_columns,
blob_columns_out));
ASSERT_OK(WideColumnSerialization::Deserialize(value_slice, output_columns,
&blob_columns_out));
// Blob column should still exist and reference the same file
ASSERT_EQ(1, blob_columns_out.size());
@@ -2491,8 +2497,8 @@ TEST_F(WideColumnEntityBlobGCTest, WideColumnEntityBlobGCPartialRelocation) {
Slice value_slice(output_values[0]);
std::vector<WideColumn> output_columns;
std::vector<std::pair<size_t, BlobIndex>> blob_columns_out;
ASSERT_OK(WideColumnSerialization::DeserializeV2(value_slice, output_columns,
blob_columns_out));
ASSERT_OK(WideColumnSerialization::Deserialize(value_slice, output_columns,
&blob_columns_out));
// All three columns should exist
ASSERT_EQ(3, output_columns.size());
@@ -2563,8 +2569,8 @@ TEST_F(WideColumnEntityBlobGCTest, WideColumnEntityBlobGCToInline) {
Slice value_slice(output_values[0]);
std::vector<WideColumn> output_columns;
std::vector<std::pair<size_t, BlobIndex>> blob_columns_out;
ASSERT_OK(WideColumnSerialization::DeserializeV2(value_slice, output_columns,
blob_columns_out));
ASSERT_OK(WideColumnSerialization::Deserialize(value_slice, output_columns,
&blob_columns_out));
// Both columns should exist
ASSERT_EQ(2, output_columns.size());
@@ -2788,8 +2794,8 @@ TEST_F(WideColumnEntityBlobGCTest,
Slice value_slice(output_values[0]);
std::vector<WideColumn> output_columns;
std::vector<std::pair<size_t, BlobIndex>> output_blob_columns;
ASSERT_OK(WideColumnSerialization::DeserializeV2(value_slice, output_columns,
output_blob_columns));
ASSERT_OK(WideColumnSerialization::Deserialize(value_slice, output_columns,
&output_blob_columns));
ASSERT_EQ(2, output_columns.size());
ASSERT_EQ("alpha", output_columns[0].name().ToString());
+47 -5
View File
@@ -777,6 +777,40 @@ static std::unordered_map<std::string, OptionTypeInfo>
OptionTypeFlags::kNone}},
};
namespace {
// Like OptionTypeInfo::Array<T, kSize> but deserialization tolerates
// size mismatches: extra elements beyond kSize are silently ignored,
// missing elements are zero-filled. Serialization and comparison are
// inherited from Array<T, kSize> unchanged; only the parse
// (deserialize) callback is overridden.
template <typename T, size_t kSize>
OptionTypeInfo SizeTolerantDeserializeArray(int offset,
OptionVerificationType verification,
OptionTypeFlags flags,
const OptionTypeInfo& elem_info,
char separator = ':') {
return OptionTypeInfo::Array<T, kSize>(offset, verification, flags, elem_info,
separator)
.SetParseFunc([elem_info, separator](
const ConfigOptions& opts, const std::string& name,
const std::string& value, void* addr) {
std::vector<T> parsed;
Status s =
ParseVector<T>(opts, elem_info, separator, name, value, &parsed);
if (!s.ok()) {
return Status(std::move(s));
}
auto* arr = static_cast<std::array<T, kSize>*>(addr);
for (size_t i = 0; i < kSize; ++i) {
(*arr)[i] = (i < parsed.size()) ? parsed[i] : T{};
}
return Status::OK();
});
}
} // namespace
static std::unordered_map<std::string, OptionTypeInfo>
compaction_stats_type_info = {
{"micros",
@@ -868,12 +902,20 @@ static std::unordered_map<std::string, OptionTypeInfo>
{offsetof(struct InternalStats::CompactionStats, count),
OptionType::kUInt64T, OptionVerificationType::kNormal,
OptionTypeFlags::kNone}},
// The remote worker may serialize a different number of
// CompactionReason counts than this primary expects (RocksDB versions
// can disagree on kNumOfReasons). Deserialization tolerates the
// mismatch: extra trailing elements are dropped, missing ones are
// zero-filled.
//
// The serialized width is intentionally kept at kNumOfReasons - 1 (the
// reduced width) so this change does NOT alter what is written -- only
// the read path becomes tolerant. Widening the serialized width to the
// full kNumOfReasons is a separate, later change that must not land
// until this tolerant read has been deployed to every primary,
// including rollback targets.
{"counts",
OptionTypeInfo::Array<
/* In release 11.2, a new compaction reason was added. This broken
* the reader and writer. To unblock release 11.1, we temporarily
* reduce the count array size to the old one. TODO add a proper
* serialization and deserialization method. */
SizeTolerantDeserializeArray<
int, static_cast<int>(CompactionReason::kNumOfReasons) - 1>(
offsetof(struct InternalStats::CompactionStats, counts),
OptionVerificationType::kNormal, OptionTypeFlags::kNone,
+129
View File
@@ -3,6 +3,8 @@
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#include <memory>
#include "db/db_test_util.h"
#include "file/file_util.h"
#include "port/stack_trace.h"
@@ -12,6 +14,71 @@
namespace ROCKSDB_NAMESPACE {
namespace {
// Append ":extra_value" to every "counts=...;" field in a serialized
// CompactionServiceResult string to simulate a worker with more
// CompactionReason enums than the reader. Loops because the serialized
// result contains multiple CompactionStats structs (output_level_stats,
// proximal_level_stats), each with its own "counts=" field.
void AppendToCountsFields(std::string* s, const std::string& extra_value) {
const std::string prefix = "counts=";
size_t pos = s->find(prefix);
while (pos != std::string::npos) {
// Find the semicolon that terminates this field's value.
size_t semi = s->find(';', pos + prefix.size());
assert(semi != std::string::npos);
// Insert ":extra_value" just before the semicolon.
s->insert(semi, ":" + extra_value);
// Advance past the inserted text to find the next occurrence.
pos = s->find(prefix, semi + extra_value.size() + 2);
}
}
// Remove the last colon-separated element from every "counts=...;" field
// to simulate a worker with fewer CompactionReason enums than the reader.
// Loops for the same reason as AppendToCountsFields (multiple counts fields).
void RemoveLastFromCountsFields(std::string* s) {
const std::string prefix = "counts=";
size_t pos = s->find(prefix);
while (pos != std::string::npos) {
size_t val_begin = pos + prefix.size();
size_t semi = s->find(';', val_begin);
assert(semi != std::string::npos);
// Find the last ':' separator within the value.
size_t last_sep = s->rfind(':', semi - 1);
assert(last_sep != std::string::npos && last_sep >= val_begin);
// Erase from the last ':' up to (not including) the ';'.
// e.g. "counts=0:3:5;" -> erase ":5" -> "counts=0:3;"
s->erase(last_sep, semi - last_sep);
pos = s->find(prefix, last_sep);
}
}
// Count the colon-separated elements in the first "counts=...;" field of a
// serialized CompactionServiceResult. Used to assert the serialized width
// (how many CompactionReason counts this binary writes).
size_t CountFirstCountsFieldElements(const std::string& s) {
const std::string prefix = "counts=";
size_t pos = s.find(prefix);
assert(pos != std::string::npos);
size_t val_begin = pos + prefix.size();
size_t semi = s.find(';', val_begin);
assert(semi != std::string::npos);
if (semi == val_begin) {
return 0;
}
size_t count = 1;
for (size_t i = val_begin; i < semi; ++i) {
if (s[i] == ':') {
++count;
}
}
return count;
}
} // namespace
class MyTestCompactionService : public CompactionService {
public:
MyTestCompactionService(
@@ -1609,6 +1676,68 @@ TEST_F(CompactionServiceTest, InvalidResultFallsBackToLocal) {
my_cs->GetFinalCompactionServiceJobStatus());
}
TEST_F(CompactionServiceTest, CompatCheckCountsWidthTolerance) {
// Number of per-CompactionReason counts this binary serializes and expects.
constexpr size_t kExpected =
static_cast<size_t>(CompactionReason::kNumOfReasons) - 1;
constexpr size_t kLastIndex = kExpected - 1;
constexpr int kFirstCount = 3;
constexpr int kLastCount = 5;
CompactionServiceResult result;
result.status = Status::OK();
result.output_level = 2;
result.output_path = "/tmp/output";
result.internal_stats.output_level_stats.counts[0] = kFirstCount;
result.internal_stats.output_level_stats.counts[kLastIndex] = kLastCount;
std::string serialized;
ASSERT_OK(result.Write(&serialized));
// Guard the produced format: this binary writes exactly kExpected counts.
// That element count is what a remote worker hands to the primary to read,
// so pinning it catches an accidental change to what is written.
ASSERT_EQ(CountFirstCountsFieldElements(serialized), kExpected);
// Peer agrees on kNumOfReasons: every written count is read back unchanged.
{
CompactionServiceResult parsed;
ASSERT_OK(CompactionServiceResult::Read(serialized, &parsed));
ASSERT_OK(parsed.status);
const auto& counts = parsed.internal_stats.output_level_stats.counts;
ASSERT_EQ(counts[0], kFirstCount);
ASSERT_EQ(counts[kLastIndex], kLastCount);
}
// Peer serialized one more count than this binary knows (a worker with an
// extra CompactionReason): the surplus trailing count is ignored, and the
// counts this binary understands are read unchanged.
{
std::string from_wider_peer = serialized;
AppendToCountsFields(&from_wider_peer, "7");
ASSERT_EQ(CountFirstCountsFieldElements(from_wider_peer), kExpected + 1);
CompactionServiceResult parsed;
ASSERT_OK(CompactionServiceResult::Read(from_wider_peer, &parsed));
ASSERT_OK(parsed.status);
const auto& counts = parsed.internal_stats.output_level_stats.counts;
ASSERT_EQ(counts[0], kFirstCount);
ASSERT_EQ(counts[kLastIndex], kLastCount);
}
// Peer serialized one fewer count than this binary knows (a worker missing
// the newest CompactionReason): the absent trailing count reads as zero.
{
std::string from_narrower_peer = serialized;
RemoveLastFromCountsFields(&from_narrower_peer);
ASSERT_EQ(CountFirstCountsFieldElements(from_narrower_peer), kExpected - 1);
CompactionServiceResult parsed;
ASSERT_OK(CompactionServiceResult::Read(from_narrower_peer, &parsed));
ASSERT_OK(parsed.status);
const auto& counts = parsed.internal_stats.output_level_stats.counts;
ASSERT_EQ(counts[0], kFirstCount);
ASSERT_EQ(counts[kLastIndex], 0);
}
}
TEST_F(CompactionServiceTest, SubCompaction) {
Options options = CurrentOptions();
options.max_subcompactions = 10;
+357 -269
View File
@@ -10,6 +10,9 @@
#include <cstring>
#include "db/db_test_util.h"
#include "db/log_writer.h"
#include "db/version_edit.h"
#include "file/writable_file_writer.h"
#include "options/options_helper.h"
#include "port/stack_trace.h"
#include "rocksdb/filter_policy.h"
@@ -78,6 +81,7 @@ class MyFlushBlockPolicyFactory : public FlushBlockPolicyFactory {
private:
const int num_keys_in_block_;
};
} // namespace
static bool enable_io_uring = true;
@@ -88,6 +92,15 @@ class DBBasicTest : public DBTestBase {
DBBasicTest() : DBTestBase("db_basic_test", /*env_do_fsync=*/false) {}
};
class DBBasicGetWithParam : public DBBasicTest,
public testing::WithParamInterface<bool> {
public:
DBBasicGetWithParam() : DBBasicTest(), use_coroutine_(GetParam()) {}
protected:
const bool use_coroutine_;
};
TEST_F(DBBasicTest, OpenWhenOpen) {
Options options = CurrentOptions();
options.env = env_;
@@ -207,8 +220,9 @@ TEST_F(DBBasicTest, OptimizeManifestForRecoveryEmitsPerCFWhenLogAdvances) {
ASSERT_EQ("v", Get("k"));
}
// With track_and_verify_wals_in_manifest=true, the wal_deletion edit
// must STILL be emitted (the DeleteWalsBefore record is required).
// With track_and_verify_wals_in_manifest=true and a clean close, the
// close-time code already advances WalSet markers. Recovery's
// DeleteWalsBefore is redundant and should be skipped.
TEST_F(DBBasicTest, OptimizeManifestForRecoveryPreservesWalTracking) {
Options options = CurrentOptions();
options.create_if_missing = true;
@@ -224,7 +238,7 @@ TEST_F(DBBasicTest, OptimizeManifestForRecoveryPreservesWalTracking) {
Reopen(options);
counters.Uninstall();
ASSERT_EQ(0, counters.wal_deletion.load());
ASSERT_EQ(1, counters.wal_deletion.load());
}
// best_efforts_recovery requires a fresh MANIFEST + CURRENT to be
@@ -497,11 +511,60 @@ TEST_F(DBBasicTest, OptimizeManifestForRecoveryEmitsWalDeletionWhenTracking) {
counters.Uninstall();
ASSERT_GT(counters.per_cf.load(), 0);
ASSERT_EQ(1, records.load());
ASSERT_EQ(0, records.load());
ASSERT_GT(dbfull()->GetVersionSet()->GetWalSet().GetMinWalNumberToKeep(), 0u);
ASSERT_EQ("v", Get("k"));
}
// Zero MANIFEST growth on clean reopen with optimize_manifest_for_recovery=1,
// reuse_manifest_on_open=1, and track_and_verify_wals_in_manifest=1.
// Regression test for the stress test failure where a redundant
// DeleteWalsBefore edit was written during recovery.
TEST_F(DBBasicTest, OptimizeManifestForRecoveryZeroGrowthOnCleanReopen) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.optimize_manifest_for_recovery = true;
options.reuse_manifest_on_open = true;
options.track_and_verify_wals_in_manifest = true;
options.avoid_flush_during_recovery = true;
options.write_dbid_to_manifest = false;
DestroyAndReopen(options);
ASSERT_OK(Put("k", "v"));
ASSERT_OK(Flush());
Close();
// Get MANIFEST size after close
std::vector<std::string> files;
ASSERT_OK(env_->GetChildren(dbname_, &files));
uint64_t manifest_size_before = 0;
for (const auto& f : files) {
if (f.find("MANIFEST-") == 0) {
ASSERT_OK(env_->GetFileSize(dbname_ + "/" + f, &manifest_size_before));
break;
}
}
ASSERT_GT(manifest_size_before, 0u);
// Reopen - should not grow MANIFEST at all
Reopen(options);
// Get MANIFEST size after reopen
files.clear();
ASSERT_OK(env_->GetChildren(dbname_, &files));
uint64_t manifest_size_after = 0;
for (const auto& f : files) {
if (f.find("MANIFEST-") == 0) {
ASSERT_OK(env_->GetFileSize(dbname_ + "/" + f, &manifest_size_after));
break;
}
}
ASSERT_EQ(manifest_size_before, manifest_size_after)
<< "MANIFEST grew by " << (manifest_size_after - manifest_size_before)
<< " bytes on clean reopen; expected zero growth";
ASSERT_EQ("v", Get("k"));
}
// Regression for the close-time marker path: if a new WAL is created while an
// otherwise-empty user CF stays empty, Close() must reserve the next file
// number before persisting SetLogNumber(cur_wal + 1) for that CF.
@@ -958,6 +1021,92 @@ TEST_F(DBBasicTest, ReuseManifestOnOpenSkipsOnTailCorruption) {
ASSERT_EQ("v", Get("k"));
}
// Regression test for corrupted atomic group when reuse_manifest_on_open
// appends after an incomplete atomic group at the MANIFEST tail.
// Before the fix, last_valid_record_end_ included records from an incomplete
// trailing atomic group, causing ReopenManifestForAppend to append new records
// after them. On subsequent recovery the incomplete group followed by new
// records triggers "corrupted atomic group".
TEST_F(DBBasicTest, ReuseManifestOnOpenIncompleteAtomicGroupAtTail) {
Options options = CurrentOptions();
options.create_if_missing = true;
options.reuse_manifest_on_open = true;
DestroyAndReopen(options);
ASSERT_OK(Put("k", "v"));
ASSERT_OK(Flush());
Close();
// Find the MANIFEST file.
std::string manifest_path;
{
std::vector<std::string> files;
ASSERT_OK(env_->GetChildren(dbname_, &files));
for (const auto& f : files) {
uint64_t number;
FileType type;
if (ParseFileName(f, &number, &type) && type == kDescriptorFile) {
manifest_path = dbname_ + "/" + f;
break;
}
}
}
ASSERT_FALSE(manifest_path.empty());
// Append an incomplete atomic group (2 records of a 3-member group) to the
// MANIFEST. These are valid log records with correct CRCs but the atomic
// group is not complete (missing the third record with remaining_entries=0).
{
uint64_t file_size;
ASSERT_OK(env_->GetFileSize(manifest_path, &file_size));
const auto& fs = env_->GetFileSystem();
std::unique_ptr<FSWritableFile> fs_file;
ASSERT_OK(fs->ReopenWritableFile(manifest_path, FileOptions(), &fs_file,
nullptr));
std::unique_ptr<WritableFileWriter> file_writer(new WritableFileWriter(
std::move(fs_file), manifest_path, FileOptions()));
log::Writer log_writer(std::move(file_writer), 0, false, false,
kNoCompression, false, file_size % log::kBlockSize);
// Write 2 records of a 3-member atomic group
VersionEdit edit1;
edit1.SetLogNumber(0);
edit1.SetNextFile(100);
edit1.SetLastSequence(100);
edit1.MarkAtomicGroup(2); // remaining_entries=2, expects 3 total
std::string record1;
ASSERT_TRUE(edit1.EncodeTo(&record1, 0));
ASSERT_OK(log_writer.AddRecord(WriteOptions(), record1));
VersionEdit edit2;
edit2.SetLogNumber(0);
edit2.SetNextFile(100);
edit2.SetLastSequence(100);
edit2.MarkAtomicGroup(1); // remaining_entries=1
std::string record2;
ASSERT_TRUE(edit2.EncodeTo(&record2, 0));
ASSERT_OK(log_writer.AddRecord(WriteOptions(), record2));
// Deliberately NOT writing the third record (remaining_entries=0)
}
// Reopen: recovery should succeed (incomplete trailing group is ignored).
// With the fix, ReopenManifestForAppend will NOT reuse the MANIFEST because
// manifest_last_valid_record_end_ < physical_size (the incomplete group
// records are excluded from the valid end).
Reopen(options);
ASSERT_EQ("v", Get("k"));
// Write new data to create new MANIFEST records.
ASSERT_OK(Put("k2", "v2"));
ASSERT_OK(Flush());
Close();
// Final reopen: must NOT fail with "corrupted atomic group".
Reopen(options);
ASSERT_EQ("v", Get("k"));
ASSERT_EQ("v2", Get("k2"));
Close();
}
TEST_F(DBBasicTest, EnableDirectIOWithZeroBuf) {
if (!IsDirectIOSupported()) {
ROCKSDB_GTEST_BYPASS("Direct IO not supported");
@@ -1320,27 +1469,36 @@ TEST_F(DBBasicTest, LevelLimitReopen) {
ASSERT_OK(TryReopenWithColumnFamilies({"default", "pikachu"}, options));
}
TEST_F(DBBasicTest, PutDeleteGet) {
TEST_P(DBBasicGetWithParam, PutDeleteGet) {
do {
CreateAndReopenWithCF({"pikachu"}, CurrentOptions());
ASSERT_OK(Put(1, "foo", "v1"));
ASSERT_EQ("v1", Get(1, "foo"));
ASSERT_EQ("v1", Get(1, "foo", nullptr, use_coroutine_));
ASSERT_OK(Put(1, "foo", "v2"));
ASSERT_EQ("v2", Get(1, "foo"));
ASSERT_EQ("v2", Get(1, "foo", nullptr, use_coroutine_));
ASSERT_OK(Delete(1, "foo"));
ASSERT_EQ("NOT_FOUND", Get(1, "foo"));
ASSERT_EQ("NOT_FOUND", Get(1, "foo", nullptr, use_coroutine_));
ASSERT_OK(Put("default_foo", "default_v1"));
ASSERT_EQ("default_v1",
Get("default_foo", static_cast<const Snapshot*>(nullptr),
use_coroutine_));
PinnableSlice default_pinnable_value;
ASSERT_OK(Get("default_foo", &default_pinnable_value, use_coroutine_));
ASSERT_EQ("default_v1", default_pinnable_value.ToString());
} while (ChangeOptions());
}
TEST_F(DBBasicTest, PutSingleDeleteGet) {
TEST_P(DBBasicGetWithParam, PutSingleDeleteGet) {
do {
CreateAndReopenWithCF({"pikachu"}, CurrentOptions());
ASSERT_OK(Put(1, "foo", "v1"));
ASSERT_EQ("v1", Get(1, "foo"));
ASSERT_EQ("v1", Get(1, "foo", nullptr, use_coroutine_));
ASSERT_OK(Put(1, "foo2", "v2"));
ASSERT_EQ("v2", Get(1, "foo2"));
ASSERT_EQ("v2", Get(1, "foo2", nullptr, use_coroutine_));
ASSERT_OK(SingleDelete(1, "foo"));
ASSERT_EQ("NOT_FOUND", Get(1, "foo"));
ASSERT_EQ("NOT_FOUND", Get(1, "foo", nullptr, use_coroutine_));
// Ski FIFO and universal compaction because they do not apply to the test
// case. Skip MergePut because single delete does not get removed when it
// encounters a merge.
@@ -1348,40 +1506,40 @@ TEST_F(DBBasicTest, PutSingleDeleteGet) {
kSkipMergePut));
}
TEST_F(DBBasicTest, TimedPutBasic) {
TEST_P(DBBasicGetWithParam, TimedPutBasic) {
do {
Options options = CurrentOptions();
options.merge_operator = MergeOperators::CreateStringAppendOperator();
CreateAndReopenWithCF({"pikachu"}, options);
ASSERT_OK(TimedPut(1, "foo", "v1", /*write_unix_time=*/0));
// Read from memtable
ASSERT_EQ("v1", Get(1, "foo"));
ASSERT_EQ("v1", Get(1, "foo", nullptr, use_coroutine_));
ASSERT_OK(TimedPut(1, "foo", "v2.1", /*write_unix_time=*/3));
ASSERT_EQ("v2.1", Get(1, "foo"));
ASSERT_EQ("v2.1", Get(1, "foo", nullptr, use_coroutine_));
// Read from sst file
ASSERT_OK(db_->Flush(FlushOptions(), handles_[1]));
ASSERT_OK(Merge(1, "foo", "v2.2"));
ASSERT_EQ("v2.1,v2.2", Get(1, "foo"));
ASSERT_EQ("v2.1,v2.2", Get(1, "foo", nullptr, use_coroutine_));
ASSERT_OK(Delete(1, "foo"));
ASSERT_EQ("NOT_FOUND", Get(1, "foo"));
ASSERT_EQ("NOT_FOUND", Get(1, "foo", nullptr, use_coroutine_));
ASSERT_OK(TimedPut(1, "bar", "bv1", /*write_unix_time=*/0));
ASSERT_EQ("bv1", Get(1, "bar"));
ASSERT_EQ("bv1", Get(1, "bar", nullptr, use_coroutine_));
ASSERT_OK(TimedPut(1, "baz", "bzv1", /*write_unix_time=*/0));
ASSERT_EQ("bzv1", Get(1, "baz"));
ASSERT_EQ("bzv1", Get(1, "baz", nullptr, use_coroutine_));
if (option_config_ != kRowCache) {
std::string range_del_begin = "b";
std::string range_del_end = "baz";
Slice begin_rdel = range_del_begin, end_rdel = range_del_end;
ASSERT_OK(
db_->DeleteRange(WriteOptions(), handles_[1], begin_rdel, end_rdel));
ASSERT_EQ("NOT_FOUND", Get(1, "bar"));
ASSERT_EQ("NOT_FOUND", Get(1, "bar", nullptr, use_coroutine_));
}
ASSERT_EQ("bzv1", Get(1, "baz"));
ASSERT_EQ("bzv1", Get(1, "baz", nullptr, use_coroutine_));
ASSERT_OK(SingleDelete(1, "baz"));
ASSERT_EQ("NOT_FOUND", Get(1, "baz"));
ASSERT_EQ("NOT_FOUND", Get(1, "baz", nullptr, use_coroutine_));
} while (ChangeOptions(kSkipPlainTable));
}
@@ -1407,17 +1565,17 @@ TEST_F(DBBasicTest, EmptyFlush) {
kSkipMergePut));
}
TEST_F(DBBasicTest, GetFromVersions) {
TEST_P(DBBasicGetWithParam, GetFromVersions) {
do {
CreateAndReopenWithCF({"pikachu"}, CurrentOptions());
ASSERT_OK(Put(1, "foo", "v1"));
ASSERT_OK(Flush(1));
ASSERT_EQ("v1", Get(1, "foo"));
ASSERT_EQ("NOT_FOUND", Get(0, "foo"));
ASSERT_EQ("v1", Get(1, "foo", nullptr, use_coroutine_));
ASSERT_EQ("NOT_FOUND", Get(0, "foo", nullptr, use_coroutine_));
} while (ChangeOptions());
}
TEST_F(DBBasicTest, GetSnapshot) {
TEST_P(DBBasicGetWithParam, GetSnapshot) {
anon::OptionsOverride options_override;
options_override.skip_policy = kSkipNoSnapshot;
do {
@@ -1428,11 +1586,11 @@ TEST_F(DBBasicTest, GetSnapshot) {
ASSERT_OK(Put(1, key, "v1"));
const Snapshot* s1 = db_->GetSnapshot();
ASSERT_OK(Put(1, key, "v2"));
ASSERT_EQ("v2", Get(1, key));
ASSERT_EQ("v1", Get(1, key, s1));
ASSERT_EQ("v2", Get(1, key, nullptr, use_coroutine_));
ASSERT_EQ("v1", Get(1, key, s1, use_coroutine_));
ASSERT_OK(Flush(1));
ASSERT_EQ("v2", Get(1, key));
ASSERT_EQ("v1", Get(1, key, s1));
ASSERT_EQ("v2", Get(1, key, nullptr, use_coroutine_));
ASSERT_EQ("v1", Get(1, key, s1, use_coroutine_));
db_->ReleaseSnapshot(s1);
}
} while (ChangeOptions());
@@ -1689,7 +1847,7 @@ TEST_F(DBBasicTest, LockFileRecovery) {
}
}
TEST_F(DBBasicTest, Snapshot) {
TEST_P(DBBasicGetWithParam, Snapshot) {
env_->SetMockSleep();
anon::OptionsOverride options_override;
options_override.skip_policy = kSkipNoSnapshot;
@@ -1723,31 +1881,31 @@ TEST_F(DBBasicTest, Snapshot) {
ASSERT_OK(Put(0, "foo", "0v4"));
ASSERT_OK(Put(1, "foo", "1v4"));
ASSERT_EQ("0v1", Get(0, "foo", s1));
ASSERT_EQ("1v1", Get(1, "foo", s1));
ASSERT_EQ("0v2", Get(0, "foo", s2));
ASSERT_EQ("1v2", Get(1, "foo", s2));
ASSERT_EQ("0v3", Get(0, "foo", s3.snapshot()));
ASSERT_EQ("1v3", Get(1, "foo", s3.snapshot()));
ASSERT_EQ("0v4", Get(0, "foo"));
ASSERT_EQ("1v4", Get(1, "foo"));
ASSERT_EQ("0v1", Get(0, "foo", s1, use_coroutine_));
ASSERT_EQ("1v1", Get(1, "foo", s1, use_coroutine_));
ASSERT_EQ("0v2", Get(0, "foo", s2, use_coroutine_));
ASSERT_EQ("1v2", Get(1, "foo", s2, use_coroutine_));
ASSERT_EQ("0v3", Get(0, "foo", s3.snapshot(), use_coroutine_));
ASSERT_EQ("1v3", Get(1, "foo", s3.snapshot(), use_coroutine_));
ASSERT_EQ("0v4", Get(0, "foo", nullptr, use_coroutine_));
ASSERT_EQ("1v4", Get(1, "foo", nullptr, use_coroutine_));
}
ASSERT_EQ(2U, GetNumSnapshots());
ASSERT_EQ(time_snap1, GetTimeOldestSnapshots());
ASSERT_EQ(GetSequenceOldestSnapshots(), s1->GetSequenceNumber());
ASSERT_EQ("0v1", Get(0, "foo", s1));
ASSERT_EQ("1v1", Get(1, "foo", s1));
ASSERT_EQ("0v2", Get(0, "foo", s2));
ASSERT_EQ("1v2", Get(1, "foo", s2));
ASSERT_EQ("0v4", Get(0, "foo"));
ASSERT_EQ("1v4", Get(1, "foo"));
ASSERT_EQ("0v1", Get(0, "foo", s1, use_coroutine_));
ASSERT_EQ("1v1", Get(1, "foo", s1, use_coroutine_));
ASSERT_EQ("0v2", Get(0, "foo", s2, use_coroutine_));
ASSERT_EQ("1v2", Get(1, "foo", s2, use_coroutine_));
ASSERT_EQ("0v4", Get(0, "foo", nullptr, use_coroutine_));
ASSERT_EQ("1v4", Get(1, "foo", nullptr, use_coroutine_));
db_->ReleaseSnapshot(s1);
ASSERT_EQ("0v2", Get(0, "foo", s2));
ASSERT_EQ("1v2", Get(1, "foo", s2));
ASSERT_EQ("0v4", Get(0, "foo"));
ASSERT_EQ("1v4", Get(1, "foo"));
ASSERT_EQ("0v2", Get(0, "foo", s2, use_coroutine_));
ASSERT_EQ("1v2", Get(1, "foo", s2, use_coroutine_));
ASSERT_EQ("0v4", Get(0, "foo", nullptr, use_coroutine_));
ASSERT_EQ("1v4", Get(1, "foo", nullptr, use_coroutine_));
ASSERT_EQ(1U, GetNumSnapshots());
ASSERT_LT(time_snap1, GetTimeOldestSnapshots());
ASSERT_EQ(GetSequenceOldestSnapshots(), s2->GetSequenceNumber());
@@ -1755,11 +1913,19 @@ TEST_F(DBBasicTest, Snapshot) {
db_->ReleaseSnapshot(s2);
ASSERT_EQ(0U, GetNumSnapshots());
ASSERT_EQ(GetSequenceOldestSnapshots(), 0);
ASSERT_EQ("0v4", Get(0, "foo"));
ASSERT_EQ("1v4", Get(1, "foo"));
ASSERT_EQ("0v4", Get(0, "foo", nullptr, use_coroutine_));
ASSERT_EQ("1v4", Get(1, "foo", nullptr, use_coroutine_));
} while (ChangeOptions());
}
#if USE_COROUTINES
INSTANTIATE_TEST_CASE_P(DBBasicGetWithParam, DBBasicGetWithParam,
testing::Bool());
#else
INSTANTIATE_TEST_CASE_P(DBBasicGetWithParam, DBBasicGetWithParam,
testing::Values(false));
#endif // USE_COROUTINES
class DBBasicMultiConfigs : public DBBasicTest,
public ::testing::WithParamInterface<int> {
public:
@@ -3708,11 +3874,15 @@ INSTANTIATE_TEST_CASE_P(DBMultiGetTestWithParam, DBMultiGetTestWithParam,
testing::Combine(testing::Bool(), testing::Bool()));
#if USE_COROUTINES
class DBMultiGetAsyncIOTest : public DBBasicTest,
public ::testing::WithParamInterface<bool> {
class DBMultiGetAsyncIOTest
: public DBBasicTest,
public ::testing::WithParamInterface<std::tuple<bool, bool>> {
public:
DBMultiGetAsyncIOTest()
: DBBasicTest(), statistics_(ROCKSDB_NAMESPACE::CreateDBStatistics()) {
: DBBasicTest(),
optimize_multiget_for_io_(std::get<0>(GetParam())),
use_coroutine_(std::get<1>(GetParam())),
statistics_(ROCKSDB_NAMESPACE::CreateDBStatistics()) {
BlockBasedTableOptions bbto;
bbto.filter_policy.reset(NewBloomFilterPolicy(10));
options_ = CurrentOptions();
@@ -3786,6 +3956,20 @@ class DBMultiGetAsyncIOTest : public DBBasicTest,
const std::shared_ptr<Statistics>& statistics() { return statistics_; }
protected:
void AssertMultiGetIOBatchSize(uint64_t expected_count,
uint64_t expected_max) {
HistogramData multiget_io_batch_size;
statistics()->histogramData(MULTIGET_IO_BATCH_SIZE,
&multiget_io_batch_size);
ASSERT_EQ(multiget_io_batch_size.count, expected_count);
ASSERT_EQ(multiget_io_batch_size.max, expected_max);
}
bool UseCoroutineRead() const {
return use_coroutine_ &&
options_.env->GetFileSystem()->GetReadExecutor() != nullptr;
}
void PrepareDBForTest() {
#ifdef ROCKSDB_IOURING_PRESENT
Reopen(options_);
@@ -3801,6 +3985,9 @@ class DBMultiGetAsyncIOTest : public DBBasicTest,
void ReopenDB() { Reopen(options_); }
const bool optimize_multiget_for_io_;
const bool use_coroutine_;
private:
std::shared_ptr<Statistics> statistics_;
Options options_;
@@ -3809,39 +3996,28 @@ class DBMultiGetAsyncIOTest : public DBBasicTest,
TEST_P(DBMultiGetAsyncIOTest, GetFromL0) {
// All 3 keys in L0. The L0 files should be read serially.
std::vector<std::string> key_strs{Key(0), Key(40), Key(80)};
std::vector<Slice> keys{key_strs[0], key_strs[1], key_strs[2]};
std::vector<PinnableSlice> values(key_strs.size());
std::vector<Status> statuses(key_strs.size());
PrepareDBForTest();
ReadOptions ro;
ro.async_io = true;
ro.optimize_multiget_for_io = GetParam();
dbfull()->MultiGet(ro, dbfull()->DefaultColumnFamily(), keys.size(),
keys.data(), values.data(), statuses.data());
std::vector<std::string> values =
MultiGet(key_strs, /*snapshot=*/nullptr, /*async=*/true,
optimize_multiget_for_io_, use_coroutine_);
ASSERT_EQ(values.size(), 3);
ASSERT_OK(statuses[0]);
ASSERT_OK(statuses[1]);
ASSERT_OK(statuses[2]);
ASSERT_EQ(values[0], "val_l0_" + std::to_string(0));
ASSERT_EQ(values[1], "val_l0_" + std::to_string(40));
ASSERT_EQ(values[2], "val_l0_" + std::to_string(80));
HistogramData multiget_io_batch_size;
statistics()->histogramData(MULTIGET_IO_BATCH_SIZE, &multiget_io_batch_size);
// With async IO, lookups will happen in parallel for each key
#ifdef ROCKSDB_IOURING_PRESENT
if (GetParam()) {
ASSERT_EQ(multiget_io_batch_size.count, 1);
ASSERT_EQ(multiget_io_batch_size.max, 3);
if (UseCoroutineRead()) {
AssertMultiGetIOBatchSize(3, 1);
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 3);
} else if (optimize_multiget_for_io_) {
AssertMultiGetIOBatchSize(1, 3);
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 3);
} else {
// Without Async IO, MultiGet will call MultiRead 3 times, once for each
// L0 file
ASSERT_EQ(multiget_io_batch_size.count, 3);
AssertMultiGetIOBatchSize(3, 1);
}
#else // ROCKSDB_IOURING_PRESENT
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 0);
@@ -3849,64 +4025,58 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL0) {
}
TEST_P(DBMultiGetAsyncIOTest, GetFromL1) {
std::vector<std::string> key_strs;
std::vector<Slice> keys;
std::vector<PinnableSlice> values;
std::vector<Status> statuses;
key_strs.push_back(Key(33));
key_strs.push_back(Key(54));
key_strs.push_back(Key(102));
keys.emplace_back(key_strs[0]);
keys.emplace_back(key_strs[1]);
keys.emplace_back(key_strs[2]);
values.resize(keys.size());
statuses.resize(keys.size());
std::vector<std::string> key_strs{Key(33), Key(54), Key(102)};
PrepareDBForTest();
ReadOptions ro;
ro.async_io = true;
ro.optimize_multiget_for_io = GetParam();
dbfull()->MultiGet(ro, dbfull()->DefaultColumnFamily(), keys.size(),
keys.data(), values.data(), statuses.data());
std::vector<std::string> values =
MultiGet(key_strs, /*snapshot=*/nullptr, /*async=*/true,
optimize_multiget_for_io_, use_coroutine_);
ASSERT_EQ(values.size(), 3);
ASSERT_EQ(statuses[0], Status::OK());
ASSERT_EQ(statuses[1], Status::OK());
ASSERT_EQ(statuses[2], Status::OK());
ASSERT_EQ(values[0], "val_l1_" + std::to_string(33));
ASSERT_EQ(values[1], "val_l1_" + std::to_string(54));
ASSERT_EQ(values[2], "val_l1_" + std::to_string(102));
HistogramData multiget_io_batch_size;
statistics()->histogramData(MULTIGET_IO_BATCH_SIZE, &multiget_io_batch_size);
#ifdef ROCKSDB_IOURING_PRESENT
// A batch of 3 async IOs is expected, one for each overlapping file in L1
ASSERT_EQ(multiget_io_batch_size.count, 1);
ASSERT_EQ(multiget_io_batch_size.max, 3);
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 3);
if (UseCoroutineRead()) {
AssertMultiGetIOBatchSize(3, 1);
} else {
// A batch of 3 async IOs is expected, one for each overlapping file in L1.
AssertMultiGetIOBatchSize(1, 3);
}
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT),
UseCoroutineRead() ? 6 : 3);
#else // ROCKSDB_IOURING_PRESENT
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 0);
#endif // ROCKSDB_IOURING_PRESENT
}
TEST_P(DBMultiGetAsyncIOTest, SingleGetFromL1UsesCoroutineRead) {
int coroutine_read_count = 0;
SyncPoint::GetInstance()->SetCallBack(
"RandomAccessFileReader::ReadCoroutine:SubmitReadAsync",
[&](void*) { ++coroutine_read_count; });
SyncPoint::GetInstance()->EnableProcessing();
PrepareDBForTest();
ASSERT_EQ(
"val_l1_" + std::to_string(33),
Get(Key(33), static_cast<const Snapshot*>(nullptr), use_coroutine_));
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
if (UseCoroutineRead()) {
ASSERT_GT(coroutine_read_count, 0);
} else {
ASSERT_EQ(coroutine_read_count, 0);
}
}
#ifdef ROCKSDB_IOURING_PRESENT
TEST_P(DBMultiGetAsyncIOTest, GetFromL1Error) {
std::vector<std::string> key_strs;
std::vector<Slice> keys;
std::vector<PinnableSlice> values;
std::vector<Status> statuses;
key_strs.push_back(Key(33));
key_strs.push_back(Key(54));
key_strs.push_back(Key(102));
keys.emplace_back(key_strs[0]);
keys.emplace_back(key_strs[1]);
keys.emplace_back(key_strs[2]);
values.resize(keys.size());
statuses.resize(keys.size());
std::vector<std::string> key_strs{Key(33), Key(54), Key(102)};
int count = 0;
SyncPoint::GetInstance()->SetCallBack(
@@ -3935,147 +4105,100 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL1Error) {
PrepareDBForTest();
ReadOptions ro;
ro.async_io = true;
ro.optimize_multiget_for_io = GetParam();
dbfull()->MultiGet(ro, dbfull()->DefaultColumnFamily(), keys.size(),
keys.data(), values.data(), statuses.data());
std::vector<std::string> values =
MultiGet(key_strs, /*snapshot=*/nullptr, /*async=*/true,
optimize_multiget_for_io_, use_coroutine_);
SyncPoint::GetInstance()->DisableProcessing();
ASSERT_EQ(values.size(), 3);
ASSERT_EQ(statuses[0], Status::OK());
ASSERT_EQ(statuses[1], Status::OK());
ASSERT_EQ(statuses[2], Status::IOError());
ASSERT_EQ(values[0], "val_l1_" + std::to_string(33));
ASSERT_EQ(values[1], "val_l1_" + std::to_string(54));
ASSERT_EQ(values[2], Status::IOError().ToString());
HistogramData multiget_io_batch_size;
statistics()->histogramData(MULTIGET_IO_BATCH_SIZE, &multiget_io_batch_size);
// A batch of 3 async IOs is expected, one for each overlapping file in L1
ASSERT_EQ(multiget_io_batch_size.count, 1);
ASSERT_EQ(multiget_io_batch_size.max, 2);
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 2);
if (UseCoroutineRead()) {
AssertMultiGetIOBatchSize(2, 1);
} else {
// A batch of 3 async IOs is expected, one for each overlapping file in L1.
AssertMultiGetIOBatchSize(1, 2);
}
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT),
UseCoroutineRead() ? 5 : 2);
}
#endif // ROCKSDB_IOURING_PRESENT
TEST_P(DBMultiGetAsyncIOTest, LastKeyInFile) {
std::vector<std::string> key_strs;
std::vector<Slice> keys;
std::vector<PinnableSlice> values;
std::vector<Status> statuses;
// 21 is the last key in the first L1 file
key_strs.push_back(Key(21));
key_strs.push_back(Key(54));
key_strs.push_back(Key(102));
keys.emplace_back(key_strs[0]);
keys.emplace_back(key_strs[1]);
keys.emplace_back(key_strs[2]);
values.resize(keys.size());
statuses.resize(keys.size());
std::vector<std::string> key_strs{Key(21), Key(54), Key(102)};
PrepareDBForTest();
ReadOptions ro;
ro.async_io = true;
ro.optimize_multiget_for_io = GetParam();
dbfull()->MultiGet(ro, dbfull()->DefaultColumnFamily(), keys.size(),
keys.data(), values.data(), statuses.data());
std::vector<std::string> values =
MultiGet(key_strs, /*snapshot=*/nullptr, /*async=*/true,
optimize_multiget_for_io_, use_coroutine_);
ASSERT_EQ(values.size(), 3);
ASSERT_EQ(statuses[0], Status::OK());
ASSERT_EQ(statuses[1], Status::OK());
ASSERT_EQ(statuses[2], Status::OK());
ASSERT_EQ(values[0], "val_l1_" + std::to_string(21));
ASSERT_EQ(values[1], "val_l1_" + std::to_string(54));
ASSERT_EQ(values[2], "val_l1_" + std::to_string(102));
#ifdef ROCKSDB_IOURING_PRESENT
HistogramData multiget_io_batch_size;
statistics()->histogramData(MULTIGET_IO_BATCH_SIZE, &multiget_io_batch_size);
// Since the first MultiGet key is the last key in a file, the MultiGet is
// expected to lookup in that file first, before moving on to other files.
// So the first file lookup will issue one async read, and the next lookup
// will lookup 2 files in parallel and issue 2 async reads
ASSERT_EQ(multiget_io_batch_size.count, 2);
ASSERT_EQ(multiget_io_batch_size.max, 2);
if (UseCoroutineRead()) {
AssertMultiGetIOBatchSize(3, 1);
} else {
AssertMultiGetIOBatchSize(2, 2);
}
#endif // ROCKSDB_IOURING_PRESENT
}
TEST_P(DBMultiGetAsyncIOTest, GetFromL1AndL2) {
std::vector<std::string> key_strs;
std::vector<Slice> keys;
std::vector<PinnableSlice> values;
std::vector<Status> statuses;
// 33 and 102 are in L1, and 56 is in L2
key_strs.push_back(Key(33));
key_strs.push_back(Key(56));
key_strs.push_back(Key(102));
keys.emplace_back(key_strs[0]);
keys.emplace_back(key_strs[1]);
keys.emplace_back(key_strs[2]);
values.resize(keys.size());
statuses.resize(keys.size());
std::vector<std::string> key_strs{Key(33), Key(56), Key(102)};
PrepareDBForTest();
ReadOptions ro;
ro.async_io = true;
ro.optimize_multiget_for_io = GetParam();
dbfull()->MultiGet(ro, dbfull()->DefaultColumnFamily(), keys.size(),
keys.data(), values.data(), statuses.data());
std::vector<std::string> values =
MultiGet(key_strs, /*snapshot=*/nullptr, /*async=*/true,
optimize_multiget_for_io_, use_coroutine_);
ASSERT_EQ(values.size(), 3);
ASSERT_EQ(statuses[0], Status::OK());
ASSERT_EQ(statuses[1], Status::OK());
ASSERT_EQ(statuses[2], Status::OK());
ASSERT_EQ(values[0], "val_l1_" + std::to_string(33));
ASSERT_EQ(values[1], "val_l2_" + std::to_string(56));
ASSERT_EQ(values[2], "val_l1_" + std::to_string(102));
#ifdef ROCKSDB_IOURING_PRESENT
HistogramData multiget_io_batch_size;
statistics()->histogramData(MULTIGET_IO_BATCH_SIZE, &multiget_io_batch_size);
// There are 2 keys in L1 in twp separate files, and 1 in L2. With
// optimize_multiget_for_io, all three lookups will happen in parallel.
// Otherwise, the L2 lookup will happen after L1.
ASSERT_EQ(multiget_io_batch_size.count, GetParam() ? 1 : 2);
ASSERT_EQ(multiget_io_batch_size.max, GetParam() ? 3 : 2);
if (UseCoroutineRead()) {
AssertMultiGetIOBatchSize(3, 1);
} else {
AssertMultiGetIOBatchSize(optimize_multiget_for_io_ ? 1 : 2,
optimize_multiget_for_io_ ? 3 : 2);
}
#endif // ROCKSDB_IOURING_PRESENT
}
TEST_P(DBMultiGetAsyncIOTest, GetFromL2WithRangeOverlapL0L1) {
std::vector<std::string> key_strs;
std::vector<Slice> keys;
std::vector<PinnableSlice> values;
std::vector<Status> statuses;
// 19 and 26 are in L2, but overlap with L0 and L1 file ranges
key_strs.push_back(Key(19));
key_strs.push_back(Key(26));
keys.emplace_back(key_strs[0]);
keys.emplace_back(key_strs[1]);
values.resize(keys.size());
statuses.resize(keys.size());
std::vector<std::string> key_strs{Key(19), Key(26)};
PrepareDBForTest();
ReadOptions ro;
ro.async_io = true;
ro.optimize_multiget_for_io = GetParam();
dbfull()->MultiGet(ro, dbfull()->DefaultColumnFamily(), keys.size(),
keys.data(), values.data(), statuses.data());
std::vector<std::string> values =
MultiGet(key_strs, /*snapshot=*/nullptr, /*async=*/true,
optimize_multiget_for_io_, use_coroutine_);
ASSERT_EQ(values.size(), 2);
ASSERT_EQ(statuses[0], Status::OK());
ASSERT_EQ(statuses[1], Status::OK());
ASSERT_EQ(values[0], "val_l2_" + std::to_string(19));
ASSERT_EQ(values[1], "val_l2_" + std::to_string(26));
#ifdef ROCKSDB_IOURING_PRESENT
// Bloom filters in L0/L1 will avoid the coroutine calls in those levels
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 2);
// Bloom filters in L0/L1 avoid the coroutine calls in those levels. The
// coroutine path counts every per-file MultiGetFromSST coroutine (including
// serial reads), whereas the async_io path counts only the parallel overlap
// batch, so the coroutine path observes one more.
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT),
UseCoroutineRead() ? 3 : 2);
#else // ROCKSDB_IOURING_PRESENT
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 0);
#endif // ROCKSDB_IOURING_PRESENT
@@ -4083,62 +4206,35 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL2WithRangeOverlapL0L1) {
#ifdef ROCKSDB_IOURING_PRESENT
TEST_P(DBMultiGetAsyncIOTest, GetFromL2WithRangeDelInL1) {
std::vector<std::string> key_strs;
std::vector<Slice> keys;
std::vector<PinnableSlice> values;
std::vector<Status> statuses;
// 139 and 163 are in L2, but overlap with a range deletes in L1
key_strs.push_back(Key(139));
key_strs.push_back(Key(163));
keys.emplace_back(key_strs[0]);
keys.emplace_back(key_strs[1]);
values.resize(keys.size());
statuses.resize(keys.size());
std::vector<std::string> key_strs{Key(139), Key(163)};
PrepareDBForTest();
ReadOptions ro;
ro.async_io = true;
ro.optimize_multiget_for_io = GetParam();
dbfull()->MultiGet(ro, dbfull()->DefaultColumnFamily(), keys.size(),
keys.data(), values.data(), statuses.data());
std::vector<std::string> values =
MultiGet(key_strs, /*snapshot=*/nullptr, /*async=*/true,
optimize_multiget_for_io_, use_coroutine_);
ASSERT_EQ(values.size(), 2);
ASSERT_EQ(statuses[0], Status::NotFound());
ASSERT_EQ(statuses[1], Status::NotFound());
ASSERT_EQ(values[0], "NOT_FOUND");
ASSERT_EQ(values[1], "NOT_FOUND");
// Bloom filters in L0/L1 will avoid the coroutine calls in those levels
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 2);
}
TEST_P(DBMultiGetAsyncIOTest, GetFromL1AndL2WithRangeDelInL1) {
std::vector<std::string> key_strs;
std::vector<Slice> keys;
std::vector<PinnableSlice> values;
std::vector<Status> statuses;
// 139 and 163 are in L2, but overlap with a range deletes in L1
key_strs.push_back(Key(139));
key_strs.push_back(Key(144));
key_strs.push_back(Key(163));
keys.emplace_back(key_strs[0]);
keys.emplace_back(key_strs[1]);
keys.emplace_back(key_strs[2]);
values.resize(keys.size());
statuses.resize(keys.size());
std::vector<std::string> key_strs{Key(139), Key(144), Key(163)};
PrepareDBForTest();
ReadOptions ro;
ro.async_io = true;
ro.optimize_multiget_for_io = GetParam();
dbfull()->MultiGet(ro, dbfull()->DefaultColumnFamily(), keys.size(),
keys.data(), values.data(), statuses.data());
ASSERT_EQ(values.size(), keys.size());
ASSERT_EQ(statuses[0], Status::NotFound());
ASSERT_EQ(statuses[1], Status::OK());
std::vector<std::string> values =
MultiGet(key_strs, /*snapshot=*/nullptr, /*async=*/true,
optimize_multiget_for_io_, use_coroutine_);
ASSERT_EQ(values.size(), 3);
ASSERT_EQ(values[0], "NOT_FOUND");
ASSERT_EQ(values[1], "val_l1_" + std::to_string(144));
ASSERT_EQ(statuses[2], Status::NotFound());
ASSERT_EQ(values[2], "NOT_FOUND");
// Bloom filters in L0/L1 will avoid the coroutine calls in those levels
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 3);
@@ -4146,32 +4242,18 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL1AndL2WithRangeDelInL1) {
#endif // ROCKSDB_IOURING_PRESENT
TEST_P(DBMultiGetAsyncIOTest, GetNoIOUring) {
std::vector<std::string> key_strs;
std::vector<Slice> keys;
std::vector<PinnableSlice> values;
std::vector<Status> statuses;
key_strs.push_back(Key(33));
key_strs.push_back(Key(54));
key_strs.push_back(Key(102));
keys.emplace_back(key_strs[0]);
keys.emplace_back(key_strs[1]);
keys.emplace_back(key_strs[2]);
values.resize(keys.size());
statuses.resize(keys.size());
std::vector<std::string> key_strs{Key(33), Key(54), Key(102)};
enable_io_uring = false;
ReopenDB();
ReadOptions ro;
ro.async_io = true;
ro.optimize_multiget_for_io = GetParam();
dbfull()->MultiGet(ro, dbfull()->DefaultColumnFamily(), keys.size(),
keys.data(), values.data(), statuses.data());
std::vector<std::string> values =
MultiGet(key_strs, /*snapshot=*/nullptr, /*async=*/true,
optimize_multiget_for_io_, use_coroutine_);
ASSERT_EQ(values.size(), 3);
ASSERT_EQ(statuses[0], Status::OK());
ASSERT_EQ(statuses[1], Status::OK());
ASSERT_EQ(statuses[2], Status::OK());
ASSERT_EQ(values[0], "val_l1_" + std::to_string(33));
ASSERT_EQ(values[1], "val_l1_" + std::to_string(54));
ASSERT_EQ(values[2], "val_l1_" + std::to_string(102));
HistogramData async_read_bytes;
@@ -4179,11 +4261,17 @@ TEST_P(DBMultiGetAsyncIOTest, GetNoIOUring) {
// A batch of 3 async IOs is expected, one for each overlapping file in L1
ASSERT_EQ(async_read_bytes.count, 0);
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 0);
const uint64_t coroutine_count =
statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT);
if (UseCoroutineRead()) {
ASSERT_GT(coroutine_count, 0);
} else {
ASSERT_EQ(coroutine_count, 0);
}
}
INSTANTIATE_TEST_CASE_P(DBMultiGetAsyncIOTest, DBMultiGetAsyncIOTest,
testing::Bool());
testing::Combine(testing::Bool(), testing::Bool()));
#endif // USE_COROUTINES
TEST_F(DBBasicTest, MultiGetStats) {
+124 -1
View File
@@ -28,6 +28,7 @@
#include "rocksdb/wal_filter.h"
#include "test_util/testutil.h"
#include "util/defer.h"
#include "util/mutexlock.h"
#include "util/random.h"
#include "utilities/fault_injection_env.h"
@@ -7902,7 +7903,10 @@ class FastOpenTestFS : public FileSystemWrapper {
IODebugContext* dbg) override {
if (file_opts.file_metadata != nullptr) {
metadata_passed_on_open_.fetch_add(1, std::memory_order_relaxed);
last_metadata_received_ = *file_opts.file_metadata;
{
MutexLock l(&last_metadata_mu_);
last_metadata_received_ = *file_opts.file_metadata;
}
}
IOStatus s = target()->NewRandomAccessFile(fname, file_opts, result, dbg);
if (s.ok()) {
@@ -7921,18 +7925,21 @@ class FastOpenTestFS : public FileSystemWrapper {
}
std::string GetLastMetadataReceived() const {
MutexLock l(&last_metadata_mu_);
return last_metadata_received_;
}
void ResetCounters() {
metadata_retrieved_count_.store(0, std::memory_order_relaxed);
metadata_passed_on_open_.store(0, std::memory_order_relaxed);
MutexLock l(&last_metadata_mu_);
last_metadata_received_.clear();
}
private:
std::atomic<int> metadata_retrieved_count_{0};
std::atomic<int> metadata_passed_on_open_{0};
mutable port::Mutex last_metadata_mu_;
std::string last_metadata_received_;
};
@@ -8274,6 +8281,122 @@ TEST_F(DBTest2, FastSstOpenDisableAfterMetadataPersisted) {
ASSERT_OK(DestroyDB(test_dbname, options));
}
TEST_F(DBTest2, FastSstOpenMetadataPreservedAcrossManifestRotation) {
// Regression test: FileMetaData::file_open_metadata for unchanged live
// SSTs must survive a MANIFEST rotation. WriteCurrentStateToManifest
// snapshots live SSTs into a new MANIFEST; if it drops file_open_metadata
// for those unchanged SSTs, the fast_sst_open optimization is silently
// disabled for them until they are rewritten by flush/compaction/ingest.
auto test_fs = std::make_shared<FastOpenTestFS>(env_->GetFileSystem());
std::unique_ptr<Env> test_env(new CompositeEnvWrapper(env_, test_fs));
Options options;
options.env = test_env.get();
options.fast_sst_open = true;
options.create_if_missing = true;
// Keep the pre-rotation SST from being rewritten by compaction.
options.disable_auto_compactions = true;
std::string test_dbname = test::PerThreadDBPath("fast_open_manifest_rotate");
ASSERT_OK(DestroyDB(test_dbname, options));
std::unique_ptr<DB> db;
ASSERT_OK(DB::Open(options, test_dbname, &db));
// Flush an SST; metadata is retrieved and persisted in the MANIFEST.
ASSERT_OK(db->Put(WriteOptions(), "key1", "value1"));
ASSERT_OK(db->Flush(FlushOptions()));
ASSERT_EQ(1, test_fs->GetMetadataRetrievedCount());
// Shrink max_manifest_file_size so the next MANIFEST write rotates and
// exercises WriteCurrentStateToManifest.
ASSERT_OK(db->SetDBOptions(
{{"max_manifest_file_size", "1"}, {"max_manifest_space_amp_pct", "0"}}));
uint64_t manifest_before =
static_cast<DBImpl*>(db.get())->TEST_Current_Manifest_FileNo();
// Flush a second SST to trigger the rotation. SST #1 is unchanged, so
// the rotated MANIFEST snapshot must carry SST #1's file_open_metadata.
ASSERT_OK(db->Put(WriteOptions(), "key2", "value2"));
ASSERT_OK(db->Flush(FlushOptions()));
uint64_t manifest_after =
static_cast<DBImpl*>(db.get())->TEST_Current_Manifest_FileNo();
ASSERT_GT(manifest_after, manifest_before);
db.reset();
test_fs->ResetCounters();
// Reopen. Every live SST should be opened with file_open_metadata.
// Pre-fix: only SST #2 (added by the rotating edit) has metadata; SST #1
// was stripped by the rotation snapshot, so the count would be 1.
ASSERT_OK(DB::Open(options, test_dbname, &db));
std::string value;
ASSERT_OK(db->Get(ReadOptions(), "key1", &value));
ASSERT_EQ("value1", value);
ASSERT_OK(db->Get(ReadOptions(), "key2", &value));
ASSERT_EQ("value2", value);
ASSERT_EQ(2, test_fs->GetMetadataPassedOnOpenCount());
db.reset();
ASSERT_OK(DestroyDB(test_dbname, options));
}
TEST_F(DBTest2, FastSstOpenMetadataPreservedAcrossTrivialMove) {
// Regression test: FileMetaData::file_open_metadata must survive a trivial
// move. Trivial move re-records an SST at a new level via DeleteFile +
// AddFile without physically rewriting the file. If AddFile drops
// file_open_metadata, the file loses its fast_sst_open advantage until
// it is next rewritten by real compaction/flush/ingest.
auto test_fs = std::make_shared<FastOpenTestFS>(env_->GetFileSystem());
std::unique_ptr<Env> test_env(new CompositeEnvWrapper(env_, test_fs));
Options options;
options.env = test_env.get();
options.fast_sst_open = true;
options.create_if_missing = true;
options.disable_auto_compactions = true;
std::string test_dbname = test::PerThreadDBPath("fast_open_trivial_move");
ASSERT_OK(DestroyDB(test_dbname, options));
std::unique_ptr<DB> db;
ASSERT_OK(DB::Open(options, test_dbname, &db));
// Flush a single SST at L0; metadata is retrieved once and persisted.
ASSERT_OK(db->Put(WriteOptions(), "key1", "value1"));
ASSERT_OK(db->Flush(FlushOptions()));
ASSERT_EQ(1, test_fs->GetMetadataRetrievedCount());
// Manual compaction. With a single L0 file and empty lower levels, the
// compaction picker converts this into a trivial move (no physical
// rewrite), exercising PrepareTrivialMoveEdit.
ASSERT_OK(db->CompactRange(CompactRangeOptions(), nullptr, nullptr));
// If the SST had been rewritten, metadata retrieval would have re-run on
// the new file. Unchanged count confirms this was a trivial move.
ASSERT_EQ(1, test_fs->GetMetadataRetrievedCount());
db.reset();
test_fs->ResetCounters();
// Reopen. The trivially-moved SST should still carry its
// file_open_metadata and be opened via the fast_sst_open fast path.
// Pre-fix, the trivial move stripped it and this count would be 0.
ASSERT_OK(DB::Open(options, test_dbname, &db));
std::string value;
ASSERT_OK(db->Get(ReadOptions(), "key1", &value));
ASSERT_EQ("value1", value);
ASSERT_EQ(1, test_fs->GetMetadataPassedOnOpenCount());
db.reset();
ASSERT_OK(DestroyDB(test_dbname, options));
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
+7 -3
View File
@@ -252,7 +252,7 @@ Status DBImpl::GetLiveFilesStorageInfo(
"capturing live files. Retry with flush enabled.");
}
if (flush_memtable) {
wal_locked = lock_wal_count_ > 0;
wal_locked = !lock_wal_owner_thread_id_counts_.empty();
if (wal_locked) {
if (needs_blob_direct_write_flush) {
mutex_.Unlock();
@@ -260,8 +260,12 @@ Status DBImpl::GetLiveFilesStorageInfo(
"Blob direct write requires flushing active blob files before "
"capturing live files. Retry with WAL unlocked.");
}
ROCKS_LOG_INFO(immutable_db_options_.info_log,
"Can't FlushForGetLiveFiles while WAL is locked");
}
if (wal_locked && !has_unpersisted_data_.load(std::memory_order_relaxed)) {
ROCKS_LOG_INFO(
immutable_db_options_.info_log,
"Skipping FlushForGetLiveFiles while WAL is locked and all data is "
"already persisted");
} else {
Status status = FlushForGetLiveFiles(opts.atomic_flush);
if (!status.ok()) {
+138 -5
View File
@@ -8,7 +8,9 @@
// found in the LICENSE file. See the AUTHORS file for names of contributors.
#include <atomic>
#include <condition_variable>
#include <limits>
#include <mutex>
#include "db/db_impl/db_impl.h"
#include "db/db_test_util.h"
@@ -2219,8 +2221,12 @@ TEST_F(DBFlushTest, FireOnFlushCompletedAfterCommittedResult) {
listener->seq1 = db_->GetLatestSequenceNumber();
// t1 will wait for the second flush complete before committing flush result.
auto t1 = port::Thread([&]() {
// flush_opts.wait = true
ASSERT_OK(db_->Flush(FlushOptions()));
// flush_opts.wait = true, and listener_wait = true so that Flush() does not
// return until the OnFlushCompleted callbacks (which set completed1 and
// completed2) have finished running.
FlushOptions flush_opts;
flush_opts.listener_wait = true;
ASSERT_OK(db_->Flush(flush_opts));
});
// Wait for first flush started.
TEST_SYNC_POINT(
@@ -2233,15 +2239,142 @@ TEST_F(DBFlushTest, FireOnFlushCompletedAfterCommittedResult) {
flush_opts.wait = false;
ASSERT_OK(db_->Flush(flush_opts));
t1.join();
// Ensure background work is fully finished including listener callbacks
// before accessing listener state.
ASSERT_OK(dbfull()->TEST_WaitForBackgroundWork());
// listener_wait on t1's flush above guarantees both OnFlushCompleted
// callbacks have finished by the time Flush() returned; no need to
// additionally wait for background work here.
ASSERT_TRUE(listener->completed1);
ASSERT_TRUE(listener->completed2);
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
}
namespace {
// EventListener whose OnFlushCompleted blocks until explicitly released, used
// to verify FlushOptions::listener_wait. Optionally only blocks on the first
// callback (useful for atomic flush, which fires one callback per CF).
class BlockingFlushListener : public EventListener {
public:
explicit BlockingFlushListener(bool block_only_first)
: block_only_first_(block_only_first) {}
void OnFlushCompleted(DB* /*db*/, const FlushJobInfo& /*info*/) override {
if (block_only_first_ && num_completed_.fetch_add(1) != 0) {
return;
}
std::unique_lock<std::mutex> lk(mu_);
entered_ = true;
cv_.notify_all();
cv_.wait(lk, [this] { return release_; });
returned_ = true;
}
void WaitUntilEntered() {
std::unique_lock<std::mutex> lk(mu_);
cv_.wait(lk, [this] { return entered_; });
}
void Release() {
{
std::lock_guard<std::mutex> lk(mu_);
release_ = true;
}
cv_.notify_all();
}
bool CallbackReturned() {
std::lock_guard<std::mutex> lk(mu_);
return returned_;
}
private:
const bool block_only_first_;
std::atomic<int> num_completed_{0};
std::mutex mu_;
std::condition_variable cv_;
bool entered_ = false;
bool release_ = false;
bool returned_ = false;
};
} // anonymous namespace
TEST_F(DBFlushTest, ListenerWaitWaitsForOnFlushCompleted) {
// With FlushOptions::listener_wait, Flush(wait=true) must not return until
// the OnFlushCompleted callback has finished, even if the flush result is
// already committed and bg_cv_ is signalled by unrelated background work.
auto listener = std::make_shared<BlockingFlushListener>(
/*block_only_first=*/false);
Options options = CurrentOptions();
options.listeners.push_back(listener);
Reopen(options);
ASSERT_OK(Put("k", "v"));
std::atomic<bool> flush_returned{false};
port::Thread flush_thread([&] {
// Exercises the TEST_ wrapper, which flushes with wait=true and
// listener_wait=true.
ASSERT_OK(dbfull()->TEST_FlushMemTableWithListenerWait());
flush_returned.store(true);
});
// Wait until we are inside OnFlushCompleted. At this point the memtable has
// already been committed/removed, which is what unblocks a plain
// Flush(wait=true).
listener->WaitUntilEntered();
// Pump spurious bg_cv_ wakeups. A waiter that ignored listener_wait would
// wake, observe the memtable already gone, and return here.
for (int i = 0; i < 10; ++i) {
db_->DisableManualCompaction();
db_->EnableManualCompaction();
}
// The OnFlushCompleted callback has not returned, so neither must Flush().
ASSERT_FALSE(listener->CallbackReturned());
ASSERT_FALSE(flush_returned.load());
listener->Release();
flush_thread.join();
ASSERT_TRUE(flush_returned.load());
ASSERT_TRUE(listener->CallbackReturned());
}
TEST_F(DBFlushTest, ListenerWaitAtomicFlushWaitsForAllOnFlushCompleted) {
// Same guarantee for atomic flush across multiple column families. The atomic
// flush removes all CFs' memtables together and then fires OnFlushCompleted
// per CF, releasing the DB mutex between CFs; listener_wait must still block
// Flush() until the (first, still-running) callback has finished.
auto listener = std::make_shared<BlockingFlushListener>(
/*block_only_first=*/true);
Options options = CurrentOptions();
options.atomic_flush = true;
options.listeners.push_back(listener);
CreateAndReopenWithCF({"pikachu"}, options);
ASSERT_OK(Put(0, "k0", "v0"));
ASSERT_OK(Put(1, "k1", "v1"));
std::atomic<bool> flush_returned{false};
port::Thread flush_thread([&] {
FlushOptions fo;
fo.wait = true;
fo.listener_wait = true;
ASSERT_OK(db_->Flush(fo, {handles_[0], handles_[1]}));
flush_returned.store(true);
});
listener->WaitUntilEntered();
for (int i = 0; i < 10; ++i) {
db_->DisableManualCompaction();
db_->EnableManualCompaction();
}
ASSERT_FALSE(listener->CallbackReturned());
ASSERT_FALSE(flush_returned.load());
listener->Release();
flush_thread.join();
ASSERT_TRUE(flush_returned.load());
ASSERT_TRUE(listener->CallbackReturned());
}
TEST_F(DBFlushTest, FlushWithBlob) {
constexpr uint64_t min_blob_size = 10;
+13 -182
View File
@@ -34,188 +34,6 @@ size_t CompactedDBImpl::FindFile(const Slice& key) {
files_.files);
}
Status CompactedDBImpl::Get(const ReadOptions& _read_options,
ColumnFamilyHandle*, const Slice& key,
PinnableSlice* value, std::string* timestamp) {
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
_read_options.io_activity != Env::IOActivity::kGet) {
return Status::InvalidArgument(
"Can only call Get with `ReadOptions::io_activity` is "
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kGet`");
}
ReadOptions read_options(_read_options);
if (read_options.io_activity == Env::IOActivity::kUnknown) {
read_options.io_activity = Env::IOActivity::kGet;
}
assert(user_comparator_);
if (read_options.timestamp) {
Status s =
FailIfTsMismatchCf(DefaultColumnFamily(), *(read_options.timestamp));
if (!s.ok()) {
return s;
}
if (read_options.timestamp->size() > 0) {
s = FailIfReadCollapsedHistory(cfd_, cfd_->GetSuperVersion(),
*(read_options.timestamp));
if (!s.ok()) {
return s;
}
}
} else {
const Status s = FailIfCfHasTs(DefaultColumnFamily());
if (!s.ok()) {
return s;
}
}
// Clear the timestamps for returning results so that we can distinguish
// between tombstone or key that has never been written
if (timestamp) {
timestamp->clear();
}
GetWithTimestampReadCallback read_cb(kMaxSequenceNumber);
BlobFetcher blob_fetcher(version_, read_options);
std::string* ts =
user_comparator_->timestamp_size() > 0 ? timestamp : nullptr;
LookupKey lkey(key, kMaxSequenceNumber, read_options.timestamp);
GetContext get_context(user_comparator_, nullptr, nullptr, nullptr,
GetContext::kNotFound, lkey.user_key(), value,
/*columns=*/nullptr, ts, nullptr, nullptr, true,
nullptr, nullptr, nullptr, nullptr, &read_cb,
nullptr /* is_blob_index */, 0 /* tracing_get_id */,
&blob_fetcher);
const FdWithKeyRange& f = files_.files[FindFile(lkey.user_key())];
if (user_comparator_->CompareWithoutTimestamp(
key, /*a_has_ts=*/false,
ExtractUserKeyAndStripTimestamp(f.smallest_key,
user_comparator_->timestamp_size()),
/*b_has_ts=*/false) < 0) {
return Status::NotFound();
}
TableReader* t = nullptr;
TableCache::TypedHandle* handle = nullptr;
Status s = cfd_->table_cache()->FindTable(
read_options, cfd_->table_cache()->file_options(),
cfd_->internal_comparator(), *f.file_metadata, &handle,
version_->GetMutableCFOptions(), &t, false /* no_io */,
nullptr /* file_read_hist */, false /* skip_filters */, files_level_,
true /* prefetch_index_and_filter_in_cache */,
0 /* max_file_size_for_l0_meta_pin */, f.file_metadata->temperature,
true /* pin_table_handle */);
if (s.ok()) {
assert(handle == nullptr);
// Use TableReader directly to avoid extra table_cache->Get() overheads
s = t->Get(read_options, lkey.internal_key(), &get_context, nullptr);
}
if (!s.ok() && !s.IsNotFound()) {
return s;
}
if (get_context.State() == GetContext::kFound) {
return Status::OK();
}
return Status::NotFound();
}
void CompactedDBImpl::MultiGet(const ReadOptions& _read_options,
size_t num_keys,
ColumnFamilyHandle** /*column_families*/,
const Slice* keys, PinnableSlice* values,
std::string* timestamps, Status* statuses,
const bool /*sorted_input*/) {
assert(user_comparator_);
Status s;
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
_read_options.io_activity != Env::IOActivity::kMultiGet) {
s = Status::InvalidArgument(
"Can only call MultiGet with `ReadOptions::io_activity` is "
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kMultiGet`");
}
ReadOptions read_options(_read_options);
if (s.ok()) {
if (read_options.io_activity == Env::IOActivity::kUnknown) {
read_options.io_activity = Env::IOActivity::kMultiGet;
}
if (read_options.timestamp) {
s = FailIfTsMismatchCf(DefaultColumnFamily(), *(read_options.timestamp));
if (s.ok()) {
if (read_options.timestamp->size() > 0) {
s = FailIfReadCollapsedHistory(cfd_, cfd_->GetSuperVersion(),
*(read_options.timestamp));
}
}
} else {
s = FailIfCfHasTs(DefaultColumnFamily());
}
}
if (!s.ok()) {
for (size_t i = 0; i < num_keys; ++i) {
statuses[i] = s;
}
return;
}
// Clear the timestamps for returning results so that we can distinguish
// between tombstone or key that has never been written
if (timestamps) {
for (size_t i = 0; i < num_keys; ++i) {
timestamps[i].clear();
}
}
GetWithTimestampReadCallback read_cb(kMaxSequenceNumber);
BlobFetcher blob_fetcher(version_, read_options);
for (size_t i = 0; i < num_keys; ++i) {
const Slice& key = keys[i];
LookupKey lkey(key, kMaxSequenceNumber, read_options.timestamp);
const FdWithKeyRange& f = files_.files[FindFile(lkey.user_key())];
if (user_comparator_->CompareWithoutTimestamp(
key, /*a_has_ts=*/false,
ExtractUserKeyAndStripTimestamp(f.smallest_key,
user_comparator_->timestamp_size()),
/*b_has_ts=*/false) < 0) {
statuses[i] = Status::NotFound();
continue;
}
PinnableSlice& pinnable_val = values[i];
std::string* timestamp = timestamps ? &timestamps[i] : nullptr;
GetContext get_context(
user_comparator_, nullptr, nullptr, nullptr, GetContext::kNotFound,
lkey.user_key(), &pinnable_val, /*columns=*/nullptr,
user_comparator_->timestamp_size() > 0 ? timestamp : nullptr, nullptr,
nullptr, true, nullptr, nullptr, nullptr, nullptr, &read_cb,
nullptr /* is_blob_index */, 0 /* tracing_get_id */, &blob_fetcher);
TableReader* t = nullptr;
TableCache::TypedHandle* handle = nullptr;
Status status = cfd_->table_cache()->FindTable(
read_options, cfd_->table_cache()->file_options(),
cfd_->internal_comparator(), *f.file_metadata, &handle,
version_->GetMutableCFOptions(), &t, false /* no_io */,
nullptr /* file_read_hist */, false /* skip_filters */, files_level_,
true /* prefetch_index_and_filter_in_cache */,
0 /* max_file_size_for_l0_meta_pin */, f.file_metadata->temperature,
true /* pin_table_handle */);
if (status.ok()) {
assert(handle == nullptr);
// Use TableReader directly to avoid extra table_cache->Get() overheads
status = t->Get(read_options, lkey.internal_key(), &get_context, nullptr);
}
if (!status.ok() && !status.IsNotFound()) {
statuses[i] = status;
} else if (get_context.State() == GetContext::kFound) {
statuses[i] = Status::OK();
} else {
statuses[i] = Status::NotFound();
}
}
}
Status CompactedDBImpl::Init(const Options& options) {
SuperVersionContext sv_context(/* create_superversion */ true);
mutex_.Lock();
@@ -301,3 +119,16 @@ Status CompactedDBImpl::Open(const Options& options, const std::string& dbname,
}
} // namespace ROCKSDB_NAMESPACE
// Generate the regular and coroutine versions of the functions defined in
// compacted_db_impl_sync_and_async.h by including it twice. The macros expand
// differently based on whether WITH_COROUTINES or WITHOUT_COROUTINES is
// defined.
// clang-format off
#define WITHOUT_COROUTINES
#include "db/db_impl/compacted_db_impl_sync_and_async.h"
#undef WITHOUT_COROUTINES
#define WITH_COROUTINES
#include "db/db_impl/compacted_db_impl_sync_and_async.h"
#undef WITH_COROUTINES
// clang-format on
+10 -7
View File
@@ -26,18 +26,21 @@ class CompactedDBImpl : public DBImpl {
// Implementations of the DB interface
using DB::Get;
Status Get(const ReadOptions& options, ColumnFamilyHandle* column_family,
const Slice& key, PinnableSlice* value,
std::string* timestamp) override;
DECLARE_SYNC_AND_ASYNC_OVERRIDE(Status, Get, const ReadOptions& options,
ColumnFamilyHandle* column_family,
const Slice& key, PinnableSlice* value,
std::string* timestamp);
using DB::MultiGet;
// Note that CompactedDBImpl::MultiGet is not the optimized version of
// MultiGet to use.
// TODO: optimize CompactedDBImpl::MultiGet, see DBImpl::MultiGet for details.
void MultiGet(const ReadOptions& options, size_t num_keys,
ColumnFamilyHandle** column_families, const Slice* keys,
PinnableSlice* values, std::string* timestamps,
Status* statuses, const bool sorted_input) override;
DECLARE_SYNC_AND_ASYNC_OVERRIDE(void, MultiGet, const ReadOptions& options,
size_t num_keys,
ColumnFamilyHandle** column_families,
const Slice* keys, PinnableSlice* values,
std::string* timestamps, Status* statuses,
const bool sorted_input);
using DBImpl::Put;
Status Put(const WriteOptions& /*options*/,
@@ -0,0 +1,199 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
//
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#include "util/coro_utils.h"
#if defined(WITHOUT_COROUTINES) || \
(defined(USE_COROUTINES) && defined(WITH_COROUTINES))
namespace ROCKSDB_NAMESPACE {
DEFINE_SYNC_AND_ASYNC(Status, CompactedDBImpl::Get)
(const ReadOptions& _read_options, ColumnFamilyHandle*, const Slice& key,
PinnableSlice* value, std::string* timestamp) {
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
_read_options.io_activity != Env::IOActivity::kGet) {
CO_RETURN Status::InvalidArgument(
"Can only call Get with `ReadOptions::io_activity` is "
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kGet`");
}
ReadOptions read_options(_read_options);
if (read_options.io_activity == Env::IOActivity::kUnknown) {
read_options.io_activity = Env::IOActivity::kGet;
}
assert(user_comparator_);
if (read_options.timestamp) {
Status s =
FailIfTsMismatchCf(DefaultColumnFamily(), *(read_options.timestamp));
if (!s.ok()) {
CO_RETURN s;
}
if (read_options.timestamp->size() > 0) {
s = FailIfReadCollapsedHistory(cfd_, cfd_->GetSuperVersion(),
*(read_options.timestamp));
if (!s.ok()) {
CO_RETURN s;
}
}
} else {
const Status s = FailIfCfHasTs(DefaultColumnFamily());
if (!s.ok()) {
CO_RETURN s;
}
}
// Clear the timestamps for returning results so that we can distinguish
// between tombstone or key that has never been written
if (timestamp) {
timestamp->clear();
}
GetWithTimestampReadCallback read_cb(kMaxSequenceNumber);
VersionBlobFetcher blob_fetcher(version_, read_options);
std::string* ts =
user_comparator_->timestamp_size() > 0 ? timestamp : nullptr;
LookupKey lkey(key, kMaxSequenceNumber, read_options.timestamp);
GetContext get_context(user_comparator_, nullptr, nullptr, nullptr,
GetContext::kNotFound, lkey.user_key(), value,
/*columns=*/nullptr, ts, nullptr, nullptr, true,
nullptr, nullptr, nullptr, nullptr, &read_cb,
nullptr /* is_blob_index */, 0 /* tracing_get_id */,
&blob_fetcher);
const FdWithKeyRange& f = files_.files[FindFile(lkey.user_key())];
if (user_comparator_->CompareWithoutTimestamp(
key, /*a_has_ts=*/false,
ExtractUserKeyAndStripTimestamp(f.smallest_key,
user_comparator_->timestamp_size()),
/*b_has_ts=*/false) < 0) {
CO_RETURN Status::NotFound();
}
TableReader* t = nullptr;
TableCache::TypedHandle* handle = nullptr;
Status s = cfd_->table_cache()->FindTable(
read_options, cfd_->table_cache()->file_options(),
cfd_->internal_comparator(), *f.file_metadata, &handle,
version_->GetMutableCFOptions(), &t, false /* no_io */,
nullptr /* file_read_hist */, false /* skip_filters */, files_level_,
true /* prefetch_index_and_filter_in_cache */,
0 /* max_file_size_for_l0_meta_pin */, f.file_metadata->temperature,
true /* pin_table_handle */);
if (s.ok()) {
assert(handle == nullptr);
// Use TableReader directly to avoid extra table_cache->Get() overheads
s = CO_AWAIT(t->Get, read_options, lkey.internal_key(), &get_context,
nullptr);
}
if (!s.ok() && !s.IsNotFound()) {
CO_RETURN s;
}
if (get_context.State() == GetContext::kFound) {
CO_RETURN Status::OK();
}
CO_RETURN Status::NotFound();
}
DEFINE_SYNC_AND_ASYNC(void, CompactedDBImpl::MultiGet)
(const ReadOptions& _read_options, size_t num_keys,
ColumnFamilyHandle** /*column_families*/, const Slice* keys,
PinnableSlice* values, std::string* timestamps, Status* statuses,
const bool /*sorted_input*/) {
assert(user_comparator_);
Status s;
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
_read_options.io_activity != Env::IOActivity::kMultiGet) {
s = Status::InvalidArgument(
"Can only call MultiGet with `ReadOptions::io_activity` is "
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kMultiGet`");
}
ReadOptions read_options(_read_options);
if (s.ok()) {
if (read_options.io_activity == Env::IOActivity::kUnknown) {
read_options.io_activity = Env::IOActivity::kMultiGet;
}
if (read_options.timestamp) {
s = FailIfTsMismatchCf(DefaultColumnFamily(), *(read_options.timestamp));
if (s.ok()) {
if (read_options.timestamp->size() > 0) {
s = FailIfReadCollapsedHistory(cfd_, cfd_->GetSuperVersion(),
*(read_options.timestamp));
}
}
} else {
s = FailIfCfHasTs(DefaultColumnFamily());
}
}
if (!s.ok()) {
for (size_t i = 0; i < num_keys; ++i) {
statuses[i] = s;
}
CO_RETURN;
}
// Clear the timestamps for returning results so that we can distinguish
// between tombstone or key that has never been written
if (timestamps) {
for (size_t i = 0; i < num_keys; ++i) {
timestamps[i].clear();
}
}
GetWithTimestampReadCallback read_cb(kMaxSequenceNumber);
VersionBlobFetcher blob_fetcher(version_, read_options);
for (size_t i = 0; i < num_keys; ++i) {
const Slice& key = keys[i];
LookupKey lkey(key, kMaxSequenceNumber, read_options.timestamp);
const FdWithKeyRange& f = files_.files[FindFile(lkey.user_key())];
if (user_comparator_->CompareWithoutTimestamp(
key, /*a_has_ts=*/false,
ExtractUserKeyAndStripTimestamp(f.smallest_key,
user_comparator_->timestamp_size()),
/*b_has_ts=*/false) < 0) {
statuses[i] = Status::NotFound();
continue;
}
PinnableSlice& pinnable_val = values[i];
std::string* timestamp = timestamps ? &timestamps[i] : nullptr;
GetContext get_context(
user_comparator_, nullptr, nullptr, nullptr, GetContext::kNotFound,
lkey.user_key(), &pinnable_val, /*columns=*/nullptr,
user_comparator_->timestamp_size() > 0 ? timestamp : nullptr, nullptr,
nullptr, true, nullptr, nullptr, nullptr, nullptr, &read_cb,
nullptr /* is_blob_index */, 0 /* tracing_get_id */, &blob_fetcher);
TableReader* t = nullptr;
TableCache::TypedHandle* handle = nullptr;
Status status = cfd_->table_cache()->FindTable(
read_options, cfd_->table_cache()->file_options(),
cfd_->internal_comparator(), *f.file_metadata, &handle,
version_->GetMutableCFOptions(), &t, false /* no_io */,
nullptr /* file_read_hist */, false /* skip_filters */, files_level_,
true /* prefetch_index_and_filter_in_cache */,
0 /* max_file_size_for_l0_meta_pin */, f.file_metadata->temperature,
true /* pin_table_handle */);
if (status.ok()) {
assert(handle == nullptr);
// Use TableReader directly to avoid extra table_cache->Get() overheads
status = CO_AWAIT(t->Get, read_options, lkey.internal_key(), &get_context,
nullptr);
}
if (!status.ok() && !status.IsNotFound()) {
statuses[i] = status;
} else if (get_context.State() == GetContext::kFound) {
statuses[i] = Status::OK();
} else {
statuses[i] = Status::NotFound();
}
}
}
} // namespace ROCKSDB_NAMESPACE
#endif
+403 -857
View File
File diff suppressed because it is too large. Load diff
+93 -49
View File
@@ -67,6 +67,7 @@
#include "rocksdb/write_buffer_manager.h"
#include "table/merging_iterator.h"
#include "util/autovector.h"
#include "util/coro_utils.h"
#include "util/hash.h"
#include "util/repeatable_thread.h"
#include "util/stop_watch.h"
@@ -268,9 +269,14 @@ class DBImpl : public DB {
bool HasAnyBlobDirectWriteColumnFamily();
using DB::Get;
Status Get(const ReadOptions& _read_options,
ColumnFamilyHandle* column_family, const Slice& key,
PinnableSlice* value, std::string* timestamp) override;
using DB::GetAsync;
DECLARE_SYNC_ASYNC_AND_CALLBACK(
Status, Get, GetAsync,
(const ReadOptions& options, ColumnFamilyHandle* column_family,
const Slice& key, PinnableSlice* value, std::string* timestamp,
Status& status, AsyncCallback& callback),
const ReadOptions& _read_options, ColumnFamilyHandle* column_family,
const Slice& key, PinnableSlice* value, std::string* timestamp);
using DB::GetEntity;
Status GetEntity(const ReadOptions& options,
@@ -295,6 +301,7 @@ class DBImpl : public DB {
}
using DB::MultiGet;
using DB::MultiGetAsync;
// This MultiGet is a batched version, which may be faster than calling Get
// multiple times, especially if the keys have some spatial locality that
// enables them to be queried in the same SST files/set of files. The larger
@@ -302,10 +309,16 @@ class DBImpl : public DB {
// The values and statuses parameters are arrays with number of elements
// equal to keys.size(). This allows the storage for those to be alloacted
// by the caller on the stack for small batches
void MultiGet(const ReadOptions& _read_options, const size_t num_keys,
ColumnFamilyHandle** column_families, const Slice* keys,
PinnableSlice* values, std::string* timestamps,
Status* statuses, const bool sorted_input = false) override;
DECLARE_SYNC_ASYNC_AND_CALLBACK(
void, MultiGet, MultiGetAsync,
(const ReadOptions& options, const size_t num_keys,
ColumnFamilyHandle** column_families, const Slice* keys,
PinnableSlice* values, std::string* timestamps, Status* statuses,
const bool sorted_input, AsyncCallback& callback),
const ReadOptions& _read_options, const size_t num_keys,
ColumnFamilyHandle** column_families, const Slice* keys,
PinnableSlice* values, std::string* timestamps, Status* statuses,
const bool sorted_input = false);
void MultiGetWithCallback(
const ReadOptions& _read_options, ColumnFamilyHandle* column_family,
@@ -718,13 +731,13 @@ class DBImpl : public DB {
int* number_of_operands = nullptr;
};
Status GetImpl(const ReadOptions& read_options,
ColumnFamilyHandle* column_family, const Slice& key,
PinnableSlice* value);
DECLARE_SYNC_AND_ASYNC(Status, GetImpl, const ReadOptions& read_options,
ColumnFamilyHandle* column_family, const Slice& key,
PinnableSlice* value);
Status GetImpl(const ReadOptions& read_options,
ColumnFamilyHandle* column_family, const Slice& key,
PinnableSlice* value, std::string* timestamp);
DECLARE_SYNC_AND_ASYNC(Status, GetImpl, const ReadOptions& read_options,
ColumnFamilyHandle* column_family, const Slice& key,
PinnableSlice* value, std::string* timestamp);
// Function that Get and KeyMayExist call with no_io true or false
// Note: 'value_found' from KeyMayExist propagates here
@@ -733,8 +746,9 @@ class DBImpl : public DB {
// get_impl_options.key via get_impl_options.value
// If get_impl_options.get_value = false get merge operands associated with
// get_impl_options.key via get_impl_options.merge_operands
virtual Status GetImpl(const ReadOptions& options, const Slice& key,
GetImplOptions& get_impl_options);
DECLARE_SYNC_AND_ASYNC_VIRTUAL(Status, GetImpl, const ReadOptions& options,
const Slice& key,
GetImplOptions& get_impl_options);
// If `snapshot` == kMaxSequenceNumber, set a recent one inside the file.
ArenaWrappedDBIter* NewIteratorImpl(const ReadOptions& options,
@@ -1189,6 +1203,13 @@ class DBImpl : public DB {
Status TEST_FlushMemTable(ColumnFamilyData* cfd,
const FlushOptions& flush_opts);
// Like TEST_FlushMemTable(bool wait, ...), but also sets
// FlushOptions::listener_wait so the flush does not return until the
// registered EventListener::OnFlushCompleted callbacks for the flushed
// memtables have finished running.
Status TEST_FlushMemTableWithListenerWait(bool allow_write_stall = false,
ColumnFamilyHandle* cfh = nullptr);
// Flush (multiple) ColumnFamilyData without using ColumnFamilyHandle. This
// is because in certain cases, we can flush column families, wait for the
// flush to complete, but delete the column family handle before the wait
@@ -1541,7 +1562,7 @@ class DBImpl : public DB {
// 2. db_mutex is NOT held
Status RenameTempFileToOptionsFile(const std::string& file_name,
bool is_remote_compaction_enabled);
Status DeleteObsoleteOptionsFiles();
Status DeleteObsoleteOptionsFiles(bool schedule_only);
void NotifyOnManualFlushScheduled(autovector<ColumnFamilyData*> cfds,
FlushReason flush_reason);
@@ -2219,7 +2240,10 @@ class DBImpl : public DB {
// Required: DB mutex held
Status PersistentStatsProcessFormatVersion();
Status ResumeImpl(DBRecoverContext context);
// `io_activity` is used to construct the ReadOptions/WriteOptions for the
// internal recovery work (e.g. the MANIFEST write in LogAndApply). Recovery
// is driven by flush operations, so callers pass Env::IOActivity::kFlush.
Status ResumeImpl(DBRecoverContext context, Env::IOActivity io_activity);
void MaybeIgnoreError(Status* s) const;
@@ -2501,18 +2525,24 @@ class DBImpl : public DB {
// memtable pending flush.
// resuming_from_bg_err indicates whether the caller is attempting to resume
// from background error.
// If wait_for_listener_notifications is true, also wait until the
// EventListener::OnFlushCompleted callbacks for the flushed memtables have
// finished running (see FlushOptions::listener_wait).
Status WaitForFlushMemTable(
ColumnFamilyData* cfd, const uint64_t* flush_memtable_id = nullptr,
bool resuming_from_bg_err = false,
std::optional<FlushReason> flush_reason = std::nullopt) {
std::optional<FlushReason> flush_reason = std::nullopt,
bool wait_for_listener_notifications = false) {
return WaitForFlushMemTables({cfd}, {flush_memtable_id},
resuming_from_bg_err, flush_reason);
resuming_from_bg_err, flush_reason,
wait_for_listener_notifications);
}
// Wait for memtables to be flushed for multiple column families.
Status WaitForFlushMemTables(
const autovector<ColumnFamilyData*>& cfds,
const autovector<const uint64_t*>& flush_memtable_ids,
bool resuming_from_bg_err, std::optional<FlushReason> flush_reason);
bool resuming_from_bg_err, std::optional<FlushReason> flush_reason,
bool wait_for_listener_notifications = false);
inline void WaitForPendingWrites() {
mutex_.AssertHeld();
@@ -2541,7 +2571,7 @@ class DBImpl : public DB {
}
// Wait for any LockWAL to clear
while (lock_wal_count_ > 0) {
while (!lock_wal_owner_thread_id_counts_.empty()) {
bg_cv_.Wait();
}
}
@@ -2954,11 +2984,12 @@ class DBImpl : public DB {
PinnableWideColumns* columns, std::string* timestamps,
Status* statuses, bool sorted_input);
void MultiGetCommon(const ReadOptions& options, const size_t num_keys,
ColumnFamilyHandle** column_families, const Slice* keys,
PinnableSlice* values, PinnableWideColumns* columns,
std::string* timestamps, Status* statuses,
bool sorted_input);
DECLARE_SYNC_AND_ASYNC(void, MultiGetCommon, const ReadOptions& options,
const size_t num_keys,
ColumnFamilyHandle** column_families,
const Slice* keys, PinnableSlice* values,
PinnableWideColumns* columns, std::string* timestamps,
Status* statuses, bool sorted_input);
// A structure to hold the information required to process MultiGet of keys
// belonging to one column family. For a multi column family MultiGet, there
@@ -3033,8 +3064,9 @@ class DBImpl : public DB {
// to have acquired the SuperVersion and pass in a snapshot sequence number
// in order to construct the LookupKeys. The start_key and num_keys specify
// the range of keys in the sorted_keys vector for a single column family.
Status MultiGetImpl(
const ReadOptions& read_options, size_t start_key, size_t num_keys,
DECLARE_SYNC_AND_ASYNC(
Status, MultiGetImpl, const ReadOptions& read_options, size_t start_key,
size_t num_keys,
autovector<KeyContext*, MultiGetContext::MAX_BATCH_SIZE>* sorted_keys,
SuperVersion* sv, SequenceNumber snap_seqnum, ReadCallback* callback);
@@ -3073,29 +3105,40 @@ 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);
// Completes primary memtable hits that can still carry direct-write blob
// references before the blob file is visible in Version metadata.
static void PostprocessDirectWriteValueRead(
const ReadOptions& read_options, const Slice& key,
const std::string* timestamp, 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(
// a BlobFetcher and therefore works for read-only/secondary DBs. Sets
// *did_resolve to true iff there was a reference to resolve -- in which case
// this function has finalized `value`/`columns` (populated on success,
// cleared on error) -- and false iff there was nothing to resolve (the caller
// finalizes the plain value). Only call with an OK incoming status.
static Status MaybeResolveMemtableBlobValue(
const Slice& key, const BlobFetcher* blob_fetcher, PinnableSlice* value,
PinnableWideColumns* columns, bool* did_resolve, bool* is_blob_index,
bool* value_found = nullptr);
// Completes read-only/secondary memtable Get()/GetEntity() hits. Given the
// status from the memtable read, resolves blob-backed payloads when
// `resolve_blob_backed_memtable_value` is true, self-pins a plain value on
// success, and clears outputs on any error (never leaving a half-populated
// result). 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. Returns the finalized status.
static Status 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);
PinnableWideColumns* columns, Status memtable_read_status,
bool* is_blob_index, bool* value_found = nullptr);
template <typename IterType, typename ImplType,
typename ErrorIteratorFuncType>
@@ -3600,12 +3643,13 @@ class DBImpl : public DB {
// Stop write token that is acquired when first LockWAL() is called.
// Destroyed when last UnlockWAL() is called. Controlled by DB mutex.
// See lock_wal_count_
// See lock_wal_owner_thread_id_counts_
std::unique_ptr<WriteControllerToken> lock_wal_write_token_;
// The number of LockWAL called without matching UnlockWAL call.
// See also lock_wal_write_token_
uint32_t lock_wal_count_ = 0;
// Thread IDs of valid LockWAL() owners and their recursive lock counts. Each
// owning thread must call UnlockWAL() the same number of times before writes
// can resume.
std::unordered_map<uint64_t, uint32_t> lock_wal_owner_thread_id_counts_;
};
class GetWithTimestampReadCallback : public ReadCallback {
+101 -15
View File
@@ -31,6 +31,7 @@
#include "util/cast_util.h"
#include "util/coding.h"
#include "util/concurrent_task_limiter_impl.h"
#include "util/defer.h"
#include "util/udt_util.h"
namespace ROCKSDB_NAMESPACE {
@@ -358,6 +359,34 @@ Status DBImpl::FlushMemTableToOutputFile(
need_cancel = false;
}
// Reserve a pending flush-completion notification before the DB mutex can be
// released, so that a Flush(wait=true, listener_wait=true) caller cannot
// observe this flush as finished until NotifyOnFlushCompleted() below has
// run. flush_job.Run() removed the memtables from the immutable list (which
// is what unblocks the waiter) and the DB mutex has been held continuously
// since, so there is no window in which the waiter could see the flush
// committed with a zero count. Reserved only when a notification will
// actually fire (matches the NotifyOnFlushCompleted call below): flush
// committed successfully, not a mempurge, listeners are registered, and this
// job committed at least one flush result to report.
const bool reserved_flush_notification =
s.ok() && !switched_to_mempurge &&
!immutable_db_options_.listeners.empty() &&
!flush_job.GetCommittedFlushJobsInfo()->empty();
if (reserved_flush_notification) {
cfd->imm()->AddPendingFlushNotifications(1);
}
// Release the reservation on every exit path (RAII) so a future early return
// cannot leak the count and hang listener_wait waiters. The DB mutex is held
// throughout this function, including at scope exit, which
// SubPendingFlushNotifications() requires. Runs after
// NotifyOnFlushCompleted() below has returned.
Defer release_flush_notification([&] {
if (reserved_flush_notification) {
cfd->imm()->SubPendingFlushNotifications(1);
}
});
if (cfd->blob_partition_manager() != nullptr &&
prepared_blob_generations > 0) {
auto unconsumed_additions = flush_job.TakeExternalBlobFileAdditions();
@@ -900,6 +929,42 @@ Status DBImpl::AtomicFlushMemTablesToOutputFiles(
directories_.GetDbDir(), log_buffer);
}
// Reserve a pending flush-completion notification for every CF that the
// NotifyOnFlushCompleted loop below will service, while the DB mutex is still
// held continuously since InstallMemtableAtomicFlushResults removed the
// memtables. This closes the window where a
// Flush(wait=true, listener_wait=true) waiter could observe an atomic flush
// as finished before a later CF's OnFlushCompleted has run, since the notify
// loop releases and re-acquires the mutex between CFs. The reservations are
// released by a matching pass after the notify loop.
autovector<uint8_t> reserved_flush_notification;
reserved_flush_notification.reserve(num_cfs);
{
const bool has_listeners = !immutable_db_options_.listeners.empty();
for (int i = 0; i != num_cfs; ++i) {
const bool reserve = s.ok() && has_listeners &&
!switched_to_mempurge[i] && !cfds[i]->IsDropped() &&
!jobs[i]->GetCommittedFlushJobsInfo()->empty();
if (reserve) {
cfds[i]->imm()->AddPendingFlushNotifications(1);
}
reserved_flush_notification.push_back(reserve ? 1 : 0);
}
}
// Release the reservations on every exit path (RAII) so a future early return
// cannot leak the count and hang listener_wait waiters. Done independently of
// the notify loop's skips so the count stays balanced even if a CF was
// dropped after it was reserved. The DB mutex is held throughout this
// function, including at scope exit, which SubPendingFlushNotifications()
// requires.
Defer release_flush_notifications([&] {
for (int i = 0; i != num_cfs; ++i) {
if (reserved_flush_notification[i]) {
cfds[i]->imm()->SubPendingFlushNotifications(1);
}
}
});
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)
@@ -1623,16 +1688,16 @@ void DBImpl::PrepareTrivialMoveEdit(Compaction& c, LogBuffer* log_buffer,
for (size_t i = 0; i < c.num_input_files(l); i++) {
FileMetaData* f = c.input(l, i);
c.edit()->DeleteFile(c.level(l), f->fd.GetNumber());
c.edit()->AddFile(c.output_level(), f->fd.GetNumber(), f->fd.GetPathId(),
f->fd.GetFileSize(), f->smallest, f->largest,
f->fd.smallest_seqno, f->fd.largest_seqno,
f->marked_for_compaction, f->temperature,
f->oldest_blob_file_number, f->oldest_ancester_time,
f->file_creation_time, f->epoch_number,
f->file_checksum, f->file_checksum_func_name,
f->unique_id, f->compensated_range_deletion_size,
f->tail_size, f->user_defined_timestamps_persisted,
f->min_timestamp, f->max_timestamp);
c.edit()->AddFile(
c.output_level(), f->fd.GetNumber(), f->fd.GetPathId(),
f->fd.GetFileSize(), f->smallest, f->largest, f->fd.smallest_seqno,
f->fd.largest_seqno, f->marked_for_compaction, f->temperature,
f->oldest_blob_file_number, f->oldest_ancester_time,
f->file_creation_time, f->epoch_number, f->file_checksum,
f->file_checksum_func_name, f->unique_id,
f->compensated_range_deletion_size, f->tail_size,
f->user_defined_timestamps_persisted, f->min_timestamp,
f->max_timestamp, f->file_open_metadata);
moved_bytes += static_cast<size_t>(c.input(l, i)->fd.GetFileSize());
ROCKS_LOG_BUFFER(
log_buffer, "[%s] Moved #%" PRIu64 " to level-%d %" PRIu64 " bytes\n",
@@ -2209,7 +2274,7 @@ Status DBImpl::ReFitLevel(ColumnFamilyData* cfd, int level, int target_level) {
f->file_checksum, f->file_checksum_func_name, f->unique_id,
f->compensated_range_deletion_size, f->tail_size,
f->user_defined_timestamps_persisted, f->min_timestamp,
f->max_timestamp);
f->max_timestamp, f->file_open_metadata);
}
ROCKS_LOG_DEBUG(immutable_db_options_.info_log,
"[%s] Apply version edit:\n%s", cfd->GetName().c_str(),
@@ -2649,6 +2714,13 @@ Status DBImpl::FlushMemTable(ColumnFamilyData* cfd,
"Please try again later after writes are resumed";
return Status::TryAgain(oss.str());
}
if (flush_options.wait) {
InstrumentedMutexLock lock(&mutex_);
if (lock_wal_owner_thread_id_counts_.count(env_->GetThreadID()) > 0) {
return Status::Aborted(
"Likely deadlock as the same thread called LockWAL()");
}
}
Status s;
if (!flush_options.allow_write_stall) {
bool flush_needed = true;
@@ -2787,7 +2859,7 @@ Status DBImpl::FlushMemTable(ColumnFamilyData* cfd,
s = WaitForFlushMemTables(
cfds, flush_memtable_ids,
flush_reason == FlushReason::kErrorRecovery /* resuming_from_bg_err */,
flush_reason);
flush_reason, flush_options.listener_wait);
InstrumentedMutexLock lock_guard(&mutex_);
for (auto* tmp_cfd : cfds) {
tmp_cfd->UnrefAndTryDelete();
@@ -2807,6 +2879,13 @@ Status DBImpl::AtomicFlushMemTables(
"Please try again later after writes are resumed";
return Status::TryAgain(oss.str());
}
if (flush_options.wait) {
InstrumentedMutexLock lock(&mutex_);
if (lock_wal_owner_thread_id_counts_.count(env_->GetThreadID()) > 0) {
return Status::Aborted(
"Likely deadlock as the same thread called LockWAL()");
}
}
Status s;
autovector<ColumnFamilyData*> candidate_cfds;
if (provided_candidate_cfds.empty()) {
@@ -2935,7 +3014,7 @@ Status DBImpl::AtomicFlushMemTables(
s = WaitForFlushMemTables(
cfds, flush_memtable_ids,
flush_reason == FlushReason::kErrorRecovery /* resuming_from_bg_err */,
flush_reason);
flush_reason, flush_options.listener_wait);
InstrumentedMutexLock lock_guard(&mutex_);
for (auto* cfd : cfds) {
cfd->UnrefAndTryDelete();
@@ -3104,7 +3183,8 @@ Status DBImpl::WaitUntilFlushWouldNotStallWrites(ColumnFamilyData* cfd,
Status DBImpl::WaitForFlushMemTables(
const autovector<ColumnFamilyData*>& cfds,
const autovector<const uint64_t*>& flush_memtable_ids,
bool resuming_from_bg_err, std::optional<FlushReason> flush_reason) {
bool resuming_from_bg_err, std::optional<FlushReason> flush_reason,
bool wait_for_listener_notifications) {
int num = static_cast<int>(cfds.size());
// Wait until the compaction completes
InstrumentedMutexLock l(&mutex_);
@@ -3149,7 +3229,13 @@ Status DBImpl::WaitForFlushMemTables(
flush_reason.value() != FlushReason::kExternalFileIngestion ||
cfds[i]->GetSuperVersion()->imm->GetID() ==
cfds[i]->imm()->current()->GetID()) {
++num_finished;
// When the caller requested it, also wait for the flush-completion
// listener callbacks for this CF to finish before counting it as
// finished (see FlushOptions::listener_wait).
if (!wait_for_listener_notifications ||
cfds[i]->imm()->NumPendingFlushNotifications() == 0) {
++num_finished;
}
}
}
}
+17 -1
View File
@@ -37,7 +37,7 @@ Status DBImpl::TEST_SwitchWAL() {
Status DBImpl::TEST_ResumeImpl(DBRecoverContext context) {
InstrumentedMutexLock l(&mutex_);
return ResumeImpl(context);
return ResumeImpl(context, Env::IOActivity::kFlush);
}
uint64_t DBImpl::TEST_MaxNextLevelOverlappingBytes(
@@ -164,6 +164,22 @@ Status DBImpl::TEST_FlushMemTable(ColumnFamilyData* cfd,
return FlushMemTable(cfd, flush_opts, FlushReason::kTest);
}
Status DBImpl::TEST_FlushMemTableWithListenerWait(bool allow_write_stall,
ColumnFamilyHandle* cfh) {
FlushOptions fo;
fo.wait = true;
fo.listener_wait = true;
fo.allow_write_stall = allow_write_stall;
ColumnFamilyData* cfd;
if (cfh == nullptr) {
cfd = default_cf_handle_->cfd();
} else {
auto cfhi = static_cast_with_check<ColumnFamilyHandleImpl>(cfh);
cfd = cfhi->cfd();
}
return FlushMemTable(cfd, fo, FlushReason::kTest);
}
Status DBImpl::TEST_AtomicFlushMemTables(
const autovector<ColumnFamilyData*>& provided_candidate_cfds,
const FlushOptions& flush_opts) {
+1 -1
View File
@@ -145,7 +145,7 @@ Status DBImpl::PromoteL0(ColumnFamilyHandle* column_family, int target_level) {
f->file_checksum_func_name, f->unique_id,
f->compensated_range_deletion_size, f->tail_size,
f->user_defined_timestamps_persisted, f->min_timestamp,
f->max_timestamp);
f->max_timestamp, f->file_open_metadata);
}
status = versions_->LogAndApply(cfd, read_options, write_options, &edit,
+25 -5
View File
@@ -243,6 +243,11 @@ Status DBImpl::ValidateOptions(
}
Status DBImpl::ValidateOptions(const DBOptions& db_options) {
if (db_options.read_io_executor_threads <= 0) {
return Status::InvalidArgument(
"read_io_executor_threads must be greater than zero");
}
if (db_options.db_paths.size() > 4) {
return Status::NotSupported(
"More than four DB paths are not supported yet. ");
@@ -683,7 +688,8 @@ Status DBImpl::Recover(
f->file_checksum, f->file_checksum_func_name,
f->unique_id, f->compensated_range_deletion_size,
f->tail_size, f->user_defined_timestamps_persisted,
f->min_timestamp, f->max_timestamp);
f->min_timestamp, f->max_timestamp,
f->file_open_metadata);
ROCKS_LOG_WARN(immutable_db_options_.info_log,
"[%s] Moving #%" PRIu64
" from from_level-%d to from_level-%d %" PRIu64
@@ -1941,10 +1947,8 @@ Status DBImpl::MaybeFlushFinalMemtableOrRestoreActiveLogFiles(
const uint64_t new_min_log = max_wal_number + 1;
VersionEdit wal_deletion;
bool emit_wal_deletion = false;
if (immutable_db_options_.track_and_verify_wals_in_manifest) {
// Determining whether DeleteWalsBefore actually shrinks WalSet
// membership requires WalSet state outside this site, so emit
// unconditionally (pre-existing behavior).
if (immutable_db_options_.track_and_verify_wals_in_manifest &&
new_min_log > versions_->GetWalSet().GetMinWalNumberToKeep()) {
wal_deletion.DeleteWalsBefore(new_min_log);
emit_wal_deletion = true;
}
@@ -2873,11 +2877,16 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname,
}
TEST_SYNC_POINT("DBImpl::Open:Opened");
Status persist_options_status;
bool cleanup_obsolete_options_files = false;
if (s.ok()) {
// Persist RocksDB Options before scheduling the compaction.
// The WriteOptionsFile() will release and lock the mutex internally.
persist_options_status =
impl->WriteOptionsFile(write_options, true /*db_mutex_already_held*/);
cleanup_obsolete_options_files =
impl->immutable_db_options_.avoid_unnecessary_blocking_io &&
impl->immutable_db_options_.compaction_service == nullptr &&
!impl->disable_delete_obsolete_files_;
impl->opened_successfully_ = true;
} else {
persist_options_status.PermitUncheckedError();
@@ -2967,6 +2976,17 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname,
s = impl->RegisterRecordSeqnoTimeWorker();
}
impl->options_mutex_.Unlock();
if (cleanup_obsolete_options_files) {
Status obsolete_options_status =
impl->DeleteObsoleteOptionsFiles(/*schedule_only=*/s.ok());
if (!obsolete_options_status.ok()) {
ROCKS_LOG_WARN(impl->immutable_db_options_.info_log,
"Unable to %s obsolete OPTIONS files%s: %s",
s.ok() ? "schedule deletion of" : "delete",
s.ok() ? "" : " after DB open failure",
obsolete_options_status.ToString().c_str());
}
}
if (s.ok()) {
*dbptr = std::move(impl);
} else {
+13 -141
View File
@@ -30,147 +30,6 @@ DBImplReadOnly::DBImplReadOnly(const DBOptions& db_options,
DBImplReadOnly::~DBImplReadOnly() = default;
// Implementations of the DB interface
Status DBImplReadOnly::GetImpl(const ReadOptions& read_options,
const Slice& key,
GetImplOptions& get_impl_options) {
assert(get_impl_options.value != nullptr ||
get_impl_options.columns != nullptr ||
get_impl_options.merge_operands != nullptr);
assert(get_impl_options.column_family);
Status s;
if (read_options.timestamp) {
s = FailIfTsMismatchCf(get_impl_options.column_family,
*(read_options.timestamp));
if (!s.ok()) {
return s;
}
} else {
s = FailIfCfHasTs(get_impl_options.column_family);
if (!s.ok()) {
return s;
}
}
// Clear the timestamps for returning results so that we can distinguish
// between tombstone or key that has never been written
if (get_impl_options.timestamp) {
get_impl_options.timestamp->clear();
}
PERF_CPU_TIMER_GUARD(get_cpu_nanos, immutable_db_options_.clock);
StopWatch sw(immutable_db_options_.clock, stats_, DB_GET);
PERF_TIMER_GUARD(get_snapshot_time);
const Comparator* ucmp = get_impl_options.column_family->GetComparator();
assert(ucmp);
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_) {
tracer_->Get(get_impl_options.column_family, key);
}
}
// In read-only mode Get(), no super version operation is needed (i.e.
// GetAndRefSuperVersion and ReturnAndCleanupSuperVersion)
SuperVersion* super_version = cfd->GetSuperVersion();
if (read_options.timestamp && read_options.timestamp->size() > 0) {
s = FailIfReadCollapsedHistory(cfd, super_version,
*(read_options.timestamp));
if (!s.ok()) {
return s;
}
}
// Prepare to store a list of merge operations if merge occurs.
MergeContext merge_context;
// TODO - Large Result Optimization for Read Only DB
// (https://github.com/facebook/rocksdb/pull/10458)
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(
lkey,
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_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);
PinnedIteratorsManager pinned_iters_mgr;
super_version->current->Get(
read_options, lkey, get_impl_options.value, get_impl_options.columns,
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=*/get_impl_options.get_value);
RecordTick(stats_, MEMTABLE_MISS);
}
{
RecordTick(stats_, NUMBER_KEYS_READ);
size_t size = 0;
if (get_impl_options.value) {
size = get_impl_options.value->size();
} else if (get_impl_options.columns) {
size = get_impl_options.columns->serialized_size();
} else if (get_impl_options.merge_operands) {
*get_impl_options.number_of_operands =
static_cast<int>(merge_context.GetNumOperands());
for (const Slice& sl : merge_context.GetOperands()) {
size += sl.size();
get_impl_options.merge_operands->PinSelf(sl);
get_impl_options.merge_operands++;
}
}
RecordTick(stats_, BYTES_READ, size);
RecordInHistogram(stats_, BYTES_PER_READ, size);
PERF_COUNTER_ADD(get_read_bytes, size);
}
return s;
}
Iterator* DBImplReadOnly::NewIterator(const ReadOptions& _read_options,
ColumnFamilyHandle* column_family) {
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
@@ -411,3 +270,16 @@ Status DBImplReadOnly::OpenForReadOnlyWithoutCheck(
}
} // namespace ROCKSDB_NAMESPACE
// Generate the regular and coroutine versions of the functions defined in
// db_impl_readonly_sync_and_async.h by including it twice. The macros expand
// differently based on whether WITH_COROUTINES or WITHOUT_COROUTINES is
// defined.
// clang-format off
#define WITHOUT_COROUTINES
#include "db/db_impl/db_impl_readonly_sync_and_async.h"
#undef WITHOUT_COROUTINES
#define WITH_COROUTINES
#include "db/db_impl/db_impl_readonly_sync_and_async.h"
#undef WITH_COROUTINES
// clang-format on
+3 -2
View File
@@ -24,8 +24,9 @@ class DBImplReadOnly : public DBImpl {
// Implementations of the DB interface
using DBImpl::GetImpl;
Status GetImpl(const ReadOptions& options, const Slice& key,
GetImplOptions& get_impl_options) override;
DECLARE_SYNC_AND_ASYNC_OVERRIDE(Status, GetImpl, const ReadOptions& options,
const Slice& key,
GetImplOptions& get_impl_options);
// TODO: Implement ReadOnly MultiGet?
@@ -0,0 +1,164 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
//
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#include "util/coro_utils.h"
#if defined(WITHOUT_COROUTINES) || \
(defined(USE_COROUTINES) && defined(WITH_COROUTINES))
namespace ROCKSDB_NAMESPACE {
// Implementations of the DB interface
DEFINE_SYNC_AND_ASYNC(Status, DBImplReadOnly::GetImpl)
(const ReadOptions& read_options, const Slice& key,
GetImplOptions& get_impl_options) {
assert(get_impl_options.value != nullptr ||
get_impl_options.columns != nullptr ||
get_impl_options.merge_operands != nullptr);
assert(get_impl_options.column_family);
Status s;
if (read_options.timestamp) {
s = FailIfTsMismatchCf(get_impl_options.column_family,
*(read_options.timestamp));
if (!s.ok()) {
CO_RETURN s;
}
} else {
s = FailIfCfHasTs(get_impl_options.column_family);
if (!s.ok()) {
CO_RETURN s;
}
}
// Clear the timestamps for returning results so that we can distinguish
// between tombstone or key that has never been written
if (get_impl_options.timestamp) {
get_impl_options.timestamp->clear();
}
#if defined(WITHOUT_COROUTINES)
PERF_CPU_TIMER_GUARD(get_cpu_nanos, immutable_db_options_.clock);
#endif
StopWatch sw(immutable_db_options_.clock, stats_, DB_GET);
PERF_TIMER_GUARD(get_snapshot_time);
const Comparator* ucmp = get_impl_options.column_family->GetComparator();
assert(ucmp);
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_) {
tracer_->Get(get_impl_options.column_family, key);
}
}
// In read-only mode Get(), no super version operation is needed (i.e.
// GetAndRefSuperVersion and ReturnAndCleanupSuperVersion)
SuperVersion* super_version = cfd->GetSuperVersion();
if (read_options.timestamp && read_options.timestamp->size() > 0) {
s = FailIfReadCollapsedHistory(cfd, super_version,
*(read_options.timestamp));
if (!s.ok()) {
CO_RETURN s;
}
}
// Prepare to store a list of merge operations if merge occurs.
MergeContext merge_context;
// TODO - Large Result Optimization for Read Only DB
// (https://github.com/facebook/rocksdb/pull/10458)
SequenceNumber max_covering_tombstone_seq = 0;
LookupKey lkey(key, snapshot, read_options.timestamp);
PERF_TIMER_STOP(get_snapshot_time);
std::optional<VersionBlobFetcher> 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(
lkey,
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_ptr,
/*do_merge=*/get_impl_options.get_value, memtable_blob_fetcher_ptr)) {
s = DBImpl::PostprocessMemtableValueRead(
key, ts, resolve_blob_backed_memtable_value, memtable_blob_fetcher_ptr,
get_impl_options.value, get_impl_options.columns, std::move(s),
&is_blob_index, get_impl_options.value_found);
RecordTick(stats_, MEMTABLE_HIT);
} else {
PERF_TIMER_GUARD(get_from_output_files_time);
PinnedIteratorsManager pinned_iters_mgr;
CO_AWAIT(super_version->current->Get, read_options, lkey,
get_impl_options.value, get_impl_options.columns, 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=*/get_impl_options.get_value);
RecordTick(stats_, MEMTABLE_MISS);
}
{
RecordTick(stats_, NUMBER_KEYS_READ);
size_t size = 0;
// Mirror DBImpl::GetImpl: only produce merge-operand output and count bytes
// read on success. A non-OK status leaves outputs cleared (see
// PostprocessMemtableValueRead) and records no read throughput.
if (s.ok()) {
if (get_impl_options.value) {
size = get_impl_options.value->size();
} else if (get_impl_options.columns) {
size = get_impl_options.columns->payload_size();
} else if (get_impl_options.merge_operands) {
*get_impl_options.number_of_operands =
static_cast<int>(merge_context.GetNumOperands());
for (const Slice& sl : merge_context.GetOperands()) {
size += sl.size();
get_impl_options.merge_operands->PinSelf(sl);
get_impl_options.merge_operands++;
}
}
RecordTick(stats_, BYTES_READ, size);
RecordInHistogram(stats_, BYTES_PER_READ, size);
PERF_COUNTER_ADD(get_read_bytes, size);
}
}
CO_RETURN s;
}
} // namespace ROCKSDB_NAMESPACE
#endif // defined(WITHOUT_COROUTINES) ||
// (defined(USE_COROUTINES) && defined(WITH_COROUTINES))
+17 -187
View File
@@ -336,193 +336,6 @@ Status DBImplSecondary::RecoverLogFiles(
return status;
}
Status DBImplSecondary::GetImpl(const ReadOptions& read_options,
const Slice& key,
GetImplOptions& get_impl_options) {
assert(get_impl_options.value != nullptr ||
get_impl_options.columns != nullptr ||
get_impl_options.merge_operands != nullptr);
assert(get_impl_options.column_family);
Status s;
if (read_options.timestamp) {
s = FailIfTsMismatchCf(get_impl_options.column_family,
*(read_options.timestamp));
if (!s.ok()) {
return s;
}
} else {
s = FailIfCfHasTs(get_impl_options.column_family);
if (!s.ok()) {
return s;
}
}
// Clear the timestamps for returning results so that we can distinguish
// between tombstone or key that has never been written
if (get_impl_options.timestamp) {
get_impl_options.timestamp->clear();
}
PERF_CPU_TIMER_GUARD(get_cpu_nanos, immutable_db_options_.clock);
StopWatch sw(immutable_db_options_.clock, stats_, DB_GET);
PERF_TIMER_GUARD(get_snapshot_time);
const Comparator* ucmp = get_impl_options.column_family->GetComparator();
assert(ucmp);
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_) {
tracer_->Get(get_impl_options.column_family, key);
}
}
// Acquire SuperVersion
SuperVersion* super_version = GetAndRefSuperVersion(cfd);
if (read_options.timestamp && read_options.timestamp->size() > 0) {
s = FailIfReadCollapsedHistory(cfd, super_version,
*(read_options.timestamp));
if (!s.ok()) {
ReturnAndCleanupSuperVersion(cfd, super_version);
return s;
}
}
MergeContext merge_context;
// TODO - Large Result Optimization for Secondary DB
// (https://github.com/facebook/rocksdb/pull/10458)
SequenceNumber max_covering_tombstone_seq = 0;
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) {
if (super_version->mem->Get(
lkey,
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_ptr,
/*do_merge=*/true, memtable_blob_fetcher_ptr)) {
done = true;
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(
lkey,
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,
is_blob_ptr, memtable_blob_fetcher_ptr)) {
done = true;
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 {
// GetMergeOperands
if (super_version->mem->Get(
lkey,
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=*/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, memtable_blob_fetcher_ptr)) {
done = true;
RecordTick(stats_, MEMTABLE_HIT);
}
}
if (!s.ok() && !s.IsMergeInProgress() && !s.IsNotFound()) {
assert(done);
ReturnAndCleanupSuperVersion(cfd, super_version);
return s;
}
if (!done) {
PERF_TIMER_GUARD(get_from_output_files_time);
PinnedIteratorsManager pinned_iters_mgr;
super_version->current->Get(
read_options, lkey, get_impl_options.value, get_impl_options.columns,
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=*/get_impl_options.get_value);
RecordTick(stats_, MEMTABLE_MISS);
}
{
PERF_TIMER_GUARD(get_post_process_time);
ReturnAndCleanupSuperVersion(cfd, super_version);
RecordTick(stats_, NUMBER_KEYS_READ);
size_t size = 0;
if (get_impl_options.value) {
size = get_impl_options.value->size();
} else if (get_impl_options.columns) {
size = get_impl_options.columns->serialized_size();
} else if (get_impl_options.merge_operands) {
*get_impl_options.number_of_operands =
static_cast<int>(merge_context.GetNumOperands());
for (const Slice& sl : merge_context.GetOperands()) {
size += sl.size();
get_impl_options.merge_operands->PinSelf(sl);
get_impl_options.merge_operands++;
}
}
RecordTick(stats_, BYTES_READ, size);
RecordTimeToHistogram(stats_, BYTES_PER_READ, size);
PERF_COUNTER_ADD(get_read_bytes, size);
}
return s;
}
Iterator* DBImplSecondary::NewIterator(const ReadOptions& _read_options,
ColumnFamilyHandle* column_family) {
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
@@ -1596,9 +1409,13 @@ Status DB::OpenAndCompact(
// needs LSM state from MANIFEST, not memtable data from WAL replay)
std::unique_ptr<DB> db;
std::vector<ColumnFamilyHandle*> handles;
const uint64_t db_open_start_micros = db_options.env->NowMicros();
s = DBImplSecondary::OpenAsSecondaryImpl(db_options, name, output_directory,
column_families, &handles, &db,
/*recover_wal=*/false);
RecordTimeToHistogram(db_options.statistics.get(),
OPEN_AND_COMPACT_DB_OPEN_MICROS,
db_options.env->NowMicros() - db_open_start_micros);
if (!s.ok()) {
return s;
}
@@ -1799,3 +1616,16 @@ Status DBImplSecondary::FinalizeCompactionProgressWriter(
return Status::OK();
}
} // namespace ROCKSDB_NAMESPACE
// Generate the regular and coroutine versions of the functions defined in
// db_impl_secondary_sync_and_async.h by including it twice. The macros expand
// differently based on whether WITH_COROUTINES or WITHOUT_COROUTINES is
// defined.
// clang-format off
#define WITHOUT_COROUTINES
#include "db/db_impl/db_impl_secondary_sync_and_async.h"
#undef WITHOUT_COROUTINES
#define WITH_COROUTINES
#include "db/db_impl/db_impl_secondary_sync_and_async.h"
#undef WITH_COROUTINES
// clang-format on
+3 -2
View File
@@ -97,8 +97,9 @@ class DBImplSecondary : public DBImpl {
// it eagerly keeps all talbe files open and is able to access the contents of
// deleted files via prior open fd.
using DBImpl::GetImpl;
Status GetImpl(const ReadOptions& options, const Slice& key,
GetImplOptions& get_impl_options) override;
DECLARE_SYNC_AND_ASYNC_OVERRIDE(Status, GetImpl, const ReadOptions& options,
const Slice& key,
GetImplOptions& get_impl_options);
using DBImpl::NewIterator;
// Operations on the created iterators can return IOError due to files being
@@ -0,0 +1,227 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
//
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#include "util/coro_utils.h"
#if defined(WITHOUT_COROUTINES) || \
(defined(USE_COROUTINES) && defined(WITH_COROUTINES))
namespace ROCKSDB_NAMESPACE {
DEFINE_SYNC_AND_ASYNC(Status, DBImplSecondary::GetImpl)
(const ReadOptions& read_options, const Slice& key,
GetImplOptions& get_impl_options) {
assert(get_impl_options.value != nullptr ||
get_impl_options.columns != nullptr ||
get_impl_options.merge_operands != nullptr);
assert(get_impl_options.column_family);
Status s;
if (read_options.timestamp) {
s = FailIfTsMismatchCf(get_impl_options.column_family,
*(read_options.timestamp));
if (!s.ok()) {
CO_RETURN s;
}
} else {
s = FailIfCfHasTs(get_impl_options.column_family);
if (!s.ok()) {
CO_RETURN s;
}
}
// Clear the timestamps for returning results so that we can distinguish
// between tombstone or key that has never been written
if (get_impl_options.timestamp) {
get_impl_options.timestamp->clear();
}
#if defined(WITHOUT_COROUTINES)
PERF_CPU_TIMER_GUARD(get_cpu_nanos, immutable_db_options_.clock);
#endif
StopWatch sw(immutable_db_options_.clock, stats_, DB_GET);
PERF_TIMER_GUARD(get_snapshot_time);
const Comparator* ucmp = get_impl_options.column_family->GetComparator();
assert(ucmp);
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_) {
tracer_->Get(get_impl_options.column_family, key);
}
}
// Acquire SuperVersion
#if defined(WITH_COROUTINES)
SuperVersion* super_version = cfd->GetReferencedSuperVersion(this);
#else
SuperVersion* super_version = GetAndRefSuperVersion(cfd);
#endif
if (read_options.timestamp && read_options.timestamp->size() > 0) {
s = FailIfReadCollapsedHistory(cfd, super_version,
*(read_options.timestamp));
if (!s.ok()) {
#if defined(WITH_COROUTINES)
CleanupSuperVersion(super_version);
#else
ReturnAndCleanupSuperVersion(cfd, super_version);
#endif
CO_RETURN s;
}
}
MergeContext merge_context;
// TODO - Large Result Optimization for Secondary DB
// (https://github.com/facebook/rocksdb/pull/10458)
SequenceNumber max_covering_tombstone_seq = 0;
LookupKey lkey(key, snapshot, read_options.timestamp);
PERF_TIMER_STOP(get_snapshot_time);
bool done = false;
std::optional<VersionBlobFetcher> 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) {
if (super_version->mem->Get(
lkey,
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_ptr,
/*do_merge=*/true, memtable_blob_fetcher_ptr)) {
done = true;
s = DBImpl::PostprocessMemtableValueRead(
key, ts, resolve_blob_backed_memtable_value,
memtable_blob_fetcher_ptr, get_impl_options.value,
get_impl_options.columns, std::move(s), &is_blob_index,
get_impl_options.value_found);
RecordTick(stats_, MEMTABLE_HIT);
} else if ((s.ok() || s.IsMergeInProgress()) &&
super_version->imm->Get(
lkey,
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,
is_blob_ptr, memtable_blob_fetcher_ptr)) {
done = true;
s = DBImpl::PostprocessMemtableValueRead(
key, ts, resolve_blob_backed_memtable_value,
memtable_blob_fetcher_ptr, get_impl_options.value,
get_impl_options.columns, std::move(s), &is_blob_index,
get_impl_options.value_found);
RecordTick(stats_, MEMTABLE_HIT);
}
} else {
// GetMergeOperands
if (super_version->mem->Get(
lkey,
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=*/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, memtable_blob_fetcher_ptr)) {
done = true;
RecordTick(stats_, MEMTABLE_HIT);
}
}
if (!s.ok() && !s.IsMergeInProgress() && !s.IsNotFound()) {
assert(done);
#if defined(WITH_COROUTINES)
CleanupSuperVersion(super_version);
#else
ReturnAndCleanupSuperVersion(cfd, super_version);
#endif
CO_RETURN s;
}
if (!done) {
PERF_TIMER_GUARD(get_from_output_files_time);
PinnedIteratorsManager pinned_iters_mgr;
CO_AWAIT(super_version->current->Get, read_options, lkey,
get_impl_options.value, get_impl_options.columns, 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=*/get_impl_options.get_value);
RecordTick(stats_, MEMTABLE_MISS);
}
{
PERF_TIMER_GUARD(get_post_process_time);
#if defined(WITH_COROUTINES)
CleanupSuperVersion(super_version);
#else
ReturnAndCleanupSuperVersion(cfd, super_version);
#endif
RecordTick(stats_, NUMBER_KEYS_READ);
size_t size = 0;
// Mirror DBImpl::GetImpl: only produce merge-operand output and count bytes
// read on success. A non-OK status leaves outputs cleared (see
// PostprocessMemtableValueRead) and records no read throughput.
if (s.ok()) {
if (get_impl_options.value) {
size = get_impl_options.value->size();
} else if (get_impl_options.columns) {
size = get_impl_options.columns->payload_size();
} else if (get_impl_options.merge_operands) {
*get_impl_options.number_of_operands =
static_cast<int>(merge_context.GetNumOperands());
for (const Slice& sl : merge_context.GetOperands()) {
size += sl.size();
get_impl_options.merge_operands->PinSelf(sl);
get_impl_options.merge_operands++;
}
}
RecordTick(stats_, BYTES_READ, size);
RecordTimeToHistogram(stats_, BYTES_PER_READ, size);
PERF_COUNTER_ADD(get_read_bytes, size);
}
}
CO_RETURN s;
}
} // namespace ROCKSDB_NAMESPACE
#endif // defined(WITHOUT_COROUTINES) ||
// (defined(USE_COROUTINES) && defined(WITH_COROUTINES))
+828
View File
@@ -0,0 +1,828 @@
// Copyright (c) Meta Platforms, Inc. and affiliates.
//
// This source code is licensed under both the GPLv2 (found in the
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#include "util/coro_utils.h"
#if defined(USE_COROUTINES) && defined(WITH_COROUTINES)
#include "folly/executors/IOExecutor.h"
#include "folly/io/async/EventBase.h"
#endif // USE_COROUTINES && WITH_COROUTINES
#if defined(WITHOUT_COROUTINES) || \
(defined(USE_COROUTINES) && defined(WITH_COROUTINES))
namespace ROCKSDB_NAMESPACE {
DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
(const ReadOptions& read_options, ColumnFamilyHandle* column_family,
const Slice& key, PinnableSlice* value) {
CO_RETURN CO_AWAIT(GetImpl, read_options, column_family, key, value,
/*timestamp=*/nullptr);
}
DEFINE_SYNC_AND_ASYNC(Status, DBImpl::Get)
(const ReadOptions& _read_options, ColumnFamilyHandle* column_family,
const Slice& key, PinnableSlice* value, std::string* timestamp) {
assert(value != nullptr);
value->Reset();
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
_read_options.io_activity != Env::IOActivity::kGet) {
CO_RETURN Status::InvalidArgument(
"Can only call Get with `ReadOptions::io_activity` is "
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kGet`");
}
ReadOptions read_options(_read_options);
if (read_options.io_activity == Env::IOActivity::kUnknown) {
read_options.io_activity = Env::IOActivity::kGet;
}
#ifdef WITH_COROUTINES
auto* read_executor = immutable_db_options_.fs->GetReadExecutor();
if (read_executor != nullptr) {
auto* read_event_base = read_executor->getEventBase();
assert(read_event_base != nullptr);
Status s = co_await folly::coro::co_nothrow(folly::coro::co_withExecutor(
folly::Executor::getKeepAliveToken(read_event_base),
GetImplCoroutine(read_options, column_family, key, value, timestamp)));
co_return s;
}
#endif
CO_RETURN GetImpl(read_options, column_family, key, value, timestamp);
}
DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
(const ReadOptions& read_options, ColumnFamilyHandle* column_family,
const Slice& key, PinnableSlice* value, std::string* timestamp) {
GetImplOptions get_impl_options;
get_impl_options.column_family = column_family;
get_impl_options.value = value;
get_impl_options.timestamp = timestamp;
Status s = CO_AWAIT(GetImpl, read_options, key, get_impl_options);
CO_RETURN s;
}
DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
(const ReadOptions& read_options, const Slice& key,
GetImplOptions& get_impl_options) {
assert(get_impl_options.value != nullptr ||
get_impl_options.merge_operands != nullptr ||
get_impl_options.columns != nullptr);
assert(get_impl_options.column_family);
if (read_options.timestamp) {
const Status s = FailIfTsMismatchCf(get_impl_options.column_family,
*(read_options.timestamp));
if (!s.ok()) {
CO_RETURN s;
}
} else {
const Status s = FailIfCfHasTs(get_impl_options.column_family);
if (!s.ok()) {
CO_RETURN s;
}
}
// Clear the timestamps for returning results so that we can distinguish
// between tombstone or key that has never been written
if (get_impl_options.timestamp) {
get_impl_options.timestamp->clear();
}
GetWithTimestampReadCallback read_cb(0); // Will call Refresh
#if defined(WITHOUT_COROUTINES)
PERF_CPU_TIMER_GUARD(get_cpu_nanos, immutable_db_options_.clock);
#endif
StopWatch sw(immutable_db_options_.clock, stats_, DB_GET);
PERF_TIMER_GUARD(get_snapshot_time);
auto cfh = static_cast_with_check<ColumnFamilyHandleImpl>(
get_impl_options.column_family);
auto cfd = cfh->cfd();
if (tracer_) {
// TODO: This mutex should be removed later, to improve performance when
// tracing is enabled.
InstrumentedMutexLock lock(&trace_mutex_);
if (tracer_) {
// TODO: maybe handle the tracing status?
tracer_->Get(get_impl_options.column_family, key).PermitUncheckedError();
}
}
if (get_impl_options.get_merge_operands_options != nullptr) {
for (int i = 0; i < get_impl_options.get_merge_operands_options
->expected_max_number_of_operands;
++i) {
get_impl_options.merge_operands[i].Reset();
}
}
// Acquire SuperVersion
#if defined(WITH_COROUTINES)
SuperVersion* sv = cfd->GetReferencedSuperVersion(this);
Defer sv_cleanup([&] { CleanupSuperVersion(sv); });
#else
SuperVersion* sv = GetAndRefSuperVersion(cfd);
Defer sv_cleanup([&] { ReturnAndCleanupSuperVersion(cfd, sv); });
#endif
if (read_options.timestamp && read_options.timestamp->size() > 0) {
const Status s =
FailIfReadCollapsedHistory(cfd, sv, *(read_options.timestamp));
if (!s.ok()) {
CO_RETURN s;
}
}
TEST_SYNC_POINT_CALLBACK("DBImpl::GetImpl:AfterAcquireSv", nullptr);
TEST_SYNC_POINT("DBImpl::GetImpl:1");
TEST_SYNC_POINT("DBImpl::GetImpl:2");
SequenceNumber snapshot;
if (read_options.snapshot != nullptr) {
if (get_impl_options.callback) {
// Already calculated based on read_options.snapshot
snapshot = get_impl_options.callback->max_visible_seq();
} else {
snapshot =
static_cast<const SnapshotImpl*>(read_options.snapshot)->number_;
}
} else {
// Note that the snapshot is assigned AFTER referencing the super
// version because otherwise a flush happening in between may compact away
// data for the snapshot, so the reader would see neither data that was be
// visible to the snapshot before compaction nor the newer data inserted
// afterwards.
snapshot = GetLastPublishedSequence();
if (get_impl_options.callback) {
// The unprep_seqs are not published for write unprepared, so it could be
// that max_visible_seq is larger. Seek to the std::max of the two.
// However, we still want our callback to contain the actual snapshot so
// that it can do the correct visibility filtering.
get_impl_options.callback->Refresh(snapshot);
// Internally, WriteUnpreparedTxnReadCallback::Refresh would set
// max_visible_seq = max(max_visible_seq, snapshot)
//
// Currently, the commented out assert is broken by
// InvalidSnapshotReadCallback, but if write unprepared recovery followed
// the regular transaction flow, then this special read callback would not
// be needed.
//
// assert(callback->max_visible_seq() >= snapshot);
snapshot = get_impl_options.callback->max_visible_seq();
}
}
// If timestamp is used, we use read callback to ensure <key,t,s> is returned
// only if t <= read_opts.timestamp and s <= snapshot.
// HACK: temporarily overwrite input struct field but restore
SaveAndRestore<ReadCallback*> restore_callback(&get_impl_options.callback);
const Comparator* ucmp = get_impl_options.column_family->GetComparator();
assert(ucmp);
if (ucmp->timestamp_size() > 0) {
assert(!get_impl_options
.callback); // timestamp with callback is not supported
read_cb.Refresh(snapshot);
get_impl_options.callback = &read_cb;
}
TEST_SYNC_POINT("DBImpl::GetImpl:3");
TEST_SYNC_POINT("DBImpl::GetImpl:4");
// Prepare to store a list of merge operations if merge occurs.
MergeContext merge_context;
merge_context.get_merge_operands_options =
get_impl_options.get_merge_operands_options;
SequenceNumber max_covering_tombstone_seq = 0;
Status s;
// First look in the memtable, then in the immutable memtable (if any).
// s is both in/out. When in, s could either be OK or MergeInProgress.
// merge_operands will contain the sequence of merges in the latter case.
LookupKey lkey(key, snapshot, read_options.timestamp);
PERF_TIMER_STOP(get_snapshot_time);
bool skip_memtable = (read_options.read_tier == kPersistedTier &&
has_unpersisted_data_.load(std::memory_order_relaxed));
bool done = false;
bool is_blob_index = false;
bool* is_blob_ptr = get_impl_options.is_blob_index;
auto* partition_mgr = cfd->blob_partition_manager();
std::string timestamp_storage;
std::string* timestamp = nullptr;
if (ucmp->timestamp_size() > 0) {
timestamp = get_impl_options.timestamp != nullptr
? get_impl_options.timestamp
: (partition_mgr != nullptr ? &timestamp_storage : nullptr);
}
if (partition_mgr != nullptr && !is_blob_ptr && get_impl_options.get_value) {
is_blob_ptr = &is_blob_index;
}
const bool resolve_direct_write_value =
partition_mgr != nullptr && (is_blob_ptr == &is_blob_index);
std::optional<VersionBlobFetcher> 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;
if (!skip_memtable) {
// Get value associated with key
if (get_impl_options.get_value) {
if (sv->mem->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,
false /* immutable_memtable */,
get_impl_options.callback, is_blob_ptr,
/*do_merge=*/true, memtable_blob_fetcher_ptr)) {
done = true;
PostprocessDirectWriteValueRead(
read_options, key, timestamp, resolve_direct_write_value,
sv->current, cfd, 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()) &&
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;
PostprocessDirectWriteValueRead(
read_options, key, timestamp, resolve_direct_write_value,
sv->current, cfd, get_impl_options.value, get_impl_options.columns,
&s, &is_blob_index, get_impl_options.value_found);
RecordTick(stats_, MEMTABLE_HIT);
}
} else {
// Get Merge Operands associated with key, Merge Operands should not be
// merged and raw values should be returned to the user.
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,
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, memtable_blob_fetcher_ptr)) {
done = true;
RecordTick(stats_, MEMTABLE_HIT);
}
}
if (!s.ok() && !s.IsMergeInProgress() && !s.IsNotFound()) {
assert(done);
CO_RETURN s;
}
}
TEST_SYNC_POINT("DBImpl::GetImpl:PostMemTableGet:0");
TEST_SYNC_POINT("DBImpl::GetImpl:PostMemTableGet:1");
PinnedIteratorsManager pinned_iters_mgr;
if (!done) {
PERF_TIMER_GUARD(get_from_output_files_time);
CO_AWAIT(
sv->current->Get, read_options, lkey, get_impl_options.value,
get_impl_options.columns, timestamp, &s, &merge_context,
&max_covering_tombstone_seq, &pinned_iters_mgr,
get_impl_options.get_value ? get_impl_options.value_found : nullptr,
nullptr, nullptr,
get_impl_options.get_value ? get_impl_options.callback : nullptr,
get_impl_options.get_value ? is_blob_ptr : nullptr,
get_impl_options.get_value);
if (get_impl_options.get_value && resolve_direct_write_value) {
std::string blob_lookup_key_storage;
MaybeResolveDirectWriteValue(
read_options,
GetBlobLookupUserKey(key, timestamp, &blob_lookup_key_storage),
resolve_direct_write_value, sv->current, cfd, get_impl_options.value,
get_impl_options.columns, &s, &is_blob_index,
get_impl_options.value_found);
}
RecordTick(stats_, MEMTABLE_MISS);
}
{
PERF_TIMER_GUARD(get_post_process_time);
RecordTick(stats_, NUMBER_KEYS_READ);
size_t size = 0;
if (s.ok()) {
const auto& merge_threshold = read_options.merge_operand_count_threshold;
if (merge_threshold.has_value() &&
merge_context.GetNumOperands() > merge_threshold.value()) {
s = Status::OkMergeOperandThresholdExceeded();
}
if (get_impl_options.get_value) {
if (get_impl_options.value) {
size = get_impl_options.value->size();
} else if (get_impl_options.columns) {
size = get_impl_options.columns->payload_size();
}
} else {
// Return all merge operands for get_impl_options.key
*get_impl_options.number_of_operands =
static_cast<int>(merge_context.GetNumOperands());
// OK status is returned, some merge operand is found.
assert(*get_impl_options.number_of_operands > 0);
if (*get_impl_options.number_of_operands >
get_impl_options.get_merge_operands_options
->expected_max_number_of_operands) {
s = Status::Incomplete(
Status::SubCode::KMergeOperandsInsufficientCapacity);
} else {
// Each operand depends on one of the following resources: `sv`,
// `pinned_iters_mgr`, or `merge_context`. It would be crazy expensive
// to reference `sv` for each operand relying on it because `sv` is
// (un)ref'd in all threads using the DB. Furthermore, we do not track
// on which resource each operand depends.
//
// To solve this, we bundle the resources in a `GetMergeOperandsState`
// and manage them with a `SharedCleanablePtr` shared among the
// `PinnableSlice`s we return. This bundle includes one `sv` reference
// and ownership of the `merge_context` and `pinned_iters_mgr`
// objects.
bool ref_sv = ShouldReferenceSuperVersion(merge_context);
if (ref_sv) {
assert(!merge_context.GetOperands().empty());
SharedCleanablePtr shared_cleanable;
GetMergeOperandsState* state = new GetMergeOperandsState(
std::move(merge_context), std::move(pinned_iters_mgr));
sv->Ref();
state->sv_handle = new SuperVersionHandle(
this, &mutex_, sv,
immutable_db_options_.avoid_unnecessary_blocking_io);
shared_cleanable.Allocate();
shared_cleanable->RegisterCleanup(CleanupGetMergeOperandsState,
state /* arg1 */,
nullptr /* arg2 */);
for (size_t i = 0; i < state->merge_context.GetOperands().size();
++i) {
const Slice& sl = state->merge_context.GetOperands()[i];
size += sl.size();
get_impl_options.merge_operands->PinSlice(
sl, nullptr /* cleanable */);
if (i == state->merge_context.GetOperands().size() - 1) {
shared_cleanable.MoveAsCleanupTo(
get_impl_options.merge_operands);
} else {
shared_cleanable.RegisterCopyWith(
get_impl_options.merge_operands);
}
get_impl_options.merge_operands++;
}
} else {
for (const Slice& sl : merge_context.GetOperands()) {
size += sl.size();
get_impl_options.merge_operands->PinSelf(sl);
get_impl_options.merge_operands++;
}
}
}
}
RecordTick(stats_, BYTES_READ, size);
PERF_COUNTER_ADD(get_read_bytes, size);
}
RecordInHistogram(stats_, BYTES_PER_READ, size);
}
CO_RETURN s;
}
// The actual implementation of batched MultiGet. Parameters -
// start_key - Index in the sorted_keys vector to start processing from
// num_keys - Number of keys to lookup, starting with sorted_keys[start_key]
// sorted_keys - The entire batch of sorted keys for this CF
//
// The per key status is returned in the KeyContext structures pointed to by
// sorted_keys. An overall Status is also returned, with the only possible
// values being Status::OK() and Status::TimedOut(). The latter indicates
// that the call exceeded read_options.deadline
DEFINE_SYNC_AND_ASYNC(Status, DBImpl::MultiGetImpl)
(const ReadOptions& read_options, size_t start_key, size_t num_keys,
autovector<KeyContext*, MultiGetContext::MAX_BATCH_SIZE>* sorted_keys,
SuperVersion* super_version, SequenceNumber snapshot, ReadCallback* callback) {
#if defined(WITHOUT_COROUTINES)
PERF_CPU_TIMER_GUARD(get_cpu_nanos, immutable_db_options_.clock);
#endif // defined(WITHOUT_COROUTINES)
StopWatch sw(immutable_db_options_.clock, stats_, DB_MULTIGET);
assert(sorted_keys);
assert(start_key + num_keys <= sorted_keys->size());
if (num_keys == 0) {
CO_RETURN Status::OK();
}
auto* cfh = static_cast_with_check<ColumnFamilyHandleImpl>(
(*sorted_keys)[start_key]->column_family);
ColumnFamilyData* cfd = cfh->cfd();
auto* partition_mgr = cfd->blob_partition_manager();
std::optional<VersionBlobFetcher> 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;
#ifdef WITH_COROUTINES
constexpr bool kUseCoroRead = true;
#else
constexpr bool kUseCoroRead = false;
#endif
// 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) {
KeyContext* kctx = (*sorted_keys)[i];
if (kctx->timestamp) {
kctx->timestamp->clear();
}
}
// For each of the given keys, apply the entire "get" process as follows:
// First look in the memtable, then in the immutable memtable (if any).
// s is both in/out. When in, s could either be OK or MergeInProgress.
// merge_operands will contain the sequence of merges in the latter case.
size_t keys_left = num_keys;
Status s;
uint64_t curr_value_size = 0;
while (keys_left) {
if (read_options.deadline.count() &&
immutable_db_options_.clock->NowMicros() >
static_cast<uint64_t>(read_options.deadline.count())) {
s = Status::TimedOut();
break;
}
size_t batch_size = (keys_left > MultiGetContext::MAX_BATCH_SIZE)
? MultiGetContext::MAX_BATCH_SIZE
: keys_left;
MultiGetContext ctx(sorted_keys, start_key + num_keys - keys_left,
batch_size, snapshot, read_options, GetFileSystem(),
stats_, kUseCoroRead);
MultiGetRange range = ctx.GetMultiGetRange();
range.AddValueSize(curr_value_size);
bool lookup_current = true;
keys_left -= batch_size;
for (auto mget_iter = range.begin(); mget_iter != range.end();
++mget_iter) {
mget_iter->merge_context.Clear();
*mget_iter->s = Status::OK();
}
bool skip_memtable =
(read_options.read_tier == kPersistedTier &&
has_unpersisted_data_.load(std::memory_order_relaxed));
if (!skip_memtable) {
super_version->mem->MultiGet(read_options, &range, callback,
false /* immutable_memtable */,
memtable_blob_fetcher_ptr);
if (!range.empty()) {
super_version->imm->MultiGet(read_options, &range, callback,
memtable_blob_fetcher_ptr);
}
if (!range.empty()) {
uint64_t left = range.KeysLeft();
RecordTick(stats_, MEMTABLE_MISS, left);
} else {
lookup_current = false;
}
}
if (lookup_current) {
PERF_TIMER_GUARD(get_from_output_files_time);
CO_AWAIT(super_version->current->MultiGet, read_options, &range,
callback);
}
curr_value_size = range.GetValueSize();
if (curr_value_size > read_options.value_size_soft_limit) {
s = Status::Aborted();
break;
}
// This could be a long-running operation
bool aborted = ROCKSDB_THREAD_YIELD_CHECK_ABORT();
if (aborted) {
s = Status::Aborted("Query abort.");
break;
}
}
// Post processing (decrement reference counts and record statistics)
PERF_TIMER_GUARD(get_post_process_time);
size_t num_found = 0;
uint64_t bytes_read = 0;
// value_size_soft_limit was enforced above (and inside per-file MultiGet) on
// pre-resolution sizes. Direct-write blob references, though, are resolved
// below -- after that check counted only their encoded blob-index bytes.
// Enforce the limit on them here too, but only abort a key once the bytes
// already returned by this MultiGet exceed the limit. That way at least one
// key is always read even if it alone exceeds the limit ("always make
// progress"), so a caller retrying aborted keys cannot loop forever on a
// single oversized value.
const bool enforce_direct_write_soft_limit =
partition_mgr != nullptr &&
read_options.value_size_soft_limit !=
std::numeric_limits<uint64_t>::max() &&
read_options.read_tier != kBlockCacheTier;
for (size_t i = start_key; i < start_key + num_keys - keys_left; ++i) {
KeyContext* key = (*sorted_keys)[i];
assert(key);
assert(key->s);
if (partition_mgr != nullptr && key->s->ok()) {
const bool has_unresolved_direct_write =
key->is_blob_index ||
(key->columns != nullptr &&
!PinnableWideColumnsHelper::GetUnresolvedBlobColumnIndices(
*key->columns)
.empty());
if (enforce_direct_write_soft_limit && has_unresolved_direct_write &&
bytes_read > read_options.value_size_soft_limit) {
// Prior keys in this MultiGet have already returned more than the soft
// limit, so skip fetching this key's (possibly large) blob value(s).
if (key->value != nullptr) {
key->value->Reset();
}
if (key->columns != nullptr) {
key->columns->Reset();
}
key->is_blob_index = false;
*key->s = Status::Aborted();
} else {
std::string blob_lookup_key_storage;
MaybeResolveDirectWriteValue(
read_options,
GetBlobLookupUserKey(*key->key, key->timestamp,
&blob_lookup_key_storage),
/*resolve_direct_write_value=*/true, super_version->current, cfd,
key->value, key->columns, key->s, &key->is_blob_index);
}
}
if (key->s->ok()) {
const auto& merge_threshold = read_options.merge_operand_count_threshold;
if (merge_threshold.has_value() &&
key->merge_context.GetNumOperands() > merge_threshold) {
*(key->s) = Status::OkMergeOperandThresholdExceeded();
}
if (key->value) {
bytes_read += key->value->size();
} else {
assert(key->columns);
bytes_read += key->columns->payload_size();
}
num_found++;
}
}
if (keys_left) {
assert(s.IsTimedOut() || s.IsAborted());
for (size_t i = start_key + num_keys - keys_left; i < start_key + num_keys;
++i) {
KeyContext* key = (*sorted_keys)[i];
*key->s = s;
}
}
RecordTick(stats_, NUMBER_MULTIGET_CALLS);
RecordTick(stats_, NUMBER_MULTIGET_KEYS_READ, num_keys);
RecordTick(stats_, NUMBER_MULTIGET_KEYS_FOUND, num_found);
RecordTick(stats_, NUMBER_MULTIGET_BYTES_READ, bytes_read);
RecordInHistogram(stats_, BYTES_PER_MULTIGET, bytes_read);
PERF_COUNTER_ADD(multiget_read_bytes, bytes_read);
PERF_TIMER_STOP(get_post_process_time);
CO_RETURN s;
}
DEFINE_SYNC_AND_ASYNC(void, DBImpl::MultiGetCommon)
(const ReadOptions& read_options, const size_t num_keys,
ColumnFamilyHandle** column_families, const Slice* keys, PinnableSlice* values,
PinnableWideColumns* columns, std::string* timestamps, Status* statuses,
const bool sorted_input) {
if (num_keys == 0) {
CO_RETURN;
}
bool should_fail = false;
for (size_t i = 0; i < num_keys; ++i) {
ColumnFamilyHandle* cfh = column_families[i];
if (read_options.timestamp) {
statuses[i] = FailIfTsMismatchCf(cfh, *(read_options.timestamp));
if (!statuses[i].ok()) {
should_fail = true;
}
} else {
statuses[i] = FailIfCfHasTs(cfh);
if (!statuses[i].ok()) {
should_fail = true;
}
}
}
if (should_fail) {
for (size_t i = 0; i < num_keys; ++i) {
if (statuses[i].ok()) {
statuses[i] = Status::Incomplete(
"DB not queried due to invalid argument(s) in the same MultiGet");
}
}
CO_RETURN;
}
if (tracer_) {
// TODO: This mutex should be removed later, to improve performance when
// tracing is enabled.
InstrumentedMutexLock lock(&trace_mutex_);
if (tracer_) {
// TODO: maybe handle the tracing status?
tracer_->MultiGet(num_keys, column_families, keys).PermitUncheckedError();
}
}
autovector<KeyContext, MultiGetContext::MAX_BATCH_SIZE> key_context;
key_context.reserve(num_keys);
autovector<KeyContext*, MultiGetContext::MAX_BATCH_SIZE> sorted_keys;
sorted_keys.resize(num_keys);
for (size_t i = 0; i < num_keys; ++i) {
PinnableSlice* val = nullptr;
PinnableWideColumns* col = nullptr;
if (values) {
val = &values[i];
val->Reset();
} else {
assert(columns);
col = &columns[i];
col->Reset();
}
key_context.emplace_back(column_families[i], keys[i], val, col,
timestamps ? &timestamps[i] : nullptr,
&statuses[i]);
}
for (size_t i = 0; i < num_keys; ++i) {
sorted_keys[i] = &key_context[i];
}
PrepareMultiGetKeys(num_keys, sorted_input, &sorted_keys);
autovector<MultiGetKeyRangePerCf, MultiGetContext::MAX_BATCH_SIZE>
key_range_per_cf;
autovector<ColumnFamilySuperVersionPair, MultiGetContext::MAX_BATCH_SIZE>
cf_sv_pairs;
size_t cf_start = 0;
ColumnFamilyHandle* cf = sorted_keys[0]->column_family;
for (size_t i = 0; i < num_keys; ++i) {
KeyContext* key_ctx = sorted_keys[i];
if (key_ctx->column_family != cf) {
key_range_per_cf.emplace_back(cf_start, i - cf_start);
cf_sv_pairs.emplace_back(cf, nullptr);
cf_start = i;
cf = key_ctx->column_family;
}
}
key_range_per_cf.emplace_back(cf_start, num_keys - cf_start);
cf_sv_pairs.emplace_back(cf, nullptr);
SequenceNumber consistent_seqnum = kMaxSequenceNumber;
bool sv_from_thread_local = false;
#ifdef WITH_COROUTINES
// The coroutine read path may suspend mid-MultiGet and resume on a shared
// executor thread, which is incompatible with the thread-local SuperVersion
// cache: it marks the thread's slot kSVInUse for the duration of the read, so
// a second coroutine resuming on the same thread would trip the kSVInUse
// assertion. Take an independent reference instead, which releases the
// thread-local slot before any suspension point.
constexpr bool kExtraSvRef = true;
#else
constexpr bool kExtraSvRef = false;
#endif
Status s = MultiCFSnapshot<autovector<ColumnFamilySuperVersionPair,
MultiGetContext::MAX_BATCH_SIZE>>(
read_options, nullptr,
[](autovector<ColumnFamilySuperVersionPair,
MultiGetContext::MAX_BATCH_SIZE>::iterator& cf_iter) {
return &(*cf_iter);
},
&cf_sv_pairs, kExtraSvRef, &consistent_seqnum, &sv_from_thread_local);
if (!s.ok()) {
for (size_t i = 0; i < num_keys; ++i) {
if (statuses[i].ok()) {
statuses[i] = s;
}
}
CO_RETURN;
}
GetWithTimestampReadCallback timestamp_read_callback(0);
ReadCallback* read_callback = nullptr;
if (read_options.timestamp && read_options.timestamp->size() > 0) {
timestamp_read_callback.Refresh(consistent_seqnum);
read_callback = &timestamp_read_callback;
}
assert(key_range_per_cf.size() == cf_sv_pairs.size());
auto key_range_per_cf_iter = key_range_per_cf.begin();
auto cf_sv_pair_iter = cf_sv_pairs.begin();
while (key_range_per_cf_iter != key_range_per_cf.end() &&
cf_sv_pair_iter != cf_sv_pairs.end()) {
s = CO_AWAIT(MultiGetImpl, read_options, key_range_per_cf_iter->start,
key_range_per_cf_iter->num_keys, &sorted_keys,
cf_sv_pair_iter->super_version, consistent_seqnum,
read_callback);
if (!s.ok()) {
break;
}
++key_range_per_cf_iter;
++cf_sv_pair_iter;
}
if (!s.ok()) {
assert(s.IsTimedOut() || s.IsAborted());
for (++key_range_per_cf_iter;
key_range_per_cf_iter != key_range_per_cf.end();
++key_range_per_cf_iter) {
for (size_t i = key_range_per_cf_iter->start;
i < key_range_per_cf_iter->start + key_range_per_cf_iter->num_keys;
++i) {
*sorted_keys[i]->s = s;
}
}
}
for (const auto& cf_sv_pair : cf_sv_pairs) {
if (sv_from_thread_local && !kExtraSvRef) {
ReturnAndCleanupSuperVersion(cf_sv_pair.cfd, cf_sv_pair.super_version);
} else {
TEST_SYNC_POINT("DBImpl::MultiCFSnapshot::BeforeLastTryUnRefSV");
CleanupSuperVersion(cf_sv_pair.super_version);
}
}
}
DEFINE_SYNC_AND_ASYNC(void, DBImpl::MultiGet)
(const ReadOptions& _read_options, const size_t num_keys,
ColumnFamilyHandle** column_families, const Slice* keys, PinnableSlice* values,
std::string* timestamps, Status* statuses, const bool sorted_input) {
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
_read_options.io_activity != Env::IOActivity::kMultiGet) {
Status s = Status::InvalidArgument(
"Can only call MultiGet with `ReadOptions::io_activity` is "
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kMultiGet`");
for (size_t i = 0; i < num_keys; ++i) {
if (statuses[i].ok()) {
statuses[i] = s;
}
}
CO_RETURN;
}
ReadOptions read_options(_read_options);
if (read_options.io_activity == Env::IOActivity::kUnknown) {
read_options.io_activity = Env::IOActivity::kMultiGet;
}
#ifdef WITH_COROUTINES
auto* read_executor = immutable_db_options_.fs->GetReadExecutor();
if (read_executor != nullptr) {
auto* read_event_base = read_executor->getEventBase();
assert(read_event_base != nullptr);
co_await folly::coro::co_nothrow(folly::coro::co_withExecutor(
folly::Executor::getKeepAliveToken(read_event_base),
MultiGetCommonCoroutine(
read_options, num_keys, column_families, keys, values,
/* columns */ nullptr, timestamps, statuses, sorted_input)));
co_return;
}
#endif
MultiGetCommon(read_options, num_keys, column_families, keys, values,
/* columns */ nullptr, timestamps, statuses, sorted_input);
}
} // namespace ROCKSDB_NAMESPACE
#endif
+1 -1
View File
@@ -3119,7 +3119,7 @@ Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context,
ReadOnlyMemTable* new_imm,
SequenceNumber last_seqno) {
mutex_.AssertHeld();
assert(lock_wal_count_ == 0);
assert(lock_wal_owner_thread_id_counts_.empty());
// TODO: plumb Env::IOActivity, Env::IOPriority
const WriteOptions write_options;
+84 -57
View File
@@ -77,10 +77,10 @@ DBIter::DBIter(Env* _env, const ReadOptions& read_options,
user_comparator_(cmp),
merge_operator_(ioptions.merge_operator.get()),
iter_(iter),
blob_state_(version, read_options.read_tier,
read_options.verify_checksums, read_options.fill_cache,
read_options.io_activity,
cfd ? cfd->blob_file_cache() : nullptr,
// Enable the direct-write blob fallback only when this CF has a
// write-path partition manager; otherwise the fetcher stays on the plain
// Version::GetBlob fast path.
blob_state_(version, read_options, cfd ? cfd->blob_file_cache() : nullptr,
cfd != nullptr && cfd->blob_partition_manager() != nullptr),
read_callback_(read_callback),
sequence_(s),
@@ -94,8 +94,6 @@ DBIter::DBIter(Env* _env, const ReadOptions& read_options,
trace_db_(db_impl),
trace_cf_id_(cfd != nullptr ? cfd->GetID() : 0),
has_trace_state_(db_impl != nullptr && cfd != nullptr),
allow_blob_write_path_fallback_(cfd != nullptr &&
cfd->blob_partition_manager() != nullptr),
ingest_sst_lock_(cfd != nullptr ? &cfd->GetIngestSstLock() : nullptr),
timestamp_ub_(read_options.timestamp),
timestamp_lb_(read_options.iter_start_ts),
@@ -259,60 +257,25 @@ void DBIter::Next() {
}
}
Status DBIter::BlobReader::RetrieveAndSetBlobValue(
const Slice& user_key, const Slice& blob_index,
bool allow_write_path_fallback) {
Status DBIter::BlobReader::RetrieveAndSetBlobValue(const Slice& user_key,
const Slice& blob_index) {
assert(blob_value_.empty());
if (!version_ && (!allow_write_path_fallback || !blob_file_cache_)) {
if (!blob_fetcher_.CanResolve()) {
return Status::Corruption("Encountered unexpected blob index.");
}
// TODO: consider moving ReadOptions from ArenaWrappedDBIter to DBIter to
// avoid having to copy options back and forth.
// TODO: plumb Env::IOPriority
ReadOptions read_options;
read_options.read_tier = read_tier_;
read_options.verify_checksums = verify_checksums_;
read_options.fill_cache = fill_cache_;
read_options.io_activity = io_activity_;
constexpr FilePrefetchBuffer* prefetch_buffer = nullptr;
constexpr uint64_t* bytes_read = nullptr;
if (!allow_write_path_fallback) {
assert(version_ != nullptr);
return version_->GetBlob(read_options, user_key, blob_index,
prefetch_buffer, &blob_value_, bytes_read);
}
BlobIndex blob_idx;
Status s = blob_idx.DecodeFrom(blob_index);
if (!s.ok()) {
return s;
}
return BlobFilePartitionManager::ResolveBlobDirectWriteIndex(
read_options, user_key, blob_idx, version_, blob_file_cache_,
prefetch_buffer, &blob_value_, bytes_read);
}
BlobFetcher DBIter::BlobReader::CreateBlobFetcher() const {
ReadOptions read_options;
read_options.read_tier = read_tier_;
read_options.verify_checksums = verify_checksums_;
read_options.fill_cache = fill_cache_;
read_options.io_activity = io_activity_;
return BlobFetcher(version_, read_options, blob_file_cache_,
allow_write_path_fallback_);
return blob_fetcher_.FetchBlob(user_key, blob_index, prefetch_buffer,
&blob_value_, bytes_read);
}
bool DBIter::SetValueAndColumnsFromBlobImpl(const Slice& user_key,
const Slice& blob_index) {
// Keep the non-BDW iterator path on the pre-existing Version::GetBlob()
// fast path. Only enable the direct-write fallback when this CF actually
// has a write-path partition manager.
const Status s = blob_state_.mut()->reader.RetrieveAndSetBlobValue(
user_key, blob_index, allow_blob_write_path_fallback_);
const Status s =
blob_state_.mut()->reader.RetrieveAndSetBlobValue(user_key, blob_index);
if (!s.ok()) {
status_ = s;
valid_ = false;
@@ -368,7 +331,8 @@ bool DBIter::SetValueAndColumnsFromEntity(Slice slice) {
}
if (LIKELY(!has_blob_columns)) {
WideColumns& wide_columns = state.wide_columns();
const Status s = WideColumnSerialization::Deserialize(slice, wide_columns);
const Status s =
WideColumnSerialization::DeserializeSimple(slice, wide_columns);
if (!s.ok()) {
status_ = s;
@@ -390,9 +354,9 @@ bool DBIter::SetValueAndColumnsFromEntity(Slice slice) {
state.SaveEntitySliceIfNeeded(slice);
{
Slice input_copy = state.PrepareForLazyEntityDeserialize();
const Status s = WideColumnSerialization::DeserializeV2(
input_copy, state.lazy_entity_columns(), state.lazy_blob_columns());
const Slice entity = state.PrepareForLazyEntityDeserialize();
const Status s = WideColumnSerialization::Deserialize(
entity, state.lazy_entity_columns(), &state.lazy_blob_columns());
if (!s.ok()) {
status_ = s;
@@ -1621,8 +1585,8 @@ bool DBIter::MergeWithBlobBaseValue(const Slice& blob_index,
return false;
}
const Status s = blob_state_.mut()->reader.RetrieveAndSetBlobValue(
user_key, blob_index, allow_blob_write_path_fallback_);
const Status s =
blob_state_.mut()->reader.RetrieveAndSetBlobValue(user_key, blob_index);
if (!s.ok()) {
status_ = s;
valid_ = false;
@@ -1644,12 +1608,11 @@ 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_state_->reader.CreateBlobFetcher();
std::string resolved_entity;
Slice effective_entity;
Status s_resolve = WideColumnSerialization::ResolveEntityForMerge(
entity, user_key, &blob_fetcher, nullptr /* prefetch_buffers */,
resolved_entity, effective_entity);
entity, user_key, &blob_state_->reader.blob_fetcher(),
nullptr /* prefetch_buffers */, resolved_entity, effective_entity);
if (!s_resolve.ok()) {
status_ = std::move(s_resolve);
valid_ = false;
@@ -1921,6 +1884,9 @@ Status DBIter::ValidateScanOptions(const MultiScanArgs& multiscan_opts) const {
const std::vector<ScanOptions>& scan_opts = multiscan_opts.GetScanRanges();
const bool has_limit = scan_opts.front().range.limit.has_value();
if (multiscan_opts.reverse && !has_limit) {
return Status::InvalidArgument("Reverse MultiScan requires limit");
}
if (!has_limit && scan_opts.size() > 1) {
return Status::InvalidArgument("Scan has no upper bound");
}
@@ -2004,6 +1970,12 @@ void DBIter::Seek(const Slice& target) {
StopWatch sw(clock_, statistics_, DB_SEEK);
if (scan_opts_.has_value()) {
if (scan_opts_->reverse) {
status_ = Status::InvalidArgument("Seek called on reverse MultiScan");
valid_ = false;
return;
}
// Validate the seek target is as expected in the previously prepared range
auto const& scan_ranges = scan_opts_.value().GetScanRanges();
if (scan_index_ >= scan_ranges.size()) {
@@ -2110,6 +2082,61 @@ void DBIter::SeekForPrev(const Slice& target) {
PERF_CPU_TIMER_GUARD(iter_seek_cpu_nanos, clock_);
StopWatch sw(clock_, statistics_, DB_SEEK);
if (scan_opts_.has_value()) {
if (!scan_opts_->reverse) {
status_ =
Status::InvalidArgument("SeekForPrev called on forward MultiScan");
valid_ = false;
return;
}
auto const& scan_ranges = scan_opts_.value().GetScanRanges();
if (scan_index_ >= scan_ranges.size()) {
status_ = Status::InvalidArgument(
"SeekForPrev called after exhausting all of the scan ranges");
valid_ = false;
return;
}
auto const& range = scan_ranges[scan_index_];
auto const& limit = range.range.limit;
assert(limit.has_value());
if (!limit.has_value() ||
user_comparator_.CompareWithoutTimestamp(target, *limit) != 0) {
status_ = Status::InvalidArgument(
"SeekForPrev target does not match the limit of the next prepared "
"range at index " +
std::to_string(scan_index_));
valid_ = false;
return;
}
auto const& start = range.range.start;
assert(start.has_value());
if (iterate_lower_bound_ == nullptr ||
user_comparator_.CompareWithoutTimestamp(start.value(),
*iterate_lower_bound_) != 0) {
status_ = Status::InvalidArgument(
"Lower bound is not set to the same start value of the next "
"prepared range at index " +
std::to_string(scan_index_));
valid_ = false;
return;
}
if (iterate_upper_bound_ == nullptr ||
user_comparator_.CompareWithoutTimestamp(limit.value(),
*iterate_upper_bound_) != 0) {
status_ = Status::InvalidArgument(
"Upper bound is not set to the same limit value of the next "
"prepared range at index " +
std::to_string(scan_index_));
valid_ = false;
return;
}
scan_index_++;
}
if (has_trace_state_) {
// TODO: What do we do if this returns an error?
Slice lower_bound, upper_bound;
+19 -29
View File
@@ -276,37 +276,22 @@ class DBIter final : public Iterator {
class BlobReader {
public:
BlobReader(const Version* version, ReadTier read_tier,
bool verify_checksums, bool fill_cache,
Env::IOActivity io_activity, BlobFileCache* blob_file_cache,
bool allow_write_path_fallback)
: version_(version),
read_tier_(read_tier),
verify_checksums_(verify_checksums),
fill_cache_(fill_cache),
io_activity_(io_activity),
blob_file_cache_(blob_file_cache),
allow_write_path_fallback_(allow_write_path_fallback) {}
BlobReader(const Version* version, const ReadOptions& read_options,
BlobFileCache* blob_file_cache, bool allow_write_path_fallback)
: blob_fetcher_(version, ReadOptions(read_options), blob_file_cache,
allow_write_path_fallback) {}
const Slice& GetBlobValue() const { return blob_value_; }
Status RetrieveAndSetBlobValue(const Slice& user_key,
const Slice& blob_index,
bool allow_write_path_fallback);
const Slice& blob_index);
void ResetBlobValue() { blob_value_.Reset(); }
// Create a BlobFetcher with the same read options as this BlobReader.
BlobFetcher CreateBlobFetcher() const;
// The blob fetcher backing this reader, for resolving wide-column entity
// blob references (the merge path). Valid for this BlobReader's lifetime.
const BlobFetcher& blob_fetcher() const { return blob_fetcher_; }
private:
PinnableSlice blob_value_;
const Version* version_;
ReadTier read_tier_;
bool verify_checksums_;
bool fill_cache_;
Env::IOActivity io_activity_;
// Cache used by the write-path fallback for in-flight direct-write blob
// files that are not yet reachable through Version.
BlobFileCache* blob_file_cache_;
bool allow_write_path_fallback_;
OwningVersionBlobFetcher blob_fetcher_;
};
struct BlobState {
BlobReader reader;
@@ -331,9 +316,7 @@ class DBIter final : public Iterator {
ValueColumnsState(const Version* version, const ReadOptions& read_options,
ColumnFamilyData* cfd)
: entity_blob_resolver_(
version, read_options.read_tier, read_options.verify_checksums,
read_options.fill_cache, read_options.io_activity,
cfd ? cfd->blob_file_cache() : nullptr,
version, read_options, cfd ? cfd->blob_file_cache() : nullptr,
cfd != nullptr && cfd->blob_partition_manager() != nullptr) {}
Slice& value() { return value_; }
@@ -399,7 +382,7 @@ class DBIter final : public Iterator {
}
// Clears the previous lazy entity metadata and returns the saved entity
// buffer as input for DeserializeV2().
// buffer as input for Deserialize().
Slice PrepareForLazyEntityDeserialize() {
ClearLazyEntity();
return Slice(saved_value_);
@@ -673,6 +656,14 @@ class DBIter final : public Iterator {
UserComparatorWrapper user_comparator_;
const MergeOperator* const merge_operator_;
IteratorWrapper iter_;
// TODO: blob_state_'s BlobReader (whole-value blob reads + the merge entity
// path) and value_columns_state_'s ReadPathBlobResolver (lazy per-column
// entity resolution) each own a Version-backed blob fetcher with the same
// {version, read_options, blob_file_cache, allow_write_path_fallback}, so an
// iterator holds two ReadOptions copies. Collapse to a single shared fetcher.
// Deferred because the resolver must keep owning its fetcher for the planned
// lazy entity read path, where the result outlives the read call and owns the
// SuperVersion pin; unifying is best done together with that work.
DirtyTracked<BlobState> blob_state_;
ReadCallback* read_callback_;
// Max visible sequence number. It is normally the snapshot seq unless we have
@@ -719,7 +710,6 @@ class DBIter final : public Iterator {
DBImpl* trace_db_;
uint32_t trace_cf_id_;
bool has_trace_state_;
bool allow_blob_write_path_fallback_;
port::RWMutex* ingest_sst_lock_;
const Slice* const timestamp_ub_;
const Slice* const timestamp_lb_;
+240 -6
View File
@@ -5418,6 +5418,157 @@ TEST_P(DBMultiScanIteratorTest, AsyncPrefetchAcrossMultipleFiles) {
iter.reset();
}
TEST_P(DBMultiScanIteratorTest, ReversePrefetchAcrossMultipleRanges) {
auto options = CurrentOptions();
options.target_file_size_base = 1 << 15; // 32KiB
options.compaction_style = kCompactionStyleUniversal;
options.num_levels = 50;
options.compression = kNoCompression;
DestroyAndReopen(options);
Random rnd(303);
for (int i = 0; i < 1000; ++i) {
ASSERT_OK(Put(Key(i), rnd.RandomString(1 << 10)));
}
ASSERT_OK(Flush());
ASSERT_OK(db_->CompactRange({}, nullptr, nullptr));
ASSERT_GT(NumTableFilesAtLevel(49), 3);
ReadOptions ro;
ro.fill_cache = GetParam();
ColumnFamilyHandle* cfh = dbfull()->DefaultColumnFamily();
auto tracking_dispatcher = std::make_shared<TrackingIODispatcher>();
MultiScanArgs scan_options(BytewiseComparator());
scan_options.use_async_io = false;
scan_options.reverse = true;
scan_options.io_dispatcher = tracking_dispatcher;
std::vector<std::string> key_ranges(
{Key(100), Key(200), Key(500), Key(600), Key(800), Key(900)});
scan_options.insert(key_ranges[0], key_ranges[1]);
scan_options.insert(key_ranges[2], key_ranges[3]);
scan_options.insert(key_ranges[4], key_ranges[5]);
std::unique_ptr<MultiScan> iter =
dbfull()->NewMultiScan(ro, cfh, scan_options);
ASSERT_NE(iter, nullptr);
std::vector<std::string> actual_keys;
try {
for (auto range : *iter) {
for (auto it : range) {
actual_keys.push_back(it.first.ToString());
}
}
} catch (MultiScanException& ex) {
FAIL() << "Iterator returned status " << ex.what();
} catch (std::logic_error& ex) {
FAIL() << "Iterator returned logic error " << ex.what();
}
std::vector<std::string> expected_keys;
for (int i = 199; i >= 100; --i) {
expected_keys.push_back(Key(i));
}
for (int i = 599; i >= 500; --i) {
expected_keys.push_back(Key(i));
}
for (int i = 899; i >= 800; --i) {
expected_keys.push_back(Key(i));
}
ASSERT_EQ(actual_keys, expected_keys);
ASSERT_GT(tracking_dispatcher->GetReadSets().size(), 0);
}
TEST_P(DBMultiScanIteratorTest, ReverseUnreleasedPrefetchBlocksCountAsWasted) {
auto options = CurrentOptions();
options.compression = kNoCompression;
options.disable_auto_compactions = true;
options.statistics = CreateDBStatistics();
BlockBasedTableOptions table_options;
table_options.flush_block_policy_factory =
std::make_shared<FlushBlockEveryKeyPolicyFactory>();
table_options.block_cache = NewLRUCache(10 * 1024 * 1024);
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
DestroyAndReopen(options);
constexpr int kNumKeys = 40;
const std::string value(100, 'v');
for (int i = 0; i < kNumKeys; ++i) {
ASSERT_OK(Put(Key(i), value));
}
ASSERT_OK(Flush());
ASSERT_OK(db_->CompactRange({}, nullptr, nullptr));
ReadOptions ro;
ro.fill_cache = GetParam();
ColumnFamilyHandle* cfh = dbfull()->DefaultColumnFamily();
auto tracking_dispatcher = std::make_shared<TrackingIODispatcher>();
MultiScanArgs scan_options(BytewiseComparator());
scan_options.use_async_io = false;
scan_options.reverse = true;
scan_options.io_dispatcher = tracking_dispatcher;
const std::string start_key = Key(0);
const std::string limit_key = Key(kNumKeys);
scan_options.insert(start_key, limit_key);
{
std::unique_ptr<MultiScan> iter =
dbfull()->NewMultiScan(ro, cfh, scan_options);
ASSERT_NE(iter, nullptr);
try {
auto range_it = iter->begin();
ASSERT_NE(range_it, iter->end());
auto range = *range_it;
auto row_it = range.begin();
ASSERT_NE(row_it, range.end());
ASSERT_EQ((*row_it).first.ToString(), Key(kNumKeys - 1));
} catch (MultiScanException& ex) {
FAIL() << "Iterator returned status " << ex.what();
} catch (std::logic_error& ex) {
FAIL() << "Iterator returned logic error " << ex.what();
}
ASSERT_GT(tracking_dispatcher->GetReadSets().size(), 0);
}
ASSERT_GT(
options.statistics->getTickerCount(MULTISCAN_PREFETCH_BLOCKS_WASTED), 0);
}
TEST_P(DBMultiScanIteratorTest, ReverseRequiresLimits) {
auto options = CurrentOptions();
options.compression = kNoCompression;
DestroyAndReopen(options);
ASSERT_OK(Put("a", "va"));
ASSERT_OK(Put("b", "vb"));
ASSERT_OK(Flush());
ReadOptions ro;
ro.fill_cache = GetParam();
ColumnFamilyHandle* cfh = dbfull()->DefaultColumnFamily();
MultiScanArgs scan_options(BytewiseComparator());
scan_options.reverse = true;
scan_options.insert("a");
scan_options.insert("b", "c");
std::unique_ptr<MultiScan> iter =
dbfull()->NewMultiScan(ro, cfh, scan_options);
ASSERT_NE(iter, nullptr);
try {
(void)iter->begin();
FAIL() << "Expected reverse MultiScan without an upper bound to fail";
} catch (MultiScanException& ex) {
ASSERT_NOK(ex.status());
ASSERT_TRUE(ex.status().IsInvalidArgument());
}
}
// Wrapper filesystem that does not support async IO.
// Used to verify that MultiScan gracefully falls back to sync IO.
class NoAsyncIOFS : public FileSystemWrapper {
@@ -6114,13 +6265,96 @@ TEST_P(DBMultiScanIteratorTest, WastedBlocksTracking) {
uint64_t wasted =
options.statistics->getTickerCount(MULTISCAN_PREFETCH_BLOCKS_WASTED);
// We expect some wasted blocks due to the gap between ranges
// The exact number depends on prefetch behavior, but should be > 0
// if blocks between k020-k050 were prefetched
std::cout << "Wasted blocks: " << wasted << std::endl;
ASSERT_GT(wasted, 0);
}
// Note: The test verifies the tracking mechanism works.
// The actual count depends on prefetch heuristics which may vary.
TEST_P(DBMultiScanIteratorTest, PrefetchStatsRecorded) {
// Regression test: the MultiScan prefetch stats must still be published after
// the prefetch path moved into the IODispatcher. A cold scan populates the
// prefetch counters; a warm re-scan registers cache hits.
auto options = CurrentOptions();
options.compression = kNoCompression;
options.disable_auto_compactions = true;
options.statistics = CreateDBStatistics();
BlockBasedTableOptions table_options;
table_options.flush_block_policy_factory =
std::make_shared<FlushBlockEveryKeyPolicyFactory>();
table_options.block_cache = NewLRUCache(10 * 1024 * 1024); // 10MB cache
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
DestroyAndReopen(options);
const int kNumKeys = 50;
std::string value(100, 'v');
for (int i = 0; i < kNumKeys; ++i) {
std::stringstream ss;
ss << "k" << std::setw(3) << std::setfill('0') << i;
ASSERT_OK(Put(ss.str(), value));
}
ASSERT_OK(Flush());
ASSERT_OK(db_->CompactRange({}, nullptr, nullptr));
ColumnFamilyHandle* cfh = dbfull()->DefaultColumnFamily();
auto run_scan = [&]() {
MultiScanArgs scan_options(BytewiseComparator());
scan_options.use_async_io = false; // deterministic sync path
scan_options.insert("k000", "k049");
ReadOptions ro;
ro.fill_cache = true;
std::unique_ptr<MultiScan> iter =
dbfull()->NewMultiScan(ro, cfh, scan_options);
ASSERT_NE(iter, nullptr);
if (iter == nullptr) {
return;
}
int count = 0;
try {
for (auto range : *iter) {
for (auto it : range) {
it.first.ToString();
count++;
}
}
} catch (MultiScanException& ex) {
FAIL() << "Scan failed: " << ex.what();
}
ASSERT_GT(count, 0);
};
// Cold scan: force blocks to be read from disk so the prefetch path runs.
table_options.block_cache->EraseUnRefEntries();
SetPerfLevel(kEnableCount);
PerfContext* perf = get_perf_context();
ASSERT_NE(perf, nullptr);
if (perf == nullptr) {
return;
}
perf->Reset();
run_scan(); // MultiScanIndexIterator destroyed here -> stats recorded
ASSERT_GT(TestGetTickerCount(options, MULTISCAN_PREPARE_CALLS), 0);
ASSERT_GT(TestGetTickerCount(options, MULTISCAN_BLOCKS_PREFETCHED), 0);
ASSERT_GT(TestGetTickerCount(options, MULTISCAN_PREFETCH_BYTES), 0);
ASSERT_GT(TestGetTickerCount(options, MULTISCAN_IO_REQUESTS), 0);
HistogramData blocks_per_prepare;
options.statistics->histogramData(MULTISCAN_BLOCKS_PER_PREPARE,
&blocks_per_prepare);
ASSERT_GT(blocks_per_prepare.count, 0);
// PerfContext mirrors the same metrics scoped to this operation/thread.
ASSERT_GT(perf->multiscan_prepare_count, 0);
ASSERT_GT(perf->multiscan_blocks_prefetched, 0);
ASSERT_GT(perf->multiscan_prefetch_bytes, 0);
ASSERT_GT(perf->multiscan_io_requests, 0);
SetPerfLevel(kDisable);
// Warm scan: the just-cached blocks now count as cache hits.
uint64_t cache_hits_before =
TestGetTickerCount(options, MULTISCAN_BLOCKS_FROM_CACHE);
run_scan();
ASSERT_GT(TestGetTickerCount(options, MULTISCAN_BLOCKS_FROM_CACHE),
cache_hits_before);
}
class ReadPathRangeTombstoneTest : public DBIteratorBaseTest,
+1
View File
@@ -7,6 +7,7 @@
// 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 <limits>
#include <memory>
#include <string>
#include <unordered_map>
+87 -14
View File
@@ -23,6 +23,10 @@
#include "table/format.h"
#include "util/random.h"
#if USE_COROUTINES
#include "folly/coro/BlockingWait.h"
#endif // USE_COROUTINES
namespace ROCKSDB_NAMESPACE {
namespace {
@@ -31,6 +35,7 @@ int64_t MaybeCurrentTime(Env* env) {
env->GetCurrentTime(&time).PermitUncheckedError();
return time;
}
} // anonymous namespace
// Special Env used to delay background operations
@@ -857,12 +862,29 @@ Status DBTestBase::SingleDelete(int cf, const std::string& k) {
return db_->SingleDelete(WriteOptions(), handles_[cf], k);
}
std::string DBTestBase::Get(const std::string& k, const Snapshot* snapshot) {
std::string DBTestBase::Get(const std::string& k, const Snapshot* snapshot,
bool use_coroutine) {
ReadOptions options;
options.verify_checksums = true;
options.snapshot = snapshot;
std::string result;
Status s = db_->Get(options, k, &result);
Status s;
#if USE_COROUTINES
if (use_coroutine) {
PinnableSlice pinnable_value(&result);
s = folly::coro::blockingWait(dbfull()->GetCoroutine(
options, dbfull()->DefaultColumnFamily(), k, &pinnable_value,
/*timestamp=*/nullptr));
if (s.ok() && pinnable_value.IsPinned()) {
result.assign(pinnable_value.data(), pinnable_value.size());
}
} else
#else
(void)use_coroutine;
#endif // USE_COROUTINES
{
s = db_->Get(options, k, &result);
}
if (s.IsNotFound()) {
result = "NOT_FOUND";
} else if (!s.ok()) {
@@ -872,12 +894,27 @@ std::string DBTestBase::Get(const std::string& k, const Snapshot* snapshot) {
}
std::string DBTestBase::Get(int cf, const std::string& k,
const Snapshot* snapshot) {
const Snapshot* snapshot, bool use_coroutine) {
ReadOptions options;
options.verify_checksums = true;
options.snapshot = snapshot;
std::string result;
Status s = db_->Get(options, handles_[cf], k, &result);
Status s;
#if USE_COROUTINES
if (use_coroutine) {
PinnableSlice pinnable_value(&result);
s = folly::coro::blockingWait(dbfull()->GetCoroutine(
options, handles_[cf], k, &pinnable_value, /*timestamp=*/nullptr));
if (s.ok() && pinnable_value.IsPinned()) {
result.assign(pinnable_value.data(), pinnable_value.size());
}
} else
#else
(void)use_coroutine;
#endif // USE_COROUTINES
{
s = db_->Get(options, handles_[cf], k, &result);
}
if (s.IsNotFound()) {
result = "NOT_FOUND";
} else if (!s.ok()) {
@@ -889,8 +926,8 @@ std::string DBTestBase::Get(int cf, const std::string& k,
std::vector<std::string> DBTestBase::MultiGet(std::vector<int> cfs,
const std::vector<std::string>& k,
const Snapshot* snapshot,
const bool batched,
const bool async) {
bool batched, bool async,
bool use_coroutine) {
ReadOptions options;
options.verify_checksums = true;
options.snapshot = snapshot;
@@ -917,8 +954,19 @@ std::vector<std::string> DBTestBase::MultiGet(std::vector<int> cfs,
std::vector<PinnableSlice> pin_values(cfs.size());
result.resize(cfs.size());
s.resize(cfs.size());
db_->MultiGet(options, cfs.size(), handles.data(), keys.data(),
pin_values.data(), s.data());
#if USE_COROUTINES
if (use_coroutine) {
folly::coro::blockingWait(dbfull()->MultiGetCoroutine(
options, cfs.size(), handles.data(), keys.data(), pin_values.data(),
/*timestamps=*/nullptr, s.data(), /*sorted_input=*/false));
} else
#else
(void)use_coroutine;
#endif // USE_COROUTINES
{
db_->MultiGet(options, cfs.size(), handles.data(), keys.data(),
pin_values.data(), s.data());
}
for (size_t i = 0; i < s.size(); ++i) {
if (s[i].IsNotFound()) {
result[i] = "NOT_FOUND";
@@ -937,11 +985,14 @@ std::vector<std::string> DBTestBase::MultiGet(std::vector<int> cfs,
std::vector<std::string> DBTestBase::MultiGet(const std::vector<std::string>& k,
const Snapshot* snapshot,
const bool async) {
bool async,
bool optimize_multiget_for_io,
bool use_coroutine) {
ReadOptions options;
options.verify_checksums = true;
options.snapshot = snapshot;
options.async_io = async;
options.optimize_multiget_for_io = optimize_multiget_for_io;
std::vector<Slice> keys;
std::vector<std::string> result(k.size());
std::vector<Status> statuses(k.size());
@@ -950,8 +1001,21 @@ std::vector<std::string> DBTestBase::MultiGet(const std::vector<std::string>& k,
for (size_t i = 0; i < k.size(); ++i) {
keys.emplace_back(k[i]);
}
db_->MultiGet(options, dbfull()->DefaultColumnFamily(), keys.size(),
keys.data(), pin_values.data(), statuses.data());
#if USE_COROUTINES
if (use_coroutine) {
std::vector<ColumnFamilyHandle*> cfs(keys.size(),
dbfull()->DefaultColumnFamily());
folly::coro::blockingWait(dbfull()->MultiGetCoroutine(
options, keys.size(), cfs.data(), keys.data(), pin_values.data(),
/*timestamps=*/nullptr, statuses.data(), /*sorted_input=*/false));
} else
#else
(void)use_coroutine;
#endif // USE_COROUTINES
{
db_->MultiGet(options, dbfull()->DefaultColumnFamily(), keys.size(),
keys.data(), pin_values.data(), statuses.data());
}
for (size_t i = 0; i < statuses.size(); ++i) {
if (statuses[i].IsNotFound()) {
result[i] = "NOT_FOUND";
@@ -967,11 +1031,20 @@ std::vector<std::string> DBTestBase::MultiGet(const std::vector<std::string>& k,
return result;
}
Status DBTestBase::Get(const std::string& k, PinnableSlice* v) {
Status DBTestBase::Get(const std::string& k, PinnableSlice* v,
bool use_coroutine) {
ReadOptions options;
options.verify_checksums = true;
Status s = dbfull()->Get(options, dbfull()->DefaultColumnFamily(), k, v);
return s;
#if USE_COROUTINES
if (use_coroutine) {
return folly::coro::blockingWait(
dbfull()->GetCoroutine(options, dbfull()->DefaultColumnFamily(), k, v,
/*timestamp=*/nullptr));
}
#else
(void)use_coroutine;
#endif // USE_COROUTINES
return dbfull()->Get(options, dbfull()->DefaultColumnFamily(), k, v);
}
Status DBTestBase::CompactRange(const CompactRangeOptions& options,
+12 -7
View File
@@ -1231,22 +1231,27 @@ class DBTestBase : public testing::Test {
Status SingleDelete(int cf, const std::string& k);
std::string Get(const std::string& k, const Snapshot* snapshot = nullptr);
std::string Get(const std::string& k, const Snapshot* snapshot = nullptr,
bool use_coroutine = false);
std::string Get(int cf, const std::string& k,
const Snapshot* snapshot = nullptr);
const Snapshot* snapshot = nullptr,
bool use_coroutine = false);
Status Get(const std::string& k, PinnableSlice* v);
Status Get(const std::string& k, PinnableSlice* v,
bool use_coroutine = false);
std::vector<std::string> MultiGet(std::vector<int> cfs,
const std::vector<std::string>& k,
const Snapshot* snapshot,
const bool batched,
const bool async = false);
const Snapshot* snapshot, bool batched,
bool async = false,
bool use_coroutine = false);
std::vector<std::string> MultiGet(const std::vector<std::string>& k,
const Snapshot* snapshot = nullptr,
const bool async = false);
bool async = false,
bool optimize_multiget_for_io = true,
bool use_coroutine = false);
Status CompactRange(const CompactRangeOptions& options,
std::optional<Slice> begin, std::optional<Slice> end);
+22 -26
View File
@@ -782,19 +782,30 @@ TEST_P(DBWriteTest, LockWALConcurrentRecursive) {
ASSERT_OK(db_->GetLiveFilesStorageInfo({lf_opts}, &files));
}
port::Thread parallel_lock_wal{[&]() {
ASSERT_OK(db_->LockWAL()); // 2 -> 3 or 1 -> 2
}};
SyncPoint::GetInstance()->LoadDependency(
{{"DBWriteTest::LockWALConcurrentRecursive:ParallelLockAcquired",
"DBWriteTest::LockWALConcurrentRecursive:MainUnlocks"},
{"DBWriteTest::LockWALConcurrentRecursive:MainAllowsParallelUnlock",
"DBWriteTest::LockWALConcurrentRecursive:ParallelUnlocks"}});
SyncPoint::GetInstance()->EnableProcessing();
ASSERT_OK(db_->UnlockWAL()); // 2 -> 1 or 3 -> 2
port::Thread parallel_lock_wal{[&]() {
ASSERT_OK(db_->LockWAL()); // 2 -> 3
TEST_SYNC_POINT(
"DBWriteTest::LockWALConcurrentRecursive:ParallelLockAcquired");
TEST_SYNC_POINT("DBWriteTest::LockWALConcurrentRecursive:ParallelUnlocks");
ASSERT_OK(db_->UnlockWAL());
}};
TEST_SYNC_POINT("DBWriteTest::LockWALConcurrentRecursive:MainUnlocks");
ASSERT_OK(db_->UnlockWAL()); // 3 -> 2
// Give parallel_put an extra chance to jump in case of bug
std::this_thread::yield();
parallel_lock_wal.join();
ASSERT_FALSE(parallel_put_completed.Load());
ASSERT_FALSE(parallel_ingest_completed.Load());
ASSERT_FALSE(flush_completed.Load());
// Should now have 2 outstanding LockWAL
// Should still have 2 outstanding LockWAL().
ASSERT_EQ(Get("k1"), "k1_orig");
ASSERT_OK(db_->UnlockWAL()); // 2 -> 1
@@ -807,26 +818,11 @@ TEST_P(DBWriteTest, LockWALConcurrentRecursive) {
ASSERT_EQ(Get("k2"), "NOT_FOUND");
ASSERT_EQ(frozen_seqno, db_->GetLatestSequenceNumber());
// Ensure final Unlock is concurrency safe and extra Unlock is safe but
// non-OK
std::atomic<int> unlock_ok{0};
port::Thread parallel_stuff{[&]() {
if (db_->UnlockWAL().ok()) {
unlock_ok++;
}
ASSERT_OK(db_->LockWAL());
if (db_->UnlockWAL().ok()) {
unlock_ok++;
}
}};
if (db_->UnlockWAL().ok()) {
unlock_ok++;
}
parallel_stuff.join();
// There was one extra unlock, so just one non-ok
ASSERT_EQ(unlock_ok.load(), 2);
TEST_SYNC_POINT(
"DBWriteTest::LockWALConcurrentRecursive:MainAllowsParallelUnlock");
parallel_lock_wal.join();
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->LoadDependency({});
// Write can proceed
parallel_put.join();
+9 -7
View File
@@ -418,6 +418,15 @@ TEST_F(DeleteFileTest, BackgroundPurgeCFDropTest) {
// Expect 1 sst file.
CheckFileTypeCounts(dbname_, 0, 1, 1);
if (bg_purge) {
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
SyncPoint::GetInstance()->LoadDependency(
{{"DeleteFileTest::BackgroundPurgeCFDropTest:1",
"DBImpl::BGWorkPurge:start"}});
SyncPoint::GetInstance()->EnableProcessing();
}
ASSERT_OK(db_->DropColumnFamily(cfh));
// Still 1 file, it won't be deleted while ColumnFamilyHandle is alive.
CheckFileTypeCounts(dbname_, 0, 1, 1);
@@ -447,13 +456,6 @@ TEST_F(DeleteFileTest, BackgroundPurgeCFDropTest) {
Reopen(options);
ASSERT_OK(dbfull()->TEST_WaitForPurge());
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
SyncPoint::GetInstance()->LoadDependency(
{{"DeleteFileTest::BackgroundPurgeCFDropTest:1",
"DBImpl::BGWorkPurge:start"}});
SyncPoint::GetInstance()->EnableProcessing();
{
SCOPED_TRACE("avoid_unnecessary_blocking_io = true");
do_test(true);
+2 -2
View File
@@ -668,7 +668,7 @@ Status ErrorHandler::RecoverFromBGError(bool is_manual) {
// can generate background errors should be the flush operations
recovery_error_ = IOStatus::OK();
recovery_error_.PermitUncheckedError();
Status s = db_->ResumeImpl(recover_context_);
Status s = db_->ResumeImpl(recover_context_, Env::IOActivity::kFlush);
if (s.ok()) {
soft_error_no_bg_work_ = false;
} else {
@@ -764,7 +764,7 @@ void ErrorHandler::RecoverFromRetryableBGIOError() {
TEST_SYNC_POINT("RecoverFromRetryableBGIOError:BeforeResume1");
recovery_error_ = IOStatus::OK();
retry_count++;
Status s = db_->ResumeImpl(context);
Status s = db_->ResumeImpl(context, Env::IOActivity::kFlush);
RecordStats({ERROR_HANDLER_AUTORESUME_RETRY_TOTAL_COUNT},
{} /* int_histograms */);
if (s.IsShutdownInProgress() ||
+11 -11
View File
@@ -158,17 +158,17 @@ Status UpdateManifestForFilesState(
// Current state inconsistent with manifest
++files_updated;
edit.DeleteFile(level, number);
edit.AddFile(level, lf->fd.GetNumber(), lf->fd.GetPathId(),
lf->fd.GetFileSize(), lf->smallest, lf->largest,
lf->fd.smallest_seqno, lf->fd.largest_seqno,
lf->marked_for_compaction, temp,
lf->oldest_blob_file_number,
lf->oldest_ancester_time, lf->file_creation_time,
lf->epoch_number, lf->file_checksum,
lf->file_checksum_func_name, lf->unique_id,
lf->compensated_range_deletion_size, lf->tail_size,
lf->user_defined_timestamps_persisted,
lf->min_timestamp, lf->max_timestamp);
edit.AddFile(
level, lf->fd.GetNumber(), lf->fd.GetPathId(),
lf->fd.GetFileSize(), lf->smallest, lf->largest,
lf->fd.smallest_seqno, lf->fd.largest_seqno,
lf->marked_for_compaction, temp, lf->oldest_blob_file_number,
lf->oldest_ancester_time, lf->file_creation_time,
lf->epoch_number, lf->file_checksum,
lf->file_checksum_func_name, lf->unique_id,
lf->compensated_range_deletion_size, lf->tail_size,
lf->user_defined_timestamps_persisted, lf->min_timestamp,
lf->max_timestamp, lf->file_open_metadata);
}
}
} else {
+12 -15
View File
@@ -520,12 +520,13 @@ TEST_F(EventListenerTest, OnSingleDBFlushTest) {
ASSERT_OK(Put(5, "nikitich", std::string(90000, 'n')));
ASSERT_OK(Put(6, "alyosha", std::string(90000, 'a')));
ASSERT_OK(Put(7, "popovich", std::string(90000, 'p')));
FlushOptions flush_opts;
flush_opts.listener_wait = true;
for (int i = 1; i < 8; ++i) {
ASSERT_OK(Flush(i));
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
// Ensure background work is fully finished including listener callbacks
// before accessing listener state.
ASSERT_OK(dbfull()->TEST_WaitForBackgroundWork());
ASSERT_OK(db_->Flush(flush_opts, handles_[i]));
// listener_wait guarantees the OnFlushCompleted callbacks have finished
// running by the time Flush() returns, so the listener state below is up to
// date without separately waiting for background work.
ASSERT_EQ(listener->flushed_dbs_.size(), i);
ASSERT_EQ(listener->flushed_column_family_names_.size(), i);
}
@@ -635,21 +636,17 @@ TEST_F(EventListenerTest, MultiDBMultiListeners) {
}
}
FlushOptions flush_opts;
flush_opts.listener_wait = true;
for (size_t c = 0; c < cf_names.size(); ++c) {
for (int d = 0; d < kNumDBs; ++d) {
ASSERT_OK(dbs[d]->Flush(FlushOptions(), vec_handles[d][c]));
ASSERT_OK(static_cast_with_check<DBImpl>(dbs[d].get())
->TEST_WaitForFlushMemTable());
// listener_wait ensures the OnFlushCompleted callbacks have finished by
// the time Flush() returns, so the listener state checked below is up to
// date without separately waiting for background work.
ASSERT_OK(dbs[d]->Flush(flush_opts, vec_handles[d][c]));
}
}
for (int d = 0; d < kNumDBs; ++d) {
// Ensure background work is fully finished including listener callbacks
// before accessing listener state.
ASSERT_OK(static_cast_with_check<DBImpl>(dbs[d].get())
->TEST_WaitForBackgroundWork());
}
for (auto* listener : listeners) {
int pos = 0;
for (size_t c = 0; c < cf_names.size(); ++c) {
+34 -45
View File
@@ -52,25 +52,6 @@ namespace ROCKSDB_NAMESPACE {
namespace {
Status GetDefaultColumnBlobIndexSlice(Slice entity, Slice* blob_index_slice) {
assert(blob_index_slice != nullptr);
std::vector<WideColumn> columns;
std::vector<std::pair<size_t, BlobIndex>> blob_columns;
Status status =
WideColumnSerialization::DeserializeV2(entity, columns, blob_columns);
if (status.ok()) {
if (columns.empty() || columns.front().name() != kDefaultWideColumnName ||
blob_columns.empty() || blob_columns.front().first != 0) {
status = Status::Corruption(
"Wide column default column blob reference missing");
} else {
*blob_index_slice = columns.front().value();
}
}
return status;
}
Status PushWideColumnEntityDefaultOperand(const Slice& user_key,
const Slice& entity,
MergeContext* merge_context,
@@ -78,29 +59,24 @@ Status PushWideColumnEntityDefaultOperand(const Slice& user_key,
const BlobFetcher* blob_fetcher) {
assert(merge_context != nullptr);
Slice entity_ref = entity;
Slice value_of_default;
bool is_blob_reference = false;
Status status = WideColumnSerialization::GetValueOfDefaultColumn(
entity_ref, value_of_default);
if (status.ok()) {
entity, value_of_default, is_blob_reference);
if (!status.ok()) {
return status;
}
if (!is_blob_reference) {
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);
status = WideColumnSerialization::ResolveDefaultColumnBlobReference(
value_of_default, user_key, blob_fetcher, resolved_default);
if (status.ok()) {
// Resolved blob values are backed by this stack-local PinnableSlice, so
// copy them into MergeContext instead of pinning their storage.
// Resolved value is backed by this stack-local PinnableSlice, so copy it
// into MergeContext instead of pinning its storage.
merge_context->PushOperand(Slice(resolved_default), false);
}
return status;
@@ -1510,21 +1486,34 @@ static bool SaveValue(void* arg, const char* entry) {
}
} else if (s->value) {
Slice value_of_default;
bool is_blob_reference = false;
*(s->status) = WideColumnSerialization::GetValueOfDefaultColumn(
v, value_of_default);
v, value_of_default, is_blob_reference);
if (s->status->ok()) {
s->value->assign(value_of_default.data(), value_of_default.size());
} else if (s->status->IsNotSupported() &&
s->is_blob_index != nullptr) {
Slice blob_index_slice;
*(s->status) = GetDefaultColumnBlobIndexSlice(v, &blob_index_slice);
if (s->status->ok()) {
s->value->assign(blob_index_slice.data(),
blob_index_slice.size());
if (!is_blob_reference) {
s->value->assign(value_of_default.data(),
value_of_default.size());
} else if (s->is_blob_index != nullptr) {
// Caller requested the raw blob index (StackableDB BlobDB); hand
// back the serialized BlobIndex bytes without resolving.
s->value->assign(value_of_default.data(),
value_of_default.size());
default_blob_index_returned = true;
} else {
// Base RocksDB read of a blob-backed default column without
// is_blob_index requested: not supported here.
*(s->status) = Status::NotSupported(
"Encountered blob-backed default column without "
"is_blob_index");
}
}
} else if (s->columns) {
// NOTE/TODO: copying all of the wide columns that are inlined in the
// memtable. (Blob-indirect columns avoid the copy in
// MaybeResolveMemtableBlobValue()). We could consider avoiding the
// copy, perhaps conditional on size, by pinning the SuperVersion with
// the result, but there's no precedent for that, even without wide
// columns (plain memtable values are copied too).
*(s->status) = s->columns->SetWideColumnValue(v);
}
@@ -1858,7 +1847,7 @@ void MemTable::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
range->AddValueSize(iter->value->size());
} else {
assert(iter->columns);
range->AddValueSize(iter->columns->serialized_size());
range->AddValueSize(iter->columns->payload_size());
}
range->MarkKeyDone(iter);
@@ -1916,7 +1905,7 @@ void MemTable::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
range->AddValueSize(iter->value->size());
} else {
assert(iter->columns);
range->AddValueSize(iter->columns->serialized_size());
range->AddValueSize(iter->columns->payload_size());
}
range->MarkKeyDone(iter);
+29 -1
View File
@@ -287,7 +287,12 @@ class MemTableList {
// Should not delete MemTableList without making sure MemTableList::current()
// is Unref()'d.
~MemTableList() {}
~MemTableList() {
// All flush-completion notifications must have drained before this column
// family's memtable list is destroyed; otherwise a reserve leaked (see
// num_pending_flush_notifications_).
assert(num_pending_flush_notifications_ == 0);
}
MemTableListVersion* current() const { return current_; }
@@ -305,6 +310,20 @@ class MemTableList {
// completely flushed and logged.
int NumFlushed() const;
// Flush-completion listener notification accounting. All require the DB
// mutex to be held (this class is not internally synchronized). See
// num_pending_flush_notifications_ for details.
void AddPendingFlushNotifications(int n) {
num_pending_flush_notifications_ += n;
}
void SubPendingFlushNotifications(int n) {
num_pending_flush_notifications_ -= n;
assert(num_pending_flush_notifications_ >= 0);
}
int NumPendingFlushNotifications() const {
return num_pending_flush_notifications_;
}
// Returns true if there is at least one memtable on which flush has
// not yet started.
bool IsFlushPending() const;
@@ -552,6 +571,15 @@ class MemTableList {
// Last memtabe list version id, increase by 1 each time a new
// MemtableListVersion is installed.
uint64_t last_memtable_list_version_id_;
// Number of committed flush results for this column family whose
// EventListener::OnFlushCompleted callbacks have not finished running yet.
// Incremented (by the background flush thread) at flush-commit time, before
// the DB mutex is released to invoke the callbacks, and decremented after the
// callbacks return. Consulted by WaitForFlushMemTables() when the caller set
// FlushOptions::listener_wait, so that Flush(wait=true) does not return
// before the callbacks complete. Guarded by the DB mutex.
int num_pending_flush_notifications_ = 0;
};
// Installs memtable atomic flush results.
+7 -3
View File
@@ -254,9 +254,13 @@ Status MergeHelper::TimedFullMergeImpl(
statistics, clock, update_num_ops_stats, op_failure_scope,
std::move(visitor));
if (s.ok()) {
// Check merge result fits in uint32_t (a BlockBuilder assumption)
size_t result_size =
result_value ? result_value->size() : result_entity->serialized_size();
// Check merge result fits in uint32_t (a BlockBuilder assumption). For an
// entity result this is the V1-serialized size that will actually be
// stored, so use SerializedSizeV1() rather than payload_size().
size_t result_size = result_value
? result_value->size()
: WideColumnSerialization::SerializedSizeV1(
result_entity->columns());
if (UNLIKELY(result_size > std::numeric_limits<uint32_t>::max())) {
return Status::Corruption("Merge result exceeds 4GB");
}
+2 -2
View File
@@ -100,8 +100,8 @@ class MergeHelper {
Slice entity_copy(entity);
WideColumns existing_columns;
const Status s =
WideColumnSerialization::Deserialize(entity_copy, existing_columns);
const Status s = WideColumnSerialization::DeserializeSimple(
entity_copy, existing_columns);
if (!s.ok()) {
return s;
}
+104 -29
View File
@@ -3,14 +3,56 @@
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#include <string>
#include <vector>
#include "file/file_util.h"
#include "rocksdb/db.h"
#include "rocksdb/file_system.h"
#include "rocksdb/iterator.h"
namespace ROCKSDB_NAMESPACE {
using MultiScanIterator = MultiScan::MultiScanIterator;
namespace {
Status ValidateScanOptionsForMultiScan(const MultiScanArgs& multiscan_opts) {
if (multiscan_opts.empty()) {
return Status::InvalidArgument("Empty MultiScanArgs");
}
const Comparator* comparator = multiscan_opts.GetComparator();
if (comparator == nullptr) {
return Status::InvalidArgument("MultiScanArgs requires comparator");
}
const std::vector<ScanOptions>& scan_opts = multiscan_opts.GetScanRanges();
for (size_t i = 0; i < scan_opts.size(); ++i) {
const auto& scan_range = scan_opts[i].range;
if (!scan_range.start.has_value()) {
return Status::InvalidArgument("Scan has no start key at index " +
std::to_string(i));
}
if (multiscan_opts.reverse && !scan_range.limit.has_value()) {
return Status::InvalidArgument(
"Reverse MultiScan requires limit at index " + std::to_string(i));
}
if (scan_range.limit.has_value() &&
comparator->CompareWithoutTimestamp(
scan_range.start.value(), /*a_has_ts=*/false,
scan_range.limit.value(), /*b_has_ts=*/false) >= 0) {
return Status::InvalidArgument(
"Scan start key is large or equal than limit at index " +
std::to_string(i));
}
}
return Status::OK();
}
} // namespace
MultiScan::MultiScan(const ReadOptions& read_options,
const MultiScanArgs& scan_opts, DB* db,
ColumnFamilyHandle* cfh)
@@ -28,14 +70,28 @@ MultiScan::MultiScan(const ReadOptions& read_options,
}()),
db_(db),
cfh_(cfh) {
Status validate_status = ValidateScanOptionsForMultiScan(scan_opts_);
if (!validate_status.ok()) {
db_iter_.reset(NewErrorIterator(validate_status));
return;
}
bool slow_path = false;
// Setup read_options with iterate_uuper_bound based on the first scan.
// Subsequent scans will update and allocate a new DB iterator as necessary
if (scan_opts_.GetScanRanges()[0].range.limit) {
upper_bound_ = *scan_opts_.GetScanRanges()[0].range.limit;
read_options_.iterate_upper_bound = &upper_bound_;
} else {
read_options_.iterate_upper_bound = nullptr;
// Setup read_options bounds based on the first scan. Subsequent scans will
// update the bounds and allocate a new DB iterator as necessary.
{
const ScanOptions& first_scan = scan_opts_.GetScanRanges()[0];
if (scan_opts_.reverse) {
lower_bound_ = *first_scan.range.start;
read_options_.iterate_lower_bound = &lower_bound_;
upper_bound_ = *first_scan.range.limit;
read_options_.iterate_upper_bound = &upper_bound_;
} else if (first_scan.range.limit) {
upper_bound_ = *first_scan.range.limit;
read_options_.iterate_upper_bound = &upper_bound_;
} else {
read_options_.iterate_upper_bound = nullptr;
}
}
for (const auto& opts : scan_opts_.GetScanRanges()) {
// Check that all the ScanOptions either specify an upper bound or not. If
@@ -54,36 +110,55 @@ MultiScan::MultiScan(const ReadOptions& read_options,
}
}
void MultiScanIterator::SeekCurrentRange() {
const ScanOptions& scan_opt = scan_opts_[idx_];
if (scan_opt.range.limit) {
*upper_bound_ = *scan_opt.range.limit;
read_options_.iterate_upper_bound = upper_bound_;
} else {
read_options_.iterate_upper_bound = nullptr;
}
if (reverse_) {
*lower_bound_ = *scan_opt.range.start;
read_options_.iterate_lower_bound = lower_bound_;
assert(scan_opt.range.limit.has_value());
db_iter_->SeekForPrev(*scan_opt.range.limit);
} else {
db_iter_->Seek(*scan_opt.range.start);
}
}
MultiScanIterator& MultiScanIterator::operator++() {
status_ = db_iter_->status();
if (!status_.ok()) {
throw MultiScanException(status_);
}
if (idx_ >= scan_opts_.size()) {
throw std::logic_error("Index out of range");
if (!valid_) {
throw std::logic_error("Incrementing exhausted MultiScan iterator");
}
idx_++;
if (idx_ < scan_opts_.size()) {
// Check if we need to update read_options_
if (scan_opts_[idx_].range.limit.has_value() !=
(read_options_.iterate_upper_bound != nullptr)) {
if (scan_opts_[idx_].range.limit) {
*upper_bound_ = *scan_opts_[idx_].range.limit;
read_options_.iterate_upper_bound = upper_bound_;
} else {
read_options_.iterate_upper_bound = nullptr;
}
db_iter_.reset(db_->NewIterator(read_options_, cfh_));
scan_.Reset(db_iter_.get());
} else if (scan_opts_[idx_].range.limit) {
*upper_bound_ = *scan_opts_[idx_].range.limit;
}
db_iter_->Seek(*scan_opts_[idx_].range.start);
status_ = db_iter_->status();
if (!status_.ok()) {
throw MultiScanException(status_);
++idx_;
if (idx_ >= scan_opts_.size()) {
valid_ = false;
return *this;
}
// Check if we need to update read_options_
if (scan_opts_[idx_].range.limit.has_value() !=
(read_options_.iterate_upper_bound != nullptr)) {
if (scan_opts_[idx_].range.limit) {
read_options_.iterate_upper_bound = upper_bound_;
} else {
read_options_.iterate_upper_bound = nullptr;
}
db_iter_.reset(db_->NewIterator(read_options_, cfh_));
scan_.Reset(db_iter_.get(), reverse_);
}
SeekCurrentRange();
status_ = db_iter_->status();
if (!status_.ok()) {
throw MultiScanException(status_);
}
return *this;
}
+191
View File
@@ -3,13 +3,17 @@
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#include <atomic>
#include <memory>
#include <string>
#include <utility>
#include "db/db_impl/db_impl.h"
#include "db/db_test_util.h"
#include "rocksdb/options.h"
#include "rocksdb/table.h"
#include "test_util/testharness.h"
#include "util/cast_util.h"
namespace ROCKSDB_NAMESPACE {
class OptionsFileTest : public testing::Test {
@@ -20,6 +24,91 @@ class OptionsFileTest : public testing::Test {
};
namespace {
std::string Basename(const std::string& fname) {
const size_t basename_pos = fname.find_last_of("/\\");
return basename_pos == std::string::npos ? fname
: fname.substr(basename_pos + 1);
}
bool IsOptionsFile(const std::string& fname) {
return Basename(fname).find(kOptionsFileNamePrefix) == 0;
}
class FailOptionsDirFsyncFileSystem : public FileSystemWrapper {
public:
explicit FailOptionsDirFsyncFileSystem(std::shared_ptr<FileSystem> target)
: FileSystemWrapper(std::move(target)) {}
static const char* kClassName() { return "FailOptionsDirFsyncFileSystem"; }
const char* Name() const override { return kClassName(); }
void FailOptionsDirFsyncAfterNextOptionsFile() {
observe_options_file_.store(true, std::memory_order_release);
fail_options_dir_fsync_.store(false, std::memory_order_release);
options_dir_fsync_failures_.store(0, std::memory_order_release);
}
int options_dir_fsync_failures() const {
return options_dir_fsync_failures_.load(std::memory_order_acquire);
}
IOStatus NewWritableFile(const std::string& fname,
const FileOptions& file_opts,
std::unique_ptr<FSWritableFile>* result,
IODebugContext* dbg) override {
IOStatus s =
FileSystemWrapper::NewWritableFile(fname, file_opts, result, dbg);
if (!s.ok()) {
return s;
}
if (observe_options_file_.load(std::memory_order_acquire) &&
IsOptionsFile(fname)) {
fail_options_dir_fsync_.store(true, std::memory_order_release);
observe_options_file_.store(false, std::memory_order_release);
}
return s;
}
IOStatus NewDirectory(const std::string& name, const IOOptions& io_opts,
std::unique_ptr<FSDirectory>* result,
IODebugContext* dbg) override {
IOStatus s = FileSystemWrapper::NewDirectory(name, io_opts, result, dbg);
if (s.ok()) {
*result = std::make_unique<FailOptionsDirFsyncDirectory>(
std::move(*result), *this);
}
return s;
}
private:
class FailOptionsDirFsyncDirectory : public FSDirectoryWrapper {
public:
FailOptionsDirFsyncDirectory(std::unique_ptr<FSDirectory>&& dir,
FailOptionsDirFsyncFileSystem& fs)
: FSDirectoryWrapper(std::move(dir)), fs_(fs) {}
IOStatus FsyncWithDirOptions(
const IOOptions& options, IODebugContext* dbg,
const DirFsyncOptions& dir_fsync_options) override {
if (fs_.fail_options_dir_fsync_.load(std::memory_order_acquire) &&
IsOptionsFile(dir_fsync_options.renamed_new_name)) {
fs_.fail_options_dir_fsync_.store(false, std::memory_order_release);
fs_.options_dir_fsync_failures_.fetch_add(1, std::memory_order_acq_rel);
return IOStatus::IOError("Injected OPTIONS directory fsync failure");
}
return FSDirectoryWrapper::FsyncWithDirOptions(options, dbg,
dir_fsync_options);
}
private:
FailOptionsDirFsyncFileSystem& fs_;
};
std::atomic<bool> observe_options_file_{false};
std::atomic<bool> fail_options_dir_fsync_{false};
std::atomic<int> options_dir_fsync_failures_{0};
};
void UpdateOptionsFiles(DB* db,
std::unordered_set<std::string>* filename_history,
int* options_files_count) {
@@ -58,6 +147,34 @@ void VerifyOptionsFileName(
}
}
}
int CountOptionsFiles(Env* env, const std::string& dbname) {
std::vector<std::string> filenames;
EXPECT_OK(env->GetChildren(dbname, &filenames));
int options_files_count = 0;
uint64_t number;
FileType type;
for (const auto& filename : filenames) {
if (ParseFileName(filename, &number, &type) && type == kOptionsFile) {
++options_files_count;
}
}
return options_files_count;
}
int CountOptionsFiles(DB* db) {
return CountOptionsFiles(db->GetEnv(), db->GetName());
}
void GenerateStaleOptionsFiles(DB* db, int options_file_count) {
ASSERT_OK(db->DisableFileDeletions());
for (int i = 0; i < options_file_count; ++i) {
ASSERT_OK(db->SetOptions(
{{"level0_file_num_compaction_trigger", std::to_string(8 + i)}}));
}
ASSERT_GT(CountOptionsFiles(db), 2);
}
} // anonymous namespace
TEST_F(OptionsFileTest, NumberOfOptionsFiles) {
@@ -79,6 +196,80 @@ TEST_F(OptionsFileTest, NumberOfOptionsFiles) {
}
}
TEST_F(OptionsFileTest, ObsoleteOptionsFilesPurgedSynchronouslyOnOpen) {
Options opt;
opt.create_if_missing = true;
ASSERT_OK(DestroyDB(dbname_, opt));
std::unique_ptr<DB> db;
ASSERT_OK(DB::Open(opt, dbname_, &db));
GenerateStaleOptionsFiles(db.get(), 5);
db.reset();
opt.create_if_missing = false;
opt.avoid_unnecessary_blocking_io = false;
ASSERT_OK(DB::Open(opt, dbname_, &db));
ASSERT_LE(CountOptionsFiles(db.get()), 2);
}
TEST_F(OptionsFileTest, ObsoleteOptionsFilesPurgedInBackgroundOnOpen) {
Options opt;
opt.create_if_missing = true;
ASSERT_OK(DestroyDB(dbname_, opt));
std::unique_ptr<DB> db;
ASSERT_OK(DB::Open(opt, dbname_, &db));
GenerateStaleOptionsFiles(db.get(), 5);
db.reset();
opt.create_if_missing = false;
opt.avoid_unnecessary_blocking_io = true;
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
SyncPoint::GetInstance()->LoadDependency(
{{"OptionsFileTest::ObsoleteOptionsFilesPurgedInBackgroundOnOpen:"
"ReleasePurge",
"DBImpl::BGWorkPurge:start"}});
SyncPoint::GetInstance()->EnableProcessing();
ASSERT_OK(DB::Open(opt, dbname_, &db));
ASSERT_GT(CountOptionsFiles(db.get()), 2);
TEST_SYNC_POINT(
"OptionsFileTest::ObsoleteOptionsFilesPurgedInBackgroundOnOpen:"
"ReleasePurge");
ASSERT_OK(static_cast_with_check<DBImpl>(db.get())->TEST_WaitForPurge());
ASSERT_LE(CountOptionsFiles(db.get()), 2);
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
}
TEST_F(OptionsFileTest, ObsoleteOptionsFilesPurgedSynchronouslyOnOpenFailure) {
auto fail_fs =
std::make_shared<FailOptionsDirFsyncFileSystem>(FileSystem::Default());
std::unique_ptr<Env> env(NewCompositeEnv(fail_fs));
Options opt;
opt.env = env.get();
opt.create_if_missing = true;
ASSERT_OK(DestroyDB(dbname_, opt));
std::unique_ptr<DB> db;
ASSERT_OK(DB::Open(opt, dbname_, &db));
GenerateStaleOptionsFiles(db.get(), 5);
db.reset();
opt.create_if_missing = false;
opt.avoid_unnecessary_blocking_io = true;
fail_fs->FailOptionsDirFsyncAfterNextOptionsFile();
ASSERT_NOK(DB::Open(opt, dbname_, &db));
ASSERT_EQ(fail_fs->options_dir_fsync_failures(), 1);
ASSERT_LE(CountOptionsFiles(opt.env, dbname_), 2);
}
TEST_F(OptionsFileTest, OptionsFileName) {
const uint64_t kOptionsFileNum = 12345;
uint64_t number;
+279 -1
View File
@@ -7,23 +7,41 @@
#include <algorithm>
#include <iostream>
#include <memory>
#include <thread>
#include <vector>
#include "env/composite_env_wrapper.h"
#include "monitoring/histogram.h"
#include "monitoring/instrumented_mutex.h"
#include "monitoring/perf_context_imp.h"
#include "monitoring/thread_status_util.h"
#include "port/port.h"
#include "rocksdb/db.h"
#include "rocksdb/env.h"
#include "rocksdb/file_system.h"
#include "rocksdb/iostats_context.h"
#include "rocksdb/memtablerep.h"
#include "rocksdb/slice_transform.h"
#include "rocksdb/system_clock.h"
#include "test_util/mock_time_env.h"
#include "test_util/testharness.h"
#include "util/stop_watch.h"
#include "util/string_util.h"
#include "utilities/merge_operators.h"
#if defined(USE_COROUTINES)
#include "folly/Executor.h"
#include "folly/coro/BlockingWait.h"
#include "folly/coro/Collect.h"
#include "folly/coro/CurrentExecutor.h"
#include "folly/coro/Task.h"
#include "folly/executors/IOThreadPoolExecutor.h"
#include "folly/io/async/EventBase.h"
#include "folly/io/async/Request.h"
#include "util/coro_stats_util.h"
#endif
bool FLAGS_random_key = false;
bool FLAGS_use_set_based_memetable = false;
int FLAGS_total_keys = 100;
@@ -64,7 +82,267 @@ std::unique_ptr<DB> OpenDb(bool read_only = false) {
return db;
}
class PerfContextTest : public testing::Test {};
class PerfContextTest : public testing::Test {
protected:
void SetUp() override {
SetPerfLevel(PerfLevel::kEnableCount);
get_perf_context()->ClearPerLevelPerfContext();
get_perf_context()->Reset();
get_iostats_context()->Reset();
get_iostats_context()->disable_iostats = false;
}
};
#if defined(USE_COROUTINES)
TEST_F(PerfContextTest, CoroutineStatsContextScopeCollectsStats) {
SetPerfLevel(PerfLevel::kEnableTimeAndCPUTimeExceptForMutex);
get_perf_context()->EnablePerLevelPerfContext();
folly::IOThreadPoolExecutor executor(1);
folly::EventBase* event_base = executor.getEventBase();
ASSERT_NE(nullptr, event_base);
auto clock = std::make_shared<MockSystemClock>(SystemClock::Default());
clock->SetCurrentTime(1);
CoroutineStatsConfig stats_config = CaptureCoroutineStatsConfig();
folly::coro::blockingWait(folly::coro::co_withExecutor(
folly::Executor::getKeepAliveToken(event_base),
[event_base, clock = clock.get(),
stats_config =
std::move(stats_config)]() mutable -> folly::coro::Task<void> {
EXPECT_TRUE(event_base->isInEventBaseThread());
CoroutineStatsContextScope scope(std::move(stats_config),
Env::Default());
EXPECT_EQ(PerfLevel::kEnableTimeAndCPUTimeExceptForMutex,
GetPerfLevel());
EXPECT_EQ(0, get_perf_context()->block_read_count);
EXPECT_TRUE(get_perf_context()->per_level_perf_context_enabled);
get_perf_context()->block_read_count += 3;
(*get_perf_context()->level_to_perf_context)[2].block_cache_hit_count +=
5;
EXPECT_EQ(0, get_iostats_context()->bytes_read);
get_iostats_context()->bytes_read += 7;
get_iostats_context()->cpu_read_nanos += 11;
get_iostats_context()
->file_io_stats_by_temperature.hot_file_read_count += 1;
{
PERF_TIMER_GUARD_WITH_CLOCK(block_read_time, clock);
clock->SleepForMicroseconds(7);
}
co_await folly::coro::co_reschedule_on_current_executor;
EXPECT_TRUE(event_base->isInEventBaseThread());
EXPECT_EQ(3, get_perf_context()->block_read_count);
EXPECT_TRUE(get_perf_context()->per_level_perf_context_enabled);
get_perf_context()->block_read_count += 5;
(*get_perf_context()->level_to_perf_context)[2].block_cache_hit_count +=
2;
EXPECT_EQ(7, get_iostats_context()->bytes_read);
get_iostats_context()->bytes_read += 11;
get_iostats_context()->cpu_read_nanos += 13;
get_iostats_context()
->file_io_stats_by_temperature.hot_file_read_count += 2;
scope.Finalize();
auto verify_stats = [] {
const PerfContext* captured_perf_context = get_perf_context();
ASSERT_NE(nullptr, captured_perf_context);
EXPECT_EQ(8, captured_perf_context->block_read_count);
EXPECT_EQ(7000, captured_perf_context->block_read_time);
ASSERT_NE(nullptr, captured_perf_context->level_to_perf_context);
EXPECT_EQ(7, (*captured_perf_context->level_to_perf_context)[2]
.block_cache_hit_count);
const IOStatsContext* iostats_context = get_iostats_context();
ASSERT_NE(nullptr, iostats_context);
EXPECT_EQ(18, iostats_context->bytes_read);
EXPECT_EQ(24, iostats_context->cpu_read_nanos);
EXPECT_EQ(3, iostats_context->file_io_stats_by_temperature
.hot_file_read_count);
};
verify_stats();
}()));
}
TEST_F(PerfContextTest, CoroutineStatsContextsRemainIsolatedWhenInterleaved) {
CoroutineStatsConfig first_stats_config = CaptureCoroutineStatsConfig();
SetPerfLevel(PerfLevel::kEnableTimeExceptForMutex);
get_perf_context()->EnablePerLevelPerfContext();
get_iostats_context()->disable_iostats = true;
CoroutineStatsConfig second_stats_config = CaptureCoroutineStatsConfig();
folly::IOThreadPoolExecutor executor(1);
folly::EventBase* event_base = executor.getEventBase();
ASSERT_NE(nullptr, event_base);
folly::coro::blockingWait(folly::coro::co_withExecutor(
folly::Executor::getKeepAliveToken(event_base),
[first_stats_config = std::move(first_stats_config),
second_stats_config = std::move(
second_stats_config)]() mutable -> folly::coro::Task<void> {
get_perf_context()->Reset();
get_perf_context()->ClearPerLevelPerfContext();
get_perf_context()->block_read_count = 100;
get_iostats_context()->Reset();
get_iostats_context()->bytes_read = 200;
auto make_task = [](CoroutineStatsConfig stats_config, uint64_t first,
uint64_t second) -> folly::coro::Task<uint64_t> {
const PerfLevel expected_perf_level = stats_config.perf_level;
const bool expected_per_level_perf_context_enabled =
stats_config.per_level_perf_context_enabled;
const bool expected_iostats_disabled = stats_config.iostats_disabled;
CoroutineStatsContextScope scope(std::move(stats_config),
Env::Default());
EXPECT_EQ(expected_perf_level, GetPerfLevel());
EXPECT_EQ(expected_per_level_perf_context_enabled,
get_perf_context()->per_level_perf_context_enabled);
EXPECT_EQ(expected_iostats_disabled,
get_iostats_context()->disable_iostats);
EXPECT_EQ(0, get_perf_context()->block_read_count);
EXPECT_EQ(0, get_iostats_context()->bytes_read);
get_perf_context()->block_read_count += first;
get_iostats_context()->bytes_read += first;
co_await folly::coro::co_reschedule_on_current_executor;
EXPECT_EQ(expected_perf_level, GetPerfLevel());
EXPECT_EQ(expected_per_level_perf_context_enabled,
get_perf_context()->per_level_perf_context_enabled);
EXPECT_EQ(expected_iostats_disabled,
get_iostats_context()->disable_iostats);
EXPECT_EQ(first, get_perf_context()->block_read_count);
EXPECT_EQ(first, get_iostats_context()->bytes_read);
get_perf_context()->block_read_count += second;
get_iostats_context()->bytes_read += second;
scope.Finalize();
EXPECT_EQ(first + second, get_iostats_context()->bytes_read);
co_return get_perf_context()->block_read_count;
};
std::vector<folly::coro::Task<uint64_t>> tasks;
tasks.push_back(make_task(std::move(first_stats_config), 1, 10));
tasks.push_back(make_task(std::move(second_stats_config), 2, 20));
std::vector<uint64_t> results =
co_await folly::coro::collectAllRange(std::move(tasks));
EXPECT_EQ((std::vector<uint64_t>{11, 22}), results);
get_perf_context()->Reset();
get_iostats_context()->Reset();
co_return;
}()));
}
TEST_F(PerfContextTest, CoroutineStatsContextScopeCollectsGetCpuNanos) {
SetPerfLevel(PerfLevel::kEnableTimeAndCPUTimeExceptForMutex);
auto clock = std::make_shared<MockSystemClock>(SystemClock::Default());
CompositeEnvWrapper env(Env::Default(), clock);
clock->SetCurrentCPUTimeNanos(0);
{
CoroutineStatsContextScope scope(CaptureCoroutineStatsConfig(), &env);
clock->MockSleepForCPUNanos(17);
clock->MockSleepForCPUNanos(5);
// Time while the request context is unset is not charged to the request.
{
folly::RequestContextScopeGuard context_scope;
clock->MockSleepForCPUNanos(13);
}
clock->MockSleepForCPUNanos(7);
scope.Finalize();
EXPECT_EQ(29, get_perf_context()->get_cpu_nanos);
}
}
#endif
TEST_F(PerfContextTest, AsyncCallbackStatsStartEmptyOnSyncFallback) {
class NoReadExecutorFileSystem final : public FileSystemWrapper {
public:
explicit NoReadExecutorFileSystem(const std::shared_ptr<FileSystem>& target)
: FileSystemWrapper(target) {}
const char* Name() const override { return "NoReadExecutorFileSystem"; }
folly::IOExecutor* GetReadExecutor() override { return nullptr; }
};
class StatsCallback final : public DB::AsyncCallback {
public:
bool EnableStats() const override { return true; }
void OnComplete(const PerfContext* callback_perf_context,
const IOStatsContext* callback_iostats_context) override {
ASSERT_NE(nullptr, callback_perf_context);
ASSERT_NE(nullptr, callback_iostats_context);
called = true;
get_read_bytes = callback_perf_context->get_read_bytes;
multiget_read_bytes = callback_perf_context->multiget_read_bytes;
}
bool called = false;
uint64_t get_read_bytes = 0;
uint64_t multiget_read_bytes = 0;
};
auto file_system =
std::make_shared<NoReadExecutorFileSystem>(FileSystem::Default());
std::unique_ptr<Env> env = NewCompositeEnv(file_system);
Options options;
options.create_if_missing = true;
options.env = env.get();
std::unique_ptr<DB> db;
ASSERT_OK(DB::Open(options, kDbName, &db));
ASSERT_OK(db->Put(WriteOptions(), "key", "value"));
ASSERT_OK(db->Put(WriteOptions(), "key2", "value2"));
get_perf_context()->get_read_bytes = 13;
StatsCallback callback;
Status status;
std::string value;
db->GetAsync(ReadOptions(), "key", &value, status, callback);
EXPECT_TRUE(callback.called);
ASSERT_OK(status);
EXPECT_EQ("value", value);
EXPECT_EQ(value.size(), callback.get_read_bytes);
get_perf_context()->Reset();
get_perf_context()->multiget_read_bytes = 29;
StatsCallback multiget_callback;
ColumnFamilyHandle* column_families[] = {db->DefaultColumnFamily(),
db->DefaultColumnFamily()};
const std::string key_strings[] = {"key", "key2"};
const Slice keys[] = {key_strings[0], key_strings[1]};
PinnableSlice values[2];
Status statuses[2];
db->MultiGetAsync(ReadOptions(), 2, column_families, keys, values, statuses,
/*sorted_input=*/false, multiget_callback);
EXPECT_TRUE(multiget_callback.called);
EXPECT_OK(statuses[0]);
EXPECT_OK(statuses[1]);
EXPECT_EQ("value", values[0].ToString());
EXPECT_EQ("value2", values[1].ToString());
EXPECT_EQ(values[0].size() + values[1].size(),
multiget_callback.multiget_read_bytes);
db.reset();
ASSERT_OK(DestroyDB(kDbName, options));
}
TEST_F(PerfContextTest, SeekIntoDeletion) {
ASSERT_OK(DestroyDB(kDbName, Options()));
-87
View File
@@ -593,93 +593,6 @@ bool TableCache::GetFromRowCache(const Slice& user_key, IterKey& row_cache_key,
return found;
}
Status TableCache::Get(const ReadOptions& options,
const InternalKeyComparator& internal_comparator,
const FileMetaData& file_meta, const Slice& k,
GetContext* get_context,
const MutableCFOptions& mutable_cf_options,
HistogramImpl* file_read_hist, bool skip_filters,
int level, size_t max_file_size_for_l0_meta_pin) {
auto& fd = file_meta.fd;
std::string* row_cache_entry = nullptr;
bool done = false;
IterKey row_cache_key;
std::string row_cache_entry_buffer;
// Check row cache if enabled.
// Reuse row_cache_key sequence number when row cache hits.
Status s;
if (ioptions_.row_cache && !get_context->NeedToReadSequence()) {
auto user_key = ExtractUserKey(k);
uint64_t cache_entry_seq_no =
CreateRowCacheKeyPrefix(options, fd, k, get_context, row_cache_key);
done = GetFromRowCache(user_key, row_cache_key, row_cache_key.Size(),
get_context, &s, cache_entry_seq_no);
if (!done) {
row_cache_entry = &row_cache_entry_buffer;
}
}
TEST_SYNC_POINT_CALLBACK("TableCache::Get::BeforeFindTable",
const_cast<FileDescriptor*>(&fd));
TableReader* t = nullptr;
TypedHandle* handle = nullptr;
if (s.ok() && !done) {
s = FindTable(options, file_options_, internal_comparator, file_meta,
&handle, mutable_cf_options, &t,
options.read_tier == kBlockCacheTier /* no_io */,
file_read_hist, skip_filters, level,
true /* prefetch_index_and_filter_in_cache */,
max_file_size_for_l0_meta_pin, file_meta.temperature,
should_pin_table_handles_);
SequenceNumber* max_covering_tombstone_seq =
get_context->max_covering_tombstone_seq();
if (s.ok() && max_covering_tombstone_seq != nullptr &&
!options.ignore_range_deletions) {
std::unique_ptr<FragmentedRangeTombstoneIterator> range_del_iter(
t->NewRangeTombstoneIterator(options));
if (range_del_iter != nullptr) {
SequenceNumber seq =
range_del_iter->MaxCoveringTombstoneSeqnum(ExtractUserKey(k));
if (seq > *max_covering_tombstone_seq) {
*max_covering_tombstone_seq = seq;
if (get_context->NeedTimestamp()) {
get_context->SetTimestampFromRangeTombstone(
range_del_iter->timestamp());
}
}
}
}
if (s.ok()) {
get_context->SetReplayLog(row_cache_entry); // nullptr if no cache.
s = t->Get(options, k, get_context,
mutable_cf_options.prefix_extractor.get(), skip_filters);
get_context->SetReplayLog(nullptr);
} else if (options.read_tier == kBlockCacheTier && s.IsIncomplete()) {
// Couldn't find table in cache and couldn't open it because of no_io.
get_context->MarkKeyMayExist();
done = true;
}
}
// Put the replay log in row cache only if something was found.
if (!done && s.ok() && row_cache_entry && !row_cache_entry->empty()) {
RowCacheInterface row_cache{ioptions_.row_cache.get()};
size_t charge = row_cache_entry->capacity() + sizeof(std::string);
auto row_ptr = new std::string(std::move(*row_cache_entry));
Status rcs = row_cache.Insert(row_cache_key.GetUserKey(), row_ptr, charge);
if (!rcs.ok()) {
// If row cache is full, it's OK to continue, but we keep ownership of
// row_ptr.
delete row_ptr;
}
}
if (handle != nullptr) {
cache_.Release(handle);
}
return s;
}
void TableCache::UpdateRangeTombstoneSeqnums(
const ReadOptions& options, TableReader* t,
MultiGetContext::Range& table_range) {
+8 -7
View File
@@ -144,13 +144,14 @@ class TableCache {
// recorded
// @param skip_filters Disables loading/accessing the filter block
// @param level The level this table is at, -1 for "not set / don't know"
Status Get(const ReadOptions& options,
const InternalKeyComparator& internal_comparator,
const FileMetaData& file_meta, const Slice& k,
GetContext* get_context,
const MutableCFOptions& mutable_cf_options,
HistogramImpl* file_read_hist = nullptr, bool skip_filters = false,
int level = -1, size_t max_file_size_for_l0_meta_pin = 0);
DECLARE_SYNC_AND_ASYNC(Status, Get, const ReadOptions& options,
const InternalKeyComparator& internal_comparator,
const FileMetaData& file_meta, const Slice& k,
GetContext* get_context,
const MutableCFOptions& mutable_cf_options,
HistogramImpl* file_read_hist = nullptr,
bool skip_filters = false, int level = -1,
size_t max_file_size_for_l0_meta_pin = 0);
// Return the range delete tombstone iterator of the file specified by
// `file_meta`.
+87 -5
View File
@@ -8,10 +8,93 @@
#if defined(WITHOUT_COROUTINES) || \
(defined(USE_COROUTINES) && defined(WITH_COROUTINES))
namespace ROCKSDB_NAMESPACE {
#if defined(WITHOUT_COROUTINES)
#endif
DEFINE_SYNC_AND_ASYNC(Status, TableCache::Get)
(const ReadOptions& options, const InternalKeyComparator& internal_comparator,
const FileMetaData& file_meta, const Slice& k, GetContext* get_context,
const MutableCFOptions& mutable_cf_options, HistogramImpl* file_read_hist,
bool skip_filters, int level, size_t max_file_size_for_l0_meta_pin) {
auto& fd = file_meta.fd;
std::string* row_cache_entry = nullptr;
bool done = false;
IterKey row_cache_key;
std::string row_cache_entry_buffer;
// Check row cache if enabled.
// Reuse row_cache_key sequence number when row cache hits.
Status s;
if (ioptions_.row_cache && !get_context->NeedToReadSequence()) {
auto user_key = ExtractUserKey(k);
uint64_t cache_entry_seq_no =
CreateRowCacheKeyPrefix(options, fd, k, get_context, row_cache_key);
done = GetFromRowCache(user_key, row_cache_key, row_cache_key.Size(),
get_context, &s, cache_entry_seq_no);
if (!done) {
row_cache_entry = &row_cache_entry_buffer;
}
}
TEST_SYNC_POINT_CALLBACK("TableCache::Get::BeforeFindTable",
const_cast<FileDescriptor*>(&fd));
TableReader* t = nullptr;
TypedHandle* handle = nullptr;
if (s.ok() && !done) {
s = FindTable(options, file_options_, internal_comparator, file_meta,
&handle, mutable_cf_options, &t,
options.read_tier == kBlockCacheTier /* no_io */,
file_read_hist, skip_filters, level,
true /* prefetch_index_and_filter_in_cache */,
max_file_size_for_l0_meta_pin, file_meta.temperature,
should_pin_table_handles_);
SequenceNumber* max_covering_tombstone_seq =
get_context->max_covering_tombstone_seq();
if (s.ok() && max_covering_tombstone_seq != nullptr &&
!options.ignore_range_deletions) {
std::unique_ptr<FragmentedRangeTombstoneIterator> range_del_iter(
t->NewRangeTombstoneIterator(options));
if (range_del_iter != nullptr) {
SequenceNumber seq =
range_del_iter->MaxCoveringTombstoneSeqnum(ExtractUserKey(k));
if (seq > *max_covering_tombstone_seq) {
*max_covering_tombstone_seq = seq;
if (get_context->NeedTimestamp()) {
get_context->SetTimestampFromRangeTombstone(
range_del_iter->timestamp());
}
}
}
}
if (s.ok()) {
get_context->SetReplayLog(row_cache_entry); // nullptr if no cache.
s = CO_AWAIT(t->Get, options, k, get_context,
mutable_cf_options.prefix_extractor.get(), skip_filters);
get_context->SetReplayLog(nullptr);
} else if (options.read_tier == kBlockCacheTier && s.IsIncomplete()) {
// Couldn't find table in cache and couldn't open it because of no_io.
get_context->MarkKeyMayExist();
done = true;
}
}
// Put the replay log in row cache only if something was found.
if (!done && s.ok() && row_cache_entry && !row_cache_entry->empty()) {
RowCacheInterface row_cache{ioptions_.row_cache.get()};
size_t charge = row_cache_entry->capacity() + sizeof(std::string);
auto row_ptr = new std::string(std::move(*row_cache_entry));
Status rcs = row_cache.Insert(row_cache_key.GetUserKey(), row_ptr, charge);
if (!rcs.ok()) {
// If row cache is full, it's OK to continue, but we keep ownership of
// row_ptr.
delete row_ptr;
}
}
if (handle != nullptr) {
cache_.Release(handle);
}
CO_RETURN s;
}
// Batched version of TableCache::MultiGet.
DEFINE_SYNC_AND_ASYNC(Status, TableCache::MultiGet)
@@ -85,9 +168,8 @@ DEFINE_SYNC_AND_ASYNC(Status, TableCache::MultiGet)
UpdateRangeTombstoneSeqnums(options, t, table_range);
}
if (s.ok()) {
CO_AWAIT(t->MultiGet)
(options, &table_range, mutable_cf_options.prefix_extractor.get(),
skip_filters);
CO_AWAIT(t->MultiGet, options, &table_range,
mutable_cf_options.prefix_extractor.get(), skip_filters);
} else if (options.read_tier == kBlockCacheTier && s.IsIncomplete()) {
for (auto iter = table_range.begin(); iter != table_range.end(); ++iter) {
Status* status = iter->s;
+409 -118
View File
@@ -15,6 +15,7 @@
#include <functional>
#include <map>
#include <memory>
#include <optional>
#include <set>
#include <sstream>
#include <thread>
@@ -36,11 +37,46 @@
#include "port/port.h"
#include "table/table_reader.h"
#include "test_util/sync_point.h"
#include "util/cast_util.h"
#include "util/hash_containers.h"
#include "util/string_util.h"
namespace ROCKSDB_NAMESPACE {
class VersionBuilder::Rep {
// Scalar/flag state that a VersionEdit can mutate while being applied. Factored
// into a small struct so the single-edit undo (VersionBuilder::Rep::EditUndo)
// can snapshot and restore all of it with a single copy/swap, and so the field
// list is defined once rather than duplicated between the live state and its
// undo record. VersionBuilder::Rep inherits this (as does EditUndo) so the
// scalar state can be snapshotted/restored via up_cast<MutableScalars> with a
// single copy/swap; Rep is a file-local implementation class, so exposing the
// base is inconsequential.
struct MutableScalars {
// Whether there are invalid new files or invalid deletions on levels larger
// than num_levels_.
bool has_invalid_levels_ = false;
// The fields below are only meaningful when track_found_and_missing_files_ is
// enabled.
// The highest file number among all missing blob files, or
// kInvalidBlobFileNumber if none are missing. Useful to check whether a
// complete Version is available.
uint64_t missing_blob_files_high_ = kInvalidBlobFileNumber;
// Cached result of the last validity check: true if all the files making up
// the Version can be found. Or, when allow_incomplete_valid_version_ is true
// and the version was never edited in an atomic group, true if only a suffix
// of L0 SST files and their associated blob files are missing.
bool valid_version_available_ = false;
// True if the version was ever edited in an atomic group.
bool edited_in_atomic_group_ = false;
// True if the Version was updated since the last validity check, so the
// cached `valid_version_available_` must be recomputed on the next check
// rather than reused.
bool version_updated_since_last_check_ = false;
};
class VersionBuilder::Rep : public MutableScalars {
class NewestFirstBySeqNo {
public:
bool operator()(const FileMetaData* lhs, const FileMetaData* rhs) const {
@@ -265,17 +301,13 @@ class VersionBuilder::Rep {
// on invalid levels. The version is not consistent if in the end the files
// on invalid levels don't cancel out.
std::unordered_map<int, size_t> invalid_level_sizes_;
// Whether there are invalid new files or invalid deletion on levels larger
// than num_levels_.
bool has_invalid_levels_;
// Current levels of table files affected by additions/deletions.
std::unordered_map<uint64_t, int> table_file_levels_;
// Current compact cursors that should be changed after the last compaction
std::unordered_map<int, InternalKey> updated_compact_cursors_;
const std::shared_ptr<const NewestFirstByEpochNumber>
level_zero_cmp_by_epochno_;
const std::shared_ptr<const NewestFirstBySeqNo> level_zero_cmp_by_seqno_;
const std::shared_ptr<const BySmallestKey> level_nonzero_cmp_;
const NewestFirstByEpochNumber level_zero_cmp_by_epochno_;
const NewestFirstBySeqNo level_zero_cmp_by_seqno_;
const BySmallestKey level_nonzero_cmp_;
// Mutable metadata objects for all blob files affected by the series of
// version edits.
@@ -302,30 +334,88 @@ class VersionBuilder::Rep {
std::unordered_set<uint64_t> non_l0_missing_files_;
// Intermediate SST files (blob files not included yet)
std::vector<std::string> intermediate_files_;
// The highest file number for all the missing blob files, useful to check
// if a complete Version is available.
uint64_t missing_blob_files_high_ = kInvalidBlobFileNumber;
// Missing blob files, useful to check if only the missing L0 files'
// associated blob files are missing.
std::unordered_set<uint64_t> missing_blob_files_;
// True if all files consisting the Version can be found. Or if
// `allow_incomplete_valid_version_` is true and the version history is not
// ever edited in an atomic group, this will be true if only a
// suffix of L0 SST files and their associated blob files are missing.
bool valid_version_available_;
// True if version is ever edited in an atomic group.
bool edited_in_atomic_group_;
// Flag to indicate if the Version is updated since last validity check. If no
// `Apply` call is made between a `Rep`'s construction and a
// `ValidVersionAvailable` check or between two `ValidVersionAvailable` calls.
// This flag will be true to indicate the cached validity value can be
// directly used without a recheck.
bool version_updated_since_last_check_;
// Note: additional scalar/flag state mutated while applying edits
// (has_invalid_levels_, missing_blob_files_high_, valid_version_available_,
// edited_in_atomic_group_, version_updated_since_last_check_) lives in the
// MutableScalars base so the single-edit undo can snapshot/restore it in one
// copy/swap.
// End of fields that are only tracked when `track_found_and_missing_files_`
// is enabled.
// Records how to undo (and redo) the most recent `Apply()` so that
// VersionEditHandlerPointInTime can build a `Version` reflecting the state
// *before* an edit (on a valid->invalid negative edge) without copying the
// whole `Rep` on every edit. Only populated when
// `track_found_and_missing_files_` is true.
//
// Each recorded map/set entry stores the "other side" value (the value not
// currently live). Applying `ToggleUndo()` swaps every recorded slot with the
// live state, so it is self-inverse: one call rolls back to the pre-edit
// state, a second call redoes to the post-edit state. Objects momentarily
// removed from the live state (e.g. blob metadata) are held inside these
// records via value/shared_ptr, so nothing referenced by the pre-edit state
// is destroyed during the tentative window. `FileMetaData` removed from
// `added_files` by a deletion is instead kept alive via `deferred_unref` and
// only released by `CommitLastApply()`.
struct EditUndo : MutableScalars {
// True while the live state has been rolled back to the pre-edit state
// (i.e. `ToggleUndo()` has run an odd number of times).
bool rolled_back = false;
// Each map below records, for the keys the edit touches, the value on the
// "other side" (the value not currently live): before the edit while in the
// forward/post-edit state, and after the edit once rolled back. `nullopt`
// means the key is absent on that side. `ToggleUndo()` swaps each recorded
// entry with the corresponding live container, so it is self-inverse.
// levels_[level].added_files entries, keyed by (level, file number). Holds
// the FileMetaData* on the non-live side (the map only owns the pointer,
// not the object; see `deferred_unref` for lifetime of deleted files).
std::map<std::pair<int, uint64_t>, std::optional<FileMetaData*>>
added_files;
// levels_[level].deleted_files membership, keyed by (level, file number);
// value is whether the file number is present on the non-live side.
std::map<std::pair<int, uint64_t>, bool> deleted_files;
// table_file_levels_ entries, keyed by file number (level, or absent).
UnorderedMap<uint64_t, std::optional<int>> table_file_levels;
// invalid_level_sizes_ entries, keyed by level (count, or absent).
UnorderedMap<int, std::optional<size_t>> invalid_level_sizes;
// updated_compact_cursors_ entries, keyed by level (cursor key, or absent).
UnorderedMap<int, std::optional<InternalKey>> compact_cursors;
// mutable_blob_file_metas_ entries, keyed by blob file number. Holds the
// MutableBlobFileMetaData by value on the non-live side; because it owns a
// shared_ptr to the SharedBlobFileMetaData, a rolled-back blob addition is
// kept alive here rather than destroyed (which would spuriously mark the
// blob obsolete).
UnorderedMap<uint64_t, std::optional<MutableBlobFileMetaData>> blob_metas;
// Membership on the non-live side for each tracking set, keyed by file
// number (blob file number for `missing_blob_files`).
UnorderedMap<uint64_t, bool> found_files;
UnorderedMap<uint64_t, bool> l0_missing_files;
UnorderedMap<uint64_t, bool> non_l0_missing_files;
UnorderedMap<uint64_t, bool> missing_blob_files;
// `intermediate_files_` is append-only within an `Apply()`. Restore by
// truncating to `intermediate_files_size` (rollback) / re-appending
// `intermediate_tail` (redo).
size_t intermediate_files_size = 0;
std::vector<std::string> intermediate_tail;
// (Scalar/flag state on the non-live side is held in the MutableScalars
// base subobject.)
// `FileMetaData` removed from `added_files` by a deletion in this edit. The
// ref is held here (not dropped) until `CommitLastApply()`, so a rollback
// can reinstall the file.
std::vector<FileMetaData*> deferred_unref;
};
std::unique_ptr<EditUndo> undo_;
public:
Rep(const FileOptions& file_options, const ImmutableCFOptions* ioptions,
TableCache* table_cache, VersionStorageInfo* base_vstorage,
@@ -339,12 +429,9 @@ class VersionBuilder::Rep {
base_vstorage_(base_vstorage),
version_set_(version_set),
num_levels_(base_vstorage->num_levels()),
has_invalid_levels_(false),
level_zero_cmp_by_epochno_(
std::make_shared<NewestFirstByEpochNumber>()),
level_zero_cmp_by_seqno_(std::make_shared<NewestFirstBySeqNo>()),
level_nonzero_cmp_(std::make_shared<BySmallestKey>(
base_vstorage_->InternalComparator())),
level_zero_cmp_by_epochno_(),
level_zero_cmp_by_seqno_(),
level_nonzero_cmp_(base_vstorage_->InternalComparator()),
file_metadata_cache_res_mgr_(file_metadata_cache_res_mgr),
cfd_(cfd),
version_edit_handler_(version_edit_handler),
@@ -367,52 +454,17 @@ class VersionBuilder::Rep {
}
}
Rep(const Rep& other)
: file_options_(other.file_options_),
ioptions_(other.ioptions_),
table_cache_(other.table_cache_),
base_vstorage_(other.base_vstorage_),
version_set_(other.version_set_),
num_levels_(other.num_levels_),
invalid_level_sizes_(other.invalid_level_sizes_),
has_invalid_levels_(other.has_invalid_levels_),
table_file_levels_(other.table_file_levels_),
updated_compact_cursors_(other.updated_compact_cursors_),
level_zero_cmp_by_epochno_(other.level_zero_cmp_by_epochno_),
level_zero_cmp_by_seqno_(other.level_zero_cmp_by_seqno_),
level_nonzero_cmp_(other.level_nonzero_cmp_),
mutable_blob_file_metas_(other.mutable_blob_file_metas_),
file_metadata_cache_res_mgr_(other.file_metadata_cache_res_mgr_),
cfd_(other.cfd_),
version_edit_handler_(other.version_edit_handler_),
track_found_and_missing_files_(other.track_found_and_missing_files_),
allow_incomplete_valid_version_(other.allow_incomplete_valid_version_),
found_files_(other.found_files_),
l0_missing_files_(other.l0_missing_files_),
non_l0_missing_files_(other.non_l0_missing_files_),
intermediate_files_(other.intermediate_files_),
missing_blob_files_high_(other.missing_blob_files_high_),
missing_blob_files_(other.missing_blob_files_),
valid_version_available_(other.valid_version_available_),
edited_in_atomic_group_(other.edited_in_atomic_group_),
version_updated_since_last_check_(
other.version_updated_since_last_check_) {
assert(ioptions_);
levels_ = new LevelState[num_levels_];
for (int level = 0; level < num_levels_; level++) {
levels_[level] = other.levels_[level];
const auto& added = levels_[level].added_files;
for (auto& pair : added) {
RefFile(pair.second);
}
}
if (track_found_and_missing_files_) {
assert(cfd_);
assert(version_edit_handler_);
}
}
// Rep is only ever held via std::unique_ptr and is not copied. (The previous
// save-point mechanism deep-copied Rep on every edit, which was quadratic;
// it has been replaced by single-edit undo, see EditUndo above.)
Rep(const Rep&) = delete;
Rep& operator=(const Rep&) = delete;
~Rep() {
// Release any FileMetaData whose deletion was deferred by an uncommitted
// Apply(), and ensure the live state is in the forward (post-edit) position
// so the loop below unrefs exactly the files still owned by added_files.
CommitLastApply();
for (int level = 0; level < num_levels_; level++) {
const auto& added = levels_[level].added_files;
for (auto& pair : added) {
@@ -423,12 +475,6 @@ class VersionBuilder::Rep {
delete[] levels_;
}
void RefFile(FileMetaData* f) {
assert(f);
assert(f->refs > 0);
f->refs++;
}
void UnrefFile(FileMetaData* f) {
f->refs--;
if (f->refs <= 0) {
@@ -439,6 +485,15 @@ class VersionBuilder::Rep {
f->fd.pinned_reader.Release(table_cache_->get_cache().get());
}
// Evict from table cache to prevent leaked entries for files whose
// last reference is held by the VersionBuilder (e.g., files from a
// failed LogAndApply that were never installed in any Version).
// Release() above only releases the pinned handle's ref but does not
// remove the entry from the cache hash table.
if (table_cache_ != nullptr) {
TableCache::Evict(table_cache_->get_cache().get(), f->fd.GetNumber());
}
if (file_metadata_cache_res_mgr_) {
Status s = file_metadata_cache_res_mgr_->UpdateCacheReservation(
f->ApproximateMemoryUsage(), false /* increase */);
@@ -448,6 +503,252 @@ class VersionBuilder::Rep {
}
}
//======== Single-edit undo support (track_found_and_missing_files_) =======//
// See the EditUndo declaration above for the overall model.
// Records the pre-edit value of `map[key]` into `saved` (first-touch only, so
// repeated touches of the same key within one edit keep the true pre-edit
// value). The stored value is the "other side" for ToggleMap().
template <typename Map, typename Saved, typename K>
static void RecordMapEntry(const Map& map, Saved& saved, const K& key) {
if (saved.find(key) != saved.end()) {
return;
}
auto it = map.find(key);
saved.emplace(key,
it != map.end()
? std::optional<typename Map::mapped_type>(it->second)
: std::nullopt);
}
// Swaps every recorded entry between `map` and `saved` (self-inverse).
template <typename Map, typename Saved>
static void ToggleMapEntries(Map& map, Saved& saved) {
for (auto& elem : saved) {
const auto& key = elem.first;
auto& other = elem.second;
auto it = map.find(key);
std::optional<typename Map::mapped_type> live =
it != map.end() ? std::optional<typename Map::mapped_type>(it->second)
: std::nullopt;
if (other.has_value()) {
// insert_or_assign avoids requiring a default-constructible mapped_type
// (e.g. MutableBlobFileMetaData has no default constructor).
map.insert_or_assign(key, std::move(*other));
} else if (it != map.end()) {
map.erase(it);
}
other = std::move(live);
}
}
template <typename Set, typename Saved, typename K>
static void RecordSetMembership(const Set& set, Saved& saved, const K& key) {
if (saved.find(key) != saved.end()) {
return;
}
saved.emplace(key, set.find(key) != set.end());
}
template <typename Set, typename Saved>
static void ToggleSetMembership(Set& set, Saved& saved) {
for (auto& elem : saved) {
const auto& key = elem.first;
bool& other_present = elem.second;
bool live_present = set.find(key) != set.end();
if (other_present) {
set.insert(key);
} else {
set.erase(key);
}
other_present = live_present;
}
}
void RecordAddedFileEntry(int level, uint64_t fn) {
assert(undo_);
assert(level < num_levels_);
auto key = std::make_pair(level, fn);
if (undo_->added_files.find(key) != undo_->added_files.end()) {
return;
}
const auto& map = levels_[level].added_files;
auto it = map.find(fn);
undo_->added_files.emplace(
key, it != map.end() ? std::optional<FileMetaData*>(it->second)
: std::nullopt);
}
void RecordDeletedFileEntry(int level, uint64_t fn) {
assert(undo_);
assert(level < num_levels_);
auto key = std::make_pair(level, fn);
if (undo_->deleted_files.find(key) != undo_->deleted_files.end()) {
return;
}
const auto& set = levels_[level].deleted_files;
undo_->deleted_files.emplace(key, set.find(fn) != set.end());
}
// Snapshots the pre-edit value of every piece of state that `edit` will
// mutate, so the edit can later be rolled back / redone. Must be called
// before the edit is applied and only in tracking mode.
void CaptureUndo(const VersionEdit* edit) {
assert(track_found_and_missing_files_);
assert(!undo_);
undo_ = std::make_unique<EditUndo>();
undo_->intermediate_files_size = intermediate_files_.size();
// Snapshot the pre-edit scalars (the MutableScalars base subobject).
up_cast<MutableScalars>(*undo_) = up_cast<MutableScalars>(*this);
auto touch_table_file = [&](int level, uint64_t fn) {
RecordMapEntry(table_file_levels_, undo_->table_file_levels, fn);
RecordSetMembership(found_files_, undo_->found_files, fn);
RecordSetMembership(l0_missing_files_, undo_->l0_missing_files, fn);
RecordSetMembership(non_l0_missing_files_, undo_->non_l0_missing_files,
fn);
if (level >= 0 && level < num_levels_) {
RecordAddedFileEntry(level, fn);
RecordDeletedFileEntry(level, fn);
} else if (level >= num_levels_) {
RecordMapEntry(invalid_level_sizes_, undo_->invalid_level_sizes, level);
}
};
auto touch_blob_file = [&](uint64_t blob_file_number) {
if (blob_file_number == kInvalidBlobFileNumber) {
return;
}
RecordMapEntry(mutable_blob_file_metas_, undo_->blob_metas,
blob_file_number);
RecordSetMembership(missing_blob_files_, undo_->missing_blob_files,
blob_file_number);
};
for (const auto& ba : edit->GetBlobFileAdditions()) {
touch_blob_file(ba.GetBlobFileNumber());
}
for (const auto& bg : edit->GetBlobFileGarbages()) {
touch_blob_file(bg.GetBlobFileNumber());
}
for (const auto& deleted_file : edit->GetDeletedFiles()) {
const int level = deleted_file.first;
const uint64_t fn = deleted_file.second;
touch_table_file(level, fn);
// Only reachable blob metadata is touched (mirrors ApplyFileDeletion).
if (level >= 0 && level < num_levels_ &&
GetCurrentLevelForTableFile(fn) == level) {
touch_blob_file(GetOldestBlobFileNumberForTableFile(level, fn));
}
}
for (const auto& new_file : edit->GetNewFiles()) {
const int level = new_file.first;
const FileMetaData& meta = new_file.second;
touch_table_file(level, meta.fd.GetNumber());
touch_blob_file(meta.oldest_blob_file_number);
}
for (const auto& cursor : edit->GetCompactCursors()) {
const int level = cursor.first;
if (level >= 0 && level < num_levels_) {
RecordMapEntry(updated_compact_cursors_, undo_->compact_cursors, level);
}
}
}
// Swaps the live state with the recorded "other side", moving between the
// post-edit and pre-edit states. Self-inverse.
void ToggleUndo() {
assert(undo_);
for (auto& elem : undo_->added_files) {
const int level = elem.first.first;
const uint64_t fn = elem.first.second;
auto& other = elem.second;
auto& map = levels_[level].added_files;
auto it = map.find(fn);
std::optional<FileMetaData*> live =
it != map.end() ? std::optional<FileMetaData*>(it->second)
: std::nullopt;
if (other.has_value()) {
map[fn] = *other;
} else if (it != map.end()) {
// Note: erasing the map entry does not delete the FileMetaData; its
// lifetime is managed via `deferred_unref` / the built Version's ref.
map.erase(it);
}
other = live;
}
for (auto& elem : undo_->deleted_files) {
const int level = elem.first.first;
const uint64_t fn = elem.first.second;
bool& other_present = elem.second;
auto& set = levels_[level].deleted_files;
bool live_present = set.find(fn) != set.end();
if (other_present) {
set.insert(fn);
} else {
set.erase(fn);
}
other_present = live_present;
}
ToggleMapEntries(table_file_levels_, undo_->table_file_levels);
ToggleMapEntries(invalid_level_sizes_, undo_->invalid_level_sizes);
ToggleMapEntries(updated_compact_cursors_, undo_->compact_cursors);
ToggleMapEntries(mutable_blob_file_metas_, undo_->blob_metas);
ToggleSetMembership(found_files_, undo_->found_files);
ToggleSetMembership(l0_missing_files_, undo_->l0_missing_files);
ToggleSetMembership(non_l0_missing_files_, undo_->non_l0_missing_files);
ToggleSetMembership(missing_blob_files_, undo_->missing_blob_files);
if (!undo_->rolled_back) {
// Forward -> pre-edit: peel off the entries appended by this edit.
for (size_t i = undo_->intermediate_files_size;
i < intermediate_files_.size(); ++i) {
undo_->intermediate_tail.push_back(std::move(intermediate_files_[i]));
}
intermediate_files_.resize(undo_->intermediate_files_size);
} else {
// Pre-edit -> forward: re-append.
for (auto& s : undo_->intermediate_tail) {
intermediate_files_.push_back(std::move(s));
}
undo_->intermediate_tail.clear();
}
// Swap the scalar/flag state (the MutableScalars base subobject).
std::swap(up_cast<MutableScalars>(*this), up_cast<MutableScalars>(*undo_));
undo_->rolled_back = !undo_->rolled_back;
}
// Reverts the most recent Apply() to the pre-edit state.
void RollbackLastApply() {
assert(undo_);
assert(!undo_->rolled_back);
ToggleUndo();
}
// Re-applies the most recent Apply() after a RollbackLastApply().
void RedoLastApply() {
assert(undo_);
assert(undo_->rolled_back);
ToggleUndo();
}
// Finalizes the most recent Apply(): frees FileMetaData whose deletion was
// deferred and drops the undo record. Safe to call with no pending edit.
void CommitLastApply() {
if (!undo_) {
return;
}
if (undo_->rolled_back) {
// Always finalize in the forward (post-edit) state.
ToggleUndo();
}
for (FileMetaData* f : undo_->deferred_unref) {
UnrefFile(f);
}
undo_.reset();
}
// Mapping used for checking the consistency of links between SST files and
// blob files. It is built using the forward links (table file -> blob file),
// and is subsequently compared with the inverse mapping stored in the
@@ -536,7 +837,7 @@ class VersionBuilder::Rep {
if (epoch_number_requirement ==
EpochNumberRequirement::kMightMissing) {
if (!level_zero_cmp_by_seqno_->operator()(lhs, rhs)) {
if (!level_zero_cmp_by_seqno_(lhs, rhs)) {
std::ostringstream oss;
oss << "L0 files are not sorted properly: files #"
<< lhs->fd.GetNumber() << " with seqnos (largest, smallest) "
@@ -568,7 +869,7 @@ class VersionBuilder::Rep {
}
}
if (!level_zero_cmp_by_epochno_->operator()(lhs, rhs)) {
if (!level_zero_cmp_by_epochno_(lhs, rhs)) {
std::ostringstream oss;
oss << "L0 files are not sorted properly: files #"
<< lhs->fd.GetNumber() << " with epoch number "
@@ -597,7 +898,7 @@ class VersionBuilder::Rep {
assert(lhs);
assert(rhs);
if (!level_nonzero_cmp_->operator()(lhs, rhs)) {
if (!level_nonzero_cmp_(lhs, rhs)) {
std::ostringstream oss;
oss << 'L' << level << " files are not sorted properly: files #"
<< lhs->fd.GetNumber() << ", #" << rhs->fd.GetNumber();
@@ -895,7 +1196,12 @@ class VersionBuilder::Rep {
auto& add_files = level_state.added_files;
auto add_it = add_files.find(file_number);
if (add_it != add_files.end()) {
UnrefFile(add_it->second);
if (undo_) {
// Defer freeing so a rollback of this edit can reinstall the file.
undo_->deferred_unref.push_back(add_it->second);
} else {
UnrefFile(add_it->second);
}
add_files.erase(add_it);
}
@@ -976,7 +1282,7 @@ class VersionBuilder::Rep {
delete f;
s = Status::MemoryLimit(
"Can't allocate " +
kCacheEntryRoleToCamelString[static_cast<std::uint32_t>(
kCacheEntryRoleToCamelString[lossless_cast<std::uint32_t>(
CacheEntryRole::kFileMetadata)] +
" due to exceeding the memory limit "
"based on "
@@ -1054,6 +1360,14 @@ class VersionBuilder::Rep {
}
}
// In tracking mode, snapshot how to undo this edit before mutating, so
// VersionEditHandlerPointInTime can build a Version from the pre-edit state
// (on a valid->invalid negative edge) via RollbackLastApply() instead of
// copying the whole Rep on every edit.
if (track_found_and_missing_files_) {
CaptureUndo(edit);
}
// Note: we process the blob file related changes first because the
// table file addition/deletion logic depends on the blob files
// already being there.
@@ -1491,13 +1805,13 @@ class VersionBuilder::Rep {
}
if (epoch_number_requirement == EpochNumberRequirement::kMightMissing) {
SaveSSTFilesTo(vstorage, /* level */ 0, *level_zero_cmp_by_seqno_);
SaveSSTFilesTo(vstorage, /* level */ 0, level_zero_cmp_by_seqno_);
} else {
SaveSSTFilesTo(vstorage, /* level */ 0, *level_zero_cmp_by_epochno_);
SaveSSTFilesTo(vstorage, /* level */ 0, level_zero_cmp_by_epochno_);
}
for (int level = 1; level < num_levels_; ++level) {
SaveSSTFilesTo(vstorage, level, *level_nonzero_cmp_);
SaveSSTFilesTo(vstorage, level, level_nonzero_cmp_);
}
}
@@ -1551,13 +1865,13 @@ class VersionBuilder::Rep {
if (epoch_number_requirement == EpochNumberRequirement::kMightMissing) {
MergeUnorderdAddedFilesWithBase(
base_files, unordered_added_files, *level_zero_cmp_by_seqno_,
base_files, unordered_added_files, level_zero_cmp_by_seqno_,
[&](FileMetaData* file) {
expected_sorted_l0_files.push_back(file);
});
} else {
MergeUnorderdAddedFilesWithBase(
base_files, unordered_added_files, *level_zero_cmp_by_epochno_,
base_files, unordered_added_files, level_zero_cmp_by_epochno_,
[&](FileMetaData* file) {
expected_sorted_l0_files.push_back(file);
});
@@ -1747,11 +2061,11 @@ Status VersionBuilder::LoadTableHandlers(
max_file_size_for_l0_meta_pin, read_options);
}
void VersionBuilder::CreateOrReplaceSavePoint() {
assert(rep_);
savepoint_ = std::move(rep_);
rep_ = std::make_unique<Rep>(*savepoint_);
}
void VersionBuilder::RollbackLastApply() { rep_->RollbackLastApply(); }
void VersionBuilder::RedoLastApply() { rep_->RedoLastApply(); }
void VersionBuilder::CommitLastApply() { rep_->CommitLastApply(); }
bool VersionBuilder::ValidVersionAvailable() {
return rep_->ValidVersionAvailable();
@@ -1765,29 +2079,6 @@ std::vector<std::string>& VersionBuilder::GetAndClearIntermediateFiles() {
void VersionBuilder::ClearFoundFiles() { return rep_->ClearFoundFiles(); }
Status VersionBuilder::SaveSavePointTo(VersionStorageInfo* vstorage) const {
if (!savepoint_ || !savepoint_->ValidVersionAvailable()) {
return Status::InvalidArgument();
}
return savepoint_->SaveTo(vstorage);
}
Status VersionBuilder::LoadSavePointTableHandlers(
InternalStats* internal_stats, int max_threads,
bool prefetch_index_and_filter_in_cache, bool is_initial_load,
const MutableCFOptions& mutable_cf_options,
size_t max_file_size_for_l0_meta_pin, const ReadOptions& read_options) {
if (!savepoint_ || !savepoint_->ValidVersionAvailable()) {
return Status::InvalidArgument();
}
return savepoint_->LoadTableHandlers(
internal_stats, max_threads, prefetch_index_and_filter_in_cache,
is_initial_load, mutable_cf_options, max_file_size_for_l0_meta_pin,
read_options);
}
void VersionBuilder::ClearSavePoint() { savepoint_.reset(nullptr); }
BaseReferencedVersionBuilder::BaseReferencedVersionBuilder(
ColumnFamilyData* cfd, VersionEditHandler* version_edit_handler,
bool track_found_and_missing_files, bool allow_incomplete_valid_version)
+12 -24
View File
@@ -64,14 +64,6 @@ class VersionBuilder {
//============APIs only used by VersionEditHandlerPointInTime ============//
// Creates a save point for the Version that has been built so far. Subsequent
// VersionEdits applied to the builder will not affect the Version in this
// save point. VersionBuilder currently only supports creating one save point,
// so when `CreateOrReplaceSavePoint` is called again, the previous save point
// is cleared. `ClearSavePoint` can be called explicitly to clear
// the save point too.
void CreateOrReplaceSavePoint();
// The builder can find all the files to build a `Version`. Or if
// `allow_incomplete_valid_version_` is true and the version history is never
// edited in an atomic group, and only a suffix of L0 SST files and their
@@ -94,27 +86,23 @@ class VersionBuilder {
// Clearing all the found files in this Version.
void ClearFoundFiles();
// Save the Version in the save point to the provided `vstorage`.
// Non-OK status will be returned if there is not a valid save point.
Status SaveSavePointTo(VersionStorageInfo* vstorage) const;
// Load all the table handlers for the Version in the save point.
// Non-OK status will be returned if there is not a valid save point.
Status LoadSavePointTableHandlers(InternalStats* internal_stats,
int max_threads,
bool prefetch_index_and_filter_in_cache,
bool is_initial_load,
const MutableCFOptions& mutable_cf_options,
size_t max_file_size_for_l0_meta_pin,
const ReadOptions& read_options);
void ClearSavePoint();
// Single-edit undo for building a Version reflecting the state *before* the
// most recent `Apply()`. On a valid->invalid negative edge,
// `RollbackLastApply()` reverts the builder to the pre-edit state (so
// `SaveTo`/`LoadTableHandlers` build the pre-edit Version), then
// `RedoLastApply()` restores the post-edit state to continue replay.
// `CommitLastApply()` finalizes the most recent `Apply()` (freeing files
// whose deletion was deferred) and must be called once per `Apply()`; it is a
// no-op if there is nothing pending. This replaces the previous per-edit
// whole-Rep save-point copy, which was quadratic over a MANIFEST.
void RollbackLastApply();
void RedoLastApply();
void CommitLastApply();
//======= End of APIs only used by VersionEditPointInTime==========//
private:
class Rep;
std::unique_ptr<Rep> savepoint_;
std::unique_ptr<Rep> rep_;
};
+123
View File
@@ -3,19 +3,25 @@
// COPYING file in the root directory) and Apache 2.0 License
// (found in the LICENSE.Apache file in the root directory).
#include "db/version_builder.h"
#include <cstring>
#include <iomanip>
#include <memory>
#include <sstream>
#include <string>
#include "db/db_test_util.h"
#include "db/version_edit.h"
#include "db/version_set.h"
#include "rocksdb/advanced_options.h"
#include "rocksdb/sst_file_writer.h"
#include "table/unique_id_impl.h"
#include "test_util/testharness.h"
#include "test_util/testutil.h"
#include "util/defer.h"
#include "util/string_util.h"
#include "utilities/fault_injection_fs.h"
namespace ROCKSDB_NAMESPACE {
@@ -153,6 +159,123 @@ class VersionBuilderTest : public testing::Test {
void UpdateVersionStorageInfo() { UpdateVersionStorageInfo(&vstorage_); }
};
class VersionBuilderDBTest : public DBTestBase {
public:
VersionBuilderDBTest()
: DBTestBase("version_builder_db_test", /*env_do_fsync=*/false) {}
};
// Regression test: when LogAndApply fails after VersionBuilder pinned a
// table reader for a newly-added file (via LoadTableHandlers), the table
// cache entry must be evicted by VersionBuilder::UnrefFile when the
// FileMetaData is destroyed.
//
// This test faithfully reproduces the crash-test failure mode
// ("File N is not live nor quarantined" from TEST_VerifyNoObsoleteFilesCached
// inside Close on ASAN):
//
// 1. IngestExternalFile submits an AddFile edit through LogAndApply.
// 2. LoadTableHandlers inside ProcessManifestWrites pins a table reader
// for the new file, creating a table cache entry.
// 3. SyncManifest fails (fault injected). The error handler adds the
// ingested file to its quarantine list.
// 4. A subsequent successful LogAndApply (via manual Resume() and then
// Flush) clears the quarantine (version_set.cc:
// `ClearFilesToQuarantine`).
// 5. Metadata read fault injection prevents FindObsoleteFiles from
// scanning the directory, so the on-disk orphan is not discovered.
// 6. On DB Close, TEST_VerifyNoObsoleteFilesCached iterates the table
// cache and finds an entry for a file that is neither live nor
// quarantined -> assertion fires (without the fix).
//
// The IngestExternalFile path issues a few no-op manifest writes before
// the actual AddFile edit; we arm the injection only after
// LoadTableHandlers has actually pinned a reader for the ingested file.
TEST_F(VersionBuilderDBTest,
IngestExternalFileEvictsTableCacheOnLogAndApplyFailure) {
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.disable_auto_compactions = true;
// Disable auto-recovery so recovery runs synchronously via our explicit
// Resume() call -- no background thread to race with test teardown.
options.max_bgerror_resume_count = 0;
options.paranoid_file_checks = true;
DestroyAndReopen(options);
// RAII: ensure the DB is closed before fault_env goes out of scope,
// even if an ASSERT_* below early-returns.
Defer closer([this]() { Close(); });
// Seed the memtable with data so a subsequent Flush actually produces a
// successful LogAndApply (and thus clears the quarantine).
ASSERT_OK(Put("k1", "v1"));
// Build an external SST through the public SstFileWriter API.
const std::string ext_file = dbname_ + "/ingest_me.sst";
SstFileWriter writer(EnvOptions(), options);
ASSERT_OK(writer.Open(ext_file));
ASSERT_OK(writer.Put("k2", "v2"));
ExternalSstFileInfo file_info;
ASSERT_OK(writer.Finish(&file_info));
// Arm the injection only once we have observed VersionBuilder actually
// pin a table reader for the ingested file (i.e. after FindTable runs).
std::atomic<bool> injection_armed{false};
std::atomic<bool> injected_once{false};
SyncPoint::GetInstance()->SetCallBack("TableCache::FindTable:0",
[&](void*) { injection_armed = true; });
SyncPoint::GetInstance()->SetCallBack(
"VersionSet::ProcessManifestWrites:AfterSyncManifest", [&](void* arg) {
if (!injection_armed.load()) {
return;
}
if (injected_once.exchange(true)) {
return;
}
auto* ptr = static_cast<IOStatus*>(arg);
assert(ptr);
*ptr = IOStatus::IOError("injected");
// Retryable IOError -> soft error, recoverable via Resume().
ptr->SetRetryable(true);
});
SyncPoint::GetInstance()->EnableProcessing();
IngestExternalFileOptions ifo;
Status s = db_->IngestExternalFile({ext_file}, ifo);
ASSERT_NOK(s);
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
// Manual synchronous recovery: clear the soft bg error, then do a
// successful LogAndApply (via Flush) which triggers
// ClearFilesToQuarantine (version_set.cc line ~6591). This puts the
// ingested file into the "not live nor quarantined" state.
ASSERT_OK(dbfull()->Resume());
ASSERT_OK(Flush());
// Prevent the FindObsoleteFiles full-directory scan from finding the
// orphan on disk. Mirrors the crash test's open_metadata_read_fault_one_in.
fault_fs->SetThreadLocalErrorContext(
FaultInjectionIOType::kMetadataRead, /*seed=*/0, /*one_in=*/1,
/*retryable=*/false, /*has_data_loss=*/false);
fault_fs->EnableThreadLocalErrorInjection(
FaultInjectionIOType::kMetadataRead);
Defer disable_fault([&] {
fault_fs->DisableThreadLocalErrorInjection(
FaultInjectionIOType::kMetadataRead);
});
// The RAII Defer above will run Close() on scope exit. Close's
// TEST_VerifyNoObsoleteFilesCached (active on ASAN) iterates the table
// cache. Without the D99759696 fix, an entry for the ingested file
// remains -- a file that is neither live nor quarantined -- and the
// assertion fires. With the fix, VersionBuilder::UnrefFile evicts the
// entry when the discarded FileMetaData is destroyed.
}
void UnrefFilesInVersion(VersionStorageInfo* new_vstorage) {
for (int i = 0; i < new_vstorage->num_levels(); i++) {
for (auto* f : new_vstorage->LevelFiles(i)) {
+5 -1
View File
@@ -811,7 +811,8 @@ class VersionEdit {
const uint64_t compensated_range_deletion_size,
uint64_t tail_size, bool user_defined_timestamps_persisted,
const std::string& min_timestamp = "",
const std::string& max_timestamp = "") {
const std::string& max_timestamp = "",
const std::string& file_open_metadata = "") {
assert(smallest_seqno <= largest_seqno);
new_files_.emplace_back(
level,
@@ -822,6 +823,9 @@ class VersionEdit {
epoch_number, file_checksum, file_checksum_func_name, unique_id,
compensated_range_deletion_size, tail_size,
user_defined_timestamps_persisted, min_timestamp, max_timestamp));
if (!file_open_metadata.empty()) {
new_files_.back().second.file_open_metadata = file_open_metadata;
}
files_to_quarantine_.push_back(file);
if (!HasLastSequence() || largest_seqno > GetLastSequence()) {
SetLastSequence(largest_seqno);
+23 -5
View File
@@ -35,13 +35,13 @@ void VersionEditHandlerBase::Iterate(log::Reader& reader,
VersionEdit edit;
s = edit.DecodeFrom(record);
if (s.ok()) {
last_valid_record_end_ = reader.LastRecordEnd();
s = read_buffer_.AddEdit(&edit);
}
if (s.ok()) {
ColumnFamilyData* cfd = nullptr;
if (edit.IsInAtomicGroup()) {
if (read_buffer_.IsFull()) {
last_valid_record_end_ = reader.LastRecordEnd();
s = OnAtomicGroupReplayBegin();
for (size_t i = 0; s.ok() && i < read_buffer_.replay_buffer().size();
i++) {
@@ -57,6 +57,7 @@ void VersionEditHandlerBase::Iterate(log::Reader& reader,
}
}
} else {
last_valid_record_end_ = reader.LastRecordEnd();
s = ApplyVersionEdit(edit, &cfd);
if (s.ok()) {
recovered_edits++;
@@ -872,7 +873,6 @@ Status VersionEditHandlerPointInTime::MaybeCreateVersionBeforeApplyEdit(
assert(builder_iter != builders_.end());
VersionBuilder* builder = builder_iter->second->version_builder();
const bool valid_pit_before_edit = builder->ValidVersionAvailable();
builder->CreateOrReplaceSavePoint();
s = builder->Apply(&edit);
const bool valid_pit_after_edit = builder->ValidVersionAvailable();
@@ -888,22 +888,40 @@ Status VersionEditHandlerPointInTime::MaybeCreateVersionBeforeApplyEdit(
if (s.ok() && !missing_info && !in_atomic_group_ &&
((!valid_pit_after_edit && valid_pit_before_edit) ||
(valid_pit_after_edit && force_create_version))) {
// The Version to be created reflects the state *before* this edit. On a
// negative edge (valid before, invalid after), roll the just-applied edit
// back so the builder reflects the pre-edit state, build the Version from
// it, then redo to restore the post-edit state and continue replay. In the
// force case the edit is empty, so pre-edit == post-edit and no rollback is
// needed.
const bool negative_edge = valid_pit_before_edit && !valid_pit_after_edit;
if (negative_edge) {
builder->RollbackLastApply();
}
const auto& mopts = cfd->GetLatestMutableCFOptions();
auto* version = new Version(
cfd, version_set_, version_set_->file_options_, mopts, io_tracer_,
version_set_->current_version_number_++, epoch_number_requirement_);
s = builder->LoadSavePointTableHandlers(
s = builder->LoadTableHandlers(
cfd->internal_stats(),
version_set_->db_options_->max_file_opening_threads, false, true, mopts,
MaxFileSizeForL0MetaPin(mopts), read_options_);
if (!s.ok()) {
delete version;
if (negative_edge) {
builder->RedoLastApply();
}
if (s.IsCorruption()) {
// This point in time cannot be recovered; skip it and continue.
s = Status::OK();
}
builder->CommitLastApply();
return s;
}
s = builder->SaveSavePointTo(version->storage_info());
s = builder->SaveTo(version->storage_info());
if (negative_edge) {
builder->RedoLastApply();
}
if (s.ok()) {
if (AtomicUpdateVersionsContains(cfd->GetID())) {
AtomicUpdateVersionsPut(version);
@@ -927,7 +945,7 @@ Status VersionEditHandlerPointInTime::MaybeCreateVersionBeforeApplyEdit(
}
}
builder->ClearSavePoint();
builder->CommitLastApply();
return s;
}
+4 -2
View File
@@ -54,8 +54,10 @@ class VersionEditHandlerBase {
const ReadOptions& read_options_;
// File offset at the end of the last successfully completed and
// decoded logical record.
// File offset at the end of the last fully applied logical unit.
// For non-atomic edits, this advances after each edit is decoded.
// For atomic groups, this only advances after the entire group is
// decoded, so an incomplete atomic group at the tail is excluded.
uint64_t last_valid_record_end_ = 0;
private:
Loaded 100 of 252 files, more files were not shown because too many files have changed in this diff. Show more