Compare commits

...
47 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
216 changed files with 12524 additions and 5550 deletions

No files matched your search

+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"],
+13 -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.
@@ -298,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 |
@@ -312,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
@@ -339,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
@@ -386,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
+18 -1
View File
@@ -1002,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
@@ -1023,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
@@ -1640,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
@@ -1707,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})
+30
View File
@@ -1,6 +1,36 @@
# 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.
+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());
+303 -265
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_;
@@ -1008,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");
@@ -1370,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.
@@ -1398,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));
}
@@ -1457,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 {
@@ -1478,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());
@@ -1739,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;
@@ -1773,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());
@@ -1805,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:
@@ -3758,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();
@@ -3836,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_);
@@ -3851,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_;
@@ -3859,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);
@@ -3899,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(
@@ -3985,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
@@ -4133,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);
@@ -4196,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;
@@ -4229,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) {
+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
+386 -848
View File
File diff suppressed because it is too large. Load diff
+87 -44
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();
@@ -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>
+87 -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(),
@@ -2794,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();
@@ -2949,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();
@@ -3118,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_);
@@ -3163,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,
+23 -1
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
@@ -2871,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();
@@ -2965,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
+20 -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;
+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_;
+89
View File
@@ -6268,6 +6268,95 @@ TEST_P(DBMultiScanIteratorTest, WastedBlocksTracking) {
ASSERT_GT(wasted, 0);
}
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,
public ::testing::WithParamInterface<bool> {
protected:
+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);
+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;
}
+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;
+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);
+2 -1
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++;
+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:
+28 -485
View File
@@ -45,12 +45,6 @@
#include "db/version_edit_handler.h"
#include "db/wide/wide_columns_helper.h"
#include "file/file_util.h"
#include "table/compaction_merging_iterator.h"
#if USE_COROUTINES
#include "folly/coro/BlockingWait.h"
#include "folly/coro/Collect.h"
#endif
#include "file/filename.h"
#include "file/random_access_file_reader.h"
#include "file/read_write_util.h"
@@ -63,6 +57,7 @@
#include "rocksdb/env.h"
#include "rocksdb/merge_operator.h"
#include "rocksdb/write_buffer_manager.h"
#include "table/compaction_merging_iterator.h"
#include "table/format.h"
#include "table/get_context.h"
#include "table/internal_iterator.h"
@@ -80,18 +75,10 @@
#include "util/string_util.h"
#include "util/user_comparator_wrapper.h"
// Generate the regular and coroutine versions of some methods by
// including version_set_sync_and_async.h twice
// Macros in the header will expand differently based on whether
// WITH_COROUTINES or WITHOUT_COROUTINES is defined
// clang-format off
#define WITHOUT_COROUTINES
#include "db/version_set_sync_and_async.h"
#undef WITHOUT_COROUTINES
#define WITH_COROUTINES
#include "db/version_set_sync_and_async.h"
#undef WITH_COROUTINES
// clang-format on
#if USE_COROUTINES
#include "folly/coro/BlockingWait.h"
#include "folly/coro/Collect.h"
#endif // USE_COROUTINES
namespace ROCKSDB_NAMESPACE {
@@ -315,6 +302,8 @@ Status OverlapWithIterator(const Comparator* ucmp,
return iter->status();
}
} // namespace
// Class to help choose the next file to search for the particular key.
// Searches and returns files level by level.
// We can search level-by-level since entries never hop across
@@ -527,7 +516,6 @@ class FilePicker {
return false;
}
};
} // anonymous namespace
class FilePickerMultiGet {
private:
@@ -2904,7 +2892,7 @@ void Version::MultiGetBlob(
} else {
assert(key_context->columns);
key_context->columns->SetPlainValue(std::move(blob.result));
range.AddValueSize(key_context->columns->serialized_size());
range.AddValueSize(key_context->columns->payload_size());
}
// MultiGetBlob() has already materialized the blob reference into the
// user-visible value, so clear this flag before later MultiGet
@@ -2924,470 +2912,6 @@ void Version::MultiGetBlob(
}
}
void Version::Get(const ReadOptions& read_options, const LookupKey& k,
PinnableSlice* value, PinnableWideColumns* columns,
std::string* timestamp, Status* status,
MergeContext* merge_context,
SequenceNumber* max_covering_tombstone_seq,
PinnedIteratorsManager* pinned_iters_mgr, bool* value_found,
bool* key_exists, SequenceNumber* seq, ReadCallback* callback,
bool* is_blob, bool do_merge) {
Slice ikey = k.internal_key();
Slice user_key = k.user_key();
assert(status->ok() || status->IsMergeInProgress());
if (key_exists != nullptr) {
// will falsify below if not found
*key_exists = true;
}
uint64_t tracing_get_id = BlockCacheTraceHelper::kReservedGetId;
if (vset_ && vset_->block_cache_tracer_ &&
vset_->block_cache_tracer_->is_tracing_enabled()) {
tracing_get_id = vset_->block_cache_tracer_->NextGetId();
}
// Note: the old StackableDB-based BlobDB passes in
// GetImplOptions::is_blob_index; for the integrated BlobDB implementation, we
// need to provide it here.
bool is_blob_index = false;
bool* const is_blob_to_use = is_blob ? is_blob : &is_blob_index;
BlobFetcher blob_fetcher(this, read_options);
assert(pinned_iters_mgr);
GetContext get_context(
user_comparator(), merge_operator_, info_log_, db_statistics_,
status->ok() ? GetContext::kNotFound : GetContext::kMerge, user_key,
do_merge ? value : nullptr, do_merge ? columns : nullptr,
do_merge ? timestamp : nullptr, value_found, merge_context, do_merge,
max_covering_tombstone_seq, clock_, seq,
merge_operator_ ? pinned_iters_mgr : nullptr, callback, is_blob_to_use,
tracing_get_id, &blob_fetcher);
// Pin blocks that we read to hold merge operands
if (merge_operator_) {
pinned_iters_mgr->StartPinning();
}
FilePicker fp(user_key, ikey, &storage_info_.level_files_brief_,
storage_info_.num_non_empty_levels_,
&storage_info_.file_indexer_, user_comparator(),
internal_comparator());
FdWithKeyRange* f = fp.GetNextFile();
while (f != nullptr) {
if (*max_covering_tombstone_seq > 0) {
// The remaining files we look at will only contain covered keys, so we
// stop here.
break;
}
if (get_context.sample()) {
sample_file_read_inc(f->file_metadata);
}
bool timer_enabled =
GetPerfLevel() >= PerfLevel::kEnableTimeExceptForMutex &&
get_perf_context()->per_level_perf_context_enabled;
StopWatchNano timer(clock_, timer_enabled /* auto_start */);
*status = table_cache_->Get(
read_options, *internal_comparator(), *f->file_metadata, ikey,
&get_context, mutable_cf_options_,
cfd_->internal_stats()->GetFileReadHist(fp.GetHitFileLevel()),
IsFilterSkipped(static_cast<int>(fp.GetHitFileLevel()),
fp.IsHitFileLastInLevel()),
fp.GetHitFileLevel(), max_file_size_for_l0_meta_pin_);
// TODO: examine the behavior for corrupted key
if (timer_enabled) {
PERF_COUNTER_BY_LEVEL_ADD(get_from_table_nanos, timer.ElapsedNanos(),
fp.GetHitFileLevel());
}
if (!status->ok()) {
if (db_statistics_ != nullptr) {
get_context.ReportCounters();
}
return;
}
// report the counters before returning
if (get_context.State() != GetContext::kNotFound &&
get_context.State() != GetContext::kMerge &&
db_statistics_ != nullptr) {
get_context.ReportCounters();
}
switch (get_context.State()) {
case GetContext::kNotFound:
// Keep searching in other files
if (get_context.sample()) {
sample_collapsible_entry_file_read_inc(f->file_metadata);
}
break;
case GetContext::kMerge:
// TODO: update per-level perfcontext user_key_return_count for kMerge
if (get_context.sample()) {
sample_collapsible_entry_file_read_inc(f->file_metadata);
}
break;
case GetContext::kFound:
if (fp.GetHitFileLevel() == 0) {
RecordTick(db_statistics_, GET_HIT_L0);
} else if (fp.GetHitFileLevel() == 1) {
RecordTick(db_statistics_, GET_HIT_L1);
} else if (fp.GetHitFileLevel() >= 2) {
RecordTick(db_statistics_, GET_HIT_L2_AND_UP);
}
PERF_COUNTER_BY_LEVEL_ADD(user_key_return_count, 1,
fp.GetHitFileLevel());
if (is_blob_index && do_merge && (value || columns)) {
Slice blob_index =
value ? *value
: WideColumnsHelper::GetDefaultColumn(columns->columns());
TEST_SYNC_POINT_CALLBACK("Version::Get::TamperWithBlobIndex",
&blob_index);
constexpr FilePrefetchBuffer* prefetch_buffer = nullptr;
PinnableSlice result;
constexpr uint64_t* bytes_read = nullptr;
*status = GetBlob(read_options, get_context.ukey_to_get_blob_value(),
blob_index, prefetch_buffer, &result, bytes_read);
if (!status->ok()) {
if (status->IsIncomplete()) {
get_context.MarkKeyMayExist();
}
return;
}
if (value) {
*value = std::move(result);
} else {
assert(columns);
columns->SetPlainValue(std::move(result));
}
}
return;
case GetContext::kDeleted:
// Use empty error message for speed
*status = Status::NotFound();
if (get_context.sample()) {
sample_collapsible_entry_file_read_inc(f->file_metadata);
}
return;
case GetContext::kCorrupt:
*status = Status::Corruption("corrupted key for ", user_key);
return;
case GetContext::kUnexpectedBlobIndex:
ROCKS_LOG_ERROR(info_log_, "Encounter unexpected blob index.");
*status = Status::NotSupported(
"Encounter unexpected blob index. Please open DB with "
"ROCKSDB_NAMESPACE::blob_db::BlobDB instead.");
return;
case GetContext::kMergeOperatorFailed:
*status = Status::Corruption(Status::SubCode::kMergeOperatorFailed);
return;
}
f = fp.GetNextFile();
}
if (db_statistics_ != nullptr) {
get_context.ReportCounters();
}
if (GetContext::kMerge == get_context.State()) {
if (!do_merge) {
*status = Status::OK();
return;
}
if (!merge_operator_) {
*status = Status::InvalidArgument(
"merge_operator is not properly initialized.");
return;
}
// merge_operands are in saver and we hit the beginning of the key history
// do a final merge of nullptr and operands;
if (value || columns) {
// `op_failure_scope` (an output parameter) is not provided (set to
// nullptr) since a failure must be propagated regardless of its value.
*status = MergeHelper::TimedFullMerge(
merge_operator_, user_key, MergeHelper::kNoBaseValue,
merge_context->GetOperands(), info_log_, db_statistics_, clock_,
/* update_num_ops_stats */ true, /* op_failure_scope */ nullptr,
value ? value->GetSelf() : nullptr, columns);
if (status->ok()) {
if (LIKELY(value != nullptr)) {
value->PinSelf();
}
}
}
} else {
if (key_exists != nullptr) {
*key_exists = false;
}
*status = Status::NotFound(); // Use an empty error message for speed
}
}
void Version::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
ReadCallback* callback) {
PinnedIteratorsManager pinned_iters_mgr;
// Pin blocks that we read to hold merge operands
if (merge_operator_) {
pinned_iters_mgr.StartPinning();
}
uint64_t tracing_mget_id = BlockCacheTraceHelper::kReservedGetId;
if (vset_ && vset_->block_cache_tracer_ &&
vset_->block_cache_tracer_->is_tracing_enabled()) {
tracing_mget_id = vset_->block_cache_tracer_->NextGetId();
}
// Even though we know the batch size won't be > MAX_BATCH_SIZE,
// use autovector in order to avoid unnecessary construction of GetContext
// objects, which is expensive
autovector<GetContext, 16> get_ctx;
BlobFetcher blob_fetcher(this, read_options);
for (auto iter = range->begin(); iter != range->end(); ++iter) {
assert(iter->s->ok() || iter->s->IsMergeInProgress());
get_ctx.emplace_back(
user_comparator(), merge_operator_, info_log_, db_statistics_,
iter->s->ok() ? GetContext::kNotFound : GetContext::kMerge,
iter->ukey_with_ts, iter->value, iter->columns, iter->timestamp,
nullptr, &(iter->merge_context), true,
&iter->max_covering_tombstone_seq, clock_, nullptr,
merge_operator_ ? &pinned_iters_mgr : nullptr, callback,
&iter->is_blob_index, tracing_mget_id, &blob_fetcher);
// MergeInProgress status, if set, has been transferred to the get_context
// state, so we set status to ok here. From now on, the iter status will
// be used for IO errors, and get_context state will be used for any
// key level errors
*(iter->s) = Status::OK();
}
int get_ctx_index = 0;
for (auto iter = range->begin(); iter != range->end();
++iter, get_ctx_index++) {
iter->get_context = &(get_ctx[get_ctx_index]);
}
Status s;
// blob_file => [[blob_idx, it], ...]
std::unordered_map<uint64_t, BlobReadContexts> blob_ctxs;
MultiGetRange keys_with_blobs_range(*range, range->begin(), range->end());
#if USE_COROUTINES
if (read_options.async_io && read_options.optimize_multiget_for_io &&
using_coroutines() && use_async_io_) {
s = MultiGetAsync(read_options, range, &blob_ctxs);
} else
#endif // USE_COROUTINES
{
MultiGetRange file_picker_range(*range, range->begin(), range->end());
FilePickerMultiGet fp(&file_picker_range, &storage_info_.level_files_brief_,
storage_info_.num_non_empty_levels_,
&storage_info_.file_indexer_, user_comparator(),
internal_comparator());
FdWithKeyRange* f = fp.GetNextFileInLevel();
uint64_t num_index_read = 0;
uint64_t num_filter_read = 0;
uint64_t num_sst_read = 0;
uint64_t num_level_read = 0;
int prev_level = -1;
while (!fp.IsSearchEnded()) {
// This will be set to true later if we actually look up in a file in L0.
// For per level stats purposes, an L0 file is treated as a level
bool dump_stats_for_l0_file = false;
// Avoid using the coroutine version if we're looking in a L0 file, since
// L0 files won't be parallelized anyway. The regular synchronous version
// is faster.
if (!read_options.async_io || !using_coroutines() || !use_async_io_ ||
fp.GetHitFileLevel() == 0 || !fp.RemainingOverlapInLevel()) {
if (f) {
bool skip_filters =
IsFilterSkipped(static_cast<int>(fp.GetHitFileLevel()),
fp.IsHitFileLastInLevel());
// Call MultiGetFromSST for looking up a single file
s = MultiGetFromSST(read_options, fp.CurrentFileRange(),
fp.GetHitFileLevel(), skip_filters,
/*skip_range_deletions=*/false, f, blob_ctxs,
/*table_handle=*/nullptr, num_filter_read,
num_index_read, num_sst_read);
if (fp.GetHitFileLevel() == 0) {
dump_stats_for_l0_file = true;
}
}
if (s.ok()) {
f = fp.GetNextFileInLevel();
}
#if USE_COROUTINES
} else {
std::vector<folly::coro::Task<Status>> mget_tasks;
while (f != nullptr) {
MultiGetRange file_range = fp.CurrentFileRange();
TableCache::TypedHandle* table_handle = nullptr;
bool skip_filters =
IsFilterSkipped(static_cast<int>(fp.GetHitFileLevel()),
fp.IsHitFileLastInLevel());
bool skip_range_deletions = false;
if (!skip_filters) {
Status status = table_cache_->MultiGetFilter(
read_options, *internal_comparator(), *f->file_metadata,
mutable_cf_options_,
cfd_->internal_stats()->GetFileReadHist(fp.GetHitFileLevel()),
fp.GetHitFileLevel(), &file_range, &table_handle);
skip_range_deletions = true;
if (status.ok()) {
skip_filters = true;
} else if (!status.IsNotSupported()) {
s = status;
}
}
if (!s.ok()) {
break;
}
if (!file_range.empty()) {
mget_tasks.emplace_back(MultiGetFromSSTCoroutine(
read_options, file_range, fp.GetHitFileLevel(), skip_filters,
skip_range_deletions, f, blob_ctxs, table_handle,
num_filter_read, num_index_read, num_sst_read));
}
if (fp.KeyMaySpanNextFile()) {
break;
}
f = fp.GetNextFileInLevel();
}
if (mget_tasks.size() > 0) {
RecordTick(db_statistics_, MULTIGET_COROUTINE_COUNT,
mget_tasks.size());
// Collect all results so far
std::vector<Status> statuses =
folly::coro::blockingWait(co_withExecutor(
&range->context()->executor(),
folly::coro::collectAllRange(std::move(mget_tasks))));
if (s.ok()) {
for (Status stat : statuses) {
if (!stat.ok()) {
s = std::move(stat);
break;
}
}
}
if (s.ok() && fp.KeyMaySpanNextFile()) {
f = fp.GetNextFileInLevel();
}
}
#endif // USE_COROUTINES
}
// If bad status or we found final result for all the keys
if (!s.ok() || file_picker_range.empty()) {
break;
}
if (!f) {
// Reached the end of this level. Prepare the next level
fp.PrepareNextLevelForSearch();
if (!fp.IsSearchEnded()) {
// Its possible there is no overlap on this level and f is nullptr
f = fp.GetNextFileInLevel();
}
if (dump_stats_for_l0_file ||
(prev_level != 0 && prev_level != (int)fp.GetHitFileLevel())) {
// Dump the stats if the search has moved to the next level and
// reset for next level.
if (num_filter_read + num_index_read) {
RecordInHistogram(db_statistics_,
NUM_INDEX_AND_FILTER_BLOCKS_READ_PER_LEVEL,
num_index_read + num_filter_read);
}
if (num_sst_read) {
RecordInHistogram(db_statistics_, NUM_SST_READ_PER_LEVEL,
num_sst_read);
num_level_read++;
}
num_filter_read = 0;
num_index_read = 0;
num_sst_read = 0;
}
prev_level = fp.GetHitFileLevel();
}
}
// Dump stats for most recent level
if (num_filter_read + num_index_read) {
RecordInHistogram(db_statistics_,
NUM_INDEX_AND_FILTER_BLOCKS_READ_PER_LEVEL,
num_index_read + num_filter_read);
}
if (num_sst_read) {
RecordInHistogram(db_statistics_, NUM_SST_READ_PER_LEVEL, num_sst_read);
num_level_read++;
}
if (num_level_read) {
RecordInHistogram(db_statistics_, NUM_LEVEL_READ_PER_MULTIGET,
num_level_read);
}
}
if (!blob_ctxs.empty()) {
MultiGetBlob(read_options, keys_with_blobs_range, blob_ctxs);
}
// Process any left over keys
for (auto iter = range->begin(); s.ok() && iter != range->end(); ++iter) {
GetContext& get_context = *iter->get_context;
Status* status = iter->s;
Slice user_key = iter->lkey->user_key();
if (db_statistics_ != nullptr) {
get_context.ReportCounters();
}
if (GetContext::kMerge == get_context.State()) {
if (!merge_operator_) {
*status = Status::InvalidArgument(
"merge_operator is not properly initialized.");
range->MarkKeyDone(iter);
continue;
}
// merge_operands are in saver and we hit the beginning of the key history
// do a final merge of nullptr and operands;
// `op_failure_scope` (an output parameter) is not provided (set to
// nullptr) since a failure must be propagated regardless of its value.
*status = MergeHelper::TimedFullMerge(
merge_operator_, user_key, MergeHelper::kNoBaseValue,
iter->merge_context.GetOperands(), info_log_, db_statistics_, clock_,
/* update_num_ops_stats */ true, /* op_failure_scope */ nullptr,
iter->value ? iter->value->GetSelf() : nullptr, iter->columns);
if (LIKELY(iter->value != nullptr)) {
iter->value->PinSelf();
range->AddValueSize(iter->value->size());
} else {
assert(iter->columns);
range->AddValueSize(iter->columns->serialized_size());
}
range->MarkKeyDone(iter);
if (range->GetValueSize() > read_options.value_size_soft_limit) {
s = Status::Aborted();
break;
}
} else {
range->MarkKeyDone(iter);
*status = Status::NotFound(); // Use an empty error message for speed
}
}
for (auto iter = range->begin(); iter != range->end(); ++iter) {
range->MarkKeyDone(iter);
*(iter->s) = s;
}
}
#ifdef USE_COROUTINES
Status Version::ProcessBatch(
const ReadOptions& read_options, FilePickerMultiGet* batch,
@@ -7677,6 +7201,11 @@ Status VersionSet::WriteCurrentStateToManifest(
for (const auto& f : level_files) {
assert(f);
// Preserve file_open_metadata so live SSTs still benefit from
// fast_sst_open after a MANIFEST rotation. Without this, the
// rotated MANIFEST snapshot drops kFileOpenMetadata for unchanged
// files and their fast-open advantage is lost until they are
// rewritten.
edit.AddFile(level, f->fd.GetNumber(), f->fd.GetPathId(),
f->fd.GetFileSize(), f->smallest, f->largest,
f->fd.smallest_seqno, f->fd.largest_seqno,
@@ -7686,7 +7215,7 @@ Status VersionSet::WriteCurrentStateToManifest(
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);
}
}
@@ -8586,3 +8115,17 @@ std::vector<VersionEdit>& ReactiveVersionSet::replay_buffer() {
}
} // namespace ROCKSDB_NAMESPACE
// Generate the regular and coroutine versions of the functions defined in
// version_set_sync_and_async.h by including it twice. Included here, after the
// namespace and all definitions in this file, so the generated bodies see the
// file-local types and helpers they use. The macros expand differently based on
// whether WITH_COROUTINES or WITHOUT_COROUTINES is defined.
// clang-format off
#define WITHOUT_COROUTINES
#include "db/version_set_sync_and_async.h"
#undef WITHOUT_COROUTINES
#define WITH_COROUTINES
#include "db/version_set_sync_and_async.h"
#undef WITH_COROUTINES
// clang-format on
+11 -14
View File
@@ -48,10 +48,6 @@
#include "db/version_edit.h"
#include "db/write_controller.h"
#include "env/file_system_tracer.h"
#if USE_COROUTINES
#include "folly/coro/BlockingWait.h"
#include "folly/coro/Collect.h"
#endif
#include "monitoring/instrumented_mutex.h"
#include "options/db_options.h"
#include "options/offpeak_time_info.h"
@@ -966,17 +962,18 @@ class Version {
// merge_context.operands_list and don't merge the operands
// REQUIRES: lock is not held
// REQUIRES: pinned_iters_mgr != nullptr
void Get(const ReadOptions&, const LookupKey& key, PinnableSlice* value,
PinnableWideColumns* columns, std::string* timestamp, Status* status,
MergeContext* merge_context,
SequenceNumber* max_covering_tombstone_seq,
PinnedIteratorsManager* pinned_iters_mgr,
bool* value_found = nullptr, bool* key_exists = nullptr,
SequenceNumber* seq = nullptr, ReadCallback* callback = nullptr,
bool* is_blob = nullptr, bool do_merge = true);
DECLARE_SYNC_AND_ASYNC(
void, Get, const ReadOptions&, const LookupKey& key, PinnableSlice* value,
PinnableWideColumns* columns, std::string* timestamp, Status* status,
MergeContext* merge_context, SequenceNumber* max_covering_tombstone_seq,
PinnedIteratorsManager* pinned_iters_mgr, bool* value_found = nullptr,
bool* key_exists = nullptr, SequenceNumber* seq = nullptr,
ReadCallback* callback = nullptr, bool* is_blob = nullptr,
bool do_merge = true);
void MultiGet(const ReadOptions&, MultiGetRange* range,
ReadCallback* callback = nullptr);
DECLARE_SYNC_AND_ASYNC(void, MultiGet, const ReadOptions&,
MultiGetRange* range,
ReadCallback* callback = nullptr);
// Interprets blob_index_slice as a blob reference, and (assuming the
// corresponding blob file is part of this Version) retrieves the blob and
+497 -8
View File
@@ -11,6 +11,216 @@
namespace ROCKSDB_NAMESPACE {
DEFINE_SYNC_AND_ASYNC(void, Version::Get)
(const ReadOptions& read_options, const LookupKey& k, PinnableSlice* value,
PinnableWideColumns* columns, std::string* timestamp, Status* status,
MergeContext* merge_context, SequenceNumber* max_covering_tombstone_seq,
PinnedIteratorsManager* pinned_iters_mgr, bool* value_found, bool* key_exists,
SequenceNumber* seq, ReadCallback* callback, bool* is_blob, bool do_merge) {
Slice ikey = k.internal_key();
Slice user_key = k.user_key();
assert(status->ok() || status->IsMergeInProgress());
if (key_exists != nullptr) {
// will falsify below if not found
*key_exists = true;
}
uint64_t tracing_get_id = BlockCacheTraceHelper::kReservedGetId;
if (vset_ && vset_->block_cache_tracer_ &&
vset_->block_cache_tracer_->is_tracing_enabled()) {
tracing_get_id = vset_->block_cache_tracer_->NextGetId();
}
// Note: the old StackableDB-based BlobDB passes in
// GetImplOptions::is_blob_index; for the integrated BlobDB implementation, we
// need to provide it here.
bool is_blob_index = false;
bool* const is_blob_to_use = is_blob ? is_blob : &is_blob_index;
VersionBlobFetcher blob_fetcher(this, read_options);
assert(pinned_iters_mgr);
GetContext get_context(
user_comparator(), merge_operator_, info_log_, db_statistics_,
status->ok() ? GetContext::kNotFound : GetContext::kMerge, user_key,
do_merge ? value : nullptr, do_merge ? columns : nullptr,
do_merge ? timestamp : nullptr, value_found, merge_context, do_merge,
max_covering_tombstone_seq, clock_, seq,
merge_operator_ ? pinned_iters_mgr : nullptr, callback, is_blob_to_use,
tracing_get_id, &blob_fetcher);
// Pin blocks that we read to hold merge operands
if (merge_operator_) {
pinned_iters_mgr->StartPinning();
}
FilePicker fp(user_key, ikey, &storage_info_.level_files_brief_,
storage_info_.num_non_empty_levels_,
&storage_info_.file_indexer_, user_comparator(),
internal_comparator());
FdWithKeyRange* f = fp.GetNextFile();
while (f != nullptr) {
if (*max_covering_tombstone_seq > 0) {
// The remaining files we look at will only contain covered keys, so we
// stop here.
break;
}
if (get_context.sample()) {
sample_file_read_inc(f->file_metadata);
}
#if defined(WITHOUT_COROUTINES)
bool timer_enabled =
GetPerfLevel() >= PerfLevel::kEnableTimeExceptForMutex &&
get_perf_context()->per_level_perf_context_enabled;
StopWatchNano timer(clock_, timer_enabled /* auto_start */);
#endif
*status =
CO_AWAIT(table_cache_->Get, read_options, *internal_comparator(),
*f->file_metadata, ikey, &get_context, mutable_cf_options_,
cfd_->internal_stats()->GetFileReadHist(fp.GetHitFileLevel()),
IsFilterSkipped(static_cast<int>(fp.GetHitFileLevel()),
fp.IsHitFileLastInLevel()),
fp.GetHitFileLevel(), max_file_size_for_l0_meta_pin_);
// TODO: examine the behavior for corrupted key
#if defined(WITHOUT_COROUTINES)
if (timer_enabled) {
PERF_COUNTER_BY_LEVEL_ADD(get_from_table_nanos, timer.ElapsedNanos(),
fp.GetHitFileLevel());
}
#endif
if (!status->ok()) {
if (db_statistics_ != nullptr) {
get_context.ReportCounters();
}
CO_RETURN;
}
// report the counters before returning
if (get_context.State() != GetContext::kNotFound &&
get_context.State() != GetContext::kMerge &&
db_statistics_ != nullptr) {
get_context.ReportCounters();
}
switch (get_context.State()) {
case GetContext::kNotFound:
// Keep searching in other files
if (get_context.sample()) {
sample_collapsible_entry_file_read_inc(f->file_metadata);
}
break;
case GetContext::kMerge:
// TODO: update per-level perfcontext user_key_return_count for kMerge
if (get_context.sample()) {
sample_collapsible_entry_file_read_inc(f->file_metadata);
}
break;
case GetContext::kFound:
if (fp.GetHitFileLevel() == 0) {
RecordTick(db_statistics_, GET_HIT_L0);
} else if (fp.GetHitFileLevel() == 1) {
RecordTick(db_statistics_, GET_HIT_L1);
} else if (fp.GetHitFileLevel() >= 2) {
RecordTick(db_statistics_, GET_HIT_L2_AND_UP);
}
PERF_COUNTER_BY_LEVEL_ADD(user_key_return_count, 1,
fp.GetHitFileLevel());
if (is_blob_index && do_merge && (value || columns)) {
Slice blob_index =
value ? *value
: WideColumnsHelper::GetDefaultColumn(columns->columns());
TEST_SYNC_POINT_CALLBACK("Version::Get::TamperWithBlobIndex",
&blob_index);
constexpr FilePrefetchBuffer* prefetch_buffer = nullptr;
PinnableSlice result;
constexpr uint64_t* bytes_read = nullptr;
*status = GetBlob(read_options, get_context.ukey_to_get_blob_value(),
blob_index, prefetch_buffer, &result, bytes_read);
if (!status->ok()) {
if (status->IsIncomplete()) {
get_context.MarkKeyMayExist();
}
CO_RETURN;
}
if (value) {
*value = std::move(result);
} else {
assert(columns);
columns->SetPlainValue(std::move(result));
}
}
CO_RETURN;
case GetContext::kDeleted:
// Use empty error message for speed
*status = Status::NotFound();
if (get_context.sample()) {
sample_collapsible_entry_file_read_inc(f->file_metadata);
}
CO_RETURN;
case GetContext::kCorrupt:
*status = Status::Corruption("corrupted key for ", user_key);
CO_RETURN;
case GetContext::kUnexpectedBlobIndex:
ROCKS_LOG_ERROR(info_log_, "Encounter unexpected blob index.");
*status = Status::NotSupported(
"Encounter unexpected blob index. Please open DB with "
"ROCKSDB_NAMESPACE::blob_db::BlobDB instead.");
CO_RETURN;
case GetContext::kMergeOperatorFailed:
*status = Status::Corruption(Status::SubCode::kMergeOperatorFailed);
CO_RETURN;
}
f = fp.GetNextFile();
}
if (db_statistics_ != nullptr) {
get_context.ReportCounters();
}
if (GetContext::kMerge == get_context.State()) {
if (!do_merge) {
*status = Status::OK();
CO_RETURN;
}
if (!merge_operator_) {
*status = Status::InvalidArgument(
"merge_operator is not properly initialized.");
CO_RETURN;
}
// merge_operands are in saver and we hit the beginning of the key history
// do a final merge of nullptr and operands;
if (value || columns) {
// `op_failure_scope` (an output parameter) is not provided (set to
// nullptr) since a failure must be propagated regardless of its value.
*status = MergeHelper::TimedFullMerge(
merge_operator_, user_key, MergeHelper::kNoBaseValue,
merge_context->GetOperands(), info_log_, db_statistics_, clock_,
/* update_num_ops_stats */ true, /* op_failure_scope */ nullptr,
value ? value->GetSelf() : nullptr, columns);
if (status->ok()) {
if (LIKELY(value != nullptr)) {
value->PinSelf();
}
}
}
} else {
if (key_exists != nullptr) {
*key_exists = false;
}
*status = Status::NotFound(); // Use an empty error message for speed
}
CO_RETURN;
}
// Lookup a batch of keys in a single SST file
DEFINE_SYNC_AND_ASYNC(Status, Version::MultiGetFromSST)
(const ReadOptions& read_options, MultiGetRange file_range, int hit_file_level,
@@ -18,21 +228,24 @@ DEFINE_SYNC_AND_ASYNC(Status, Version::MultiGetFromSST)
std::unordered_map<uint64_t, BlobReadContexts>& blob_ctxs,
TableCache::TypedHandle* table_handle, uint64_t& num_filter_read,
uint64_t& num_index_read, uint64_t& num_sst_read) {
Status s;
#if defined(WITHOUT_COROUTINES)
bool timer_enabled = GetPerfLevel() >= PerfLevel::kEnableTimeExceptForMutex &&
get_perf_context()->per_level_perf_context_enabled;
Status s;
StopWatchNano timer(clock_, timer_enabled /* auto_start */);
s = CO_AWAIT(table_cache_->MultiGet)(
read_options, *internal_comparator(), *f->file_metadata, &file_range,
mutable_cf_options_,
cfd_->internal_stats()->GetFileReadHist(hit_file_level), skip_filters,
skip_range_deletions, hit_file_level, table_handle);
#endif
s = CO_AWAIT(table_cache_->MultiGet, read_options, *internal_comparator(),
*f->file_metadata, &file_range, mutable_cf_options_,
cfd_->internal_stats()->GetFileReadHist(hit_file_level),
skip_filters, skip_range_deletions, hit_file_level,
table_handle);
// TODO: examine the behavior for corrupted key
#if defined(WITHOUT_COROUTINES)
if (timer_enabled) {
PERF_COUNTER_BY_LEVEL_ADD(get_from_table_nanos, timer.ElapsedNanos(),
hit_file_level);
}
#endif
if (!s.ok()) {
// TODO: Set status for individual keys appropriately
for (auto iter = file_range.begin(); iter != file_range.end(); ++iter) {
@@ -133,7 +346,7 @@ DEFINE_SYNC_AND_ASYNC(Status, Version::MultiGetFromSST)
file_range.AddValueSize(iter->value->size());
} else {
assert(iter->columns);
file_range.AddValueSize(iter->columns->serialized_size());
file_range.AddValueSize(iter->columns->payload_size());
}
if (file_range.GetValueSize() > read_options.value_size_soft_limit) {
@@ -169,5 +382,281 @@ DEFINE_SYNC_AND_ASYNC(Status, Version::MultiGetFromSST)
RecordInHistogram(db_statistics_, SST_BATCH_SIZE, batch_size);
CO_RETURN s;
}
DEFINE_SYNC_AND_ASYNC(void, Version::MultiGet)
(const ReadOptions& read_options, MultiGetRange* range,
ReadCallback* callback) {
PinnedIteratorsManager pinned_iters_mgr;
// Pin blocks that we read to hold merge operands
if (merge_operator_) {
pinned_iters_mgr.StartPinning();
}
uint64_t tracing_mget_id = BlockCacheTraceHelper::kReservedGetId;
if (vset_ && vset_->block_cache_tracer_ &&
vset_->block_cache_tracer_->is_tracing_enabled()) {
tracing_mget_id = vset_->block_cache_tracer_->NextGetId();
}
// Even though we know the batch size won't be > MAX_BATCH_SIZE,
// use autovector in order to avoid unnecessary construction of GetContext
// objects, which is expensive
autovector<GetContext, 16> get_ctx;
VersionBlobFetcher blob_fetcher(this, read_options);
for (auto iter = range->begin(); iter != range->end(); ++iter) {
assert(iter->s->ok() || iter->s->IsMergeInProgress());
get_ctx.emplace_back(
user_comparator(), merge_operator_, info_log_, db_statistics_,
iter->s->ok() ? GetContext::kNotFound : GetContext::kMerge,
iter->ukey_with_ts, iter->value, iter->columns, iter->timestamp,
nullptr, &(iter->merge_context), true,
&iter->max_covering_tombstone_seq, clock_, nullptr,
merge_operator_ ? &pinned_iters_mgr : nullptr, callback,
&iter->is_blob_index, tracing_mget_id, &blob_fetcher);
// MergeInProgress status, if set, has been transferred to the get_context
// state, so we set status to ok here. From now on, the iter status will
// be used for IO errors, and get_context state will be used for any
// key level errors
*(iter->s) = Status::OK();
}
int get_ctx_index = 0;
for (auto iter = range->begin(); iter != range->end();
++iter, get_ctx_index++) {
iter->get_context = &(get_ctx[get_ctx_index]);
}
Status s;
// blob_file => [[blob_idx, it], ...]
std::unordered_map<uint64_t, BlobReadContexts> blob_ctxs;
MultiGetRange keys_with_blobs_range(*range, range->begin(), range->end());
#if defined(WITHOUT_COROUTINES) && defined(USE_COROUTINES)
// optimize_multiget_for_io overlaps reads across levels via blockingWait; it
// is only used by the synchronous variant. The coroutine variant overlaps
// within a level via co_await below.
if (read_options.async_io && read_options.optimize_multiget_for_io &&
using_coroutines() && use_async_io_) {
s = MultiGetAsync(read_options, range, &blob_ctxs);
} else
#endif // WITHOUT_COROUTINES && USE_COROUTINES
{
MultiGetRange file_picker_range(*range, range->begin(), range->end());
FilePickerMultiGet fp(&file_picker_range, &storage_info_.level_files_brief_,
storage_info_.num_non_empty_levels_,
&storage_info_.file_indexer_, user_comparator(),
internal_comparator());
FdWithKeyRange* f = fp.GetNextFileInLevel();
uint64_t num_index_read = 0;
uint64_t num_filter_read = 0;
uint64_t num_sst_read = 0;
uint64_t num_level_read = 0;
int prev_level = -1;
while (!fp.IsSearchEnded()) {
// This will be set to true later if we actually look up in a file in L0.
// For per level stats purposes, an L0 file is treated as a level
bool dump_stats_for_l0_file = false;
// Avoid using the coroutine version if we're looking in a L0 file, since
// L0 files won't be parallelized anyway. The regular synchronous version
// is faster.
#ifdef WITH_COROUTINES
if (fp.GetHitFileLevel() == 0 || !fp.RemainingOverlapInLevel()) {
#else
if (!read_options.async_io || !using_coroutines() || !use_async_io_ ||
fp.GetHitFileLevel() == 0 || !fp.RemainingOverlapInLevel()) {
#endif
if (f) {
bool skip_filters =
IsFilterSkipped(static_cast<int>(fp.GetHitFileLevel()),
fp.IsHitFileLastInLevel());
s = CO_AWAIT(MultiGetFromSST, read_options, fp.CurrentFileRange(),
fp.GetHitFileLevel(), skip_filters,
/*skip_range_deletions=*/false, f, blob_ctxs,
/*table_handle=*/nullptr, num_filter_read,
num_index_read, num_sst_read);
#ifdef WITH_COROUTINES
// Count this per-file coroutine read. The overlap branch below
// records the same ticker for its batch of parallel reads.
RecordTick(db_statistics_, MULTIGET_COROUTINE_COUNT);
#endif
if (fp.GetHitFileLevel() == 0) {
dump_stats_for_l0_file = true;
}
}
if (s.ok()) {
f = fp.GetNextFileInLevel();
}
#if USE_COROUTINES
} else {
std::vector<folly::coro::Task<Status>> mget_tasks;
while (f != nullptr) {
MultiGetRange file_range = fp.CurrentFileRange();
TableCache::TypedHandle* table_handle = nullptr;
bool skip_filters =
IsFilterSkipped(static_cast<int>(fp.GetHitFileLevel()),
fp.IsHitFileLastInLevel());
bool skip_range_deletions = false;
if (!skip_filters) {
Status status = table_cache_->MultiGetFilter(
read_options, *internal_comparator(), *f->file_metadata,
mutable_cf_options_,
cfd_->internal_stats()->GetFileReadHist(fp.GetHitFileLevel()),
fp.GetHitFileLevel(), &file_range, &table_handle);
skip_range_deletions = true;
if (status.ok()) {
skip_filters = true;
} else if (!status.IsNotSupported()) {
s = status;
}
}
if (!s.ok()) {
break;
}
if (!file_range.empty()) {
mget_tasks.emplace_back(MultiGetFromSSTCoroutine(
read_options, file_range, fp.GetHitFileLevel(), skip_filters,
skip_range_deletions, f, blob_ctxs, table_handle,
num_filter_read, num_index_read, num_sst_read));
}
if (fp.KeyMaySpanNextFile()) {
break;
}
f = fp.GetNextFileInLevel();
}
if (mget_tasks.size() > 0) {
RecordTick(db_statistics_, MULTIGET_COROUTINE_COUNT,
mget_tasks.size());
// Collect all results so far.
#ifdef WITH_COROUTINES
std::vector<Status> statuses = co_await folly::coro::co_nothrow(
folly::coro::collectAllRange(std::move(mget_tasks)));
#else
std::vector<Status> statuses =
folly::coro::blockingWait(co_withExecutor(
&range->context()->executor(),
folly::coro::collectAllRange(std::move(mget_tasks))));
#endif
if (s.ok()) {
for (Status stat : statuses) {
if (!stat.ok()) {
s = std::move(stat);
break;
}
}
}
if (s.ok() && fp.KeyMaySpanNextFile()) {
f = fp.GetNextFileInLevel();
}
}
#endif // USE_COROUTINES
}
// If bad status or we found final result for all the keys
if (!s.ok() || file_picker_range.empty()) {
break;
}
if (!f) {
// Reached the end of this level. Prepare the next level
fp.PrepareNextLevelForSearch();
if (!fp.IsSearchEnded()) {
// Its possible there is no overlap on this level and f is nullptr
f = fp.GetNextFileInLevel();
}
if (dump_stats_for_l0_file ||
(prev_level != 0 && prev_level != (int)fp.GetHitFileLevel())) {
// Dump the stats if the search has moved to the next level and
// reset for next level.
if (num_filter_read + num_index_read) {
RecordInHistogram(db_statistics_,
NUM_INDEX_AND_FILTER_BLOCKS_READ_PER_LEVEL,
num_index_read + num_filter_read);
}
if (num_sst_read) {
RecordInHistogram(db_statistics_, NUM_SST_READ_PER_LEVEL,
num_sst_read);
num_level_read++;
}
num_filter_read = 0;
num_index_read = 0;
num_sst_read = 0;
}
prev_level = fp.GetHitFileLevel();
}
}
// Dump stats for most recent level
if (num_filter_read + num_index_read) {
RecordInHistogram(db_statistics_,
NUM_INDEX_AND_FILTER_BLOCKS_READ_PER_LEVEL,
num_index_read + num_filter_read);
}
if (num_sst_read) {
RecordInHistogram(db_statistics_, NUM_SST_READ_PER_LEVEL, num_sst_read);
num_level_read++;
}
if (num_level_read) {
RecordInHistogram(db_statistics_, NUM_LEVEL_READ_PER_MULTIGET,
num_level_read);
}
}
if (!blob_ctxs.empty()) {
MultiGetBlob(read_options, keys_with_blobs_range, blob_ctxs);
}
// Process any left over keys
for (auto iter = range->begin(); s.ok() && iter != range->end(); ++iter) {
GetContext& get_context = *iter->get_context;
Status* status = iter->s;
Slice user_key = iter->lkey->user_key();
if (db_statistics_ != nullptr) {
get_context.ReportCounters();
}
if (GetContext::kMerge == get_context.State()) {
if (!merge_operator_) {
*status = Status::InvalidArgument(
"merge_operator is not properly initialized.");
range->MarkKeyDone(iter);
continue;
}
// merge_operands are in saver and we hit the beginning of the key history
// do a final merge of nullptr and operands;
// `op_failure_scope` (an output parameter) is not provided (set to
// nullptr) since a failure must be propagated regardless of its value.
*status = MergeHelper::TimedFullMerge(
merge_operator_, user_key, MergeHelper::kNoBaseValue,
iter->merge_context.GetOperands(), info_log_, db_statistics_, clock_,
/* update_num_ops_stats */ true, /* op_failure_scope */ nullptr,
iter->value ? iter->value->GetSelf() : nullptr, iter->columns);
if (LIKELY(iter->value != nullptr)) {
iter->value->PinSelf();
range->AddValueSize(iter->value->size());
} else {
assert(iter->columns);
range->AddValueSize(iter->columns->payload_size());
}
range->MarkKeyDone(iter);
if (range->GetValueSize() > read_options.value_size_soft_limit) {
s = Status::Aborted();
break;
}
} else {
range->MarkKeyDone(iter);
*status = Status::NotFound(); // Use an empty error message for speed
}
}
for (auto iter = range->begin(); iter != range->end(); ++iter) {
range->MarkKeyDone(iter);
*(iter->s) = s;
}
CO_RETURN;
}
} // namespace ROCKSDB_NAMESPACE
#endif
+225
View File
@@ -1703,6 +1703,231 @@ TEST_F(DBWideBlobDirectWriteTest,
}
}
TEST_F(DBWideBlobDirectWriteTest,
DirectWriteGetEntityReturnsZeroCopyBlobBuffersAcrossMove) {
// Protects the zero-copy blob resolution optimization for direct-write wide
// entities read pre-flush: each resolved blob-backed column Slice must point
// directly at the buffer fetched during resolution (no copy), and must stay
// valid and identical after the PinnableWideColumns result is moved (the
// backing store uses address-stable forward_list nodes). A sync point
// captures the fetched buffer pointers so we can compare them against
// columns() before and after moving, for both GetEntity and MultiGetEntity.
Options options = GetDirectWriteOptions();
options.min_blob_size = 64;
Reopen(options);
const std::string key = "zero_copy_entity";
const std::string inline_value = "inline"; // < min_blob_size: stays inline
const std::string large1(128, 'a'); // >= min_blob_size: blob column
const std::string large2(160, 'b'); // >= min_blob_size: blob column
const WideColumns columns{{kDefaultWideColumnName, inline_value},
{"big1", large1},
{"big2", large2}};
ASSERT_OK(
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
// Intentionally do not flush: the entity (and its direct-write blob
// references) stays in the memtable so reads resolve through
// DBImpl::ResolveDirectWriteWideColumns().
std::vector<const char*> fetched_ptrs;
SyncPoint::GetInstance()->SetCallBack(
"DBImpl::ResolveDirectWriteWideColumns:BlobFetched", [&](void* arg) {
fetched_ptrs.push_back(static_cast<PinnableSlice*>(arg)->data());
});
SyncPoint::GetInstance()->EnableProcessing();
// GetEntity: two blob columns are fetched, each resolved column Slice points
// straight at its fetched buffer, and the pointers survive move-construction
// and move-assignment.
{
fetched_ptrs.clear();
PinnableWideColumns result;
ASSERT_OK(db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(), key,
&result));
ASSERT_EQ(result.columns(), columns);
ASSERT_EQ(fetched_ptrs.size(), 2U);
// columns() is sorted by name -> [default(""), big1, big2]; blob fetches
// happen in that order, so fetched_ptrs[0]=big1, fetched_ptrs[1]=big2.
const char* big1_ptr = result.columns()[1].value().data();
const char* big2_ptr = result.columns()[2].value().data();
ASSERT_EQ(big1_ptr, fetched_ptrs[0]);
ASSERT_EQ(big2_ptr, fetched_ptrs[1]);
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);
PinnableWideColumns move_assigned;
move_assigned = std::move(moved);
ASSERT_EQ(move_assigned.columns(), columns);
ASSERT_EQ(move_assigned.columns()[1].value().data(), big1_ptr);
ASSERT_EQ(move_assigned.columns()[2].value().data(), big2_ptr);
}
// MultiGetEntity: same zero-copy guarantee, including across a move.
{
fetched_ptrs.clear();
const std::array<Slice, 1> keys{Slice(key)};
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(fetched_ptrs.size(), 2U);
const char* big1_ptr = results[0].columns()[1].value().data();
const char* big2_ptr = results[0].columns()[2].value().data();
ASSERT_EQ(big1_ptr, fetched_ptrs[0]);
ASSERT_EQ(big2_ptr, fetched_ptrs[1]);
PinnableWideColumns moved(std::move(results[0]));
ASSERT_EQ(moved.columns(), columns);
ASSERT_EQ(moved.columns()[1].value().data(), big1_ptr);
ASSERT_EQ(moved.columns()[2].value().data(), big2_ptr);
}
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
}
TEST_F(DBWideBlobDirectWriteTest,
MultiGetEntitySoftLimitBoundsDirectWriteBlobResolution) {
// value_size_soft_limit bounds direct-write blob resolution, which happens in
// MultiGetImpl's post-processing loop (after the batch-loop check counted
// only the small encoded blob-index bytes). With two over-limit entities, the
// first one is read -- making progress even though it exceeds the limit --
// and once the returned size is over the limit the second one is aborted
// before its blob is fetched.
Options options = GetDirectWriteOptions();
Reopen(options);
// Sorted order visits "a_large" before "b_large".
const std::string first_key = "a_large";
const std::string second_key = "b_large";
const std::string first_blob(10000, 'a'); // >= min_blob_size: blob column
const std::string second_blob(10000, 'b'); // >= min_blob_size: blob column
const WideColumns first_columns{{kDefaultWideColumnName, "1"},
{"c", first_blob}};
const WideColumns second_columns{{kDefaultWideColumnName, "2"},
{"c", second_blob}};
ASSERT_OK(db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(),
first_key, first_columns));
ASSERT_OK(db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(),
second_key, second_columns));
// Intentionally do not flush: the entities (and their direct-write blob
// references) stay in the memtable so reads resolve through
// DBImpl::ResolveDirectWriteWideColumns() in the post-processing loop.
int blob_fetches = 0;
SyncPoint::GetInstance()->SetCallBack(
"DBImpl::ResolveDirectWriteWideColumns:BlobFetched",
[&](void* /*arg*/) { ++blob_fetches; });
SyncPoint::GetInstance()->EnableProcessing();
// Below either entity's resolved size (~10000 bytes), so reading the first
// already pushes the returned total over the limit.
ReadOptions read_options;
read_options.value_size_soft_limit = 2000;
const std::array<Slice, 2> keys{Slice(first_key), Slice(second_key)};
std::array<PinnableWideColumns, 2> results;
std::array<Status, 2> statuses;
db_->MultiGetEntity(read_options, db_->DefaultColumnFamily(), keys.size(),
keys.data(), results.data(), statuses.data());
ASSERT_OK(statuses[0]);
ASSERT_EQ(results[0].columns(), first_columns);
ASSERT_TRUE(statuses[1].IsAborted());
// Only the first key's blob was fetched; the second key's fetch was skipped
// because the returned size was already over the limit.
ASSERT_EQ(blob_fetches, 1);
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
}
TEST_F(DBWideBlobDirectWriteTest,
MultiGetEntitySoftLimitAlwaysReadsOversizedKey) {
// "Always make progress": a single entity whose blob value alone exceeds the
// soft limit is still read (not aborted), so a caller cannot loop forever
// retrying a key that by itself is over the limit.
Options options = GetDirectWriteOptions();
Reopen(options);
const std::string key = "oversized";
const std::string large_blob(10000, 'b'); // >= min_blob_size: blob column
const WideColumns columns{{kDefaultWideColumnName, "v"}, {"c", large_blob}};
ASSERT_OK(
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
// Intentionally do not flush: resolution happens in the post-processing loop.
int blob_fetches = 0;
SyncPoint::GetInstance()->SetCallBack(
"DBImpl::ResolveDirectWriteWideColumns:BlobFetched",
[&](void* /*arg*/) { ++blob_fetches; });
SyncPoint::GetInstance()->EnableProcessing();
// Far below the entity's resolved size (~10000 bytes).
ReadOptions read_options;
read_options.value_size_soft_limit = 2000;
const std::array<Slice, 1> keys{Slice(key)};
std::array<PinnableWideColumns, 1> results;
std::array<Status, 1> statuses;
db_->MultiGetEntity(read_options, db_->DefaultColumnFamily(), keys.size(),
keys.data(), results.data(), statuses.data());
ASSERT_OK(statuses[0]);
ASSERT_EQ(results[0].columns(), columns);
ASSERT_EQ(blob_fetches, 1);
SyncPoint::GetInstance()->DisableProcessing();
SyncPoint::GetInstance()->ClearAllCallBacks();
}
TEST_F(DBWideBlobDirectWriteTest,
MultiGetSoftLimitBoundsDirectWritePlainValue) {
// Same behavior for plain values stored as direct-write blob references (the
// is_blob_index path): the first over-limit value is read (progress), and the
// second is aborted once the returned size is over the limit.
Options options = GetDirectWriteOptions();
Reopen(options);
// Sorted order visits "a_large" before "b_large".
const std::string first_key = "a_large";
const std::string second_key = "b_large";
const std::string first_value(10000, 'a'); // >= min_blob_size: direct write
const std::string second_value(10000, 'b'); // >= min_blob_size: direct write
ASSERT_OK(db_->Put(WriteOptions(), first_key, first_value));
ASSERT_OK(db_->Put(WriteOptions(), second_key, second_value));
// Intentionally do not flush: the values stay in the memtable as direct-write
// blob references resolved in the post-processing loop.
ReadOptions read_options;
read_options.value_size_soft_limit = 2000;
const std::array<Slice, 2> keys{Slice(first_key), Slice(second_key)};
std::array<PinnableSlice, 2> values;
std::array<Status, 2> statuses;
db_->MultiGet(read_options, db_->DefaultColumnFamily(), keys.size(),
keys.data(), values.data(), statuses.data());
ASSERT_OK(statuses[0]);
ASSERT_EQ(values[0], first_value);
ASSERT_TRUE(statuses[1].IsAborted());
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
+6 -24
View File
@@ -12,30 +12,12 @@
namespace ROCKSDB_NAMESPACE {
namespace {
ReadOptions BuildReadPathBlobResolverReadOptions(ReadTier read_tier,
bool verify_checksums,
bool fill_cache,
Env::IOActivity io_activity) {
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 read_options;
}
} // namespace
ReadPathBlobResolver::ReadPathBlobResolver(
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)
: blob_fetcher_(version,
BuildReadPathBlobResolverReadOptions(
read_tier, verify_checksums, fill_cache, io_activity),
blob_file_cache, allow_write_path_fallback) {}
ReadPathBlobResolver::ReadPathBlobResolver(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) {}
void ReadPathBlobResolver::Reset(
const Slice& user_key, const std::vector<WideColumn>* columns,
+5 -4
View File
@@ -43,9 +43,7 @@ class Version;
// resolver (ensured by SuperVersion pinning in the caller).
class ReadPathBlobResolver {
public:
ReadPathBlobResolver(const Version* version, ReadTier read_tier,
bool verify_checksums, bool fill_cache,
Env::IOActivity io_activity,
ReadPathBlobResolver(const Version* version, const ReadOptions& read_options,
BlobFileCache* blob_file_cache = nullptr,
bool allow_write_path_fallback = false);
@@ -93,7 +91,10 @@ class ReadPathBlobResolver {
}
private:
BlobFetcher blob_fetcher_;
// Owns its ReadOptions: a resolver's lifetime is independent of the caller
// that created it (e.g. it may outlive the originating ReadOptions once
// returned as part of a lazy result).
OwningVersionBlobFetcher blob_fetcher_;
Slice user_key_;
const std::vector<WideColumn>* columns_ = nullptr;
+157 -166
View File
@@ -14,6 +14,7 @@
#include "db/wide/wide_columns_helper.h"
#include "rocksdb/slice.h"
#include "util/autovector.h"
#include "util/cast_util.h"
#include "util/coding.h"
namespace ROCKSDB_NAMESPACE {
@@ -101,6 +102,20 @@ Status WideColumnSerialization::Serialize(const WideColumns& columns,
return Status::OK();
}
size_t WideColumnSerialization::SerializedSizeV1(const WideColumns& columns) {
size_t size = VarintLength(kVersion1) +
VarintLength(static_cast<uint32_t>(columns.size()));
for (const auto& column : columns) {
const size_t name_size = column.name().size();
const size_t value_size = column.value().size();
size += VarintLength(name_size) + name_size + VarintLength(value_size) +
value_size;
}
return size;
}
template <typename GetName, typename GetValue>
Status WideColumnSerialization::SerializeV2Impl(
size_t num_columns,
@@ -319,6 +334,14 @@ Status WideColumnSerialization::DeserializeV1(
pos += value_size;
}
// Reject any trailing bytes after the last value: a serialized entity must be
// exactly the whole input (V2 already enforces this via its section-size
// check; this keeps V1 consistent).
if (pos != data.size()) {
return Status::Corruption(
"Unexpected trailing data after wide column entity");
}
return Status::OK();
}
@@ -413,7 +436,9 @@ Status WideColumnSerialization::DeserializeV2Impl(
value_pos += vs;
}
// Validate that the consumed bytes match the declared section sizes
// Validate that the consumed bytes match the declared section sizes. Because
// a V2 entity's values section is defined to extend to the end of `input`,
// this also rejects any trailing bytes after the entity.
const size_t total_values_bytes = static_cast<size_t>(input_end - s7);
if (s4 != s4_limit || s5 != s5_limit || name_pos != names_bytes ||
value_pos != total_values_bytes) {
@@ -423,17 +448,23 @@ Status WideColumnSerialization::DeserializeV2Impl(
return Status::OK();
}
Status WideColumnSerialization::Deserialize(Slice& input,
WideColumns& columns) {
Status WideColumnSerialization::Deserialize(
const Slice& entity, WideColumns& columns,
std::vector<std::pair<size_t, BlobIndex>>* blob_columns) {
assert(columns.empty());
assert(!blob_columns || blob_columns->empty());
Slice input = entity;
uint32_t version = 0;
if (!GetVarint32(&input, &version)) {
return Status::Corruption("Error decoding wide column version");
}
// A version newer than kVersion2 is reported as Corruption, consistent with
// how the rest of the codebase classifies an unrecognized serialized
// version/type (BlobIndex, blob log header, table footer).
if (version > kVersion2) {
return Status::NotSupported("Unsupported wide column version");
return Status::Corruption("Corrupt or unsupported wide column version");
}
uint32_t num_columns = 0;
@@ -441,82 +472,44 @@ Status WideColumnSerialization::Deserialize(Slice& input,
return Status::Corruption("Error decoding number of wide columns");
}
if (!num_columns) {
return Status::OK();
}
// A zero-column entity is still dispatched to the version-specific parser
// below (rather than returning OK here) so that trailing bytes after an empty
// entity are rejected as Corruption too -- both parsers handle num_columns==0
// and validate that the input is exactly one serialized entity.
if (version < kVersion2) {
return DeserializeV1(input, num_columns, columns);
}
// V2: need to check for blob columns
// V2 layout: parse columns and extract any blob column info.
std::vector<ValueType> column_types;
if (Status s = DeserializeV2Impl(input, num_columns, columns, column_types);
!s.ok()) {
return s;
}
assert(column_types.size() == num_columns);
assert(columns.size() == num_columns);
for (uint32_t i = 0; i < num_columns; ++i) {
if (column_types[i] == kTypeBlobIndex) {
return Status::NotSupported(
"Wide column contains blob references. Use DeserializeV2.");
if (column_types[i] != kTypeBlobIndex) {
continue;
}
}
return Status::OK();
}
Status WideColumnSerialization::DeserializeV2(
Slice& input, std::vector<WideColumn>& columns,
std::vector<std::pair<size_t, BlobIndex>>& blob_columns) {
assert(columns.empty());
assert(blob_columns.empty());
uint32_t version = 0;
if (!GetVarint32(&input, &version)) {
return Status::Corruption("Error decoding wide column version");
}
if (version > kVersion2) {
return Status::NotSupported("Unsupported wide column version");
}
uint32_t num_columns = 0;
if (!GetVarint32(&input, &num_columns)) {
return Status::Corruption("Error decoding number of wide columns");
}
if (!num_columns) {
return Status::OK();
}
if (version >= kVersion2) {
// V2 layout: parse columns and extract blob column info
std::vector<ValueType> column_types;
if (Status s = DeserializeV2Impl(input, num_columns, columns, column_types);
!s.ok()) {
return s;
if (!blob_columns) {
// The caller promised a fully resolved entity; a blob reference here is
// unexpected and may indicate corruption (or a logic error)
return Status::Corruption(
"Wide column blob reference in a context where it is not expected.");
}
assert(column_types.size() == num_columns);
assert(columns.size() == num_columns);
// Decode blob indices from value data
for (uint32_t i = 0; i < num_columns; ++i) {
if (column_types[i] == kTypeBlobIndex) {
BlobIndex blob_idx;
Slice blob_slice = columns[i].value();
if (blob_slice.empty()) {
return Status::Corruption("Empty blob index in wide column");
}
if (Status bs = blob_idx.DecodeFrom(blob_slice); !bs.ok()) {
return Status::Corruption("Error decoding blob index in wide column");
}
blob_columns.emplace_back(i, blob_idx);
}
BlobIndex blob_idx;
Slice blob_slice = columns[i].value();
if (blob_slice.empty()) {
return Status::Corruption("Empty blob index in wide column");
}
} else {
return DeserializeV1(input, num_columns, columns);
if (Status bs = blob_idx.DecodeFrom(blob_slice); !bs.ok()) {
return Status::Corruption("Error decoding blob index in wide column");
}
blob_columns->emplace_back(i, blob_idx);
}
return Status::OK();
@@ -537,6 +530,9 @@ Status WideColumnSerialization::HasBlobColumns(const Slice& input,
if (version < kVersion2) {
return Status::OK();
}
if (version > kVersion2) {
return Status::Corruption("Corrupt or unsupported wide column version");
}
uint32_t num_columns = 0;
if (!GetVarint32(&input_ref, &num_columns)) {
@@ -582,6 +578,9 @@ Status WideColumnSerialization::ForEachBlobFileNumber(
if (version < kVersion2) {
return Status::OK();
}
if (version > kVersion2) {
return Status::Corruption("Corrupt or unsupported wide column version");
}
uint32_t num_columns = 0;
if (!GetVarint32(&input_ref, &num_columns)) {
@@ -607,12 +606,18 @@ Status WideColumnSerialization::ForEachBlobFileNumber(
return Status::Corruption("Error decoding wide column types");
}
// Collect blob column indices
// Validate every column type, and note whether any blob columns are present.
// An unrecognized/unsupported type byte is Corruption (consistent with
// Deserialize() and HasBlobColumns()) rather than silently treated as a
// non-blob column.
bool has_any_blob = false;
for (uint32_t i = 0; i < num_columns; ++i) {
if (static_cast<uint8_t>(input_ref[i]) == kTypeBlobIndex) {
const auto type = lossless_cast<ValueType>(input_ref[i]);
if (!IsValidColumnValueType(type)) {
return Status::Corruption("Unsupported wide column ValueType");
}
if (type == kTypeBlobIndex) {
has_any_blob = true;
break;
}
}
@@ -692,8 +697,10 @@ Status WideColumnSerialization::GetVersion(const Slice& input,
return Status::OK();
}
Status WideColumnSerialization::GetValueOfDefaultColumn(Slice& input,
Slice& value) {
Status WideColumnSerialization::GetValueOfDefaultColumn(
const Slice& input, Slice& value, bool& is_blob_reference) {
is_blob_reference = false;
Slice input_ref = input;
uint32_t version = 0;
@@ -701,8 +708,9 @@ Status WideColumnSerialization::GetValueOfDefaultColumn(Slice& input,
return Status::Corruption("Error decoding wide column version");
}
// See Deserialize(): a too-new version is Corruption, not NotSupported.
if (version > kVersion2) {
return Status::NotSupported("Unsupported wide column version");
return Status::Corruption("Corrupt or unsupported wide column version");
}
uint32_t num_columns = 0;
@@ -733,15 +741,19 @@ Status WideColumnSerialization::GetValueOfDefaultColumn(Slice& input,
return Status::Corruption("Error decoding wide column names bytes");
}
// Read COLUMN TYPES (N bytes)
// Read COLUMN TYPES (N bytes). We only need column 0's type here (it is the
// default column's type only if column 0 is actually the default column,
// checked via its name size below), but validate it is a recognized type so
// a corrupt/unsupported type byte is reported as Corruption rather than
// silently treated as an inline value.
if (input_ref.size() < num_columns) {
return Status::Corruption("Error decoding wide column types");
}
// Check if default column (index 0) is a blob reference
if (static_cast<uint8_t>(input_ref[0]) == kTypeBlobIndex) {
return Status::NotSupported(
"Wide column contains blob references. Use DeserializeV2.");
const auto column0_type = lossless_cast<ValueType>(input_ref[0]);
if (!IsValidColumnValueType(column0_type)) {
return Status::Corruption("Unsupported wide column ValueType");
}
const bool column0_is_blob = column0_type == kTypeBlobIndex;
input_ref.remove_prefix(num_columns);
// Peek first name size from NAME SIZES section
@@ -779,18 +791,20 @@ Status WideColumnSerialization::GetValueOfDefaultColumn(Slice& input,
}
input_ref.remove_prefix(names_bytes);
// Read the first value from VALUES section
// Read the first value from VALUES section. For a blob-referenced default
// column these are the raw serialized BlobIndex bytes.
if (input_ref.size() < first_value_size) {
return Status::Corruption("Error decoding wide column value payload");
}
value = Slice(input_ref.data(), first_value_size);
is_blob_reference = column0_is_blob;
return Status::OK();
}
// V1 fallback: full deserialization
// V1 fallback: full deserialization. V1 has no blob references.
WideColumns columns;
if (Status s = Deserialize(input, columns); !s.ok()) {
if (Status s = DeserializeSimple(input, columns); !s.ok()) {
return s;
}
@@ -809,15 +823,31 @@ Status WideColumnSerialization::ResolveEntityBlobColumns(
const BlobFetcher* blob_fetcher, PrefetchBufferCollection* prefetch_buffers,
std::string& resolved_entity, bool& resolved, uint64_t* total_bytes_read,
uint64_t* num_blobs_resolved) {
assert(blob_fetcher);
// Resolve into zero-copy columns backed by fetched blob buffers, then
// serialize that result as a V1 (all-inline) entity.
WideColumns resolved_columns;
std::forward_list<PinnableSlice> extra_buffers;
Status s = ResolveEntityBlobColumnsMultiBuffer(
entity_value, user_key, blob_fetcher, prefetch_buffers, resolved_columns,
extra_buffers, resolved, total_bytes_read, num_blobs_resolved);
if (s.ok() && resolved) {
s = Serialize(resolved_columns, resolved_entity);
}
return s;
}
Status WideColumnSerialization::ResolveEntityBlobColumnsMultiBuffer(
const Slice& entity_value, const Slice& user_key,
const BlobFetcher* blob_fetcher, PrefetchBufferCollection* prefetch_buffers,
WideColumns& resolved_columns,
std::forward_list<PinnableSlice>& extra_buffers, bool& resolved,
uint64_t* total_bytes_read, uint64_t* num_blobs_resolved) {
resolved = false;
std::vector<WideColumn> columns;
std::vector<std::pair<size_t, BlobIndex>> blob_columns;
Slice input_copy = entity_value;
if (Status s = DeserializeV2(input_copy, columns, blob_columns); !s.ok()) {
if (Status s = Deserialize(entity_value, columns, &blob_columns); !s.ok()) {
return s;
}
@@ -827,19 +857,26 @@ Status WideColumnSerialization::ResolveEntityBlobColumns(
resolved = true;
// Fetch each blob value
std::vector<std::string> resolved_blob_values;
resolved_blob_values.reserve(blob_columns.size());
for (const auto& blob_col : blob_columns) {
const size_t column_idx = blob_col.first;
const BlobIndex& blob_idx = blob_col.second;
if (blob_idx.IsInlined()) {
resolved_blob_values.emplace_back(blob_idx.value().data(),
blob_idx.value().size());
// The inlined value bytes live in `entity_value`; point the column value
// at them directly (zero copy). The caller keeps `entity_value` alive.
columns[column_idx].value() = blob_idx.value();
continue;
}
if (blob_fetcher == nullptr) {
// A blob-backed entity reached a read context with no blob fetcher (a
// reader without blob support, e.g. SstFileReader, or corrupt/legacy
// data). This is the single place entity blob resolution reports that
// condition; callers no longer pre-check the fetcher.
return Status::Corruption(
"Cannot resolve blob columns in entity without a blob fetcher");
}
FilePrefetchBuffer* prefetch_buffer =
prefetch_buffers ? prefetch_buffers->GetOrCreatePrefetchBuffer(
blob_idx.file_number())
@@ -847,14 +884,17 @@ Status WideColumnSerialization::ResolveEntityBlobColumns(
uint64_t bytes_read = 0;
PinnableSlice blob_value;
// Fetch the blob value into a fresh, address-stable backing node.
extra_buffers.emplace_front();
PinnableSlice& blob_value = extra_buffers.front();
const Status fetch_s = blob_fetcher->FetchBlob(
user_key, blob_idx, prefetch_buffer, &blob_value, &bytes_read);
if (!fetch_s.ok()) {
return fetch_s;
}
resolved_blob_values.emplace_back(blob_value.data(), blob_value.size());
// Zero-copy Slice into the node we just added.
columns[column_idx].value() = blob_value;
if (total_bytes_read) {
*total_bytes_read += bytes_read;
@@ -865,53 +905,41 @@ Status WideColumnSerialization::ResolveEntityBlobColumns(
*num_blobs_resolved += blob_columns.size();
}
return SerializeResolvedEntity(columns, blob_columns, resolved_blob_values,
resolved_entity);
resolved_columns = std::move(columns);
return Status::OK();
}
Status WideColumnSerialization::GetValueOfDefaultColumnResolvingBlobs(
const Slice& entity_value, const Slice& user_key,
const BlobFetcher* blob_fetcher, PinnableSlice& result, bool& resolved) {
assert(blob_fetcher);
resolved = false;
std::vector<WideColumn> columns;
std::vector<std::pair<size_t, BlobIndex>> blob_columns;
Slice input_copy = entity_value;
if (Status s = DeserializeV2(input_copy, columns, blob_columns); !s.ok()) {
return s;
Status WideColumnSerialization::ResolveDefaultColumnBlobReference(
const Slice& blob_index, const Slice& user_key,
const BlobFetcher* blob_fetcher, PinnableSlice& value) {
if (blob_index.empty()) {
return Status::Corruption("Empty blob index in wide column default value");
}
// The default column (empty name) is always at index 0 when present
// (columns are sorted by name).
if (columns.empty() || columns[0].name() != kDefaultWideColumnName) {
result.PinSelf(Slice());
BlobIndex blob_idx;
if (Status s = blob_idx.DecodeFrom(blob_index); !s.ok()) {
return Status::Corruption(
"Error decoding blob index in wide column default value");
}
if (blob_idx.IsInlined()) {
value.PinSelf(blob_idx.value());
return Status::OK();
}
// Check if the default column (index 0) is a blob reference
for (const auto& blob_col : blob_columns) {
if (blob_col.first == 0) {
const BlobIndex& blob_idx = blob_col.second;
resolved = true;
if (blob_idx.IsInlined()) {
result.PinSelf(blob_idx.value());
return Status::OK();
}
return blob_fetcher->FetchBlob(user_key, blob_idx,
nullptr /* prefetch_buffer */, &result,
nullptr /* bytes_read */);
}
if (blob_fetcher == nullptr) {
// A blob-backed default column reached a read context with no blob fetcher
// (a reader without blob support, e.g. SstFileReader, or corrupt/legacy
// data). This is the single place default-column blob resolution reports
// that condition; callers no longer pre-check the fetcher.
return Status::Corruption(
"Cannot resolve blob-backed default column without a blob fetcher");
}
// Default column is inline
result.PinSelf(columns[0].value());
return Status::OK();
return blob_fetcher->FetchBlob(user_key, blob_idx,
nullptr /* prefetch_buffer */, &value,
nullptr /* bytes_read */);
}
Status WideColumnSerialization::ResolveEntityForMerge(
@@ -923,10 +951,9 @@ Status WideColumnSerialization::ResolveEntityForMerge(
if (status.ok()) {
if (!has_blob_columns) {
effective_entity = entity_value;
} else if (!blob_fetcher) {
status = Status::Corruption(
"Cannot resolve blob columns in entity without a blob fetcher");
} else {
// A null blob_fetcher with blob columns present is reported as Corruption
// inside ResolveEntityBlobColumns -> ResolveEntityBlobColumnsMultiBuffer.
bool resolved = false;
status = ResolveEntityBlobColumns(
entity_value, user_key, blob_fetcher, prefetch_buffers,
@@ -940,40 +967,4 @@ Status WideColumnSerialization::ResolveEntityForMerge(
return status;
}
Status WideColumnSerialization::SerializeResolvedEntity(
const std::vector<WideColumn>& columns,
const std::vector<std::pair<size_t, BlobIndex>>& blob_columns,
const std::vector<std::string>& resolved_blob_values, std::string& output) {
assert(blob_columns.size() == resolved_blob_values.size());
#ifndef NDEBUG
for (size_t i = 1; i < blob_columns.size(); ++i) {
assert(blob_columns[i].first > blob_columns[i - 1].first);
}
#endif
// blob_columns is sorted by column index and typically small, so use a
// linear scan with a cursor instead of an unordered_map.
size_t blob_cursor = 0;
// Build result columns with resolved blob values
WideColumns result_columns;
result_columns.reserve(columns.size());
for (size_t i = 0; i < columns.size(); ++i) {
if (blob_cursor < blob_columns.size() &&
blob_columns[blob_cursor].first == i) {
// This is a blob column - use the resolved value
result_columns.emplace_back(columns[i].name(),
Slice(resolved_blob_values[blob_cursor]));
++blob_cursor;
} else {
// This is an inline column - use the original value
result_columns.emplace_back(columns[i].name(), columns[i].value());
}
}
// Serialize using V1 format (all values inline)
return Serialize(result_columns, output);
}
} // namespace ROCKSDB_NAMESPACE
+76 -36
View File
@@ -6,6 +6,7 @@
#pragma once
#include <cstdint>
#include <forward_list>
#include <functional>
#include <limits>
#include <string>
@@ -87,8 +88,9 @@ class Slice;
// ct values are ValueType entries from db/dbformat.h, e.g.:
// kTypeValue (0x01) = inline value
// kTypeBlobIndex (0x11) = blob index reference
// Future per-column types (kTypeMerge, kTypeDeletion, etc.) can be
// added without format changes.
// Future per-column types (kTypeMerge, kTypeDeletion, etc.) can be added
// without changing the byte layout, though a reader predating a given type
// rejects it as Corruption (see IsValidColumnValueType).
//
// Section 4: NAME SIZES (N varints)
// +----------+----------+---...---+------------+
@@ -129,6 +131,10 @@ class WideColumnSerialization {
// Serialize columns using version 1 format (no blob support)
static Status Serialize(const WideColumns& columns, std::string& output);
// Returns the exact number of bytes Serialize() would produce for `columns`
// (the version 1 serialized size), computed without allocating.
static size_t SerializedSizeV1(const WideColumns& columns);
// Serialize columns with some columns replaced by blob indices (version 2)
// columns: vector of (column_name, column_value) pairs
// blob_columns: vector of (column_index, blob_index) pairs indicating which
@@ -145,24 +151,35 @@ class WideColumnSerialization {
const std::vector<std::pair<size_t, BlobIndex>>& blob_columns,
std::string& output);
// Deserialize columns (version 1 format only)
static Status Deserialize(Slice& input, WideColumns& columns);
// Deserialize a V2 entity.
// Deserialize a wide-column entity (version 1, or version 2 with or without
// blob references). `input` must be exactly one serialized entity; trailing
// bytes are reported as Corruption (the deserializers are not prefix-parsers
// -- an entity value is always the whole stored value).
//
// columns: receives the full sorted column set. For blob-backed columns,
// value() is initially the serialized BlobIndex bytes from the entity.
// blob_columns: receives (column_index, blob_index) pairs identifying which
// entries in `columns` are blob references and their decoded BlobIndex data.
// For valid input, this must agree with HasBlobColumns() about whether blob
// column references are present.
static Status DeserializeV2(
Slice& input, std::vector<WideColumn>& columns,
std::vector<std::pair<size_t, BlobIndex>>& blob_columns);
//
// blob_columns: if non-null, receives (column_index, blob_index) pairs
// identifying which entries in `columns` are blob references, and their
// decoded BlobIndex data. If null, the caller is promising the entity is
// fully resolved (no blob references); encountering one returns Corruption
// (as it could theoretically arise from data corruption).
// When non-null, this agrees with HasBlobColumns() about whether blob column
// references are present.
static Status Deserialize(
const Slice& input, WideColumns& columns,
std::vector<std::pair<size_t, BlobIndex>>* blob_columns);
// Convenience wrapper of Deserialize() for callers that own a fully resolved
// entity with no blob references (returns Corruption if one is present).
static Status DeserializeSimple(const Slice& input, WideColumns& columns) {
return Deserialize(input, columns, nullptr /* blob_columns */);
}
// Check if the serialized entity has any blob column references.
// Sets *has_blob_columns to true if version >= 2 and at least one column
// has blob type; false otherwise.
// For valid input, this must agree with DeserializeV2() about whether blob
// For valid input, this must agree with Deserialize() about whether blob
// column references are present.
// Returns Status::Corruption on decode errors.
static Status HasBlobColumns(const Slice& input, bool& has_blob_columns);
@@ -175,7 +192,17 @@ class WideColumnSerialization {
const Slice& input,
const std::function<Status(const BlobIndex&)>& callback);
static Status GetValueOfDefaultColumn(Slice& input, Slice& value);
// Extracts the default column (empty name) from a serialized entity, without
// fetching any blob. Uses an O(1) fast path (no full deserialization).
// is_blob_reference == false: `value` is the inline default column value.
// is_blob_reference == true : `value` is the raw serialized BlobIndex of
// the default column; resolve it with
// ResolveDefaultColumnBlobReference().
// Either way `value` is a zero-copy Slice into `input`. If there is no
// default column, `value` is empty and is_blob_reference is false. Returns
// Corruption on decode errors (never NotSupported).
static Status GetValueOfDefaultColumn(const Slice& input, Slice& value,
bool& is_blob_reference);
// Resolves all blob column references in a V2 wide-column entity,
// fetches the blob values, and re-serializes as a V1 entity (all inline).
@@ -200,17 +227,40 @@ class WideColumnSerialization {
PrefetchBufferCollection* prefetch_buffers, std::string& resolved_entity,
bool& resolved, uint64_t* total_bytes_read, uint64_t* num_blobs_resolved);
// Extracts the default column value from a V2 entity, resolving its
// blob reference if needed. The default column (empty name) is always
// at index 0 when present (columns are sorted).
// Multi-buffer counterpart to ResolveEntityBlobColumns that avoids
// re-serialization. Whereas ResolveEntityBlobColumns produces a single
// re-serialized V1 entity string, this deserializes the V2 entity and, for
// each blob column, fetches the blob value into a freshly appended node of
// `extra_buffers` (whose address is stable). On success, `resolved_columns`
// holds the full column set: inline columns keep zero-copy Slices into
// `entity_value`, and blob columns get zero-copy Slices into the
// corresponding `extra_buffers` node. The caller must keep both
// `entity_value` and `extra_buffers` alive while using `resolved_columns`
// (e.g. via PinnableWideColumnsHelper::ResolveColumns, which splices
// `extra_buffers` in alongside the original entity buffer).
//
// Sets result to the resolved default column value (fetching from blob
// file if it's a blob reference). If there is no default column, result
// is set to empty. Sets *resolved to true if a blob was found for the
// default column, false otherwise.
static Status GetValueOfDefaultColumnResolvingBlobs(
// Sets `resolved` to false and leaves the outputs empty when no blob columns
// are present. A non-inlined blob column with a null blob_fetcher (nothing to
// read the blob with) is reported as Corruption because this could
// hypothetically be caused by data corruption (more likely a programming
// mistake); inlined blob columns need no fetcher.
static Status ResolveEntityBlobColumnsMultiBuffer(
const Slice& entity_value, const Slice& user_key,
const BlobFetcher* blob_fetcher, PinnableSlice& result, bool& resolved);
const BlobFetcher* blob_fetcher,
PrefetchBufferCollection* prefetch_buffers, WideColumns& resolved_columns,
std::forward_list<PinnableSlice>& extra_buffers, bool& resolved,
uint64_t* total_bytes_read, uint64_t* num_blobs_resolved);
// Resolves the default column's blob reference -- the raw serialized
// BlobIndex returned by GetValueOfDefaultColumn() when is_blob_reference is
// true -- into its value. An inlined BlobIndex needs no fetch. A non-inlined
// reference with a null blob_fetcher returns Corruption: a blob-backed value
// with no way to read the blob (a reader without blob support, e.g.
// SstFileReader, or corrupt/legacy data). This is the single place that
// condition is handled for default-column reads.
static Status ResolveDefaultColumnBlobReference(
const Slice& blob_index, const Slice& user_key,
const BlobFetcher* blob_fetcher, PinnableSlice& value);
// Resolves V2 entity blob columns if present, returning an effective
// Slice for use with TimedFullMerge. If the entity has no blob columns
@@ -231,14 +281,6 @@ class WideColumnSerialization {
// Returns Status::Corruption on decode errors.
static Status GetVersion(const Slice& input, uint32_t& version);
// Merges deserialized columns with resolved blob values and serializes
// the result using version 1 format (all values inline).
static Status SerializeResolvedEntity(
const std::vector<WideColumn>& columns,
const std::vector<std::pair<size_t, BlobIndex>>& blob_columns,
const std::vector<std::string>& resolved_blob_values,
std::string& output);
// Returns InvalidArgument with the given message if size exceeds uint32_t.
static Status ValidateWideColumnLimit(size_t size, const char* msg) {
if (size > static_cast<size_t>(std::numeric_limits<uint32_t>::max())) {
@@ -272,14 +314,12 @@ class WideColumnSerialization {
std::vector<const BlobIndex*>& blob_index_map);
// Parses V1 layout (interleaved name/value_size pairs followed by values)
// into columns. Used by both Deserialize and DeserializeV2 to avoid
// code duplication.
// into columns. Used by Deserialize() for version 1 entities.
static Status DeserializeV1(Slice& input, uint32_t num_columns,
std::vector<WideColumn>& columns);
// Parses V2 layout sections 2-7 (skip info through values) into columns and
// column types. Used by both Deserialize and DeserializeV2 to avoid
// code duplication.
// column types. Used by Deserialize() for version 2 entities.
static Status DeserializeV2Impl(Slice& input, uint32_t num_columns,
std::vector<WideColumn>& columns,
std::vector<ValueType>& column_types);
+363 -89
View File
@@ -6,6 +6,7 @@
#include "db/wide/wide_column_serialization.h"
#include <chrono>
#include <forward_list>
#include <limits>
#include "db/blob/blob_index.h"
@@ -23,15 +24,6 @@ class WideColumnSerializationTest : public testing::Test {
static Status GetVersion(const Slice& input, uint32_t& version) {
return WideColumnSerialization::GetVersion(input, version);
}
static Status SerializeResolvedEntity(
const std::vector<WideColumn>& columns,
const std::vector<std::pair<size_t, BlobIndex>>& blob_columns,
const std::vector<std::string>& resolved_blob_values,
std::string& output) {
return WideColumnSerialization::SerializeResolvedEntity(
columns, blob_columns, resolved_blob_values, output);
}
};
TEST_F(WideColumnSerializationTest, Construct) {
@@ -119,7 +111,8 @@ TEST_F(WideColumnSerializationTest, SerializeDeserialize) {
Slice input(output);
WideColumns deserialized_columns;
ASSERT_OK(WideColumnSerialization::Deserialize(input, deserialized_columns));
ASSERT_OK(
WideColumnSerialization::DeserializeSimple(input, deserialized_columns));
ASSERT_EQ(columns, deserialized_columns);
{
@@ -173,13 +166,15 @@ TEST_F(WideColumnSerializationTest, DeserializeVersionError) {
Slice input(buf);
WideColumns columns;
const Status s = WideColumnSerialization::Deserialize(input, columns);
const Status s = WideColumnSerialization::DeserializeSimple(input, columns);
ASSERT_TRUE(s.IsCorruption());
ASSERT_TRUE(std::strstr(s.getState(), "version"));
}
TEST_F(WideColumnSerializationTest, DeserializeUnsupportedVersion) {
// Unsupported version
// A version newer than kVersion2 is reported as Corruption (an unrecognized
// serialized version is treated as bad data, consistent with the rest of the
// codebase's format parsers).
constexpr uint32_t future_version = 1000;
std::string buf;
@@ -188,11 +183,40 @@ TEST_F(WideColumnSerializationTest, DeserializeUnsupportedVersion) {
Slice input(buf);
WideColumns columns;
const Status s = WideColumnSerialization::Deserialize(input, columns);
ASSERT_TRUE(s.IsNotSupported());
const Status s = WideColumnSerialization::DeserializeSimple(input, columns);
ASSERT_TRUE(s.IsCorruption());
ASSERT_TRUE(std::strstr(s.getState(), "version"));
}
TEST_F(WideColumnSerializationTest, FutureVersionRejectedConsistently) {
// Every entry point must reject a future/unknown version as Corruption, so a
// corrupt version byte cannot be misclassified (e.g. as a blob reference) by
// any read path.
constexpr uint32_t future_version = 1000;
std::string buf;
PutVarint32(&buf, future_version);
Slice input(buf);
WideColumns columns;
ASSERT_TRUE(WideColumnSerialization::DeserializeSimple(input, columns)
.IsCorruption());
Slice value;
bool is_blob_reference = false;
ASSERT_TRUE(WideColumnSerialization::GetValueOfDefaultColumn(
input, value, is_blob_reference)
.IsCorruption());
bool has_blob_columns = false;
ASSERT_TRUE(WideColumnSerialization::HasBlobColumns(input, has_blob_columns)
.IsCorruption());
ASSERT_TRUE(WideColumnSerialization::ForEachBlobFileNumber(
input, [](const BlobIndex&) { return Status::OK(); })
.IsCorruption());
}
TEST_F(WideColumnSerializationTest, DeserializeNumberOfColumnsError) {
// Can't decode number of columns
@@ -202,7 +226,7 @@ TEST_F(WideColumnSerializationTest, DeserializeNumberOfColumnsError) {
Slice input(buf);
WideColumns columns;
const Status s = WideColumnSerialization::Deserialize(input, columns);
const Status s = WideColumnSerialization::DeserializeSimple(input, columns);
ASSERT_TRUE(s.IsCorruption());
ASSERT_TRUE(std::strstr(s.getState(), "number"));
}
@@ -220,7 +244,7 @@ TEST_F(WideColumnSerializationTest, DeserializeV2Error) {
Slice input(buf);
WideColumns columns;
const Status s = WideColumnSerialization::Deserialize(input, columns);
const Status s = WideColumnSerialization::DeserializeSimple(input, columns);
ASSERT_TRUE(s.IsCorruption());
ASSERT_TRUE(std::strstr(s.getState(), "name"));
}
@@ -233,7 +257,7 @@ TEST_F(WideColumnSerializationTest, DeserializeV2Error) {
Slice input(buf);
WideColumns columns;
const Status s = WideColumnSerialization::Deserialize(input, columns);
const Status s = WideColumnSerialization::DeserializeSimple(input, columns);
ASSERT_TRUE(s.IsCorruption());
ASSERT_TRUE(std::strstr(s.getState(), "value size"));
}
@@ -246,7 +270,7 @@ TEST_F(WideColumnSerializationTest, DeserializeV2Error) {
Slice input(buf);
WideColumns columns;
const Status s = WideColumnSerialization::Deserialize(input, columns);
const Status s = WideColumnSerialization::DeserializeSimple(input, columns);
ASSERT_TRUE(s.IsCorruption());
ASSERT_TRUE(std::strstr(s.getState(), "name"));
}
@@ -259,7 +283,7 @@ TEST_F(WideColumnSerializationTest, DeserializeV2Error) {
Slice input(buf);
WideColumns columns;
const Status s = WideColumnSerialization::Deserialize(input, columns);
const Status s = WideColumnSerialization::DeserializeSimple(input, columns);
ASSERT_TRUE(s.IsCorruption());
ASSERT_TRUE(std::strstr(s.getState(), "value size"));
}
@@ -272,7 +296,7 @@ TEST_F(WideColumnSerializationTest, DeserializeV2Error) {
Slice input(buf);
WideColumns columns;
const Status s = WideColumnSerialization::Deserialize(input, columns);
const Status s = WideColumnSerialization::DeserializeSimple(input, columns);
ASSERT_TRUE(s.IsCorruption());
ASSERT_TRUE(std::strstr(s.getState(), "payload"));
}
@@ -284,7 +308,7 @@ TEST_F(WideColumnSerializationTest, DeserializeV2Error) {
Slice input(buf);
WideColumns columns;
const Status s = WideColumnSerialization::Deserialize(input, columns);
const Status s = WideColumnSerialization::DeserializeSimple(input, columns);
ASSERT_TRUE(s.IsCorruption());
ASSERT_TRUE(std::strstr(s.getState(), "payload"));
}
@@ -296,7 +320,7 @@ TEST_F(WideColumnSerializationTest, DeserializeV2Error) {
Slice input(buf);
WideColumns columns;
ASSERT_OK(WideColumnSerialization::Deserialize(input, columns));
ASSERT_OK(WideColumnSerialization::DeserializeSimple(input, columns));
}
}
@@ -320,7 +344,7 @@ TEST_F(WideColumnSerializationTest, DeserializeV2OutOfOrder) {
Slice input(buf);
WideColumns columns;
const Status s = WideColumnSerialization::Deserialize(input, columns);
const Status s = WideColumnSerialization::DeserializeSimple(input, columns);
ASSERT_TRUE(s.IsCorruption());
ASSERT_TRUE(std::strstr(s.getState(), "order"));
}
@@ -359,13 +383,13 @@ TEST_F(WideColumnSerializationTest, DeserializeV2RejectsRecursiveType) {
// Section 7: VALUES (8 bytes of placeholder data)
buf.append(8, 'x');
// DeserializeV2 should reject with Corruption
// Deserialize should reject with Corruption
{
Slice input(buf);
std::vector<WideColumn> columns;
std::vector<std::pair<size_t, BlobIndex>> blob_columns;
const Status s =
WideColumnSerialization::DeserializeV2(input, columns, blob_columns);
WideColumnSerialization::Deserialize(input, columns, &blob_columns);
ASSERT_TRUE(s.IsCorruption());
ASSERT_TRUE(std::strstr(s.getState(), "Unsupported wide column ValueType"));
}
@@ -374,11 +398,54 @@ TEST_F(WideColumnSerializationTest, DeserializeV2RejectsRecursiveType) {
{
Slice input(buf);
WideColumns columns;
const Status s = WideColumnSerialization::Deserialize(input, columns);
const Status s = WideColumnSerialization::DeserializeSimple(input, columns);
ASSERT_TRUE(s.IsCorruption());
}
}
TEST_F(WideColumnSerializationTest, FastPathsRejectUnsupportedColumnType) {
// GetValueOfDefaultColumn() and ForEachBlobFileNumber() read the COLUMN TYPES
// section on a fast path. An unrecognized/unsupported type byte (here
// kTypeWideColumnEntity) must be rejected as Corruption -- consistent with
// Deserialize()/HasBlobColumns() -- rather than silently treated as an inline
// value or a non-blob column.
std::string buf;
PutVarint32(&buf, WideColumnSerialization::kVersion2);
PutVarint32(&buf, 1); // num_columns
// Section 2: SKIP INFO
PutVarint32(&buf, 1); // name_sizes_bytes (one varint: the name size 0)
PutVarint32(&buf, 1); // value_sizes_bytes (one varint: the value size 3)
PutVarint32(&buf, 0); // names_bytes (default column name is empty)
// Section 3: COLUMN TYPES -- unsupported type for the (default) column
buf.push_back(static_cast<char>(kTypeWideColumnEntity));
// Section 4: NAME SIZES
PutVarint32(&buf, 0); // empty default column name
// Section 5: VALUE SIZES
PutVarint32(&buf, 3);
// Section 6: NAMES (empty)
// Section 7: VALUES
buf.append("xyz");
{
Slice value;
bool is_blob_reference = false;
const Status s = WideColumnSerialization::GetValueOfDefaultColumn(
Slice(buf), value, is_blob_reference);
ASSERT_TRUE(s.IsCorruption());
ASSERT_TRUE(std::strstr(s.getState(), "Unsupported wide column ValueType"));
}
{
const Status s = WideColumnSerialization::ForEachBlobFileNumber(
Slice(buf), [](const BlobIndex&) { return Status::OK(); });
ASSERT_TRUE(s.IsCorruption());
ASSERT_TRUE(std::strstr(s.getState(), "Unsupported wide column ValueType"));
}
}
// Helper: create a BlobIndex from EncodeBlob parameters.
static BlobIndex MakeBlobIndex(uint64_t file_number, uint64_t offset,
uint64_t size,
@@ -391,7 +458,7 @@ static BlobIndex MakeBlobIndex(uint64_t file_number, uint64_t offset,
return bi;
}
// Helper: V2 serialize -> DeserializeV2 round-trip, returning
// Helper: V2 serialize -> Deserialize round-trip, returning
// deserialized columns and blob column info.
static void V2SerializeAndDeserialize(
const std::vector<std::pair<std::string, std::string>>& columns,
@@ -403,8 +470,8 @@ static void V2SerializeAndDeserialize(
*serialized_out));
Slice input(*serialized_out);
ASSERT_OK(WideColumnSerialization::DeserializeV2(input, *deserialized,
*blob_columns_out));
ASSERT_OK(WideColumnSerialization::Deserialize(input, *deserialized,
blob_columns_out));
ASSERT_EQ(deserialized->size(), columns.size());
for (size_t i = 0; i < columns.size(); ++i) {
ASSERT_EQ((*deserialized)[i].name(), columns[i].first);
@@ -430,7 +497,7 @@ static void VerifyDeserialize(
const std::vector<std::string>& expected_values) {
Slice input(serialized);
WideColumns deserialized;
ASSERT_OK(WideColumnSerialization::Deserialize(input, deserialized));
ASSERT_OK(WideColumnSerialization::DeserializeSimple(input, deserialized));
ASSERT_EQ(deserialized.size(), expected.size());
for (size_t i = 0; i < expected.size(); ++i) {
ASSERT_EQ(deserialized[i].name(), expected[i].first);
@@ -481,58 +548,13 @@ static void VerifyGetDefaultColumn(
Slice input(serialized);
Slice value;
ASSERT_OK(WideColumnSerialization::GetValueOfDefaultColumn(input, value));
bool is_blob_reference = false;
ASSERT_OK(WideColumnSerialization::GetValueOfDefaultColumn(
input, value, is_blob_reference));
ASSERT_FALSE(is_blob_reference);
ASSERT_EQ(value, expected_value);
}
TEST_F(WideColumnSerializationTest, SerializeResolvedEntity) {
// Test resolve with mixed, all-blob, and no-blob configurations
struct TestCase {
std::vector<std::pair<std::string, std::string>> columns;
std::vector<std::pair<size_t, BlobIndex>> blob_cols;
std::vector<std::string> resolved_values;
std::vector<std::string> expected_values;
};
std::vector<TestCase> cases = {
// Mixed inline and blob
{.columns = {{"a", "inline_a"}, {"b", "ph"}, {"c", "inline_c"}},
.blob_cols = {{1, MakeBlobIndex(50, 500, 100)}},
.resolved_values = {"resolved_b"},
.expected_values = {"inline_a", "resolved_b", "inline_c"}},
// All blob columns
{.columns = {{"x", "ph1"}, {"y", "ph2"}, {"z", "ph3"}},
.blob_cols = {{0, MakeBlobIndex(10, 100, 50)},
{1, MakeBlobIndex(20, 200, 60)},
{2, MakeBlobIndex(30, 300, 70)}},
.resolved_values = {"val_x", "val_y", "val_z"},
.expected_values = {"val_x", "val_y", "val_z"}},
// No blob columns
{.columns = {{"alpha", "val_alpha"}, {"beta", "val_beta"}},
.blob_cols = {},
.resolved_values = {},
.expected_values = {"val_alpha", "val_beta"}},
};
for (const auto& tc : cases) {
std::string serialized;
std::vector<WideColumn> deserialized;
std::vector<std::pair<size_t, BlobIndex>> blob_out;
V2SerializeAndDeserialize(tc.columns, tc.blob_cols, &deserialized,
&blob_out, &serialized);
std::string resolved_output;
ASSERT_OK(WideColumnSerializationTest::SerializeResolvedEntity(
deserialized, blob_out, tc.resolved_values, resolved_output));
uint32_t v = 0;
ASSERT_OK(GetVersion(Slice(resolved_output), v));
ASSERT_EQ(v, WideColumnSerialization::kVersion1);
VerifyDeserialize(resolved_output, tc.columns, tc.expected_values);
}
}
TEST_F(WideColumnSerializationTest, V2GetValueOfDefaultColumn) {
// V2 with default column present
VerifyGetDefaultColumn({{"", "default_value"}, {"col1", "value1"}},
@@ -550,7 +572,10 @@ TEST_F(WideColumnSerializationTest, V2GetValueOfDefaultColumn) {
Slice input(serialized);
Slice value;
ASSERT_OK(WideColumnSerialization::GetValueOfDefaultColumn(input, value));
bool is_blob_reference = false;
ASSERT_OK(WideColumnSerialization::GetValueOfDefaultColumn(
input, value, is_blob_reference));
ASSERT_FALSE(is_blob_reference);
ASSERT_EQ(value, "v1_default");
}
}
@@ -567,8 +592,8 @@ TEST_F(WideColumnSerializationTest, V2BlobColumnRejectsDeserialize) {
Slice input(serialized);
WideColumns deserialized;
ASSERT_TRUE(WideColumnSerialization::Deserialize(input, deserialized)
.IsNotSupported());
ASSERT_TRUE(WideColumnSerialization::DeserializeSimple(input, deserialized)
.IsCorruption());
}
TEST_F(WideColumnSerializationTest, PinnableWideColumnsFallbacksToV2) {
@@ -584,8 +609,8 @@ TEST_F(WideColumnSerializationTest, PinnableWideColumnsFallbacksToV2) {
std::vector<WideColumn> expected_columns;
std::vector<std::pair<size_t, BlobIndex>> expected_blob_columns;
Slice input(serialized);
ASSERT_OK(WideColumnSerialization::DeserializeV2(input, expected_columns,
expected_blob_columns));
ASSERT_OK(WideColumnSerialization::Deserialize(input, expected_columns,
&expected_blob_columns));
PinnableWideColumns result;
ASSERT_OK(result.SetWideColumnValue(serialized));
@@ -598,8 +623,9 @@ TEST_F(WideColumnSerializationTest, PinnableWideColumnsFallbacksToV2) {
}
TEST_F(WideColumnSerializationTest, V2GetValueOfDefaultColumnBlobRef) {
// When default column (index 0) is a blob reference,
// GetValueOfDefaultColumn should return NotSupported.
// When the default column (index 0) is a blob reference,
// GetValueOfDefaultColumn succeeds and flags it via is_blob_reference,
// returning the raw serialized BlobIndex bytes (no fetch).
std::vector<std::pair<std::string, std::string>> columns = {
{"", "placeholder"}, {"col1", "value1"}};
std::vector<std::pair<size_t, BlobIndex>> blob_columns = {
@@ -611,8 +637,17 @@ TEST_F(WideColumnSerializationTest, V2GetValueOfDefaultColumnBlobRef) {
Slice input(serialized);
Slice value;
ASSERT_TRUE(WideColumnSerialization::GetValueOfDefaultColumn(input, value)
.IsNotSupported());
bool is_blob_reference = false;
ASSERT_OK(WideColumnSerialization::GetValueOfDefaultColumn(
input, value, is_blob_reference));
ASSERT_TRUE(is_blob_reference);
ASSERT_FALSE(value.empty());
// Resolving a non-inlined reference without a blob fetcher is Corruption.
PinnableSlice resolved;
ASSERT_TRUE(WideColumnSerialization::ResolveDefaultColumnBlobReference(
value, "user_key", /*blob_fetcher=*/nullptr, resolved)
.IsCorruption());
}
TEST_F(WideColumnSerializationTest, SerializeV2Errors) {
@@ -798,7 +833,7 @@ TEST_F(WideColumnSerializationTest, RandomizedSerializeDeserializeRoundTrip) {
}
}
// V2 serialize -> DeserializeV2 round-trip
// V2 serialize -> Deserialize round-trip
std::string serialized;
std::vector<WideColumn> deserialized;
std::vector<std::pair<size_t, BlobIndex>> blob_out;
@@ -895,6 +930,245 @@ TEST_F(WideColumnSerializationTest, ResolveEntityForMergeNullBlobFetcher) {
ASSERT_TRUE(s.IsCorruption());
}
TEST_F(WideColumnSerializationTest, DeserializeRejectsTrailingData) {
const WideColumns columns{{kDefaultWideColumnName, "d"}, {"attr", "val"}};
auto check = [](const std::string& serialized) {
std::vector<WideColumn> out;
std::vector<std::pair<size_t, BlobIndex>> blob_columns;
ASSERT_OK(WideColumnSerialization::Deserialize(Slice(serialized), out,
&blob_columns));
// A trailing byte must be rejected as Corruption.
const std::string with_trailing = serialized + "x";
out.clear();
blob_columns.clear();
ASSERT_TRUE(WideColumnSerialization::Deserialize(Slice(with_trailing), out,
&blob_columns)
.IsCorruption());
};
// V1 layout (the underlying DeserializeV1 must reject trailing data, matching
// the V2 layout below).
{
std::string serialized;
ASSERT_OK(WideColumnSerialization::Serialize(columns, serialized));
check(serialized);
}
// V2 layout with no blob columns.
{
std::string serialized;
ASSERT_OK(WideColumnSerialization::SerializeV2(
columns, {} /* blob_columns */, serialized));
check(serialized);
}
// Empty (zero-column) entities must reject trailing data too.
const WideColumns empty_columns;
{
std::string serialized;
ASSERT_OK(WideColumnSerialization::Serialize(empty_columns, serialized));
check(serialized);
}
{
std::string serialized;
ASSERT_OK(WideColumnSerialization::SerializeV2(
empty_columns, {} /* blob_columns */, serialized));
check(serialized);
}
}
namespace {
size_t ExpectedPayloadSize(const WideColumns& columns) {
size_t total = 0;
for (const auto& column : columns) {
total += column.name().size() + column.value().size();
}
return total;
}
} // namespace
TEST(PinnableWideColumnsTest, SetPlainValue) {
PinnableWideColumns columns;
columns.SetPlainValue(Slice("hello"));
ASSERT_EQ(columns.columns().size(), 1);
EXPECT_EQ(columns.columns()[0].name(), kDefaultWideColumnName);
EXPECT_EQ(columns.columns()[0].value(), Slice("hello"));
// For a plain value, payload_size() equals the raw value size.
EXPECT_EQ(columns.payload_size(), 5);
}
TEST(PinnableWideColumnsTest, SetWideColumnValueAndPayloadSize) {
// Columns must be sorted (default/empty name first).
const WideColumns entity_columns{
{kDefaultWideColumnName, "default"}, {"a", "bb"}, {"ccc", "dddd"}};
std::string serialized;
ASSERT_OK(WideColumnSerialization::Serialize(entity_columns, serialized));
PinnableWideColumns columns;
ASSERT_OK(columns.SetWideColumnValue(std::move(serialized)));
ASSERT_EQ(columns.columns(), entity_columns);
EXPECT_EQ(columns.payload_size(), ExpectedPayloadSize(entity_columns));
}
TEST(PinnableWideColumnsTest, SerializedSizeMatchesSerializedSizeV1) {
{
// Plain value: default column with the raw value.
PinnableWideColumns columns;
columns.SetPlainValue(Slice("plain-value"));
EXPECT_EQ(columns.serialized_size(),
WideColumnSerialization::SerializedSizeV1(columns.columns()));
}
{
// V1 entity: serialized_size() equals the length of the exact bytes that
// Serialize() produced.
const WideColumns entity_columns{
{kDefaultWideColumnName, "default"}, {"attr1", "val1"}, {"attr2", ""}};
std::string serialized;
ASSERT_OK(WideColumnSerialization::Serialize(entity_columns, serialized));
const size_t serialized_len = serialized.size();
PinnableWideColumns columns;
ASSERT_OK(columns.SetWideColumnValue(std::move(serialized)));
EXPECT_EQ(columns.serialized_size(),
WideColumnSerialization::SerializedSizeV1(columns.columns()));
EXPECT_EQ(columns.serialized_size(), serialized_len);
}
}
TEST(PinnableWideColumnsTest, Reset) {
PinnableWideColumns columns;
columns.SetPlainValue(Slice("something"));
ASSERT_FALSE(columns.columns().empty());
columns.Reset();
EXPECT_TRUE(columns.columns().empty());
EXPECT_EQ(columns.payload_size(), 0);
// Reusable after Reset.
columns.SetPlainValue(Slice("again"));
ASSERT_EQ(columns.columns().size(), 1);
EXPECT_EQ(columns.columns()[0].value(), Slice("again"));
}
TEST(PinnableWideColumnsTest, MovePlainValueSelfPinnedStable) {
// A short (SSO) plain value is self-pinned inside a backing node. The node
// must not relocate on move, so the column Slice stays valid and its data
// pointer is preserved -- the guarantee that lets us drop the old
// re-derivation-on-move logic.
PinnableWideColumns columns;
columns.SetPlainValue(std::string("tiny"));
const char* const value_data = columns.columns()[0].value().data();
PinnableWideColumns moved(std::move(columns));
ASSERT_EQ(moved.columns().size(), 1);
EXPECT_EQ(moved.columns()[0].name(), kDefaultWideColumnName);
EXPECT_EQ(moved.columns()[0].value(), Slice("tiny"));
EXPECT_EQ(moved.columns()[0].value().data(), value_data);
}
TEST(PinnableWideColumnsTest, ResolveColumnsZeroCopy) {
// Original entity with two inline columns.
const WideColumns entity_columns{{kDefaultWideColumnName, "default-value"},
{"blob_col", "encoded-blob-index"}};
std::string serialized;
ASSERT_OK(WideColumnSerialization::Serialize(entity_columns, serialized));
PinnableWideColumns columns;
ASSERT_OK(columns.SetWideColumnValue(std::move(serialized)));
// Inline (default) column keeps its Slice into the original entity buffer.
const Slice inline_name = columns.columns()[0].name();
const Slice inline_value = columns.columns()[0].value();
const char* const inline_value_data = inline_value.data();
// Resolve "blob_col" into its own backing buffer.
std::forward_list<PinnableSlice> extra_buffers;
extra_buffers.emplace_front();
extra_buffers.front().PinSelf(Slice("resolved-blob-payload"));
const char* const resolved_data = extra_buffers.front().data();
WideColumns resolved_columns;
resolved_columns.emplace_back(inline_name, inline_value);
resolved_columns.emplace_back(columns.columns()[1].name(),
Slice(extra_buffers.front()));
PinnableWideColumnsHelper::ResolveColumns(
columns, std::move(resolved_columns), std::move(extra_buffers));
ASSERT_EQ(columns.columns().size(), 2);
// Inline column still points into the original entity buffer (no copy, no
// re-serialization).
EXPECT_EQ(columns.columns()[0].value(), Slice("default-value"));
EXPECT_EQ(columns.columns()[0].value().data(), inline_value_data);
// Resolved blob column points directly into the spliced-in backing buffer,
// not into a re-serialized entity.
EXPECT_EQ(columns.columns()[1].name(), Slice("blob_col"));
EXPECT_EQ(columns.columns()[1].value(), Slice("resolved-blob-payload"));
EXPECT_EQ(columns.columns()[1].value().data(), resolved_data);
}
TEST(PinnableWideColumnsTest, MultiBufferMovePreservesPointers) {
const WideColumns entity_columns{{kDefaultWideColumnName, "default-value"},
{"blob_col", "encoded-blob-index"}};
std::string serialized;
ASSERT_OK(WideColumnSerialization::Serialize(entity_columns, serialized));
PinnableWideColumns columns;
ASSERT_OK(columns.SetWideColumnValue(std::move(serialized)));
std::forward_list<PinnableSlice> extra_buffers;
extra_buffers.emplace_front();
extra_buffers.front().PinSelf(Slice("a-fairly-long-resolved-blob-payload"));
WideColumns resolved_columns;
resolved_columns.emplace_back(columns.columns()[0].name(),
columns.columns()[0].value());
resolved_columns.emplace_back(columns.columns()[1].name(),
Slice(extra_buffers.front()));
PinnableWideColumnsHelper::ResolveColumns(
columns, std::move(resolved_columns), std::move(extra_buffers));
const char* const inline_data = columns.columns()[0].value().data();
const char* const resolved_data = columns.columns()[1].value().data();
// Move construction: nodes are stolen, not relocated -- data pointers stable.
PinnableWideColumns moved(std::move(columns));
ASSERT_EQ(moved.columns().size(), 2);
EXPECT_EQ(moved.columns()[0].value(), Slice("default-value"));
EXPECT_EQ(moved.columns()[1].value(),
Slice("a-fairly-long-resolved-blob-payload"));
EXPECT_EQ(moved.columns()[0].value().data(), inline_data);
EXPECT_EQ(moved.columns()[1].value().data(), resolved_data);
// Move assignment: same guarantee.
PinnableWideColumns move_assigned;
move_assigned = std::move(moved);
ASSERT_EQ(move_assigned.columns().size(), 2);
EXPECT_EQ(move_assigned.columns()[0].value(), Slice("default-value"));
EXPECT_EQ(move_assigned.columns()[1].value(),
Slice("a-fairly-long-resolved-blob-payload"));
EXPECT_EQ(move_assigned.columns()[0].value().data(), inline_data);
EXPECT_EQ(move_assigned.columns()[1].value().data(), resolved_data);
}
} // namespace ROCKSDB_NAMESPACE
int main(int argc, char** argv) {
+20 -17
View File
@@ -5,6 +5,8 @@
#include "rocksdb/wide_columns.h"
#include <iterator>
#include "db/blob/blob_index.h"
#include "db/wide/wide_column_serialization.h"
@@ -14,30 +16,31 @@ const Slice kDefaultWideColumnName;
const WideColumns kNoWideColumns;
Status PinnableWideColumns::CreateIndexForWideColumns() {
Status PinnableWideColumns::BuildColumnsForEntity() {
columns_.clear();
unresolved_blob_column_indices_.clear();
Slice value_copy = value_;
Status status = WideColumnSerialization::Deserialize(value_copy, columns_);
if (status.IsNotSupported()) {
// Deserialize() may already populate columns_ before discovering V2 blob
// references and returning NotSupported. Clear the partial result before
// falling back to DeserializeV2(), which requires an empty output vector.
columns_.clear();
value_copy = value_;
std::vector<std::pair<size_t, BlobIndex>> blob_columns;
status = WideColumnSerialization::DeserializeV2(value_copy, columns_,
blob_columns);
if (status.ok()) {
unresolved_blob_column_indices_.reserve(blob_columns.size());
for (const auto& blob_column : blob_columns) {
unresolved_blob_column_indices_.push_back(blob_column.first);
}
// Called right after a Set*() has placed the serialized entity in a single
// backing buffer; the whole entity must live in backing_.front().
assert(!backing_.empty());
assert(std::next(backing_.begin()) == backing_.end());
// Collect any blob column references; a resolved entity simply yields none.
std::vector<std::pair<size_t, BlobIndex>> blob_columns;
Status status = WideColumnSerialization::Deserialize(backing_.front(),
columns_, &blob_columns);
if (status.ok()) {
unresolved_blob_column_indices_.reserve(blob_columns.size());
for (const auto& blob_column : blob_columns) {
unresolved_blob_column_indices_.push_back(blob_column.first);
}
}
return status;
}
size_t PinnableWideColumns::serialized_size() const {
return WideColumnSerialization::SerializedSizeV1(columns_);
}
} // namespace ROCKSDB_NAMESPACE
+2 -1
View File
@@ -34,7 +34,8 @@ Status WideColumnsHelper::DumpSliceAsWideColumns(const Slice& value,
std::ostream& os, bool hex) {
WideColumns columns;
Slice value_copy = value;
const Status s = WideColumnSerialization::Deserialize(value_copy, columns);
const Status s =
WideColumnSerialization::DeserializeSimple(value_copy, columns);
if (s.ok()) {
DumpWideColumns(columns, os, hex);
}
+46
View File
@@ -7,9 +7,15 @@
#include <algorithm>
#include <cassert>
#include <cstddef>
#include <forward_list>
#include <iterator>
#include <ostream>
#include <utility>
#include <vector>
#include "rocksdb/rocksdb_namespace.h"
#include "rocksdb/slice.h"
#include "rocksdb/wide_columns.h"
namespace ROCKSDB_NAMESPACE {
@@ -63,4 +69,44 @@ class WideColumnsHelper {
}
};
// Internal helper for mutating the private backing storage of a
// PinnableWideColumns without exposing these operations on its public API.
class PinnableWideColumnsHelper {
public:
// Replaces `columns`'s current columns with an already-resolved set. Every
// name/value Slice in `resolved_columns` must point into stable memory:
// either a buffer already held by `columns` (e.g. the original entity, for
// inline columns) or one of the buffers in `extra_buffers` (e.g. fetched blob
// values). Ownership of `extra_buffers` is transferred via splice; its nodes
// are individually heap-allocated and never relocate, so Slices into them
// stay valid. Clears any unresolved-blob metadata. No serialization occurs.
static void ResolveColumns(PinnableWideColumns& columns,
WideColumns&& resolved_columns,
std::forward_list<PinnableSlice>&& extra_buffers) {
columns.backing_.splice_after(columns.backing_.before_begin(),
std::move(extra_buffers));
columns.columns_ = std::move(resolved_columns);
columns.unresolved_blob_column_indices_.clear();
}
// Sorted positions of the columns whose values still hold encoded BlobIndex
// payloads (V2 entities that have not been blob-resolved yet). Empty once
// resolved or for entities without blob references.
static const std::vector<size_t>& GetUnresolvedBlobColumnIndices(
const PinnableWideColumns& columns) {
return columns.unresolved_blob_column_indices_;
}
// The backing buffer holding the serialized entity. Valid only before
// resolution, when the entity occupies a single backing buffer (the state
// right after SetWideColumnValue()); used as the input for blob resolution.
static Slice GetSerializedEntity(const PinnableWideColumns& columns) {
assert(!columns.backing_.empty());
// Exactly one backing buffer: there is a single serialized entity and it
// has not been split across resolved blob buffers yet.
assert(std::next(columns.backing_.begin()) == columns.backing_.end());
return columns.backing_.front();
}
};
} // namespace ROCKSDB_NAMESPACE
+3 -3
View File
@@ -163,7 +163,7 @@ class BatchedOpsStressTest : public StressTest {
for (int i = 0; i < 10; i++) {
keys[i] += key.ToString();
key_slices[i] = keys[i];
s = db_->Get(readoptionscopy, cfh, key_slices[i], &from_db);
s = DbStressGet(db_, readoptionscopy, cfh, key_slices[i], &from_db);
if (!s.ok() && !s.IsNotFound()) {
fprintf(stderr, "get error: %s\n", s.ToString().c_str());
values[i] = "";
@@ -235,8 +235,8 @@ class BatchedOpsStressTest : public StressTest {
key_str.emplace_back(keys[key] + Key(rand_keys[rand_key]));
key_slices.emplace_back(key_str.back());
}
db_->MultiGet(readoptionscopy, cfh, num_prefixes, key_slices.data(),
values.data(), statuses.data());
DbStressMultiGet(db_, readoptionscopy, cfh, num_prefixes,
key_slices.data(), values.data(), statuses.data());
for (size_t i = 0; i < num_prefixes; i++) {
Status s = statuses[i];
if (!s.ok() && !s.IsNotFound()) {
+10 -9
View File
@@ -218,7 +218,7 @@ class CfConsistencyStressTest : public StressTest {
column_families_[rand_column_families[thread->rand.Next() %
rand_column_families.size()]];
std::string from_db;
s = db_->Get(readoptions, cfh, key, &from_db);
s = DbStressGet(db_, readoptions, cfh, key, &from_db);
} else {
// 1/2 chance, comparing one key is the same across all CFs
const Snapshot* snapshot = db_->GetSnapshot();
@@ -226,8 +226,8 @@ class CfConsistencyStressTest : public StressTest {
readoptionscopy.snapshot = snapshot;
std::string value0;
s = db_->Get(readoptionscopy, column_families_[rand_column_families[0]],
key, &value0);
s = DbStressGet(db_, readoptionscopy,
column_families_[rand_column_families[0]], key, &value0);
// Temporarily disable error injection for verification
if (db_fault_injection_fs_) {
@@ -241,8 +241,9 @@ class CfConsistencyStressTest : public StressTest {
bool found = s.ok();
for (size_t i = 1; i < rand_column_families.size(); i++) {
std::string value1;
s = db_->Get(readoptionscopy,
column_families_[rand_column_families[i]], key, &value1);
s = DbStressGet(db_, readoptionscopy,
column_families_[rand_column_families[i]], key,
&value1);
if (!s.ok() && !s.IsNotFound()) {
break;
}
@@ -326,8 +327,8 @@ class CfConsistencyStressTest : public StressTest {
key_str.emplace_back(Key(rand_keys[i]));
keys.emplace_back(key_str.back());
}
db_->MultiGet(readoptionscopy, cfh, num_keys, keys.data(), values.data(),
statuses.data());
DbStressMultiGet(db_, readoptionscopy, cfh, num_keys, keys.data(),
values.data(), statuses.data());
for (const auto& s : statuses) {
if (s.ok()) {
// found case
@@ -1477,14 +1478,14 @@ class CfConsistencyStressTest : public StressTest {
db_->GetEntity(snapshot_read_opts, cfh, key, &snapshot_entity);
std::string snapshot_value;
const Status snapshot_value_status =
db_->Get(snapshot_read_opts, cfh, key, &snapshot_value);
DbStressGet(db_, snapshot_read_opts, cfh, key, &snapshot_value);
PinnableWideColumns latest_entity;
const Status latest_entity_status =
db_->GetEntity(latest_read_opts, cfh, key, &latest_entity);
std::string latest_value;
const Status latest_value_status =
db_->Get(latest_read_opts, cfh, key, &latest_value);
DbStressGet(db_, latest_read_opts, cfh, key, &latest_value);
std::string snapshot_verify = "n/a";
if (snapshot_entity_status.ok()) {
+80
View File
@@ -12,6 +12,8 @@
#include "db_stress_tool/db_stress_common.h"
#include <cmath>
#include <condition_variable>
#include <mutex>
#include "db_stress_tool/db_stress_test_base.h"
#include "file/file_util.h"
@@ -36,6 +38,84 @@ constexpr int64_t zipf_sum_size = 100000;
namespace ROCKSDB_NAMESPACE {
namespace {
class BlockingAsyncCallback : public DB::AsyncCallback {
public:
void OnComplete(const PerfContext* /*perf_context*/,
const IOStatsContext* /*iostats_context*/) override {
std::lock_guard<std::mutex> lock(mu_);
done_ = true;
cv_.notify_one();
}
void Wait() {
std::unique_lock<std::mutex> lock(mu_);
cv_.wait(lock, [this] { return done_; });
}
private:
std::mutex mu_;
std::condition_variable cv_;
bool done_ = false;
};
} // namespace
Status DbStressGet(DB* db, const ReadOptions& options,
ColumnFamilyHandle* column_family, const Slice& key,
PinnableSlice* value, std::string* timestamp) {
#if USE_COROUTINES
if (FLAGS_use_async_db_api) {
BlockingAsyncCallback callback;
Status status;
db->GetAsync(options, column_family, key, value, timestamp, status,
callback);
callback.Wait();
return status;
}
#endif // USE_COROUTINES
return db->Get(options, column_family, key, value, timestamp);
}
Status DbStressGet(DB* db, const ReadOptions& options,
ColumnFamilyHandle* column_family, const Slice& key,
std::string* value, std::string* timestamp) {
#if USE_COROUTINES
if (FLAGS_use_async_db_api) {
BlockingAsyncCallback callback;
Status status;
db->GetAsync(options, column_family, key, value, timestamp, status,
callback);
callback.Wait();
return status;
}
#endif // USE_COROUTINES
return db->Get(options, column_family, key, value, timestamp);
}
Status DbStressGet(DB* db, const ReadOptions& options, const Slice& key,
std::string* value) {
return DbStressGet(db, options, db->DefaultColumnFamily(), key, value);
}
void DbStressMultiGet(DB* db, const ReadOptions& options,
ColumnFamilyHandle* column_family, size_t num_keys,
const Slice* keys, PinnableSlice* values,
Status* statuses) {
#if USE_COROUTINES
if (FLAGS_use_async_db_api) {
std::vector<ColumnFamilyHandle*> column_families(num_keys, column_family);
BlockingAsyncCallback callback;
db->MultiGetAsync(options, num_keys, column_families.data(), keys, values,
statuses, callback);
callback.Wait();
return;
}
#endif // USE_COROUTINES
db->MultiGet(options, column_family, num_keys, keys, values, statuses);
}
// Zipfian distribution is generated based on a pre-calculated array.
// It should be used before start the stress test.
// First, the probability distribution function (PDF) of this Zipfian follows
Loaded 100 of 216 files, more files were not shown because too many files have changed in this diff. Show more