mirror of
https://github.com/facebook/rocksdb.git
synced 2026-11-07 11:32:34 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec93377878 | ||
|
|
0e1c7f9e62 | ||
|
|
c12d364ad1 | ||
|
|
766ca8f985 | ||
|
|
3c8f8f417d | ||
|
|
507206e46b | ||
|
|
3c30d43baf | ||
|
|
96e7ef0269 | ||
|
|
5d6f321b70 | ||
|
|
256a699be3 | ||
|
|
7530287bde | ||
|
|
b03a058d61 | ||
|
|
8108b49e0c | ||
|
|
dd54c5abc9 | ||
|
|
7d721f398b | ||
|
|
bcf8711165 | ||
|
|
34d5074bfa | ||
|
|
79b58b0dd6 | ||
|
|
613a642b5c | ||
|
|
e231c1826c | ||
|
|
1419ae41b1 | ||
|
|
db114777b1 | ||
|
|
7a90dc5ba8 | ||
|
|
a4e298b8c4 | ||
|
|
576dc39899 | ||
|
|
4c2a977aa5 | ||
|
|
88a8fdf849 | ||
|
|
a844cbf5b7 | ||
|
|
3d8e4be2f5 | ||
|
|
7d3150ffce | ||
|
|
91b96f41cf | ||
|
|
dfde6a60b0 | ||
|
|
bcda8c4b1a | ||
|
|
37234200b5 | ||
|
|
ac0f4ae69f | ||
|
|
bec9dcec7c | ||
|
|
897162a6bc | ||
|
|
1ca38e1442 | ||
|
|
da3a0961fa | ||
|
|
96ed085402 | ||
|
|
2c1c7ea2f1 | ||
|
|
fe6406f7b2 | ||
|
|
0df7ed563b | ||
|
|
068aeda9d8 | ||
|
|
a0966fd94e | ||
|
|
fe5123681e | ||
|
|
f9c117e203 | ||
|
|
82dfd6a510 | ||
|
|
80a5a6e4b2 | ||
|
|
d699ffbd5c | ||
|
|
e4a7136d3e | ||
|
|
f7b791b213 | ||
|
|
6dbb6f30e6 | ||
|
|
bd16531142 | ||
|
|
e6a2ee0bd2 | ||
|
|
ad81b654a4 | ||
|
|
8f492b2f47 | ||
|
|
e8554f1b11 | ||
|
|
330ded5129 | ||
|
|
4b41ccd2a7 | ||
|
|
2846163d9e | ||
|
|
03b8d1afa0 | ||
|
|
42fca1bb39 | ||
|
|
d3a82f28ce | ||
|
|
d8536d3274 | ||
|
|
136d0d1fa1 | ||
|
|
49faa51e11 | ||
|
|
7d18ffe316 | ||
|
|
db1adf47bb | ||
|
|
bbdcd2825f | ||
|
|
495c21dc5c | ||
|
|
575b6df1b8 | ||
|
|
f0c1e3d7d9 | ||
|
|
b616b203b8 | ||
|
|
92a6308300 | ||
|
|
021374c62c | ||
|
|
9475798205 |
No files matched your search
@@ -96,6 +96,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"db/merge_operator.cc",
|
||||
"db/multi_scan.cc",
|
||||
"db/output_validator.cc",
|
||||
"db/periodic_compaction_phaser.cc",
|
||||
"db/periodic_task_scheduler.cc",
|
||||
"db/range_del_aggregator.cc",
|
||||
"db/range_tombstone_fragmenter.cc",
|
||||
@@ -205,6 +206,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"table/block_based/block_cache.cc",
|
||||
"table/block_based/block_prefetcher.cc",
|
||||
"table/block_based/block_prefix_index.cc",
|
||||
"table/block_based/builtin_index_factory.cc",
|
||||
"table/block_based/data_block_footer.cc",
|
||||
"table/block_based/data_block_hash_index.cc",
|
||||
"table/block_based/filter_block_reader_common.cc",
|
||||
@@ -4613,6 +4615,12 @@ cpp_unittest_wrapper(name="bloom_test",
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="builtin_index_factory_test",
|
||||
srcs=["table/block_based/builtin_index_factory_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="cache_reservation_manager_test",
|
||||
srcs=["cache/cache_reservation_manager_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
|
||||
@@ -8,12 +8,18 @@ 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. Avoid static_cast, reinterpret_cast, and C-style casts; static_cast_with_check, up_cast, and lossless_cast from cast_util.h are preferred.
|
||||
**Clarity and Readability:** Write clear, self-documenting code. Use meaningful variable names, add comments for complex logic, and structure code to minimize cognitive load. Avoid clever tricks that sacrifice readability for marginal performance gains unless absolutely necessary.
|
||||
|
||||
**Consistent Style:** Follow existing code style conventions. RocksDB uses `.clang-format` for formatting, specific naming conventions, and structural patterns. Deviations from these patterns are frequently flagged in reviews.
|
||||
|
||||
**Error Handling:** Ensure robust error handling throughout the codebase. Use RocksDB's `Status` type consistently, propagate errors appropriately, and avoid silently ignoring failures. Reviewers pay close attention to edge cases and failure modes.
|
||||
|
||||
**Refactoring traps:** At least in production code, avoid constructs that could allow existing code to unexpectedly, quietly change meaning.
|
||||
Specifically:
|
||||
* Avoid new defaulted parameters. This is the #1 trap on refactoring!
|
||||
* Avoid static_cast, reinterpret_cast, and C-style casts; static_cast_with_check, up_cast, and lossless_cast from cast_util.h are preferred.
|
||||
* Avoid declared type `auto` (`auto&`, `auto*` OK).
|
||||
|
||||
### Testing Philosophy
|
||||
|
||||
**Comprehensive Coverage:** Every change should include appropriate test coverage. This includes unit tests for isolated functionality, integration tests for component interactions, and stress tests for concurrency and performance validation. Reviewers will ask for additional tests if coverage is insufficient.
|
||||
@@ -32,7 +38,7 @@ This document provides guidance for generating and reviewing code in the RocksDB
|
||||
|
||||
**Memory Copy:** Avoid unnecessary memory copies. Use move semantics, `std::string_view`, `Slice`, and pass-by-reference where appropriate. Be aware of implicit copies in STL containers and function returns. Prefer in-place operations over copy-and-modify patterns.
|
||||
|
||||
**CPU Cache Efficiency:** Design data structures and access patterns to be cache-friendly. Keep frequently accessed data together (data locality). Prefer sequential memory access over random access. Be mindful of cache line sizes (typically 64 bytes) and avoid false sharing in concurrent code. Consider struct packing and field ordering to improve cache utilization.
|
||||
**CPU Cache Efficiency:** Design data structures and access patterns to be cache-friendly. Keep frequently accessed data together (data locality). Prefer sequential memory access over random access. Be mindful of cache line sizes (CACHE_LINE_SIZE, typically 64 bytes) and avoid false sharing in concurrent code (see CacheAlignedWrapper). Especially when adding new data members, be mindful of ordering within structs and classes to reduce unnecessary padding (special care with bool members) and avoid larger-than-necessary types (e.g. prefer OptSlice to std::optional<Slice>).
|
||||
|
||||
**Loop Optimization:** Look for opportunities to collapse nested loops, reduce loop overhead, and minimize branch mispredictions. Hoist invariant computations out of loops. Consider loop unrolling for tight inner loops. Batch operations when possible to amortize per-operation overhead.
|
||||
|
||||
@@ -47,12 +53,14 @@ This document provides guidance for generating and reviewing code in the RocksDB
|
||||
- **Cold path** (executed rarely, e.g., DB open, configuration parsing, error handling): Maintainability and clarity are more important. Prefer readable code over micro-optimizations. Complex optimizations here add maintenance burden with negligible performance benefit.
|
||||
- **Warm path** (moderate frequency): Balance both concerns. Use profiling data to guide optimization decisions.
|
||||
|
||||
**Avoid Premature Optimization:** While performance is critical, focus on correctness first, then optimize based on profiling data. However, be performance-aware from the start—choosing the right algorithm and data structure upfront is not premature optimization. Use the hot path analysis above to decide how much optimization effort is warranted.
|
||||
**Avoid Premature Optimization:** Performance is known critical in places like inner loops of compaction, read path and write path. Elsewhere, try to find a "holy trinity" solution of *correct, simple, and reasonably efficient*. More complex solutions are only appropriate when profiling/benchmarking data shows a measurable payoff.
|
||||
|
||||
### API Design and Compatibility
|
||||
|
||||
**Backwards Compatibility:** RocksDB maintains strong backwards compatibility guarantees. Breaking changes are rare and require extensive justification. When deprecating features, follow the project's deprecation policy (typically spanning multiple releases).
|
||||
|
||||
**Behavioral Compatibility:** Behavior changes are human judgment calls that must be intentional, well-documented (including release notes), and well researched for potential downstream production impact. Agents and reviewers must call out potential gaps, ambiguity, or adverse impacts in intentionality, documentation, and production impact. For an existing accepted input—including an undocumented, discouraged, or historically ignored value—the analysis should compare old and new observable behavior, including performance and resource impact, identify known direct callers and stored configurations, and add or update regression coverage as appropriate.
|
||||
|
||||
**API Consistency:** New APIs should be consistent with existing patterns. Use similar naming conventions, parameter ordering, and return types. Reviewers will suggest changes to improve consistency with the broader codebase.
|
||||
|
||||
**Documentation:** Public APIs must be thoroughly documented, without unnecessary embelishment nor dwelling on implementation details nor project planning. When non-obvious, include usage examples, parameter descriptions, known bugs or limitations, notes on thread safety, performance characteristics, and compatibility considerations. Re-read comments for ambiguous terminology and phrasing, such as ambiguously re-purposed programming jargon.
|
||||
@@ -73,7 +81,7 @@ The database core handles write-ahead logging (WAL), memtables, compaction, and
|
||||
|
||||
**Testing:** Database core changes require extensive testing, including unit tests, integration tests, and stress tests. Test with various configurations, compaction styles, and concurrent workloads.
|
||||
|
||||
### Public Headers (`include`)
|
||||
### Public Headers / "Public API" (`include/rocksdb/`)
|
||||
|
||||
Public headers define RocksDB's API surface. Changes here have the highest compatibility impact.
|
||||
|
||||
@@ -302,6 +310,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 |
|
||||
@@ -316,6 +325,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
|
||||
@@ -366,6 +381,9 @@ rather than relying on libstdc++ transitive includes.
|
||||
### Adding new public API
|
||||
Refer to claude_md/add_public_api.md
|
||||
|
||||
### Changing existing public API behavior
|
||||
Follow the Behavioral Compatibility requirements above.
|
||||
|
||||
### Adding new option
|
||||
Refer to claude_md/add_option.md
|
||||
|
||||
@@ -395,7 +413,7 @@ rather than relying on libstdc++ transitive includes.
|
||||
* Release note should be kept short at high level for external user consumption.
|
||||
Release notes identify what users might care about most in a release. They
|
||||
are not exhaustive and are not a guide. PLEASE learn from past agents who
|
||||
ried to build elaborate release notes with implementation details and
|
||||
tried to build elaborate release notes with implementation details and
|
||||
elsewhere-documented nuance. That wastes time. Fight the bias that
|
||||
"my change" is important so must be worthy of release note mention.
|
||||
* If more than single markdown line, consider how their formatting will be
|
||||
@@ -411,7 +429,9 @@ rather than relying on libstdc++ transitive includes.
|
||||
multiple minutes.
|
||||
* Run `AUTO_CLEAN=1 ASSERT_STATUS_CHECKED=1 make check` to verify all Status
|
||||
objects are properly checked. This catches missing error handling that can
|
||||
lead to silent data corruption.
|
||||
lead to silent data corruption. Merely building with ASSERT_STATUS_CHECKED=1
|
||||
accomplishes nothing; it enables a runtime check. New unit tests missing
|
||||
Status checks is a common failure point, even from agents.
|
||||
|
||||
### Monitoring make check progress
|
||||
* Use `make check-progress` to get machine-parseable JSON progress while
|
||||
|
||||
@@ -838,6 +838,7 @@ set(SOURCES
|
||||
db/merge_operator.cc
|
||||
db/multi_scan.cc
|
||||
db/output_validator.cc
|
||||
db/periodic_compaction_phaser.cc
|
||||
db/periodic_task_scheduler.cc
|
||||
db/range_del_aggregator.cc
|
||||
db/range_tombstone_fragmenter.cc
|
||||
@@ -937,6 +938,7 @@ set(SOURCES
|
||||
table/block_based/block_cache.cc
|
||||
table/block_based/block_prefetcher.cc
|
||||
table/block_based/block_prefix_index.cc
|
||||
table/block_based/builtin_index_factory.cc
|
||||
table/block_based/data_block_hash_index.cc
|
||||
table/block_based/data_block_footer.cc
|
||||
table/block_based/filter_block_reader_common.cc
|
||||
@@ -1594,6 +1596,7 @@ if(WITH_TESTS)
|
||||
options/options_test.cc
|
||||
table/block_based/block_based_table_reader_test.cc
|
||||
table/block_based/block_test.cc
|
||||
table/block_based/builtin_index_factory_test.cc
|
||||
table/block_based/data_block_hash_index_test.cc
|
||||
table/block_based/full_filter_block_test.cc
|
||||
table/block_based/partitioned_filter_block_test.cc
|
||||
|
||||
+85
@@ -1,6 +1,91 @@
|
||||
# Rocksdb Change Log
|
||||
> NOTE: Entries for next release do not go here. Follow instructions in `unreleased_history/README.txt`
|
||||
|
||||
## 11.11.1 (09/24/2026)
|
||||
### Behavior Changes
|
||||
* `Iterator::Refresh()` now returns `NotSupported` on iterators from a secondary DB's `DB::NewIterators()` only, because those iterators are pinned to one cross-column-family read view. Iterators from `DB::NewIterator()` are unaffected; to advance a batch, call `DB::NewIterators()` again after `TryCatchUpWithPrimary()`.
|
||||
|
||||
### Bug Fixes
|
||||
* Fixed secondary DB `NewIterators()` returning iterators from different database states or blocking for `TryCatchUpWithPrimary()` by serving the last completed catch-up view.
|
||||
|
||||
## 11.11.0 (09/21/2026)
|
||||
### New Features
|
||||
* External tables now support DB-assigned global sequence numbers when `IngestExternalFileOptions::allow_global_seqno` is enabled, allowing overlapping external tables to be ingested across multiple levels when compactions are disabled. This includes the deprecated `write_global_seqno` path when the reader supplies the properties block's file offset.
|
||||
* Added checksum handoff support for SST file writes when `BlockBasedTableOptions::checksum` is `kCRC32c`, allowing block and trailer CRC32C checksums to be passed from the table builder to the file writer for buffered-data validation and filesystem in-flight write protection.
|
||||
* Enhanced `sst_dump --command=recompress` to be more powerful for benchmarking, including through automation with a new `--json` option for machine-readable output.
|
||||
|
||||
### Public API Changes
|
||||
* Changed the experimental async read statistics API by removing `DB::AsyncCallback::EnableStats()` and changing `OnComplete(const PerfContext*, const IOStatsContext*)` to `OnComplete()`. Callers must configure TLS statistics before each async or coroutine read. `DB::GetAsync()` and `DB::MultiGetAsync()` callbacks must copy per-operation statistics from TLS during `OnComplete()`; coroutine users must copy them from inside a `CoroStackableDB` hook before suspending again. Each operation leaves TLS statistics collection disabled.
|
||||
* Added `Compressor::MaybeOverrideCompressionOptions()` (EXPERIMENTAL), an omnibus hook letting a custom compressor adjust the subset of `CompressionOptions` that the SST builder applies rather than the compressor itself: `parallel_threads`, `max_compressed_bytes_per_kb`, `auto_skip`, and `auto_skip_min_sample_every` (and potentially more in the future). This deprecates `Compressor::GetRecommendedParallelThreads()`, which it subsumes; the default implementation still honors an overridden `GetRecommendedParallelThreads()` for compatibility.
|
||||
* Added experimental `CoroDB::CoGet()` and `CoroDB::CoMultiGet()` overloads for point reads from an `SstFileReader`.
|
||||
* Added experimental `DB::GetWithMetadata()` and `DB::MultiGetWithMetadata()` APIs with opt-in output metadata for key timestamps and explicit-snapshot newer-version detection.
|
||||
* Added a `DB::NewIterators()` overload that accepts parallel vectors of `ReadOptions` and column family handles, allowing each returned iterator to use independent read settings while sharing a consistent database state. The vectors must have the same size, all snapshots must be null or identical, and all `tailing` settings must match.
|
||||
* Added `ReadOptions::custom_context`, an opaque, non-owning pointer passed through table reads so external table implementations can consume application-defined read options.
|
||||
|
||||
### Behavior Changes
|
||||
* `ExternalTableReader::MultiGet()` is now used by RocksDB's `DB::MultiGet()` path. Previously, the external table adapter used `TableReader`'s fallback, which called `ExternalTableReader::Get()` separately for each key.
|
||||
* `DB::OpenAndCompact()` now requires `output_directory` to be a simple name for a new subdirectory when the new `use_session_tmp_dir_for_remote_compaction` DB option is enabled. The option places that subdirectory under the canonical `<dbname>/session_tmp/` root, and `DB::Open()` deletes stale contents under the root after recovery. The disabled default preserves the existing `<dbname>/<output_directory>` layout without automatic cleanup. Previously, remote-compaction output directories were created directly under `<dbname>`, which made it challenging to safely identify and delete obsolete files.
|
||||
* Several block-based SST table properties now better support compression benchmarking and optimization: `uncompressed_data_size` is now persisted in the file (previously it was computed only at write time and read back as 0); a new `lsm_info_at_creation` property records the LSM level and "bottommost" status at the file's creation, distinguishing "not applicable" (e.g. SstFileWriter files) from "unknown" (older files); and the `compression_options` property now records the full compression manager configuration instead of only its name.
|
||||
|
||||
### Bug Fixes
|
||||
* Fixed a usually-minor performance bug where `Cache::ApplyToAllEntries()` used single-entry batches instead of the intended 256-entry default for `ApplyToAllEntriesOptions::average_entries_per_lock`. All of the Cache implementations (LRU and HCC) benefit from the fix, but the disfavored LRU cache was especially impacted by repeated mutex acquisition and release.
|
||||
* Fixed stats-disabled coroutine and asynchronous reads inheriting enabled TLS statistics state when interleaved with stats-enabled reads on the same executor. Each read now uses its captured per-call configuration, while enabled request counters remain isolated across suspensions.
|
||||
* Fixed a bug where a remote compaction (`CompactionService` / `DB::OpenAndCompact`) requested with `OpenAndCompactOptions::allow_resumption=true` but for which resumption was internally disabled (incompatible with `paranoid_file_checks` or output-iteration verification) could fail a retried attempt, because the output directory was not cleaned before starting fresh and output files left by a previously interrupted attempt collided with the reused file numbers.
|
||||
* Fixed a bug where a remote compaction (`CompactionService` / `DB::OpenAndCompact`) could fail when the primary rotated its MANIFEST while the worker was opening the primary's directory read-only. The open reads `CURRENT`, and the primary publishes a new MANIFEST by renaming a freshly written `CURRENT` over the old one; on file systems without POSIX read-after-unlink semantics (remote/object stores) the replaced object is then reported as gone (`PathNotFound`/`NotFound`, or `TryAgain` for the MANIFEST equivalent) instead of remaining readable. `DB::OpenAndCompact()` now retries the secondary open a bounded, configurable number of times for those transient statuses, so a concurrent rotation no longer fails the compaction (which, for a `CompactionService` that does not fall back to a local compaction, became a background error on the primary).
|
||||
* Fixed secondary DB `NewIterators()` returning iterators from different database states when `TryCatchUpWithPrimary()` runs concurrently.
|
||||
* Fixed a bug where a `SeekToFirst()` on an iterator previously used for a `Seek()` could end the scan early and silently omit keys with `ReadOptions::async_io`, or otherwise run with readahead effectively disabled. Affects scans using `ReadOptions::prefix_same_as_start` together with `ReadOptions::auto_readahead_size` (enabled by default); memory-mapped reads are unaffected.
|
||||
|
||||
### Performance Improvements
|
||||
* `DB::MultiGetEntityLazy()` now reads all keys of a batch under a single shared per-column-family SuperVersion pin, and a single `LazyWideColumnsBatch::MultiResolve()` coalesces the resulting blob reads across keys: one read per blob file (and one per SST for embedded blobs), for whole-column and byte-range reads alike, instead of reading each key's blobs separately.
|
||||
|
||||
## 11.10.0 (08/28/2026)
|
||||
### New Features
|
||||
* Added `BlockBasedTableOptions::optimize_key_common_prefix` as part of new `format_version=8`. When enabled, each data block and index block stores the common user-key prefix of its keys once instead of at every restart point, reducing block size and block-cache footprint for prefix-heavy data and speeding up Seek for bytewise-ordered keys.
|
||||
* A new experimental feature spreads out periodic (time-based) compaction trigger times across a fleet of similarly-configured DBs to avoid recurring "thundering herd" triggers, especially with remote compaction. See the new experimental DB option `periodic_compaction_phase_recovery_percent`.
|
||||
|
||||
### Public API Changes
|
||||
* `IngestExternalFileArg::atomic_replace_range` now supports a range that only partially overlaps existing files, instead of failing. Requires universal compaction and a table factory that supports range deletion; other configurations still fail on partial overlap. The range is also now specified as half-open (`start` inclusive, `limit` exclusive), resolving a previously documented ambiguity in the upper bound.
|
||||
* Added an immutable `external_table_config` option that is persisted in OPTIONS files and passed to external table factories during bootstrap.
|
||||
|
||||
### Bug Fixes
|
||||
* Fixed a liveness bug where RocksDB could remain indefinitely in recovery from a soft background error when concurrent writes continuously generated normal flush requests.
|
||||
* Fixed a race in online error recovery where a concurrent higher-severity I/O error could leave a stale retry-flush plan active, prevent the required WAL switch, and create duplicate internal WAL history detected on DB reopen.
|
||||
* Fix "bottommost files" infinite compaction loops (`kBottommostFiles`). This happens with the unsupported combination of leveled compaction and `preserve_internal_time_seconds` / `preclude_last_level_data_seconds`, and in an edge case with user-defined timestamps when `full_history_ts_low` is unset. The underlying logic is now more unified to reduce risk of a similar divergence in logic in the future.
|
||||
* Fixed `reuse_manifest_on_open` to create a fresh MANIFEST instead of appending after recovery sees an incomplete atomic group at the MANIFEST tail.
|
||||
* Starting remote compaction (`CompactionService` / `DB::OpenAndCompact`) is now more efficient and more reliable, by avoiding unnecessary file operations (latency) while using more accurate consistency / precondition checks, both avoiding some spurious failures and rejecting more legitimate inconsistencies. New DBOption `remote_compaction_manifest_floor` can be disabled in case of unexpected problems with the change.
|
||||
* Fixed unbounded retention of recovered transaction state by a long-lived secondary opened with `allow_2pc`.
|
||||
* Fixed stale reads on secondary instances, where entries replayed from a WAL that the primary had already flushed could shadow the newer values in the flushed files indefinitely. Also fixed a case where a secondary could stop returning such a key altogether while the primary's flushed file holding it was unreadable.
|
||||
* Fixed subset checkpoints to discard tolerated MANIFEST tail corruption before appending column-family drop records.
|
||||
|
||||
### Performance Improvements
|
||||
* Improved compaction CPU efficiency for column families using an `SstPartitioner` (such as the built-in `SstPartitionerFixedPrefix`): when the partitioner can express its next boundary as a key prefix via the new `SstPartitioner::ShouldPartitionByPrefix()` API, compaction now checks that cached prefix inline instead of copying every user key and making a virtual `ShouldPartition()` call for each key-value written to an SST.
|
||||
|
||||
## 11.9.0 (08/14/2026)
|
||||
### New Features
|
||||
* Added an integrated and improved compression auto-skip feature (new CompressionOptions, still experimental) that saves CPU by skipping compression when and where the overall ratio is inadequate. This improves on the previous auto-skip compression manager by (a) taking achieved compression ratios into account, (b) using a continuously evolving payoff estimate, and (c) combining natively with parallel compression.
|
||||
* Added the `blob_file_writable_file_max_buffer_size` mutable column family option. When set to a non-zero value, blob-file writers use it as their `WritableFileWriter` max buffer size instead of inheriting `DBOptions::writable_file_max_buffer_size`.
|
||||
* Added an experimental lazy wide-column read API, `DB::GetEntityLazy()` / `DB::MultiGetEntityLazy()` (see `include/rocksdb/lazy_wide_columns.h`). The returned `LazyWideColumns` leaves blob-backed columns as unresolved references and, like an iterator, stays valid after the call returns, so the caller resolves only the columns -- and only the byte ranges -- it needs; a byte-range read of an uncompressed blob (separate-file or embedded) reads just those bytes from storage. Adds `Env::IOActivity::kLazyResolve` and `rocksdb.blobdb.lazy.*` statistics. Requires `max_open_files == -1`.
|
||||
* Added DB property `rocksdb.num-running-remote-compactions`, reporting the number of compaction service jobs currently waiting in `CompactionService::Wait()`.
|
||||
* Added DB property `rocksdb.num-unscheduled-compactions`, reporting compactions waiting in the DB's internal compaction queue but not yet assigned to a background job.
|
||||
* Added `DB::AbortCompactions()` and `DB::ResumeCompactions()` for per-column-family compaction abort and resume, with the same active-abort semantics as the DB-wide `AbortAllCompactions()`/`ResumeAllCompactions()` but scoped to a single column family. Calls are reference-counted independently per column family and compose with the DB-wide abort.
|
||||
|
||||
### Public API Changes
|
||||
* Add IndexFactory compatibility names for the existing user-defined index API.
|
||||
* Added the experimental `CoroDB` capability, exposed by `DB::GetCoroDB()`, with lazy native coroutine variants of `Get` and `MultiGet`. The existing `DB::GetAsync()` and `DB::MultiGetAsync()` APIs dispatch through it.
|
||||
* Added `rocksdb_batched_multi_get_pinned_cf()` to return batch-owned pinned MultiGet values and per-key errors without allocating one wrapper per found value.
|
||||
* 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.
|
||||
* `ReadOptions::table_filter` is now a *pointer* to a caller-owned `std::function` instead of a `std::function` value, to improve efficiency in copying and destroying `ReadOptions` objects (which RocksDB does internally). This is a breaking change for users of this relatively obscure option, who will now need to declare a `std::function` that outlives the read. As a side effect of `ReadOptions` being trivially copyable, new but legitimate compiler warnings may surface.
|
||||
* Removed the experimental `CreateAutoSkipCompressionManager()`. Auto-skip compression is now a first-class feature of the block-based table builder, controlled by the new experimental `CompressionOptions::auto_skip`.
|
||||
* Added `CoroStackableDB` for implementing wrappers over native coroutine reads and used it to add coroutine `Get` and `MultiGet` support to `DBWithTTL` and WritePrepared `TransactionDB`. A stackable wrapper advertises coroutine support only when its wrapped DB supports it; TTL value decoding and transaction visibility semantics are preserved.
|
||||
* Added a `kSstFileWriter` value to `TableFileCreationReason`, which `SstFileWriter` now reports instead of the catch-all `kMisc`. This, along with new `db_name` and `is_remote_compaction` fields in `FilterBuildingContext`, improves the contextual information available to custom filter and compression strategies.
|
||||
* Renamed `TransactionLogIterator` to `WalIterator` (and `include/rocksdb/transaction_log.h` to `include/rocksdb/wal_iterator.h`), because it iterates the write-ahead log and is unrelated to `TransactionDB`. The old names still work. `DB::GetUpdatesSince()` is unchanged. In Java, `WalIterator` is the new name and `TransactionLogIterator` remains as a subclass. Also documented existing behavior that was previously unstated: only writes that reached the WAL are visible (so `WriteOptions::disableWAL` and `IngestExternalFile()` leave holes that end iteration), what each iterator state means, and that `GetUpdatesSince()` silently starts later when the requested sequence number is unavailable.
|
||||
|
||||
### Bug Fixes
|
||||
* Fixed a bug where `BlobFileBuilder` passed an SST path to `OnBlobFileCompleted()` instead of the completed blob path during BlobDB garbage-collection compactions, causing incorrect `SstFileManager` accounting and compaction failures on filesystems where the SST was not yet pathname-visible.
|
||||
|
||||
### 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.
|
||||
|
||||
@@ -2018,6 +2018,9 @@ block_fetcher_test: table/block_fetcher_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
block_test: $(OBJ_DIR)/table/block_based/block_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
builtin_index_factory_test: $(OBJ_DIR)/table/block_based/builtin_index_factory_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
data_block_hash_index_test: $(OBJ_DIR)/table/block_based/data_block_hash_index_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
|
||||
@@ -212,6 +212,11 @@ uint64_t rocksdb_table_properties_separate_key_value_in_data_block(
|
||||
return props->rep.separate_key_value_in_data_block;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_table_properties_lsm_info_at_creation(
|
||||
const rocksdb_table_properties_t* props) {
|
||||
return props->rep.lsm_info_at_creation;
|
||||
}
|
||||
|
||||
const char* rocksdb_table_properties_db_id(
|
||||
const rocksdb_table_properties_t* props, size_t* size) {
|
||||
*size = props->rep.db_id.size();
|
||||
|
||||
@@ -151,6 +151,10 @@ extern ROCKSDB_LIBRARY_API uint64_t
|
||||
rocksdb_table_properties_separate_key_value_in_data_block(
|
||||
const rocksdb_table_properties_t* props);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API uint64_t
|
||||
rocksdb_table_properties_lsm_info_at_creation(
|
||||
const rocksdb_table_properties_t* props);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API const char* rocksdb_table_properties_db_id(
|
||||
const rocksdb_table_properties_t* props, size_t* size);
|
||||
|
||||
|
||||
@@ -816,6 +816,26 @@ const char* rocksdb_options_get_db_host_id(rocksdb_options_t* opt,
|
||||
return opt->rep.db_host_id.data();
|
||||
}
|
||||
|
||||
void rocksdb_options_set_remote_compaction_manifest_floor(
|
||||
rocksdb_options_t* opt, unsigned char v) {
|
||||
opt->rep.remote_compaction_manifest_floor = v;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_options_get_remote_compaction_manifest_floor(
|
||||
rocksdb_options_t* opt) {
|
||||
return opt->rep.remote_compaction_manifest_floor;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_use_session_tmp_dir_for_remote_compaction(
|
||||
rocksdb_options_t* opt, unsigned char v) {
|
||||
opt->rep.use_session_tmp_dir_for_remote_compaction = v;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_options_get_use_session_tmp_dir_for_remote_compaction(
|
||||
rocksdb_options_t* opt) {
|
||||
return opt->rep.use_session_tmp_dir_for_remote_compaction;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_lowest_used_cache_tier(rocksdb_options_t* opt, int v) {
|
||||
opt->rep.lowest_used_cache_tier =
|
||||
static_cast<decltype(opt->rep.lowest_used_cache_tier)>(v);
|
||||
@@ -856,6 +876,16 @@ uint64_t rocksdb_options_get_max_compaction_trigger_wakeup_seconds(
|
||||
return opt->rep.max_compaction_trigger_wakeup_seconds;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_periodic_compaction_phase_recovery_percent(
|
||||
rocksdb_options_t* opt, int v) {
|
||||
opt->rep.periodic_compaction_phase_recovery_percent = v;
|
||||
}
|
||||
|
||||
int rocksdb_options_get_periodic_compaction_phase_recovery_percent(
|
||||
rocksdb_options_t* opt) {
|
||||
return opt->rep.periodic_compaction_phase_recovery_percent;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_follower_refresh_catchup_period_ms(
|
||||
rocksdb_options_t* opt, uint64_t v) {
|
||||
opt->rep.follower_refresh_catchup_period_ms = v;
|
||||
@@ -2117,6 +2147,16 @@ int rocksdb_ingestexternalfileoptions_get_file_opening_threads(
|
||||
|
||||
/* OpenAndCompactOptions */
|
||||
|
||||
void rocksdb_open_and_compact_options_set_max_secondary_open_retries(
|
||||
rocksdb_open_and_compact_options_t* opt, uint32_t v) {
|
||||
opt->rep.max_secondary_open_retries = v;
|
||||
}
|
||||
|
||||
uint32_t rocksdb_open_and_compact_options_get_max_secondary_open_retries(
|
||||
rocksdb_open_and_compact_options_t* opt) {
|
||||
return opt->rep.max_secondary_open_retries;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_open_and_compact_options_get_allow_resumption(
|
||||
rocksdb_open_and_compact_options_t* opt) {
|
||||
return opt->rep.allow_resumption;
|
||||
@@ -2514,6 +2554,17 @@ double rocksdb_block_based_options_get_uniform_cv_threshold(
|
||||
return opt->rep.uniform_cv_threshold;
|
||||
}
|
||||
|
||||
void rocksdb_block_based_options_set_optimize_key_common_prefix(
|
||||
rocksdb_block_based_table_options_t* opt, int v) {
|
||||
opt->rep.optimize_key_common_prefix =
|
||||
static_cast<decltype(opt->rep.optimize_key_common_prefix)>(v);
|
||||
}
|
||||
|
||||
int rocksdb_block_based_options_get_optimize_key_common_prefix(
|
||||
rocksdb_block_based_table_options_t* opt) {
|
||||
return static_cast<int>(opt->rep.optimize_key_common_prefix);
|
||||
}
|
||||
|
||||
void rocksdb_block_based_options_set_enable_index_compression(
|
||||
rocksdb_block_based_table_options_t* opt, unsigned char v) {
|
||||
opt->rep.enable_index_compression = v;
|
||||
|
||||
@@ -247,6 +247,21 @@ extern ROCKSDB_LIBRARY_API void rocksdb_options_set_db_host_id(
|
||||
extern ROCKSDB_LIBRARY_API const char* rocksdb_options_get_db_host_id(
|
||||
rocksdb_options_t* opt, size_t* size);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_options_set_remote_compaction_manifest_floor(rocksdb_options_t* opt,
|
||||
unsigned char v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API unsigned char
|
||||
rocksdb_options_get_remote_compaction_manifest_floor(rocksdb_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_options_set_use_session_tmp_dir_for_remote_compaction(
|
||||
rocksdb_options_t* opt, unsigned char v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API unsigned char
|
||||
rocksdb_options_get_use_session_tmp_dir_for_remote_compaction(
|
||||
rocksdb_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_options_set_lowest_used_cache_tier(
|
||||
rocksdb_options_t* opt, int v);
|
||||
|
||||
@@ -267,6 +282,14 @@ extern ROCKSDB_LIBRARY_API const char*
|
||||
rocksdb_options_get_daily_offpeak_time_utc(rocksdb_options_t* opt,
|
||||
size_t* size);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_options_set_periodic_compaction_phase_recovery_percent(
|
||||
rocksdb_options_t* opt, int v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API int
|
||||
rocksdb_options_get_periodic_compaction_phase_recovery_percent(
|
||||
rocksdb_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_options_set_follower_refresh_catchup_period_ms(rocksdb_options_t* opt,
|
||||
uint64_t v);
|
||||
@@ -863,6 +886,14 @@ rocksdb_ingestexternalfileoptions_get_file_opening_threads(
|
||||
|
||||
/* OpenAndCompactOptions */
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_open_and_compact_options_set_max_secondary_open_retries(
|
||||
rocksdb_open_and_compact_options_t* opt, uint32_t v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API uint32_t
|
||||
rocksdb_open_and_compact_options_get_max_secondary_open_retries(
|
||||
rocksdb_open_and_compact_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API unsigned char
|
||||
rocksdb_open_and_compact_options_get_allow_resumption(
|
||||
rocksdb_open_and_compact_options_t* opt);
|
||||
@@ -1170,6 +1201,14 @@ extern ROCKSDB_LIBRARY_API double
|
||||
rocksdb_block_based_options_get_uniform_cv_threshold(
|
||||
rocksdb_block_based_table_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_block_based_options_set_optimize_key_common_prefix(
|
||||
rocksdb_block_based_table_options_t* opt, int v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API int
|
||||
rocksdb_block_based_options_get_optimize_key_common_prefix(
|
||||
rocksdb_block_based_table_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_block_based_options_set_enable_index_compression(
|
||||
rocksdb_block_based_table_options_t* opt, unsigned char v);
|
||||
|
||||
@@ -151,6 +151,12 @@ static void rocksdb_roundtrip_test_rocksdb_block_based_options(void) {
|
||||
rocksdb_block_based_options_set_optimize_filters_for_memory(obj, 0);
|
||||
CheckCondition(
|
||||
rocksdb_block_based_options_get_optimize_filters_for_memory(obj) == 0);
|
||||
rocksdb_block_based_options_set_optimize_key_common_prefix(obj, 1);
|
||||
CheckCondition(
|
||||
rocksdb_block_based_options_get_optimize_key_common_prefix(obj) == 1);
|
||||
rocksdb_block_based_options_set_optimize_key_common_prefix(obj, 0);
|
||||
CheckCondition(
|
||||
rocksdb_block_based_options_get_optimize_key_common_prefix(obj) == 0);
|
||||
rocksdb_block_based_options_set_partition_filters(obj, 1);
|
||||
CheckCondition(rocksdb_block_based_options_get_partition_filters(obj) == 1);
|
||||
rocksdb_block_based_options_set_partition_filters(obj, 0);
|
||||
@@ -586,6 +592,20 @@ static void rocksdb_roundtrip_test_rocksdb_livefiles_storage_info_options(
|
||||
rocksdb_livefiles_storage_info_options_destroy(obj);
|
||||
}
|
||||
|
||||
static void rocksdb_roundtrip_test_rocksdb_open_and_compact_options(void) {
|
||||
rocksdb_open_and_compact_options_t* obj =
|
||||
rocksdb_open_and_compact_options_create();
|
||||
rocksdb_open_and_compact_options_set_max_secondary_open_retries(obj, 1);
|
||||
CheckCondition(
|
||||
rocksdb_open_and_compact_options_get_max_secondary_open_retries(obj) ==
|
||||
1);
|
||||
rocksdb_open_and_compact_options_set_max_secondary_open_retries(obj, 0);
|
||||
CheckCondition(
|
||||
rocksdb_open_and_compact_options_get_max_secondary_open_retries(obj) ==
|
||||
0);
|
||||
rocksdb_open_and_compact_options_destroy(obj);
|
||||
}
|
||||
|
||||
static void rocksdb_roundtrip_test_rocksdb_optimistictransactiondb_options(
|
||||
void) {
|
||||
rocksdb_optimistictransactiondb_options_t* obj =
|
||||
@@ -833,6 +853,12 @@ static void rocksdb_roundtrip_test_rocksdb_options(void) {
|
||||
CheckCondition(rocksdb_options_get_paranoid_memory_checks(obj) == 1);
|
||||
rocksdb_options_set_paranoid_memory_checks(obj, 0);
|
||||
CheckCondition(rocksdb_options_get_paranoid_memory_checks(obj) == 0);
|
||||
rocksdb_options_set_periodic_compaction_phase_recovery_percent(obj, 1);
|
||||
CheckCondition(
|
||||
rocksdb_options_get_periodic_compaction_phase_recovery_percent(obj) == 1);
|
||||
rocksdb_options_set_periodic_compaction_phase_recovery_percent(obj, 0);
|
||||
CheckCondition(
|
||||
rocksdb_options_get_periodic_compaction_phase_recovery_percent(obj) == 0);
|
||||
rocksdb_options_set_persist_stats_to_disk(obj, 1);
|
||||
CheckCondition(rocksdb_options_get_persist_stats_to_disk(obj) == 1);
|
||||
rocksdb_options_set_persist_stats_to_disk(obj, 0);
|
||||
@@ -859,6 +885,12 @@ static void rocksdb_roundtrip_test_rocksdb_options(void) {
|
||||
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_remote_compaction_manifest_floor(obj, 1);
|
||||
CheckCondition(rocksdb_options_get_remote_compaction_manifest_floor(obj) ==
|
||||
1);
|
||||
rocksdb_options_set_remote_compaction_manifest_floor(obj, 0);
|
||||
CheckCondition(rocksdb_options_get_remote_compaction_manifest_floor(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);
|
||||
@@ -905,6 +937,12 @@ static void rocksdb_roundtrip_test_rocksdb_options(void) {
|
||||
CheckCondition(rocksdb_options_get_use_fsync(obj) == 1);
|
||||
rocksdb_options_set_use_fsync(obj, 0);
|
||||
CheckCondition(rocksdb_options_get_use_fsync(obj) == 0);
|
||||
rocksdb_options_set_use_session_tmp_dir_for_remote_compaction(obj, 1);
|
||||
CheckCondition(
|
||||
rocksdb_options_get_use_session_tmp_dir_for_remote_compaction(obj) == 1);
|
||||
rocksdb_options_set_use_session_tmp_dir_for_remote_compaction(obj, 0);
|
||||
CheckCondition(
|
||||
rocksdb_options_get_use_session_tmp_dir_for_remote_compaction(obj) == 0);
|
||||
rocksdb_options_set_verify_manifest_content_on_close(obj, 1);
|
||||
CheckCondition(rocksdb_options_get_verify_manifest_content_on_close(obj) ==
|
||||
1);
|
||||
@@ -1363,6 +1401,7 @@ static void rocksdb_run_generated_roundtrip_tests(void) {
|
||||
rocksdb_roundtrip_test_rocksdb_flushwaloptions();
|
||||
rocksdb_roundtrip_test_rocksdb_ingestexternalfileoptions();
|
||||
rocksdb_roundtrip_test_rocksdb_livefiles_storage_info_options();
|
||||
rocksdb_roundtrip_test_rocksdb_open_and_compact_options();
|
||||
rocksdb_roundtrip_test_rocksdb_optimistictransactiondb_options();
|
||||
rocksdb_roundtrip_test_rocksdb_options();
|
||||
rocksdb_roundtrip_test_rocksdb_readoptions();
|
||||
|
||||
Vendored
+1
-1
@@ -245,7 +245,7 @@ class ShardedCache : public ShardedCacheBase {
|
||||
std::unique_ptr<size_t[]> states(new size_t[num_shards]{});
|
||||
|
||||
size_t aepl = opts.average_entries_per_lock;
|
||||
aepl = std::min(aepl, size_t{1});
|
||||
aepl = std::max(aepl, size_t{1});
|
||||
|
||||
bool remaining_work;
|
||||
do {
|
||||
|
||||
@@ -4,6 +4,30 @@
|
||||
Thorough multi-agent code review for RocksDB commits following CLAUDE.md guidelines,
|
||||
with structured codebase exploration, inter-agent debate, and verification.
|
||||
|
||||
## Behavioral Compatibility Review Perspective
|
||||
|
||||
Behavior changes are human judgment calls. They must be intentional,
|
||||
well-documented (including release notes), and researched for potential
|
||||
downstream production impact. Agents and reviewers must call out concrete gaps,
|
||||
ambiguity, or adverse impacts in the change's intent, documentation, and
|
||||
production-impact analysis.
|
||||
|
||||
When a diff changes how an existing accepted public or serialized input is
|
||||
interpreted—including an undocumented, discouraged, or historically ignored
|
||||
value—reviewers must:
|
||||
- Compare the old and new observable behavior, including performance and
|
||||
resource impact.
|
||||
- Check that the rationale and user-facing documentation clearly describe the
|
||||
change.
|
||||
- Identify known direct callers and stored configurations that may depend on
|
||||
the previous behavior.
|
||||
- Check for appropriate regression coverage, or a documented reason why such
|
||||
coverage is not practical.
|
||||
|
||||
A behavior change is not automatically a defect. Report a finding when the
|
||||
intent, documentation, downstream-impact analysis, or mitigation has a concrete
|
||||
gap or ambiguity that could lead to an unintended compatibility regression.
|
||||
|
||||
IMPORTANT — Where the review goes: Your FINAL RESPONSE TEXT is the review.
|
||||
The PR comment is generated verbatim from your final response, so the
|
||||
complete, polished review MUST appear there in full. Do NOT end your turn with
|
||||
@@ -393,8 +417,8 @@ Write findings to `findings-caller-audit.md`.
|
||||
- Documentation completeness
|
||||
- Deprecation policy compliance
|
||||
- Forward compatibility (struct extensibility)
|
||||
- **Behavioral compatibility**: Do changed semantics (e.g., kOutOfBound vs
|
||||
kUnknown) break implicit contracts with callers?
|
||||
- Apply the Behavioral Compatibility Review Perspective above to changes in
|
||||
observable semantics and implicit caller contracts.
|
||||
|
||||
#### Agent: Serialization & Deserialization Reviewer
|
||||
- Format correctness and versioning
|
||||
|
||||
@@ -25,6 +25,7 @@ CRASHTEST_PY=$(PYTHON) -u tools/db_crashtest.py --stress_cmd=$(DB_STRESS_CMD) --
|
||||
crash_test_with_multiops_wup_txn \
|
||||
crash_test_with_optimistic_txn \
|
||||
crash_test_with_tiered_storage \
|
||||
liveness_crash_test \
|
||||
blackbox_crash_test blackbox_crash_test_with_atomic_flush \
|
||||
blackbox_crash_test_with_wc_txn blackbox_crash_test_with_wp_txn \
|
||||
blackbox_crash_test_with_wup_txn \
|
||||
@@ -94,6 +95,9 @@ crash_test_with_multiops_wp_txn: $(DB_STRESS_CMD)
|
||||
crash_test_with_multiops_wup_txn: $(DB_STRESS_CMD)
|
||||
$(CRASHTEST_MAKE) blackbox_crash_test_with_multiops_wup_txn
|
||||
|
||||
liveness_crash_test: $(DB_STRESS_CMD)
|
||||
$(CRASHTEST_PY) liveness $(CRASH_TEST_EXT_ARGS)
|
||||
|
||||
blackbox_crash_test: $(DB_STRESS_CMD)
|
||||
$(CRASHTEST_PY) --simple blackbox $(CRASH_TEST_EXT_ARGS)
|
||||
$(CRASHTEST_PY) blackbox $(CRASH_TEST_EXT_ARGS)
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <limits>
|
||||
|
||||
#include "rocksdb/rocksdb_namespace.h"
|
||||
|
||||
@@ -31,4 +33,43 @@ constexpr uint64_t kInvalidBlobFileNumber = 0;
|
||||
constexpr uint64_t kCurrentFileBlobIndexFileNumber = kInvalidBlobFileNumber;
|
||||
static_assert(kCurrentFileBlobIndexFileNumber == kInvalidBlobFileNumber);
|
||||
|
||||
// Sentinel range length meaning "the whole blob value" for the unified
|
||||
// blob-read primitives, which take a range descriptor {range_offset,
|
||||
// range_length}; range_length == kWholeBlobLength selects the whole value and
|
||||
// any other value selects the strict sub-range
|
||||
// [range_offset, range_offset + range_length).
|
||||
//
|
||||
// This is the internal counterpart of the public kLazyWholeColumn
|
||||
// (include/rocksdb/lazy_wide_columns.h); both are the maximum size_t. The lazy
|
||||
// resolver does not forward the public value as a range descriptor -- it
|
||||
// normalizes a whole-column request to kWholeBlobLength -- so the two are not
|
||||
// required to be equal, but both rely on the sentinel exceeding any real
|
||||
// blob/column size so a "whole" request is never mistaken for a strict
|
||||
// sub-range. Keep them in sync.
|
||||
inline constexpr size_t kWholeBlobLength = std::numeric_limits<size_t>::max();
|
||||
|
||||
// How aggressively a blob read verifies the record's checksum. Derived once at
|
||||
// the lazy read-path boundary (ReadPathBlobResolver) from
|
||||
// (ReadOptions::verify_checksums, LazyColumnReadRequest::force_verify) and
|
||||
// threaded down through the blob-read primitives, replacing the earlier mix of
|
||||
// a verify_checksums bool and a separate force-verify method. (Not literally a
|
||||
// constant, but kept here as a small shared blob-read enum rather than its own
|
||||
// header.)
|
||||
//
|
||||
// - kVerifyIfPresent: verify the whole-record checksum whenever the format
|
||||
// provides one, even if ReadOptions::verify_checksums is off. A byte-range
|
||||
// request under this policy is escalated to a whole-record read (a strict
|
||||
// sub-range cannot cover the checksum). This is the public force_verify.
|
||||
// - kVerifyIfNoAmplification: verify when the checksummable unit is already
|
||||
// being read -- a whole-value read verifies (the check is "free"), while a
|
||||
// strict sub-range read skips verification rather than amplify the read to
|
||||
// the whole record. Corresponds to verify_checksums on, force_verify off.
|
||||
// - kSkip: never verify. Corresponds to verify_checksums off, force_verify
|
||||
// off.
|
||||
enum class BlobVerifyPolicy : uint8_t {
|
||||
kVerifyIfPresent,
|
||||
kVerifyIfNoAmplification,
|
||||
kSkip,
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
+41
-36
@@ -44,43 +44,41 @@ Status VersionBlobFetcherBase::FetchBlob(const Slice& user_key,
|
||||
prefetch_buffer, blob_value, bytes_read);
|
||||
}
|
||||
|
||||
Status VersionBlobFetcherBase::FetchBlobRange(const Slice& user_key,
|
||||
const BlobIndex& blob_index,
|
||||
uint64_t range_offset,
|
||||
size_t range_length,
|
||||
PinnableSlice* blob_value,
|
||||
uint64_t* bytes_read) const {
|
||||
// Only ever called when SupportsRangeRead() holds (a Version to read through,
|
||||
// no direct-write fallback); the direct-write path resolves whole records and
|
||||
// has no range variant.
|
||||
assert(SupportsRangeRead());
|
||||
assert(version_);
|
||||
return version_->GetBlobRange(read_options(), user_key, blob_index,
|
||||
range_offset, range_length, blob_value,
|
||||
bytes_read);
|
||||
}
|
||||
|
||||
Status VersionBlobFetcherBase::FetchBlobForceVerify(
|
||||
const Slice& user_key, const BlobIndex& blob_index,
|
||||
FilePrefetchBuffer* prefetch_buffer, PinnableSlice* blob_value,
|
||||
uint64_t* bytes_read) const {
|
||||
if (read_options().verify_checksums) {
|
||||
// Verification already happens on the normal path.
|
||||
Status VersionBlobFetcherBase::FetchBlobRange(
|
||||
const Slice& user_key, const BlobIndex& blob_index, uint64_t range_offset,
|
||||
size_t range_length, BlobVerifyPolicy verify_policy,
|
||||
PinnableSlice* blob_value, uint64_t* bytes_read) const {
|
||||
if (range_length == kWholeBlobLength) {
|
||||
// Whole-value read. Delegate to FetchBlob (which handles the direct-write
|
||||
// fallback). For kVerifyIfPresent, force whole-record checksum verification
|
||||
// even when ReadOptions::verify_checksums is off, via a transient fetcher
|
||||
// over a verify-enabled copy of the read options (both locals outlive the
|
||||
// borrowed-options VersionBlobFetcher).
|
||||
constexpr FilePrefetchBuffer* prefetch_buffer = nullptr;
|
||||
if (verify_policy == BlobVerifyPolicy::kVerifyIfPresent &&
|
||||
!read_options().verify_checksums) {
|
||||
ReadOptions verify_read_options = read_options();
|
||||
verify_read_options.verify_checksums = true;
|
||||
VersionBlobFetcher verifying_fetcher(version_, verify_read_options,
|
||||
blob_file_cache_,
|
||||
allow_write_path_fallback_);
|
||||
return verifying_fetcher.FetchBlob(user_key, blob_index, prefetch_buffer,
|
||||
blob_value, bytes_read);
|
||||
}
|
||||
return FetchBlob(user_key, blob_index, prefetch_buffer, blob_value,
|
||||
bytes_read);
|
||||
}
|
||||
|
||||
// Force verification via a transient fetcher over a verify-enabled copy of
|
||||
// the read options. `verify_read_options` outlives `verifying_fetcher` (both
|
||||
// are local to this call), so the borrowed-options VersionBlobFetcher is
|
||||
// safe.
|
||||
ReadOptions verify_read_options = read_options();
|
||||
verify_read_options.verify_checksums = true;
|
||||
VersionBlobFetcher verifying_fetcher(version_, verify_read_options,
|
||||
blob_file_cache_,
|
||||
allow_write_path_fallback_);
|
||||
return verifying_fetcher.FetchBlob(user_key, blob_index, prefetch_buffer,
|
||||
blob_value, bytes_read);
|
||||
// Strict sub-range read: value bytes only, no whole-record checksum, no cache
|
||||
// fill. Only ever chosen when SupportsRangeRead() holds (a Version to read
|
||||
// through, no direct-write fallback) and verification is not forced (a strict
|
||||
// sub-range cannot cover the whole-record checksum).
|
||||
assert(SupportsRangeRead());
|
||||
assert(version_);
|
||||
assert(verify_policy != BlobVerifyPolicy::kVerifyIfPresent);
|
||||
return version_->GetBlobRange(read_options(), user_key, blob_index,
|
||||
range_offset, range_length, blob_value,
|
||||
bytes_read);
|
||||
}
|
||||
|
||||
Status EmbeddedAwareBlobFetcher::FetchBlob(const Slice& user_key,
|
||||
@@ -94,9 +92,16 @@ Status EmbeddedAwareBlobFetcher::FetchBlob(const Slice& user_key,
|
||||
// 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);
|
||||
// account their own BLOB_DB_* stats via BlobSource). This is a whole-value
|
||||
// read; the verify policy follows read_options().verify_checksums (the
|
||||
// decorator carries no separate force-verify signal).
|
||||
const BlobVerifyPolicy verify_policy =
|
||||
read_options().verify_checksums
|
||||
? BlobVerifyPolicy::kVerifyIfNoAmplification
|
||||
: BlobVerifyPolicy::kSkip;
|
||||
return same_file_reader_->GetSameFileBlob(
|
||||
read_options(), blob_index,
|
||||
/*range_offset=*/0, kWholeBlobLength, verify_policy, blob_value);
|
||||
}
|
||||
|
||||
if (base_ == nullptr) {
|
||||
|
||||
+23
-27
@@ -7,6 +7,7 @@
|
||||
|
||||
#include <utility>
|
||||
|
||||
#include "db/blob/blob_constants.h"
|
||||
#include "rocksdb/options.h"
|
||||
#include "rocksdb/status.h"
|
||||
|
||||
@@ -63,37 +64,26 @@ class VersionBlobFetcherBase : public BlobFetcher {
|
||||
PinnableSlice* blob_value,
|
||||
uint64_t* bytes_read) const override;
|
||||
|
||||
// Fetches a byte sub-range [range_offset, range_offset + range_length) of an
|
||||
// *uncompressed*, separate-file blob's value into *blob_value, reading only
|
||||
// those bytes on a cache miss (see Version::GetBlobRange /
|
||||
// BlobSource::GetBlobRange). Only valid when SupportsRangeRead() is true (a
|
||||
// Version to read through and no direct-write fallback); callers check that
|
||||
// first and take the whole-value FetchBlob path otherwise. Not routed through
|
||||
// the same-file (embedded) decorator -- same-file range reads are a later
|
||||
// phase.
|
||||
// Fetches the value of a separate-file blob reference into *blob_value,
|
||||
// applying `verify_policy` (see BlobVerifyPolicy). range_length ==
|
||||
// kWholeBlobLength reads the whole value (delegating to FetchBlob, which also
|
||||
// handles the direct-write fallback, and forcing whole-record checksum
|
||||
// verification for kVerifyIfPresent even when ReadOptions::verify_checksums
|
||||
// is off). Any other length reads only the sub-range [range_offset,
|
||||
// range_offset + range_length) directly from the blob file, reading just
|
||||
// those bytes on a cache miss -- no record-header/key read, no whole-record
|
||||
// checksum, no cache fill (see Version::GetBlobRange /
|
||||
// BlobSource::GetBlobRange). A strict sub-range is only valid when
|
||||
// SupportsRangeRead() is true and verify_policy != kVerifyIfPresent
|
||||
// (verifying a strict sub-range would have to amplify the read to the whole
|
||||
// record); callers check that first and take the whole-value path otherwise.
|
||||
// Not routed through the same-file (embedded) decorator -- same-file
|
||||
// references are resolved by the caller through a SameFileBlobReader.
|
||||
Status FetchBlobRange(const Slice& user_key, const BlobIndex& blob_index,
|
||||
uint64_t range_offset, size_t range_length,
|
||||
BlobVerifyPolicy verify_policy,
|
||||
PinnableSlice* blob_value, uint64_t* bytes_read) const;
|
||||
|
||||
// Fetches the whole blob with checksum verification forced on, regardless of
|
||||
// this fetcher's ReadOptions::verify_checksums. Used to honor a lazy read's
|
||||
// force_verify when verify_checksums is off: the caller wants the record's
|
||||
// checksum checked even though the global option would skip it. When
|
||||
// verify_checksums is already on this is just FetchBlob.
|
||||
//
|
||||
// TODO(lazy-blob-resolution-cleanup): fold FetchBlobRange /
|
||||
// FetchBlobForceVerify (and the whole-vs-range GetBlob twins throughout the
|
||||
// blob stack) into one parameterized primitive taking a BlobReadRequest -- a
|
||||
// range descriptor ({offset, len}; whole == [0, npos)) plus a 3-valued verify
|
||||
// policy (must-verify-if-present / verify-if-no-amplification / ignore). That
|
||||
// retires this method and the range/whole method pairs. See the lazy blob
|
||||
// resolution plan (deferred for feature velocity).
|
||||
Status FetchBlobForceVerify(const Slice& user_key,
|
||||
const BlobIndex& blob_index,
|
||||
FilePrefetchBuffer* prefetch_buffer,
|
||||
PinnableSlice* blob_value,
|
||||
uint64_t* bytes_read) const;
|
||||
|
||||
// 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
|
||||
@@ -111,6 +101,12 @@ class VersionBlobFetcherBase : public BlobFetcher {
|
||||
return version_ != nullptr && !allow_write_path_fallback_;
|
||||
}
|
||||
|
||||
// The Version this fetcher reads separate-file blob references through (may
|
||||
// be null for a direct-write-fallback-only fetcher). Used by the cross-key
|
||||
// lazy coalescing path to group and dispatch reads via
|
||||
// Version::MultiGetBlobLazy.
|
||||
const Version* version() const { return version_; }
|
||||
|
||||
protected:
|
||||
VersionBlobFetcherBase(const Version* version, BlobFileCache* blob_file_cache,
|
||||
bool allow_write_path_fallback)
|
||||
|
||||
+202
-7
@@ -296,7 +296,11 @@ Status BlobFileReader::ReadFromFile(const RandomAccessFileReader* file_reader,
|
||||
}
|
||||
|
||||
if (slice->size() != read_size) {
|
||||
return Status::Corruption("Failed to read data from blob file");
|
||||
return Status::Corruption("Incomplete blob read from " +
|
||||
file_reader->file_name() + " at offset " +
|
||||
std::to_string(read_offset) + ": expected " +
|
||||
std::to_string(read_size) + " bytes, got " +
|
||||
std::to_string(slice->size()));
|
||||
}
|
||||
|
||||
return Status::OK();
|
||||
@@ -330,7 +334,10 @@ Status BlobFileReader::GetBlob(
|
||||
|
||||
if (!IsValidBlobOffset(offset, key_size, value_size, file_size_,
|
||||
has_footer_)) {
|
||||
return Status::Corruption("Invalid blob offset");
|
||||
return Status::Corruption(
|
||||
"Invalid blob offset " + std::to_string(offset) + " for value size " +
|
||||
std::to_string(value_size) + " in " + file_reader_->file_name() +
|
||||
" (file size " + std::to_string(file_size_) + ")");
|
||||
}
|
||||
|
||||
if (compression_type != compression_type_) {
|
||||
@@ -438,12 +445,18 @@ Status BlobFileReader::GetBlobRange(const ReadOptions& read_options,
|
||||
// performs); this guards the sub-range read below.
|
||||
if (!IsValidBlobOffset(offset, key_size, value_size, file_size_,
|
||||
has_footer_)) {
|
||||
return Status::Corruption("Invalid blob offset");
|
||||
return Status::Corruption(
|
||||
"Invalid blob offset " + std::to_string(offset) + " for value size " +
|
||||
std::to_string(value_size) + " in " + file_reader_->file_name() +
|
||||
" (file size " + std::to_string(file_size_) + ")");
|
||||
}
|
||||
|
||||
// The requested sub-range must lie within the value.
|
||||
if (range_offset > value_size || range_length > value_size - range_offset) {
|
||||
return Status::InvalidArgument("Blob range out of bounds");
|
||||
return Status::InvalidArgument(
|
||||
"Blob range [" + std::to_string(range_offset) + ", +" +
|
||||
std::to_string(range_length) + ") out of bounds for value size " +
|
||||
std::to_string(value_size) + " in " + file_reader_->file_name());
|
||||
}
|
||||
|
||||
// Read only the requested bytes, at the value's file position plus the range
|
||||
@@ -518,7 +531,10 @@ void BlobFileReader::MultiGetBlob(
|
||||
|
||||
if (!IsValidBlobOffset(offset, key_size, value_size, file_size_,
|
||||
has_footer_)) {
|
||||
*req->status = Status::Corruption("Invalid blob offset");
|
||||
*req->status = Status::Corruption(
|
||||
"Invalid blob offset " + std::to_string(offset) + " for value size " +
|
||||
std::to_string(value_size) + " in " + file_reader_->file_name() +
|
||||
" (file size " + std::to_string(file_size_) + ")");
|
||||
continue;
|
||||
}
|
||||
if (req->compression != compression_type_) {
|
||||
@@ -612,8 +628,11 @@ void BlobFileReader::MultiGetBlob(
|
||||
auto& read_req = read_reqs[j++];
|
||||
const auto& record_slice = read_req.result;
|
||||
if (read_req.status.ok() && record_slice.size() != read_req.len) {
|
||||
read_req.status =
|
||||
IOStatus::Corruption("Failed to read data from blob file");
|
||||
read_req.status = IOStatus::Corruption(
|
||||
"Incomplete blob read from " + file_reader_->file_name() +
|
||||
" at offset " + std::to_string(read_req.offset) + ": expected " +
|
||||
std::to_string(read_req.len) + " bytes, got " +
|
||||
std::to_string(record_slice.size()));
|
||||
}
|
||||
|
||||
*req->status = read_req.status;
|
||||
@@ -644,6 +663,182 @@ void BlobFileReader::MultiGetBlob(
|
||||
}
|
||||
}
|
||||
|
||||
void BlobFileReader::MultiGetBlobRange(
|
||||
const ReadOptions& read_options,
|
||||
autovector<std::pair<BlobRangeReadRequest*, std::unique_ptr<BlobContents>>>&
|
||||
blob_reqs,
|
||||
uint64_t* bytes_read) const {
|
||||
const size_t num_blobs = blob_reqs.size();
|
||||
assert(num_blobs > 0);
|
||||
assert(num_blobs <= MultiGetContext::MAX_BATCH_SIZE);
|
||||
|
||||
// Range reads are only issued for uncompressed blob references (a strict
|
||||
// sub-range of a compressed record cannot be decompressed in isolation).
|
||||
// Version::MultiGetBlobLazy rejects a compressed blob index before it reaches
|
||||
// here, so every request's blob index is uncompressed by contract; a
|
||||
// compressed blob file therefore means the blob index disagrees with the
|
||||
// file. Report that as the compression-type mismatch it is -- exactly like
|
||||
// the whole-value MultiGetBlob path -- rather than as a usage error. (Unlike
|
||||
// the single-read path, BlobSource has no per-request compression to check
|
||||
// for a range read, so this reader-level check is the range multi path's
|
||||
// compression backstop.)
|
||||
if (compression_type_ != kNoCompression) {
|
||||
for (auto& blob_req : blob_reqs) {
|
||||
assert(blob_req.first);
|
||||
assert(blob_req.first->status);
|
||||
*blob_req.first->status =
|
||||
Status::Corruption("Compression type mismatch when reading a blob");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#ifndef NDEBUG
|
||||
for (size_t i = 0; i < num_blobs - 1; ++i) {
|
||||
assert(blob_reqs[i].first->offset + blob_reqs[i].first->range_offset <=
|
||||
blob_reqs[i + 1].first->offset +
|
||||
blob_reqs[i + 1].first->range_offset);
|
||||
}
|
||||
#endif // !NDEBUG
|
||||
|
||||
std::vector<FSReadRequest> read_reqs;
|
||||
uint64_t total_len = 0;
|
||||
read_reqs.reserve(num_blobs);
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
BlobRangeReadRequest* const req = blob_reqs[i].first;
|
||||
assert(req);
|
||||
assert(req->status);
|
||||
|
||||
const size_t key_size = req->user_key.size();
|
||||
// Validate that the full value region is within the file (same check
|
||||
// GetBlob performs); this guards the sub-range read below.
|
||||
if (!IsValidBlobOffset(req->offset, key_size, req->value_size, file_size_,
|
||||
has_footer_)) {
|
||||
*req->status = Status::Corruption(
|
||||
"Invalid blob offset " + std::to_string(req->offset) +
|
||||
" for value size " + std::to_string(req->value_size) + " in " +
|
||||
file_reader_->file_name() + " (file size " +
|
||||
std::to_string(file_size_) + ")");
|
||||
continue;
|
||||
}
|
||||
// The requested sub-range must lie within the value.
|
||||
if (req->range_offset > req->value_size ||
|
||||
req->range_length > req->value_size - req->range_offset) {
|
||||
*req->status = Status::InvalidArgument(
|
||||
"Blob range [" + std::to_string(req->range_offset) + ", +" +
|
||||
std::to_string(req->range_length) +
|
||||
") out of bounds for value size " + std::to_string(req->value_size) +
|
||||
" in " + file_reader_->file_name());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Read only the requested bytes, at the value's file position plus the
|
||||
// range offset -- no record-header adjustment, no whole-record checksum.
|
||||
FSReadRequest read_req;
|
||||
read_req.offset = req->offset + req->range_offset;
|
||||
read_req.len = req->range_length;
|
||||
total_len += read_req.len;
|
||||
read_reqs.emplace_back(std::move(read_req));
|
||||
}
|
||||
|
||||
RecordTick(statistics_, BLOB_DB_BLOB_FILE_BYTES_READ, total_len);
|
||||
|
||||
if (read_reqs.empty()) {
|
||||
if (bytes_read) {
|
||||
*bytes_read = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Buffer buf;
|
||||
AlignedBuffer direct_io_buffer;
|
||||
|
||||
Status s;
|
||||
bool direct_io = file_reader_->use_direct_io();
|
||||
if (direct_io) {
|
||||
for (size_t i = 0; i < read_reqs.size(); ++i) {
|
||||
read_reqs[i].scratch = nullptr;
|
||||
}
|
||||
} else {
|
||||
buf.reset(new char[total_len]);
|
||||
std::ptrdiff_t pos = 0;
|
||||
for (size_t i = 0; i < read_reqs.size(); ++i) {
|
||||
read_reqs[i].scratch = buf.get() + pos;
|
||||
pos += read_reqs[i].len;
|
||||
}
|
||||
}
|
||||
TEST_SYNC_POINT("BlobFileReader::MultiGetBlobRange:ReadFromFile");
|
||||
PERF_COUNTER_ADD(blob_read_count, num_blobs);
|
||||
PERF_COUNTER_ADD(blob_read_byte, total_len);
|
||||
IOOptions opts;
|
||||
IODebugContext dbg;
|
||||
s = file_reader_->PrepareIOOptions(read_options, opts, &dbg);
|
||||
if (s.ok()) {
|
||||
AlignedBufferAllocationContext direct_io_context{&direct_io_buffer};
|
||||
s = file_reader_->MultiRead(opts, read_reqs.data(), read_reqs.size(),
|
||||
&direct_io_context, &dbg);
|
||||
}
|
||||
if (!s.ok()) {
|
||||
// Batch read failed; s is written to every request's status below, so these
|
||||
// per-request FS statuses carry nothing extra.
|
||||
for (auto& req : read_reqs) {
|
||||
req.status.PermitUncheckedError();
|
||||
}
|
||||
for (auto& blob_req : blob_reqs) {
|
||||
BlobRangeReadRequest* const req = blob_req.first;
|
||||
assert(req);
|
||||
assert(req->status);
|
||||
if (!req->status->IsCorruption() && !req->status->IsInvalidArgument()) {
|
||||
// Avoid overwriting per-request validation errors.
|
||||
*req->status = s;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
assert(s.ok());
|
||||
|
||||
TEST_SYNC_POINT_CALLBACK("BlobFileReader::MultiGetBlobRange:TamperWithResult",
|
||||
&read_reqs);
|
||||
|
||||
uint64_t total_bytes = 0;
|
||||
for (size_t i = 0, j = 0; i < num_blobs; ++i) {
|
||||
BlobRangeReadRequest* const req = blob_reqs[i].first;
|
||||
assert(req);
|
||||
assert(req->status);
|
||||
|
||||
if (!req->status->ok()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
assert(j < read_reqs.size());
|
||||
auto& read_req = read_reqs[j++];
|
||||
const Slice& range_slice = read_req.result;
|
||||
if (read_req.status.ok() && range_slice.size() != read_req.len) {
|
||||
read_req.status = IOStatus::Corruption(
|
||||
"Incomplete blob range read from " + file_reader_->file_name() +
|
||||
" at offset " + std::to_string(read_req.offset) + ": expected " +
|
||||
std::to_string(read_req.len) + " bytes, got " +
|
||||
std::to_string(range_slice.size()));
|
||||
}
|
||||
|
||||
*req->status = read_req.status;
|
||||
if (!req->status->ok()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The blob is uncompressed, so the bytes read are exactly the requested
|
||||
// value bytes; copy them into an owned BlobContents (no decompression).
|
||||
BlobContentsCreator::Create(&blob_reqs[i].second, /*out_charge=*/nullptr,
|
||||
range_slice, kNoCompression,
|
||||
/*allocator=*/nullptr);
|
||||
total_bytes += range_slice.size();
|
||||
}
|
||||
|
||||
if (bytes_read) {
|
||||
*bytes_read = total_bytes;
|
||||
}
|
||||
}
|
||||
|
||||
Status BlobFileReader::VerifyBlob(const Slice& record_slice,
|
||||
const Slice& user_key, uint64_t value_size) {
|
||||
PERF_TIMER_GUARD(blob_checksum_time);
|
||||
|
||||
@@ -88,6 +88,21 @@ class BlobFileReader {
|
||||
blob_reqs,
|
||||
uint64_t* bytes_read) const;
|
||||
|
||||
// Byte-range (partial) multi-read counterpart of MultiGetBlob: for each
|
||||
// request reads only its sub-range [offset + range_offset, +range_length) of
|
||||
// an *uncompressed* blob value, coalescing all requests into a single
|
||||
// MultiRead. Unlike MultiGetBlob it reads no record header/key, verifies no
|
||||
// whole-record checksum (a strict sub-range cannot cover it), and never
|
||||
// decompresses -- so it is only valid when this reader's blob file is
|
||||
// uncompressed (checked). Each result owns a copy of its requested bytes.
|
||||
// Requests must be sorted ascending by effective read offset (offset +
|
||||
// range_offset) by the caller.
|
||||
void MultiGetBlobRange(
|
||||
const ReadOptions& read_options,
|
||||
autovector<std::pair<BlobRangeReadRequest*,
|
||||
std::unique_ptr<BlobContents>>>& blob_reqs,
|
||||
uint64_t* bytes_read) const;
|
||||
|
||||
CompressionType GetCompressionType() const { return compression_type_; }
|
||||
|
||||
uint64_t GetFileSize() const { return file_size_; }
|
||||
|
||||
+291
-15
@@ -5,9 +5,11 @@
|
||||
|
||||
#include "db/blob/blob_gen2_format.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "file/random_access_file_reader.h"
|
||||
#include "file/writable_file_writer.h"
|
||||
@@ -16,9 +18,56 @@
|
||||
#include "table/format.h"
|
||||
#include "util/cast_util.h"
|
||||
#include "util/coding.h"
|
||||
#include "util/crc32c.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
namespace {
|
||||
|
||||
// Verifies a SimpleGen2Blob record's builtin checksum: the stored 4-byte
|
||||
// checksum (at record[payload_size + 1 ..], context-modified by the record's
|
||||
// file offset) against the checksum computed over the payload plus its 1-byte
|
||||
// compression marker. Returns OK, or a Corruption carrying debugging
|
||||
// information -- stored (with the per-offset context modifier removed),
|
||||
// computed, and checksum type, with CRC values unmasked so they can be compared
|
||||
// against a reference. Mirrors VerifyBlockChecksum() for the block-based
|
||||
// format, since a SimpleGen2Blob record's trailer uses the same
|
||||
// builtin-checksum scheme; shared by the scalar and batched readers so both
|
||||
// report the same diagnostics.
|
||||
Status VerifySimpleGen2BlobChecksum(ChecksumType checksum_type,
|
||||
uint32_t base_context_checksum,
|
||||
const char* record, size_t payload_size,
|
||||
uint64_t record_offset,
|
||||
const std::string& file_name) {
|
||||
uint32_t stored = DecodeFixed32(record + payload_size + 1);
|
||||
const uint32_t modifier =
|
||||
ChecksumModifierForContext(base_context_checksum, record_offset);
|
||||
stored -= modifier;
|
||||
const uint32_t computed = ComputeBuiltinChecksumWithLastByte(
|
||||
checksum_type, record, payload_size, record[payload_size]);
|
||||
if (stored == computed) {
|
||||
return Status::OK();
|
||||
}
|
||||
// Unmask CRC values (as VerifyBlockChecksum does) so a reader of the error
|
||||
// can compare against a reference checksum.
|
||||
uint32_t stored_for_msg = stored;
|
||||
uint32_t computed_for_msg = computed;
|
||||
if (checksum_type == kCRC32c) {
|
||||
stored_for_msg = crc32c::Unmask(stored);
|
||||
computed_for_msg = crc32c::Unmask(computed);
|
||||
}
|
||||
return Status::Corruption("SimpleGen2 blob record checksum mismatch: stored" +
|
||||
std::string(modifier ? "(context removed)" : "") +
|
||||
" = " + std::to_string(stored_for_msg) +
|
||||
", computed = " + std::to_string(computed_for_msg) +
|
||||
", type = " + std::to_string(checksum_type) +
|
||||
" in " + file_name + " offset " +
|
||||
std::to_string(record_offset) + " size " +
|
||||
std::to_string(payload_size));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Status ReadAndVerifySimpleGen2BlobRecord(
|
||||
const ReadOptions& read_options, RandomAccessFileReader* file,
|
||||
uint64_t record_offset, size_t payload_size, size_t record_size,
|
||||
@@ -40,7 +89,11 @@ Status ReadAndVerifySimpleGen2BlobRecord(
|
||||
return s;
|
||||
}
|
||||
if (result.size() != record_size) {
|
||||
return Status::Corruption("Could not read complete blob record");
|
||||
return Status::Corruption("Incomplete blob record read from " +
|
||||
file->file_name() + " at offset " +
|
||||
std::to_string(record_offset) + ": expected " +
|
||||
std::to_string(record_size) + " bytes, got " +
|
||||
std::to_string(result.size()));
|
||||
}
|
||||
// With mmap reads the data lands outside `buf`; copy it in so the caller can
|
||||
// rely on `buf` owning the bytes (this is the only copy on the mmap path).
|
||||
@@ -54,24 +107,32 @@ Status ReadAndVerifySimpleGen2BlobRecord(
|
||||
static_cast<CompressionType>(record[payload_size]);
|
||||
if (compression != expected_compression) {
|
||||
return Status::Corruption(
|
||||
"Blob record compression does not match blob index");
|
||||
"Blob record compression mismatch in " + file->file_name() +
|
||||
" at offset " + std::to_string(record_offset) + ": index expects " +
|
||||
std::to_string(static_cast<int>(expected_compression)) +
|
||||
", record has " + std::to_string(static_cast<int>(compression)));
|
||||
}
|
||||
|
||||
if (read_options.verify_checksums) {
|
||||
uint32_t stored = DecodeFixed32(record + payload_size + 1);
|
||||
stored -= ChecksumModifierForContext(base_context_checksum, record_offset);
|
||||
const uint32_t computed = ComputeBuiltinChecksumWithLastByte(
|
||||
checksum_type, record, payload_size, record[payload_size]);
|
||||
if (stored != computed) {
|
||||
return Status::Corruption("Blob record checksum mismatch in " +
|
||||
file->file_name() + " offset " +
|
||||
std::to_string(record_offset) + " size " +
|
||||
std::to_string(payload_size));
|
||||
const Status checksum_status = VerifySimpleGen2BlobChecksum(
|
||||
checksum_type, base_context_checksum, record, payload_size,
|
||||
record_offset, file->file_name());
|
||||
if (!checksum_status.ok()) {
|
||||
return checksum_status;
|
||||
}
|
||||
}
|
||||
|
||||
// The compression marker matched the blob index above, but SimpleGen2Blob
|
||||
// payloads are currently written uncompressed only (see
|
||||
// WriteSimpleGen2BlobRecord), so a record that actually carries a compression
|
||||
// type is not readable here. This guards a corrupt or forward-incompatible
|
||||
// record whose (compressed) marker happened to agree with a (compressed)
|
||||
// index.
|
||||
if (compression != kNoCompression) {
|
||||
return Status::Corruption("Blob record compression is not supported");
|
||||
return Status::Corruption("Blob record compression " +
|
||||
std::to_string(static_cast<int>(compression)) +
|
||||
" is not supported in " + file->file_name() +
|
||||
" at offset " + std::to_string(record_offset));
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
@@ -88,14 +149,21 @@ Status ReadSimpleGen2BlobRange(const ReadOptions& read_options,
|
||||
// A strict sub-range of a compressed payload cannot be decompressed in
|
||||
// isolation; callers resolve such payloads whole and slice instead.
|
||||
if (expected_compression != kNoCompression) {
|
||||
return Status::Corruption("Cannot range-read a compressed blob");
|
||||
return Status::Corruption(
|
||||
"Cannot range-read a compressed blob (compression " +
|
||||
std::to_string(static_cast<int>(expected_compression)) + ") in " +
|
||||
file->file_name() + " at offset " + std::to_string(record_offset));
|
||||
}
|
||||
|
||||
// The requested sub-range must lie within the payload (the trailer that
|
||||
// follows the payload is never part of a range read).
|
||||
if (range_offset > payload_size ||
|
||||
range_length > payload_size - range_offset) {
|
||||
return Status::InvalidArgument("Blob range out of bounds");
|
||||
return Status::InvalidArgument(
|
||||
"Blob range [" + std::to_string(range_offset) + ", +" +
|
||||
std::to_string(range_length) + ") out of bounds for payload size " +
|
||||
std::to_string(payload_size) + " in " + file->file_name() +
|
||||
" at offset " + std::to_string(record_offset));
|
||||
}
|
||||
|
||||
Slice result;
|
||||
@@ -110,7 +178,11 @@ Status ReadSimpleGen2BlobRange(const ReadOptions& read_options,
|
||||
return s;
|
||||
}
|
||||
if (result.size() != range_length) {
|
||||
return Status::Corruption("Could not read complete blob range");
|
||||
return Status::Corruption("Incomplete blob range read from " +
|
||||
file->file_name() + " at offset " +
|
||||
std::to_string(record_offset + range_offset) +
|
||||
": expected " + std::to_string(range_length) +
|
||||
" bytes, got " + std::to_string(result.size()));
|
||||
}
|
||||
// With mmap reads the data lands outside `buf`; copy it in so the caller can
|
||||
// rely on `buf` owning the bytes (the only copy on the mmap path).
|
||||
@@ -124,6 +196,210 @@ Status ReadSimpleGen2BlobRange(const ReadOptions& read_options,
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// One record's read description for the batched SimpleGen2Blob readers below:
|
||||
// where/how much to read, where to put it, and where to report the outcome.
|
||||
struct Gen2ReadSlot {
|
||||
uint64_t offset = 0;
|
||||
size_t len = 0;
|
||||
char* buf = nullptr;
|
||||
Status* status = nullptr;
|
||||
};
|
||||
|
||||
// Issues a single MultiRead for the records described by `slots` and copies
|
||||
// each result into its buf, setting *slots[i].status. Shared by the
|
||||
// whole-record and range batch readers; per-record format checks (compression
|
||||
// marker / checksum) are done by the whole-record caller afterwards from the
|
||||
// (now populated) buf.
|
||||
void MultiReadGen2(const ReadOptions& read_options,
|
||||
RandomAccessFileReader* file,
|
||||
std::vector<Gen2ReadSlot>& slots) {
|
||||
assert(file != nullptr);
|
||||
assert(!slots.empty());
|
||||
|
||||
// RandomAccessFileReader::MultiRead requires the requests sorted ascending by
|
||||
// file offset -- it asserts this, and in direct-I/O mode it aligns and merges
|
||||
// only consecutive requests, so unsorted input reads the wrong bytes. The
|
||||
// callers group records by SST but not by offset, so sort here. Each slot
|
||||
// carries its own output buf/status pointer, so sorting does not disturb
|
||||
// result routing.
|
||||
std::sort(slots.begin(), slots.end(),
|
||||
[](const Gen2ReadSlot& a, const Gen2ReadSlot& b) {
|
||||
return a.offset < b.offset;
|
||||
});
|
||||
|
||||
const size_t num = slots.size();
|
||||
std::vector<FSReadRequest> read_reqs;
|
||||
read_reqs.reserve(num);
|
||||
for (size_t i = 0; i < num; ++i) {
|
||||
FSReadRequest read_req;
|
||||
read_req.offset = slots[i].offset;
|
||||
read_req.len = slots[i].len;
|
||||
read_reqs.emplace_back(std::move(read_req));
|
||||
}
|
||||
|
||||
AlignedBuffer direct_io_buffer;
|
||||
const bool direct_io = file->use_direct_io();
|
||||
if (direct_io) {
|
||||
for (size_t i = 0; i < num; ++i) {
|
||||
read_reqs[i].scratch = nullptr;
|
||||
}
|
||||
} else {
|
||||
for (size_t i = 0; i < num; ++i) {
|
||||
read_reqs[i].scratch = slots[i].buf;
|
||||
}
|
||||
}
|
||||
|
||||
IOOptions opts;
|
||||
IODebugContext dbg;
|
||||
Status s = file->PrepareIOOptions(read_options, opts, &dbg);
|
||||
if (s.ok()) {
|
||||
AlignedBufferAllocationContext direct_io_context{&direct_io_buffer};
|
||||
s = file->MultiRead(opts, read_reqs.data(), read_reqs.size(),
|
||||
&direct_io_context, &dbg);
|
||||
}
|
||||
if (!s.ok()) {
|
||||
// Batch read failed; s is written to every slot's status below, so these
|
||||
// per-request FS statuses carry nothing extra.
|
||||
for (auto& read_req : read_reqs) {
|
||||
read_req.status.PermitUncheckedError();
|
||||
}
|
||||
for (size_t i = 0; i < num; ++i) {
|
||||
*slots[i].status = s;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < num; ++i) {
|
||||
FSReadRequest& read_req = read_reqs[i];
|
||||
if (read_req.status.ok() && read_req.result.size() != read_req.len) {
|
||||
read_req.status = IOStatus::Corruption(
|
||||
"Incomplete blob record read from " + file->file_name() +
|
||||
" at offset " + std::to_string(read_req.offset) + ": expected " +
|
||||
std::to_string(read_req.len) + " bytes, got " +
|
||||
std::to_string(read_req.result.size()));
|
||||
}
|
||||
*slots[i].status = read_req.status;
|
||||
if (!slots[i].status->ok()) {
|
||||
continue;
|
||||
}
|
||||
// With mmap/direct reads the data lands outside the caller's buf; copy it
|
||||
// in so the caller can rely on buf owning the bytes.
|
||||
if (read_req.result.data() != slots[i].buf) {
|
||||
memcpy(slots[i].buf, read_req.result.data(), read_req.len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ReadAndVerifySimpleGen2BlobRecords(const ReadOptions& read_options,
|
||||
RandomAccessFileReader* file,
|
||||
ChecksumType checksum_type,
|
||||
uint32_t base_context_checksum,
|
||||
size_t num_records,
|
||||
SimpleGen2RecordReadRequest* reqs) {
|
||||
assert(file != nullptr);
|
||||
assert(num_records > 0);
|
||||
|
||||
std::vector<Gen2ReadSlot> slots;
|
||||
slots.reserve(num_records);
|
||||
for (size_t i = 0; i < num_records; ++i) {
|
||||
assert(reqs[i].buf != nullptr);
|
||||
assert(reqs[i].status != nullptr);
|
||||
assert(reqs[i].record_size ==
|
||||
reqs[i].payload_size + kSimpleGen2BlobTrailerSize);
|
||||
slots.push_back({reqs[i].record_offset, reqs[i].record_size, reqs[i].buf,
|
||||
reqs[i].status});
|
||||
}
|
||||
|
||||
MultiReadGen2(read_options, file, slots);
|
||||
|
||||
for (size_t i = 0; i < num_records; ++i) {
|
||||
if (!reqs[i].status->ok()) {
|
||||
continue;
|
||||
}
|
||||
const char* record = reqs[i].buf;
|
||||
const size_t payload_size = reqs[i].payload_size;
|
||||
const CompressionType compression =
|
||||
static_cast<CompressionType>(record[payload_size]);
|
||||
if (compression != reqs[i].expected_compression) {
|
||||
*reqs[i].status = Status::Corruption(
|
||||
"Blob record compression mismatch in " + file->file_name() +
|
||||
" at offset " + std::to_string(reqs[i].record_offset) +
|
||||
": index expects " +
|
||||
std::to_string(static_cast<int>(reqs[i].expected_compression)) +
|
||||
", record has " + std::to_string(static_cast<int>(compression)));
|
||||
continue;
|
||||
}
|
||||
if (read_options.verify_checksums) {
|
||||
*reqs[i].status = VerifySimpleGen2BlobChecksum(
|
||||
checksum_type, base_context_checksum, record, payload_size,
|
||||
reqs[i].record_offset, file->file_name());
|
||||
if (!reqs[i].status->ok()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// See ReadAndVerifySimpleGen2BlobRecord: the marker matched the index, but
|
||||
// only uncompressed SimpleGen2Blob payloads are currently supported.
|
||||
if (compression != kNoCompression) {
|
||||
*reqs[i].status = Status::Corruption(
|
||||
"Blob record compression " +
|
||||
std::to_string(static_cast<int>(compression)) +
|
||||
" is not supported in " + file->file_name() + " at offset " +
|
||||
std::to_string(reqs[i].record_offset));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ReadSimpleGen2BlobRanges(const ReadOptions& read_options,
|
||||
RandomAccessFileReader* file, size_t num_records,
|
||||
SimpleGen2RangeReadRequest* reqs) {
|
||||
assert(file != nullptr);
|
||||
assert(num_records > 0);
|
||||
|
||||
std::vector<Gen2ReadSlot> slots;
|
||||
slots.reserve(num_records);
|
||||
for (size_t i = 0; i < num_records; ++i) {
|
||||
assert(reqs[i].buf != nullptr);
|
||||
assert(reqs[i].status != nullptr);
|
||||
// A strict sub-range of a compressed payload cannot be decompressed in
|
||||
// isolation; callers resolve such payloads whole and slice instead.
|
||||
if (reqs[i].expected_compression != kNoCompression) {
|
||||
*reqs[i].status = Status::Corruption(
|
||||
"Cannot range-read a compressed blob (compression " +
|
||||
std::to_string(static_cast<int>(reqs[i].expected_compression)) +
|
||||
") in " + file->file_name() + " at offset " +
|
||||
std::to_string(reqs[i].record_offset));
|
||||
continue;
|
||||
}
|
||||
// The requested sub-range must lie within the payload (the trailer that
|
||||
// follows the payload is never part of a range read).
|
||||
if (reqs[i].range_offset > reqs[i].payload_size ||
|
||||
reqs[i].range_length > reqs[i].payload_size - reqs[i].range_offset) {
|
||||
*reqs[i].status = Status::InvalidArgument(
|
||||
"Blob range [" + std::to_string(reqs[i].range_offset) + ", +" +
|
||||
std::to_string(reqs[i].range_length) +
|
||||
") out of bounds for payload size " +
|
||||
std::to_string(reqs[i].payload_size) + " in " + file->file_name() +
|
||||
" at offset " + std::to_string(reqs[i].record_offset));
|
||||
continue;
|
||||
}
|
||||
slots.push_back({reqs[i].record_offset + reqs[i].range_offset,
|
||||
reqs[i].range_length, reqs[i].buf, reqs[i].status});
|
||||
}
|
||||
|
||||
if (slots.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// slots only covers requests that passed validation above; a failed
|
||||
// validation left *reqs[i].status non-OK and is skipped here.
|
||||
MultiReadGen2(read_options, file, slots);
|
||||
}
|
||||
|
||||
IOStatus WriteSimpleGen2BlobRecord(WritableFileWriter* file,
|
||||
const WriteOptions& write_options,
|
||||
ChecksumType checksum_type,
|
||||
|
||||
@@ -91,6 +91,57 @@ Status ReadSimpleGen2BlobRange(const ReadOptions& read_options,
|
||||
uint64_t range_offset, size_t range_length,
|
||||
CompressionType expected_compression, char* buf);
|
||||
|
||||
// One whole-record request in a batched SimpleGen2Blob read (see
|
||||
// ReadAndVerifySimpleGen2BlobRecords). Mirrors the scalar
|
||||
// ReadAndVerifySimpleGen2BlobRecord inputs, with the read bytes going into
|
||||
// caller-provided `buf` (capacity >= record_size) and the per-record outcome in
|
||||
// `*status`.
|
||||
struct SimpleGen2RecordReadRequest {
|
||||
uint64_t record_offset = 0;
|
||||
size_t payload_size = 0;
|
||||
size_t record_size = 0; // == payload_size + kSimpleGen2BlobTrailerSize
|
||||
CompressionType expected_compression = kNoCompression;
|
||||
char* buf = nullptr;
|
||||
Status* status = nullptr;
|
||||
};
|
||||
|
||||
// One sub-range request in a batched SimpleGen2Blob range read (see
|
||||
// ReadSimpleGen2BlobRanges). Mirrors the scalar ReadSimpleGen2BlobRange inputs.
|
||||
struct SimpleGen2RangeReadRequest {
|
||||
uint64_t record_offset = 0;
|
||||
size_t payload_size = 0;
|
||||
uint64_t range_offset = 0;
|
||||
size_t range_length = 0;
|
||||
CompressionType expected_compression = kNoCompression;
|
||||
char* buf = nullptr;
|
||||
Status* status = nullptr;
|
||||
};
|
||||
|
||||
// Batch counterpart of ReadAndVerifySimpleGen2BlobRecord: reads `num_records`
|
||||
// whole records from `file` in a single MultiRead, then per record verifies the
|
||||
// compression marker and (when read_options.verify_checksums) the
|
||||
// context-modified checksum, copying the record into reqs[i].buf. Per-record
|
||||
// outcome is written to *reqs[i].status; on success reqs[i].buf[0,
|
||||
// payload_size) holds the verified payload (the trailer sits unused at the
|
||||
// tail). Requests may be given in any order; they are sorted internally, as
|
||||
// RandomAccessFileReader::MultiRead requires ascending offsets.
|
||||
void ReadAndVerifySimpleGen2BlobRecords(const ReadOptions& read_options,
|
||||
RandomAccessFileReader* file,
|
||||
ChecksumType checksum_type,
|
||||
uint32_t base_context_checksum,
|
||||
size_t num_records,
|
||||
SimpleGen2RecordReadRequest* reqs);
|
||||
|
||||
// Batch counterpart of ReadSimpleGen2BlobRange: reads the requested sub-range
|
||||
// of `num_records` *uncompressed* payloads from `file` in a single MultiRead
|
||||
// (no trailer read, no checksum). Per-record outcome is in *reqs[i].status; on
|
||||
// success reqs[i].buf[0, range_length) holds the requested bytes. Requests may
|
||||
// be given in any order; they are sorted internally, as
|
||||
// RandomAccessFileReader::MultiRead requires ascending offsets.
|
||||
void ReadSimpleGen2BlobRanges(const ReadOptions& read_options,
|
||||
RandomAccessFileReader* file, size_t num_records,
|
||||
SimpleGen2RangeReadRequest* reqs);
|
||||
|
||||
// Writes a SimpleGen2Blob record for `payload` at the current end of `file`,
|
||||
// which the caller asserts is byte offset `record_offset`. Appends the payload
|
||||
// bytes followed by the 5-byte trailer (compression marker + context-modified
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace ROCKSDB_NAMESPACE {
|
||||
// BlobFileReader::MultiGetBlob.
|
||||
struct BlobReadRequest {
|
||||
// User key to lookup the paired blob
|
||||
// TODO: consider simplifying to copied Slice?
|
||||
const Slice* user_key = nullptr;
|
||||
|
||||
// File offset in bytes
|
||||
@@ -55,4 +56,48 @@ using BlobFileReadRequests =
|
||||
std::tuple<uint64_t /* file_number */, uint64_t /* file_size */,
|
||||
autovector<BlobReadRequest>>;
|
||||
|
||||
// A byte-range (partial) blob read request for the lazy blob-read multi
|
||||
// primitives (BlobFileReader::MultiGetBlobRange /
|
||||
// BlobSource::MultiGetBlobRange). Unlike BlobReadRequest (whole value), it
|
||||
// reads only the sub-range [range_offset, range_offset + range_length) of an
|
||||
// *uncompressed* blob value, skipping whole-record checksum verification and
|
||||
// blob-cache population -- the multi-read counterpart of
|
||||
// BlobSource::GetBlobRange / BlobFileReader::GetBlobRange.
|
||||
struct BlobRangeReadRequest {
|
||||
// User key that pairs with the blob value (used for offset validation).
|
||||
Slice user_key;
|
||||
|
||||
// The blob value's file offset and full (logical == on-disk, uncompressed)
|
||||
// size, from the BlobIndex.
|
||||
uint64_t offset = 0;
|
||||
uint64_t value_size = 0;
|
||||
|
||||
// The requested sub-range within the value. `range_length` is the number of
|
||||
// bytes to read; the caller must ensure range_offset + range_length <=
|
||||
// value_size (it clamps before building the request).
|
||||
uint64_t range_offset = 0;
|
||||
size_t range_length = 0;
|
||||
|
||||
// Output parameter set to point to the requested bytes, and the read status.
|
||||
PinnableSlice* result = nullptr;
|
||||
Status* status = nullptr;
|
||||
|
||||
BlobRangeReadRequest(Slice _user_key, uint64_t _offset, uint64_t _value_size,
|
||||
uint64_t _range_offset, size_t _range_length,
|
||||
PinnableSlice* _result, Status* _status)
|
||||
: user_key(_user_key),
|
||||
offset(_offset),
|
||||
value_size(_value_size),
|
||||
range_offset(_range_offset),
|
||||
range_length(_range_length),
|
||||
result(_result),
|
||||
status(_status) {}
|
||||
|
||||
BlobRangeReadRequest() = default;
|
||||
};
|
||||
|
||||
using BlobFileRangeReadRequests =
|
||||
std::tuple<uint64_t /* file_number */, uint64_t /* file_size */,
|
||||
autovector<BlobRangeReadRequest>>;
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
+339
-3
@@ -7,6 +7,7 @@
|
||||
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "cache/cache_reservation_manager.h"
|
||||
#include "cache/charged_cache.h"
|
||||
@@ -416,7 +417,8 @@ Status BlobSource::GetBlobRange(const ReadOptions& read_options,
|
||||
|
||||
const bool no_io = read_options.read_tier == kBlockCacheTier;
|
||||
if (no_io) {
|
||||
return Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
|
||||
s = Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
|
||||
return s;
|
||||
}
|
||||
|
||||
// Cache miss: read only the requested sub-range from the file. The result is
|
||||
@@ -501,7 +503,8 @@ Status BlobSource::GetSimpleGen2Blob(
|
||||
|
||||
const bool no_io = read_options.read_tier == kBlockCacheTier;
|
||||
if (no_io) {
|
||||
return Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
|
||||
s = Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
|
||||
return s;
|
||||
}
|
||||
|
||||
// Cache miss (or no cache configured). Read the record into a buffer
|
||||
@@ -601,7 +604,8 @@ Status BlobSource::GetSimpleGen2BlobRange(
|
||||
|
||||
const bool no_io = read_options.read_tier == kBlockCacheTier;
|
||||
if (no_io) {
|
||||
return Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
|
||||
s = Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
|
||||
return s;
|
||||
}
|
||||
|
||||
// Cache miss: read only the requested sub-range from the file. The result is
|
||||
@@ -633,6 +637,188 @@ Status BlobSource::GetSimpleGen2BlobRange(
|
||||
return s;
|
||||
}
|
||||
|
||||
void BlobSource::MultiGetSimpleGen2Blob(
|
||||
const ReadOptions& read_options, const OffsetableCacheKey& base_cache_key,
|
||||
RandomAccessFileReader* file, ChecksumType checksum_type,
|
||||
uint32_t base_context_checksum, size_t num_records,
|
||||
SimpleGen2BlobReadRequest* reqs) {
|
||||
assert(file);
|
||||
if (num_records == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool no_io = read_options.read_tier == kBlockCacheTier;
|
||||
MemoryAllocator* const allocator = (blob_cache_ && read_options.fill_cache)
|
||||
? blob_cache_.get()->memory_allocator()
|
||||
: nullptr;
|
||||
|
||||
// Records that missed the cache and must be read from disk, in read order;
|
||||
// each owns its buffer until we build a BlobContents from it below.
|
||||
struct Pending {
|
||||
size_t idx;
|
||||
CacheAllocationPtr buf;
|
||||
uint64_t record_size;
|
||||
};
|
||||
std::vector<Pending> pending;
|
||||
std::vector<SimpleGen2RecordReadRequest> read_reqs;
|
||||
|
||||
for (size_t i = 0; i < num_records; ++i) {
|
||||
SimpleGen2BlobReadRequest& req = reqs[i];
|
||||
assert(req.result);
|
||||
assert(req.status);
|
||||
|
||||
const uint64_t record_size = req.payload_size + kSimpleGen2BlobTrailerSize;
|
||||
const CacheKey cache_key =
|
||||
GetSimpleGen2BlobCacheKey(base_cache_key, req.record_offset);
|
||||
|
||||
if (blob_cache_) {
|
||||
CacheHandleGuard<BlobContents> blob_handle;
|
||||
const Status cs = GetBlobFromCache(cache_key.AsSlice(), &blob_handle);
|
||||
if (cs.ok()) {
|
||||
*req.status = cs;
|
||||
PinCachedBlob(&blob_handle, req.result);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (no_io) {
|
||||
*req.status =
|
||||
Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
|
||||
continue;
|
||||
}
|
||||
|
||||
CacheAllocationPtr buf =
|
||||
AllocateBlock(static_cast<size_t>(record_size), allocator);
|
||||
read_reqs.push_back(SimpleGen2RecordReadRequest{
|
||||
req.record_offset, static_cast<size_t>(req.payload_size),
|
||||
static_cast<size_t>(record_size), req.expected_compression, buf.get(),
|
||||
req.status});
|
||||
pending.push_back(Pending{i, std::move(buf), record_size});
|
||||
}
|
||||
|
||||
if (read_reqs.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ReadAndVerifySimpleGen2BlobRecords(read_options, file, checksum_type,
|
||||
base_context_checksum, read_reqs.size(),
|
||||
read_reqs.data());
|
||||
|
||||
for (size_t k = 0; k < pending.size(); ++k) {
|
||||
SimpleGen2BlobReadRequest& req = reqs[pending[k].idx];
|
||||
if (!req.status->ok()) {
|
||||
continue;
|
||||
}
|
||||
const uint64_t record_size = pending[k].record_size;
|
||||
std::unique_ptr<BlobContents> blob_contents(new BlobContents(
|
||||
std::move(pending[k].buf), static_cast<size_t>(req.payload_size)));
|
||||
|
||||
RecordTick(statistics_, BLOB_DB_BLOB_FILE_BYTES_READ, record_size);
|
||||
PERF_COUNTER_ADD(blob_read_count, 1);
|
||||
PERF_COUNTER_ADD(blob_read_byte, record_size);
|
||||
if (read_options.io_activity == Env::IOActivity::kLazyResolve) {
|
||||
RecordLazyRead(statistics_, record_size);
|
||||
}
|
||||
|
||||
if (blob_cache_ && read_options.fill_cache) {
|
||||
const CacheKey cache_key =
|
||||
GetSimpleGen2BlobCacheKey(base_cache_key, req.record_offset);
|
||||
CacheHandleGuard<BlobContents> blob_handle;
|
||||
const Status ps =
|
||||
PutBlobIntoCache(cache_key.AsSlice(), &blob_contents, &blob_handle);
|
||||
if (!ps.ok()) {
|
||||
*req.status = ps;
|
||||
} else {
|
||||
PinCachedBlob(&blob_handle, req.result);
|
||||
}
|
||||
} else {
|
||||
PinOwnedBlob(&blob_contents, req.result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BlobSource::MultiGetSimpleGen2BlobRange(
|
||||
const ReadOptions& read_options, const OffsetableCacheKey& base_cache_key,
|
||||
RandomAccessFileReader* file, size_t num_records,
|
||||
SimpleGen2BlobRangeReadRequest* reqs) {
|
||||
assert(file);
|
||||
// Range reads are only issued while resolving a lazy result (partial reads
|
||||
// are lazy-only), so the lazy read stats below are recorded unconditionally.
|
||||
assert(read_options.io_activity == Env::IOActivity::kLazyResolve);
|
||||
if (num_records == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool no_io = read_options.read_tier == kBlockCacheTier;
|
||||
|
||||
struct Pending {
|
||||
size_t idx;
|
||||
CacheAllocationPtr buf;
|
||||
};
|
||||
std::vector<Pending> pending;
|
||||
std::vector<SimpleGen2RangeReadRequest> read_reqs;
|
||||
|
||||
for (size_t i = 0; i < num_records; ++i) {
|
||||
SimpleGen2BlobRangeReadRequest& req = reqs[i];
|
||||
assert(req.result);
|
||||
assert(req.status);
|
||||
|
||||
const CacheKey cache_key =
|
||||
GetSimpleGen2BlobCacheKey(base_cache_key, req.record_offset);
|
||||
|
||||
// Probe the whole-payload cache; on a hit slice the requested sub-range.
|
||||
if (blob_cache_) {
|
||||
CacheHandleGuard<BlobContents> blob_handle;
|
||||
const Status cs = GetBlobFromCache(cache_key.AsSlice(), &blob_handle);
|
||||
if (cs.ok()) {
|
||||
*req.status = cs;
|
||||
PinCacheHitSubRange(&blob_handle, req.range_offset, req.range_length,
|
||||
req.result);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (no_io) {
|
||||
*req.status =
|
||||
Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
|
||||
continue;
|
||||
}
|
||||
|
||||
// No cache-fill allocator: a partial payload is never inserted.
|
||||
CacheAllocationPtr buf =
|
||||
AllocateBlock(req.range_length, /*allocator=*/nullptr);
|
||||
read_reqs.push_back(SimpleGen2RangeReadRequest{
|
||||
req.record_offset, static_cast<size_t>(req.payload_size),
|
||||
req.range_offset, req.range_length, req.expected_compression, buf.get(),
|
||||
req.status});
|
||||
pending.push_back(Pending{i, std::move(buf)});
|
||||
}
|
||||
|
||||
if (read_reqs.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ReadSimpleGen2BlobRanges(read_options, file, read_reqs.size(),
|
||||
read_reqs.data());
|
||||
|
||||
for (size_t k = 0; k < pending.size(); ++k) {
|
||||
SimpleGen2BlobRangeReadRequest& req = reqs[pending[k].idx];
|
||||
if (!req.status->ok()) {
|
||||
continue;
|
||||
}
|
||||
std::unique_ptr<BlobContents> blob_contents(
|
||||
new BlobContents(std::move(pending[k].buf), req.range_length));
|
||||
|
||||
RecordTick(statistics_, BLOB_DB_BLOB_FILE_BYTES_READ, req.range_length);
|
||||
PERF_COUNTER_ADD(blob_read_count, 1);
|
||||
PERF_COUNTER_ADD(blob_read_byte, req.range_length);
|
||||
RecordLazyRead(statistics_, req.range_length);
|
||||
RecordLazyPartialRead(statistics_, req.range_length, req.payload_size);
|
||||
|
||||
PinOwnedBlob(&blob_contents, req.result);
|
||||
}
|
||||
}
|
||||
|
||||
void BlobSource::MultiGetBlob(const ReadOptions& read_options,
|
||||
autovector<BlobFileReadRequests>& blob_reqs,
|
||||
uint64_t* bytes_read) {
|
||||
@@ -884,6 +1070,24 @@ void BlobSource::MultiGetBlobFromOneFile(const ReadOptions& read_options,
|
||||
}
|
||||
}
|
||||
|
||||
// Whole-column reads on the lazy resolve path count as lazy reads (mirrors
|
||||
// BlobSource::GetBlob, which records only on a disk read, not a cache hit).
|
||||
// Gated on the activity so regular MultiGet is unaffected; the misses read
|
||||
// above are exactly the disk reads for these blob columns.
|
||||
if (read_options.io_activity == Env::IOActivity::kLazyResolve) {
|
||||
for (auto& [req, blob_contents] : _blob_reqs) {
|
||||
assert(req);
|
||||
if (req->status->ok()) {
|
||||
const uint64_t adjustment =
|
||||
read_options.verify_checksums
|
||||
? BlobLogRecord::CalculateAdjustmentForRecordHeader(
|
||||
req->user_key->size())
|
||||
: 0;
|
||||
RecordLazyRead(statistics_, req->len + adjustment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
total_bytes += _bytes_read;
|
||||
if (bytes_read) {
|
||||
*bytes_read = total_bytes;
|
||||
@@ -891,6 +1095,138 @@ void BlobSource::MultiGetBlobFromOneFile(const ReadOptions& read_options,
|
||||
}
|
||||
}
|
||||
|
||||
void BlobSource::MultiGetBlobRange(
|
||||
const ReadOptions& read_options,
|
||||
autovector<BlobFileRangeReadRequests>& blob_reqs, uint64_t* bytes_read) {
|
||||
assert(blob_reqs.size() > 0);
|
||||
|
||||
uint64_t total_bytes_read = 0;
|
||||
uint64_t bytes_read_in_file = 0;
|
||||
|
||||
for (auto& [file_number, file_size, blob_reqs_in_file] : blob_reqs) {
|
||||
// Sort by effective read offset so the file system layer can coalesce
|
||||
// adjacent sub-range reads within one MultiRead.
|
||||
std::sort(blob_reqs_in_file.begin(), blob_reqs_in_file.end(),
|
||||
[](const BlobRangeReadRequest& lhs,
|
||||
const BlobRangeReadRequest& rhs) -> bool {
|
||||
return lhs.offset + lhs.range_offset <
|
||||
rhs.offset + rhs.range_offset;
|
||||
});
|
||||
|
||||
MultiGetBlobRangeFromOneFile(read_options, file_number, file_size,
|
||||
blob_reqs_in_file, &bytes_read_in_file);
|
||||
|
||||
total_bytes_read += bytes_read_in_file;
|
||||
bytes_read_in_file = 0; // reset: some early-return paths do not write it
|
||||
}
|
||||
|
||||
if (bytes_read) {
|
||||
*bytes_read = total_bytes_read;
|
||||
}
|
||||
}
|
||||
|
||||
void BlobSource::MultiGetBlobRangeFromOneFile(
|
||||
const ReadOptions& read_options, uint64_t file_number,
|
||||
uint64_t /*file_size*/, autovector<BlobRangeReadRequest>& blob_reqs,
|
||||
uint64_t* bytes_read) {
|
||||
const size_t num_blobs = blob_reqs.size();
|
||||
assert(num_blobs > 0);
|
||||
assert(num_blobs <= MultiGetContext::MAX_BATCH_SIZE);
|
||||
// Range reads are only issued while resolving a lazy result (partial reads
|
||||
// are lazy-only), so the lazy read stats below are recorded unconditionally.
|
||||
assert(read_options.io_activity == Env::IOActivity::kLazyResolve);
|
||||
|
||||
using Mask = uint64_t;
|
||||
Mask cache_hit_mask = 0;
|
||||
|
||||
uint64_t total_bytes = 0;
|
||||
const OffsetableCacheKey base_cache_key(db_id_, db_session_id_, file_number);
|
||||
|
||||
// Probe the whole-value cache per request; on a hit, slice the requested
|
||||
// sub-range (zero-copy, no disk read). A partial read is never cache-filled.
|
||||
if (blob_cache_) {
|
||||
size_t cached_blob_count = 0;
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
BlobRangeReadRequest& req = blob_reqs[i];
|
||||
assert(req.status);
|
||||
|
||||
CacheHandleGuard<BlobContents> blob_handle;
|
||||
const CacheKey cache_key = base_cache_key.WithOffset(req.offset);
|
||||
const Status s = GetBlobFromCache(cache_key.AsSlice(), &blob_handle);
|
||||
if (s.ok()) {
|
||||
*req.status = s;
|
||||
PinCacheHitSubRange(&blob_handle, req.range_offset, req.range_length,
|
||||
req.result);
|
||||
++cached_blob_count;
|
||||
cache_hit_mask |= (Mask{1} << i); // hit -> no disk read
|
||||
}
|
||||
}
|
||||
|
||||
if (cached_blob_count == num_blobs) {
|
||||
if (bytes_read) {
|
||||
*bytes_read = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const bool no_io = read_options.read_tier == kBlockCacheTier;
|
||||
if (no_io) {
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
if (!(cache_hit_mask & (Mask{1} << i))) {
|
||||
assert(blob_reqs[i].status);
|
||||
*blob_reqs[i].status =
|
||||
Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
autovector<std::pair<BlobRangeReadRequest*, std::unique_ptr<BlobContents>>>
|
||||
_blob_reqs;
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
if (!(cache_hit_mask & (Mask{1} << i))) {
|
||||
_blob_reqs.emplace_back(&blob_reqs[i], std::unique_ptr<BlobContents>());
|
||||
}
|
||||
}
|
||||
|
||||
// Lazy range reads run against a pinned SuperVersion (max_open_files == -1),
|
||||
// and blob file numbers are never reused, so the stale cached-reader race
|
||||
// that MultiGetBlobFromOneFile guards against cannot occur here; a Corruption
|
||||
// is simply surfaced per request. Hence no reader-refresh retry and no
|
||||
// cache-fill (a partial value cannot represent the whole-record cache entry).
|
||||
CacheHandleGuard<BlobFileReader> blob_file_reader;
|
||||
Status s = blob_file_cache_->GetBlobFileReader(read_options, file_number,
|
||||
&blob_file_reader);
|
||||
if (!s.ok()) {
|
||||
for (auto& blob_req : _blob_reqs) {
|
||||
assert(blob_req.first);
|
||||
assert(blob_req.first->status);
|
||||
*blob_req.first->status = s;
|
||||
}
|
||||
return;
|
||||
}
|
||||
assert(blob_file_reader.GetValue());
|
||||
|
||||
uint64_t _bytes_read = 0;
|
||||
blob_file_reader.GetValue()->MultiGetBlobRange(read_options, _blob_reqs,
|
||||
&_bytes_read);
|
||||
|
||||
for (auto& [req, blob_contents] : _blob_reqs) {
|
||||
assert(req);
|
||||
if (req->status->ok()) {
|
||||
PinOwnedBlob(&blob_contents, req->result);
|
||||
RecordLazyRead(statistics_, req->range_length);
|
||||
RecordLazyPartialRead(statistics_, req->range_length, req->value_size);
|
||||
}
|
||||
}
|
||||
|
||||
total_bytes += _bytes_read;
|
||||
if (bytes_read) {
|
||||
*bytes_read = total_bytes;
|
||||
}
|
||||
}
|
||||
|
||||
bool BlobSource::TEST_BlobInCache(uint64_t file_number, uint64_t file_size,
|
||||
uint64_t offset, size_t* charge) const {
|
||||
const CacheKey cache_key = GetCacheKey(file_number, file_size, offset);
|
||||
|
||||
@@ -28,6 +28,30 @@ class Slice;
|
||||
class RandomAccessFileReader;
|
||||
enum ChecksumType : char;
|
||||
|
||||
// One whole-record request for BlobSource::MultiGetSimpleGen2Blob (the batched
|
||||
// counterpart of GetSimpleGen2Blob). The per-file context (base cache key,
|
||||
// file, checksum context) is passed once to the batch call; these are the
|
||||
// per-record inputs and outputs.
|
||||
struct SimpleGen2BlobReadRequest {
|
||||
uint64_t record_offset = 0;
|
||||
uint64_t payload_size = 0;
|
||||
CompressionType expected_compression = kNoCompression;
|
||||
PinnableSlice* result = nullptr;
|
||||
Status* status = nullptr;
|
||||
};
|
||||
|
||||
// One sub-range request for BlobSource::MultiGetSimpleGen2BlobRange (the
|
||||
// batched counterpart of GetSimpleGen2BlobRange).
|
||||
struct SimpleGen2BlobRangeReadRequest {
|
||||
uint64_t record_offset = 0;
|
||||
uint64_t payload_size = 0;
|
||||
uint64_t range_offset = 0;
|
||||
size_t range_length = 0;
|
||||
CompressionType expected_compression = kNoCompression;
|
||||
PinnableSlice* result = nullptr;
|
||||
Status* status = nullptr;
|
||||
};
|
||||
|
||||
// BlobSource is a class that provides universal access to blobs, regardless of
|
||||
// whether they are in the blob cache, secondary cache, or (remote) storage.
|
||||
// Depending on user settings, it always fetch blobs from multi-tier cache and
|
||||
@@ -146,6 +170,33 @@ class BlobSource {
|
||||
uint64_t range_offset, size_t range_length,
|
||||
PinnableSlice* value, uint64_t* bytes_read);
|
||||
|
||||
// Batched counterpart of GetSimpleGen2Blob: resolves `num_records` whole
|
||||
// embedded (SimpleGen2Blob) records from one file, coalescing all cache-miss
|
||||
// reads into a single MultiRead. The per-file context (base cache key, file,
|
||||
// checksum type, base context checksum) is shared by all records; per-record
|
||||
// inputs/outputs are in `reqs`. Each record independently probes the blob
|
||||
// cache and, on a miss, is read + verified from the file; whole records are
|
||||
// cache-filled (when configured and fill_cache) exactly as GetSimpleGen2Blob.
|
||||
// Per-record outcome is written to *reqs[i].status.
|
||||
void MultiGetSimpleGen2Blob(const ReadOptions& read_options,
|
||||
const OffsetableCacheKey& base_cache_key,
|
||||
RandomAccessFileReader* file,
|
||||
ChecksumType checksum_type,
|
||||
uint32_t base_context_checksum,
|
||||
size_t num_records,
|
||||
SimpleGen2BlobReadRequest* reqs);
|
||||
|
||||
// Batched counterpart of GetSimpleGen2BlobRange: resolves `num_records`
|
||||
// uncompressed embedded sub-ranges from one file, coalescing cache-miss reads
|
||||
// into a single MultiRead. Like GetSimpleGen2BlobRange, each record probes
|
||||
// the whole-payload cache (slicing on a hit) and a partial read is never
|
||||
// inserted into the cache. Per-record outcome is in *reqs[i].status.
|
||||
void MultiGetSimpleGen2BlobRange(const ReadOptions& read_options,
|
||||
const OffsetableCacheKey& base_cache_key,
|
||||
RandomAccessFileReader* file,
|
||||
size_t num_records,
|
||||
SimpleGen2BlobRangeReadRequest* reqs);
|
||||
|
||||
// Read multiple blobs from the underlying cache or blob file(s).
|
||||
//
|
||||
// If successful, returns ok and sets "result" in the elements of "blob_reqs"
|
||||
@@ -185,6 +236,24 @@ class BlobSource {
|
||||
autovector<BlobReadRequest>& blob_reqs,
|
||||
uint64_t* bytes_read);
|
||||
|
||||
// Byte-range (partial) multi-read counterpart of MultiGetBlob: for each
|
||||
// request reads only its sub-range of an *uncompressed* blob value, from the
|
||||
// blob cache (whole-value hit, sliced) or coalesced disk reads (one MultiRead
|
||||
// per file). Like GetBlobRange, a partial read never fills the blob cache.
|
||||
// Groups requests by file and dispatches each file via
|
||||
// MultiGetBlobRangeFromOneFile. Per-request outcome is in
|
||||
// *blob_reqs[..].status.
|
||||
void MultiGetBlobRange(const ReadOptions& read_options,
|
||||
autovector<BlobFileRangeReadRequests>& blob_reqs,
|
||||
uint64_t* bytes_read);
|
||||
|
||||
// Byte-range (partial) multi-read counterpart of MultiGetBlobFromOneFile for
|
||||
// a single blob file. See MultiGetBlobRange.
|
||||
void MultiGetBlobRangeFromOneFile(const ReadOptions& read_options,
|
||||
uint64_t file_number, uint64_t file_size,
|
||||
autovector<BlobRangeReadRequest>& blob_reqs,
|
||||
uint64_t* bytes_read);
|
||||
|
||||
inline Status GetBlobFileReader(
|
||||
const ReadOptions& read_options, uint64_t blob_file_number,
|
||||
CacheHandleGuard<BlobFileReader>* blob_file_reader) {
|
||||
|
||||
@@ -1032,6 +1032,53 @@ TEST_F(DBBlobIndexTest, EmbeddedBlobWideColumnGetEntityBlockCacheTier) {
|
||||
ASSERT_TRUE(s.IsIncomplete()) << s.ToString();
|
||||
}
|
||||
|
||||
// Companion to the test above (genuine miss -> Incomplete): with a blob cache
|
||||
// configured, a warm read populates the embedded record into the blob cache, so
|
||||
// a subsequent kBlockCacheTier GetEntity serves it from cache instead of
|
||||
// returning Incomplete. This exercises the non-lazy Get/GetEntity embedded path
|
||||
// (get_context -> EmbeddedAwareBlobFetcher -> GetSameFileBlob whole ->
|
||||
// ResolveEmbeddedBlobCached), which shares the block-cache-tier behavior with
|
||||
// the lazy resolver (a same-file column already in the blob cache is served).
|
||||
TEST_F(DBBlobIndexTest,
|
||||
EmbeddedBlobWideColumnGetEntityBlockCacheTierServesCached) {
|
||||
Options options = GetTestOptions();
|
||||
options.create_if_missing = true;
|
||||
LRUCacheOptions co;
|
||||
co.capacity = 8 << 20;
|
||||
options.blob_cache = NewLRUCache(co);
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string sst_path = dbname_ + "/embedded_wc_blockcache_hit.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 both the block cache (data block) and the blob cache (embedded record)
|
||||
// with a full read.
|
||||
{
|
||||
PinnableWideColumns result;
|
||||
ASSERT_OK(db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(), "k",
|
||||
&result));
|
||||
ASSERT_EQ(result.columns(), columns);
|
||||
}
|
||||
|
||||
// The embedded record is now cached, so a block-cache-only read serves it.
|
||||
ReadOptions read_options;
|
||||
read_options.read_tier = kBlockCacheTier;
|
||||
PinnableWideColumns result;
|
||||
ASSERT_OK(
|
||||
db_->GetEntity(read_options, db_->DefaultColumnFamily(), "k", &result));
|
||||
ASSERT_EQ(result.columns(), columns);
|
||||
}
|
||||
|
||||
// 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
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "db/blob/blob_constants.h"
|
||||
#include "rocksdb/rocksdb_namespace.h"
|
||||
#include "rocksdb/status.h"
|
||||
|
||||
@@ -17,6 +18,18 @@ class BlobIndex;
|
||||
class PinnableSlice;
|
||||
struct ReadOptions;
|
||||
|
||||
// One request for SameFileBlobReader::MultiGetSameFileBlob (the batched
|
||||
// counterpart of GetSameFileBlob). See GetSameFileBlob for the per-field
|
||||
// semantics (range_length == kWholeBlobLength selects the whole value).
|
||||
struct SameFileBlobReadRequest {
|
||||
const BlobIndex* blob_index = nullptr;
|
||||
uint64_t range_offset = 0;
|
||||
size_t range_length = kWholeBlobLength;
|
||||
BlobVerifyPolicy verify_policy = BlobVerifyPolicy::kVerifyIfNoAmplification;
|
||||
PinnableSlice* result = nullptr;
|
||||
Status* status = nullptr;
|
||||
};
|
||||
|
||||
// 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)
|
||||
@@ -30,28 +43,48 @@ 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.
|
||||
// Resolves a same-file ("embedded") 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).
|
||||
//
|
||||
// range_length == kWholeBlobLength resolves the whole value. Any other length
|
||||
// resolves only the sub-range [range_offset, range_offset + range_length),
|
||||
// reading just those bytes on a cache miss -- skipping whole-record checksum
|
||||
// verification (a strict sub-range cannot cover it) and never populating the
|
||||
// blob cache; valid only for an uncompressed record and a verify_policy other
|
||||
// than kVerifyIfPresent, with range_offset + range_length <=
|
||||
// blob_index.size().
|
||||
//
|
||||
// verify_policy controls whole-record checksum verification (see
|
||||
// BlobVerifyPolicy); a whole read under kVerifyIfPresent verifies even when
|
||||
// ReadOptions::verify_checksums is off. 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,
|
||||
uint64_t range_offset, size_t range_length,
|
||||
BlobVerifyPolicy verify_policy,
|
||||
PinnableSlice* value) const = 0;
|
||||
|
||||
// Resolves a byte sub-range [range_offset, range_offset + range_length) of an
|
||||
// *uncompressed* same-file blob reference into `value`, reading only those
|
||||
// bytes on a cache miss (the embedded counterpart of a separate-file blob
|
||||
// range read). Pins the payload zero-copy (a sliced cached record, or an
|
||||
// owned sub-range buffer). Unlike GetSameFileBlob it skips whole-record
|
||||
// checksum verification (a strict sub-range cannot cover it) and never
|
||||
// populates the blob cache. The caller must ensure range_offset +
|
||||
// range_length <= blob_index.size(). read_tier == kBlockCacheTier without a
|
||||
// cached record yields Status::Incomplete.
|
||||
virtual Status GetSameFileBlobRange(const ReadOptions& read_options,
|
||||
const BlobIndex& blob_index,
|
||||
uint64_t range_offset,
|
||||
size_t range_length,
|
||||
PinnableSlice* value) const = 0;
|
||||
// Batched counterpart of GetSameFileBlob: resolves `num_reads` same-file blob
|
||||
// references (whole or sub-range, mixed) against this reader, writing each
|
||||
// outcome to *reqs[i].status. Implementations may coalesce the underlying
|
||||
// disk reads into a single MultiRead. The default implementation simply loops
|
||||
// over GetSameFileBlob (correct, but not coalesced); BlockBasedTable
|
||||
// overrides it to coalesce reads of embedded records in the same SST.
|
||||
virtual void MultiGetSameFileBlob(const ReadOptions& read_options,
|
||||
size_t num_reads,
|
||||
SameFileBlobReadRequest* reqs) const {
|
||||
for (size_t i = 0; i < num_reads; ++i) {
|
||||
SameFileBlobReadRequest& req = reqs[i];
|
||||
const Status s =
|
||||
GetSameFileBlob(read_options, *req.blob_index, req.range_offset,
|
||||
req.range_length, req.verify_policy, req.result);
|
||||
if (req.status) {
|
||||
*req.status = s;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
+2
-1
@@ -207,7 +207,8 @@ Status BuildTable(
|
||||
std::move(file), fname, file_options, ioptions.clock, io_tracer,
|
||||
ioptions.stats, Histograms::SST_WRITE_MICROS, ioptions.listeners,
|
||||
ioptions.file_checksum_gen_factory.get(),
|
||||
tmp_set.Contains(FileType::kTableFile), false));
|
||||
tmp_set.Contains(FileType::kTableFile),
|
||||
tmp_set.Contains(FileType::kTableFile)));
|
||||
|
||||
builder = NewTableBuilder(tboptions, file_writer.get());
|
||||
}
|
||||
|
||||
@@ -2623,6 +2623,59 @@ char* rocksdb_get(rocksdb_t* db, const rocksdb_readoptions_t* options,
|
||||
return result;
|
||||
}
|
||||
|
||||
static char* rocksdb_get_with_metadata_impl(
|
||||
rocksdb_t* db, const rocksdb_readoptions_t* options,
|
||||
ColumnFamilyHandle* column_family, const char* key, size_t keylen,
|
||||
size_t* vallen, char** timestamp, size_t* timestamp_len,
|
||||
unsigned char* newer_version_present, char** errptr) {
|
||||
ROCKSDB_NAMESPACE::OutputMetadata output_metadata;
|
||||
if (timestamp != nullptr && timestamp_len != nullptr) {
|
||||
output_metadata.WantTimestamp();
|
||||
}
|
||||
if (newer_version_present != nullptr) {
|
||||
output_metadata.WantNewerVersionPresent();
|
||||
}
|
||||
|
||||
PinnableSlice pinnable_val;
|
||||
const Status s =
|
||||
db->rep->GetWithMetadata(options->rep, column_family, Slice(key, keylen),
|
||||
&pinnable_val, &output_metadata);
|
||||
if (newer_version_present != nullptr) {
|
||||
*newer_version_present = *output_metadata.newer_version_present ? 1 : 0;
|
||||
}
|
||||
|
||||
char* result = nullptr;
|
||||
if (s.ok()) {
|
||||
*vallen = pinnable_val.size();
|
||||
result = CopyString(pinnable_val);
|
||||
if (output_metadata.timestamp.has_value()) {
|
||||
*timestamp_len = output_metadata.timestamp->size();
|
||||
*timestamp = CopyString(*output_metadata.timestamp);
|
||||
}
|
||||
} else {
|
||||
*vallen = 0;
|
||||
if (output_metadata.timestamp.has_value()) {
|
||||
*timestamp = nullptr;
|
||||
*timestamp_len = 0;
|
||||
}
|
||||
if (!s.IsNotFound()) {
|
||||
SaveError(errptr, s);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
char* rocksdb_get_with_metadata(rocksdb_t* db,
|
||||
const rocksdb_readoptions_t* options,
|
||||
const char* key, size_t keylen, size_t* vallen,
|
||||
char** timestamp, size_t* timestamp_len,
|
||||
unsigned char* newer_version_present,
|
||||
char** errptr) {
|
||||
return rocksdb_get_with_metadata_impl(
|
||||
db, options, db->rep->DefaultColumnFamily(), key, keylen, vallen,
|
||||
timestamp, timestamp_len, newer_version_present, errptr);
|
||||
}
|
||||
|
||||
char* rocksdb_get_cf(rocksdb_t* db, const rocksdb_readoptions_t* options,
|
||||
rocksdb_column_family_handle_t* column_family,
|
||||
const char* key, size_t keylen, size_t* vallen,
|
||||
@@ -2645,6 +2698,16 @@ char* rocksdb_get_cf(rocksdb_t* db, const rocksdb_readoptions_t* options,
|
||||
return result;
|
||||
}
|
||||
|
||||
char* rocksdb_get_cf_with_metadata(
|
||||
rocksdb_t* db, const rocksdb_readoptions_t* options,
|
||||
rocksdb_column_family_handle_t* column_family, const char* key,
|
||||
size_t keylen, size_t* vallen, char** timestamp, size_t* timestamp_len,
|
||||
unsigned char* newer_version_present, char** errptr) {
|
||||
return rocksdb_get_with_metadata_impl(
|
||||
db, options, column_family->rep, key, keylen, vallen, timestamp,
|
||||
timestamp_len, newer_version_present, errptr);
|
||||
}
|
||||
|
||||
char* rocksdb_get_with_ts(rocksdb_t* db, const rocksdb_readoptions_t* options,
|
||||
const char* key, size_t keylen, size_t* vallen,
|
||||
char** ts, size_t* tslen, char** errptr) {
|
||||
@@ -2736,6 +2799,72 @@ void rocksdb_multi_get(rocksdb_t* db, const rocksdb_readoptions_t* options,
|
||||
}
|
||||
}
|
||||
|
||||
static void rocksdb_multi_get_with_metadata_impl(
|
||||
rocksdb_t* db, const rocksdb_readoptions_t* options,
|
||||
ColumnFamilyHandle* const* column_families, size_t num_keys,
|
||||
const char* const* keys_list, const size_t* keys_list_sizes,
|
||||
char** values_list, size_t* values_list_sizes, char** timestamp_list,
|
||||
size_t* timestamp_list_sizes, unsigned char* newer_version_present,
|
||||
char** errs) {
|
||||
std::unique_ptr<Slice[]> keys(new Slice[num_keys]);
|
||||
std::vector<PinnableSlice> values(num_keys);
|
||||
std::vector<Status> statuses(num_keys);
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
keys[i] = Slice(keys_list[i], keys_list_sizes[i]);
|
||||
}
|
||||
|
||||
ROCKSDB_NAMESPACE::MultiGetOutputMetadata output_metadata;
|
||||
if (timestamp_list != nullptr && timestamp_list_sizes != nullptr) {
|
||||
output_metadata.WantTimestamps();
|
||||
}
|
||||
if (newer_version_present != nullptr) {
|
||||
output_metadata.WantNewerVersionPresent();
|
||||
}
|
||||
db->rep->MultiGetWithMetadata(options->rep, num_keys, column_families,
|
||||
keys.get(), values.data(), statuses.data(),
|
||||
&output_metadata);
|
||||
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
if (newer_version_present != nullptr) {
|
||||
newer_version_present[i] =
|
||||
(*output_metadata.newer_version_present)[i] ? 1 : 0;
|
||||
}
|
||||
if (statuses[i].ok()) {
|
||||
values_list[i] = CopyString(values[i]);
|
||||
values_list_sizes[i] = values[i].size();
|
||||
if (output_metadata.timestamps.has_value()) {
|
||||
timestamp_list[i] = CopyString((*output_metadata.timestamps)[i]);
|
||||
timestamp_list_sizes[i] = (*output_metadata.timestamps)[i].size();
|
||||
}
|
||||
errs[i] = nullptr;
|
||||
} else {
|
||||
values_list[i] = nullptr;
|
||||
values_list_sizes[i] = 0;
|
||||
if (output_metadata.timestamps.has_value()) {
|
||||
timestamp_list[i] = nullptr;
|
||||
timestamp_list_sizes[i] = 0;
|
||||
}
|
||||
errs[i] = statuses[i].IsNotFound()
|
||||
? nullptr
|
||||
: strdup(statuses[i].ToString().c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void rocksdb_multi_get_with_metadata(
|
||||
rocksdb_t* db, const rocksdb_readoptions_t* options, size_t num_keys,
|
||||
const char* const* keys_list, const size_t* keys_list_sizes,
|
||||
char** values_list, size_t* values_list_sizes, char** timestamp_list,
|
||||
size_t* timestamp_list_sizes, unsigned char* newer_version_present,
|
||||
char** errs) {
|
||||
std::vector<ColumnFamilyHandle*> column_families(
|
||||
num_keys, db->rep->DefaultColumnFamily());
|
||||
rocksdb_multi_get_with_metadata_impl(
|
||||
db, options, column_families.data(), num_keys, keys_list, keys_list_sizes,
|
||||
values_list, values_list_sizes, timestamp_list, timestamp_list_sizes,
|
||||
newer_version_present, errs);
|
||||
}
|
||||
|
||||
void rocksdb_multi_get_with_ts(rocksdb_t* db,
|
||||
const rocksdb_readoptions_t* options,
|
||||
size_t num_keys, const char* const* keys_list,
|
||||
@@ -2811,6 +2940,24 @@ void rocksdb_multi_get_cf(
|
||||
}
|
||||
}
|
||||
|
||||
void rocksdb_multi_get_cf_with_metadata(
|
||||
rocksdb_t* db, const rocksdb_readoptions_t* options,
|
||||
const rocksdb_column_family_handle_t* const* column_families,
|
||||
size_t num_keys, const char* const* keys_list,
|
||||
const size_t* keys_list_sizes, char** values_list,
|
||||
size_t* values_list_sizes, char** timestamp_list,
|
||||
size_t* timestamp_list_sizes, unsigned char* newer_version_present,
|
||||
char** errs) {
|
||||
std::vector<ColumnFamilyHandle*> cfs(num_keys);
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
cfs[i] = column_families[i]->rep;
|
||||
}
|
||||
rocksdb_multi_get_with_metadata_impl(
|
||||
db, options, cfs.data(), num_keys, keys_list, keys_list_sizes,
|
||||
values_list, values_list_sizes, timestamp_list, timestamp_list_sizes,
|
||||
newer_version_present, errs);
|
||||
}
|
||||
|
||||
void rocksdb_multi_get_cf_with_ts(
|
||||
rocksdb_t* db, const rocksdb_readoptions_t* options,
|
||||
const rocksdb_column_family_handle_t* const* column_families,
|
||||
@@ -5120,6 +5267,11 @@ uint64_t rocksdb_table_properties_separate_key_value_in_data_block(
|
||||
return props->rep.separate_key_value_in_data_block;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_table_properties_lsm_info_at_creation(
|
||||
const rocksdb_table_properties_t* props) {
|
||||
return props->rep.lsm_info_at_creation;
|
||||
}
|
||||
|
||||
const char* rocksdb_table_properties_db_id(
|
||||
const rocksdb_table_properties_t* props, size_t* size) {
|
||||
*size = props->rep.db_id.size();
|
||||
@@ -9883,6 +10035,26 @@ const char* rocksdb_options_get_db_host_id(rocksdb_options_t* opt,
|
||||
return opt->rep.db_host_id.data();
|
||||
}
|
||||
|
||||
void rocksdb_options_set_remote_compaction_manifest_floor(
|
||||
rocksdb_options_t* opt, unsigned char v) {
|
||||
opt->rep.remote_compaction_manifest_floor = v;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_options_get_remote_compaction_manifest_floor(
|
||||
rocksdb_options_t* opt) {
|
||||
return opt->rep.remote_compaction_manifest_floor;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_use_session_tmp_dir_for_remote_compaction(
|
||||
rocksdb_options_t* opt, unsigned char v) {
|
||||
opt->rep.use_session_tmp_dir_for_remote_compaction = v;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_options_get_use_session_tmp_dir_for_remote_compaction(
|
||||
rocksdb_options_t* opt) {
|
||||
return opt->rep.use_session_tmp_dir_for_remote_compaction;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_lowest_used_cache_tier(rocksdb_options_t* opt, int v) {
|
||||
opt->rep.lowest_used_cache_tier =
|
||||
static_cast<decltype(opt->rep.lowest_used_cache_tier)>(v);
|
||||
@@ -9923,6 +10095,16 @@ uint64_t rocksdb_options_get_max_compaction_trigger_wakeup_seconds(
|
||||
return opt->rep.max_compaction_trigger_wakeup_seconds;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_periodic_compaction_phase_recovery_percent(
|
||||
rocksdb_options_t* opt, int v) {
|
||||
opt->rep.periodic_compaction_phase_recovery_percent = v;
|
||||
}
|
||||
|
||||
int rocksdb_options_get_periodic_compaction_phase_recovery_percent(
|
||||
rocksdb_options_t* opt) {
|
||||
return opt->rep.periodic_compaction_phase_recovery_percent;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_follower_refresh_catchup_period_ms(
|
||||
rocksdb_options_t* opt, uint64_t v) {
|
||||
opt->rep.follower_refresh_catchup_period_ms = v;
|
||||
@@ -11184,6 +11366,16 @@ int rocksdb_ingestexternalfileoptions_get_file_opening_threads(
|
||||
|
||||
/* OpenAndCompactOptions */
|
||||
|
||||
void rocksdb_open_and_compact_options_set_max_secondary_open_retries(
|
||||
rocksdb_open_and_compact_options_t* opt, uint32_t v) {
|
||||
opt->rep.max_secondary_open_retries = v;
|
||||
}
|
||||
|
||||
uint32_t rocksdb_open_and_compact_options_get_max_secondary_open_retries(
|
||||
rocksdb_open_and_compact_options_t* opt) {
|
||||
return opt->rep.max_secondary_open_retries;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_open_and_compact_options_get_allow_resumption(
|
||||
rocksdb_open_and_compact_options_t* opt) {
|
||||
return opt->rep.allow_resumption;
|
||||
@@ -11581,6 +11773,17 @@ double rocksdb_block_based_options_get_uniform_cv_threshold(
|
||||
return opt->rep.uniform_cv_threshold;
|
||||
}
|
||||
|
||||
void rocksdb_block_based_options_set_optimize_key_common_prefix(
|
||||
rocksdb_block_based_table_options_t* opt, int v) {
|
||||
opt->rep.optimize_key_common_prefix =
|
||||
static_cast<decltype(opt->rep.optimize_key_common_prefix)>(v);
|
||||
}
|
||||
|
||||
int rocksdb_block_based_options_get_optimize_key_common_prefix(
|
||||
rocksdb_block_based_table_options_t* opt) {
|
||||
return static_cast<int>(opt->rep.optimize_key_common_prefix);
|
||||
}
|
||||
|
||||
void rocksdb_block_based_options_set_enable_index_compression(
|
||||
rocksdb_block_based_table_options_t* opt, unsigned char v) {
|
||||
opt->rep.enable_index_compression = v;
|
||||
|
||||
+29
@@ -2474,6 +2474,35 @@ int main(int argc, char** argv) {
|
||||
CheckNoError(err);
|
||||
rocksdb_readoptions_set_snapshot(roptions, snap);
|
||||
CheckGet(db, roptions, "foo", "hello");
|
||||
|
||||
size_t metadata_value_len = 0;
|
||||
unsigned char newer_version_present = 0;
|
||||
char* metadata_value =
|
||||
rocksdb_get_with_metadata(db, roptions, "foo", 3, &metadata_value_len,
|
||||
NULL, NULL, &newer_version_present, &err);
|
||||
// NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling)
|
||||
CheckNoError(err);
|
||||
CheckEqual("hello", metadata_value, metadata_value_len);
|
||||
// NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling)
|
||||
CheckCondition(newer_version_present == 1);
|
||||
Free(&metadata_value);
|
||||
|
||||
const char* metadata_keys[] = {"foo"};
|
||||
const size_t metadata_key_sizes[] = {3};
|
||||
char* metadata_values[1];
|
||||
size_t metadata_value_sizes[1];
|
||||
unsigned char newer_versions[1];
|
||||
char* metadata_errors[1];
|
||||
rocksdb_multi_get_with_metadata(
|
||||
db, roptions, 1, metadata_keys, metadata_key_sizes, metadata_values,
|
||||
metadata_value_sizes, NULL, NULL, newer_versions, metadata_errors);
|
||||
// NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling)
|
||||
CheckCondition(metadata_errors[0] == NULL);
|
||||
CheckEqual("hello", metadata_values[0], metadata_value_sizes[0]);
|
||||
// NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling)
|
||||
CheckCondition(newer_versions[0] == 1);
|
||||
Free(&metadata_values[0]);
|
||||
|
||||
rocksdb_readoptions_set_snapshot(roptions, NULL);
|
||||
CheckGet(db, roptions, "foo", NULL);
|
||||
rocksdb_release_snapshot(db, snap);
|
||||
|
||||
+11
-8
@@ -1281,15 +1281,15 @@ Compaction* ColumnFamilyData::PickCompaction(
|
||||
}
|
||||
|
||||
bool ColumnFamilyData::RangeOverlapWithCompaction(
|
||||
const Slice& smallest_user_key, const Slice& largest_user_key,
|
||||
int level) const {
|
||||
const Slice& smallest_user_key, const Slice& largest_user_key, int level,
|
||||
bool range_limit_exclusive) const {
|
||||
return compaction_picker_->RangeOverlapWithCompaction(
|
||||
smallest_user_key, largest_user_key, level);
|
||||
smallest_user_key, largest_user_key, level, range_limit_exclusive);
|
||||
}
|
||||
|
||||
Status ColumnFamilyData::RangesOverlapWithMemtables(
|
||||
const autovector<UserKeyRange>& ranges, SuperVersion* super_version,
|
||||
bool allow_data_in_errors, bool* overlap) {
|
||||
bool allow_data_in_errors, bool* overlap, bool range_limit_exclusive) {
|
||||
assert(overlap != nullptr);
|
||||
*overlap = false;
|
||||
// Create an InternalIterator over all unflushed memtables
|
||||
@@ -1334,12 +1334,15 @@ Status ColumnFamilyData::RangesOverlapWithMemtables(
|
||||
}
|
||||
|
||||
if (status.ok()) {
|
||||
const int limit_cmp = memtable_iter->Valid()
|
||||
? ucmp->CompareWithoutTimestamp(
|
||||
seek_result.user_key, ranges[i].limit)
|
||||
: 1;
|
||||
if (memtable_iter->Valid() &&
|
||||
ucmp->CompareWithoutTimestamp(seek_result.user_key,
|
||||
ranges[i].limit) <= 0) {
|
||||
(range_limit_exclusive ? limit_cmp < 0 : limit_cmp <= 0)) {
|
||||
*overlap = true;
|
||||
} else if (range_del_agg.IsRangeOverlapped(ranges[i].start,
|
||||
ranges[i].limit)) {
|
||||
} else if (range_del_agg.IsRangeOverlapped(
|
||||
ranges[i].start, ranges[i].limit, range_limit_exclusive)) {
|
||||
*overlap = true;
|
||||
}
|
||||
}
|
||||
|
||||
+4
-3
@@ -445,8 +445,8 @@ class ColumnFamilyData {
|
||||
// Check if the passed range overlap with any running compactions.
|
||||
// REQUIRES: DB mutex held
|
||||
bool RangeOverlapWithCompaction(const Slice& smallest_user_key,
|
||||
const Slice& largest_user_key,
|
||||
int level) const;
|
||||
const Slice& largest_user_key, int level,
|
||||
bool range_limit_exclusive) const;
|
||||
|
||||
// Check if the passed ranges overlap with any unflushed memtables
|
||||
// (immutable or mutable).
|
||||
@@ -457,7 +457,8 @@ class ColumnFamilyData {
|
||||
// Thread-safe
|
||||
Status RangesOverlapWithMemtables(const autovector<UserKeyRange>& ranges,
|
||||
SuperVersion* super_version,
|
||||
bool allow_data_in_errors, bool* overlap);
|
||||
bool allow_data_in_errors, bool* overlap,
|
||||
bool range_limit_exclusive);
|
||||
|
||||
// A flag to tell a manual compaction is to compact all levels together
|
||||
// instead of a specific level.
|
||||
|
||||
@@ -491,7 +491,8 @@ int Compaction::GetProximalLevel() const { return proximal_level_; }
|
||||
// smallest_key and largest_key include timestamps if user-defined timestamp is
|
||||
// enabled.
|
||||
bool Compaction::OverlapProximalLevelOutputRange(
|
||||
const Slice& smallest_key, const Slice& largest_key) const {
|
||||
const Slice& smallest_key, const Slice& largest_key,
|
||||
bool range_limit_exclusive) const {
|
||||
if (!SupportsPerKeyPlacement()) {
|
||||
return false;
|
||||
}
|
||||
@@ -506,10 +507,11 @@ bool Compaction::OverlapProximalLevelOutputRange(
|
||||
const Comparator* ucmp =
|
||||
input_vstorage_->InternalComparator()->user_comparator();
|
||||
|
||||
const int largest_cmp = ucmp->CompareWithoutTimestamp(
|
||||
largest_key, proximal_level_smallest_.user_key());
|
||||
return ucmp->CompareWithoutTimestamp(
|
||||
smallest_key, proximal_level_largest_.user_key()) <= 0 &&
|
||||
ucmp->CompareWithoutTimestamp(
|
||||
largest_key, proximal_level_smallest_.user_key()) >= 0;
|
||||
(range_limit_exclusive ? largest_cmp > 0 : largest_cmp >= 0);
|
||||
}
|
||||
|
||||
// key includes timestamp if user-defined timestamp is enabled.
|
||||
|
||||
@@ -397,7 +397,8 @@ class Compaction {
|
||||
// Both smallest_key and largest_key include timestamps if user-defined
|
||||
// timestamp is enabled.
|
||||
bool OverlapProximalLevelOutputRange(const Slice& smallest_key,
|
||||
const Slice& largest_key) const;
|
||||
const Slice& largest_key,
|
||||
bool range_limit_exclusive) const;
|
||||
|
||||
// For testing purposes, check that a key is within proximal level
|
||||
// output range for per_key_placement feature, which is safe to place the key
|
||||
|
||||
@@ -1909,8 +1909,7 @@ void CompactionIterator::PrepareOutput() {
|
||||
// KeyNotExistsBeyondOutputLevel() return true?
|
||||
if (Valid() && bottommost_level_ &&
|
||||
DefinitelyInSnapshot(ikey_.sequence, earliest_snapshot_) &&
|
||||
ikey_.type != kTypeMerge && current_key_committed_ &&
|
||||
ikey_.sequence <= preserve_seqno_after_ && !is_range_del_) {
|
||||
ikey_.type != kTypeMerge && current_key_committed_ && !is_range_del_) {
|
||||
assert(compaction_ != nullptr && !compaction_->allow_ingest_behind());
|
||||
if (ikey_.type == kTypeDeletion ||
|
||||
(ikey_.type == kTypeSingleDeletion && timestamp_size_ == 0)) {
|
||||
@@ -1932,19 +1931,29 @@ void CompactionIterator::PrepareOutput() {
|
||||
}
|
||||
|
||||
bool zeroed_seqno = false;
|
||||
if (!timestamp_size_) {
|
||||
current_key_.UpdateInternalKey(0, ikey_.type);
|
||||
zeroed_seqno = true;
|
||||
} else if (full_history_ts_low_ && cmp_with_history_ts_low_ < 0) {
|
||||
// For UDT, the seqno and timestamp could only be zeroed out after the
|
||||
// key is below history_ts_low_.
|
||||
// For the same user key (excluding timestamp), the timestamp-based
|
||||
// history can be collapsed to save some space if the timestamp is
|
||||
// older than *full_history_ts_low_.
|
||||
const std::string kTsMin(timestamp_size_, static_cast<char>(0));
|
||||
const Slice ts_slice = kTsMin;
|
||||
ikey_.SetTimestamp(ts_slice);
|
||||
current_key_.UpdateInternalKey(0, ikey_.type, &ts_slice);
|
||||
// Whether the sequence number can actually be zeroed out here is decided
|
||||
// by the shared BottommostSeqnoCanBeZeroed() predicate, which the
|
||||
// bottommost-file marking logic
|
||||
// (VersionStorageInfo::ComputeBottommostFilesMarkedForCompaction) also
|
||||
// consults. Keeping both on one predicate ensures a file is only marked
|
||||
// for a compaction that can make progress; otherwise the file would be
|
||||
// re-marked forever (infinite compaction loop).
|
||||
if (BottommostSeqnoCanBeZeroed(
|
||||
ikey_.sequence, preserve_seqno_after_, timestamp_size_,
|
||||
full_history_ts_low_ != nullptr, cmp_with_history_ts_low_ < 0)) {
|
||||
if (!timestamp_size_) {
|
||||
current_key_.UpdateInternalKey(0, ikey_.type);
|
||||
} else {
|
||||
// For UDT, the seqno and timestamp could only be zeroed out after the
|
||||
// key is below history_ts_low_.
|
||||
// For the same user key (excluding timestamp), the timestamp-based
|
||||
// history can be collapsed to save some space if the timestamp is
|
||||
// older than *full_history_ts_low_.
|
||||
const std::string kTsMin(timestamp_size_, static_cast<char>(0));
|
||||
const Slice ts_slice = kTsMin;
|
||||
ikey_.SetTimestamp(ts_slice);
|
||||
current_key_.UpdateInternalKey(0, ikey_.type, &ts_slice);
|
||||
}
|
||||
zeroed_seqno = true;
|
||||
}
|
||||
|
||||
|
||||
@@ -395,6 +395,24 @@ void CompactionJob::Prepare(
|
||||
std::min(preclude_last_level_min_seqno, preserve_time_min_seqno);
|
||||
#endif
|
||||
|
||||
// For a bottommost-file compaction, which exists only because the marker
|
||||
// (VersionStorageInfo::ComputeBottommostFilesMarkedForCompaction) decided the
|
||||
// file's largest sequence number could be zeroed, honor that same
|
||||
// version-carried preserve boundary here. The marker reads DBImpl's live,
|
||||
// DB-wide seqno->time mapping, whereas the value computed above is derived
|
||||
// only from the selected file's persisted mapping, which can be sparse or
|
||||
// empty (e.g. a file written before preserve/preclude was enabled). Taking
|
||||
// the max ensures this compaction can zero at least the sequence numbers the
|
||||
// marker considered zeroable, so a marked file makes progress instead of
|
||||
// being re-marked forever (infinite compaction loop). Other compaction
|
||||
// reasons keep the per-input-file boundary so their seqno-zeroing behavior is
|
||||
// unchanged. When preserve/preclude is inactive the version value is
|
||||
// kMaxSequenceNumber, making this a no-op.
|
||||
if (c->compaction_reason() == CompactionReason::kBottommostFiles) {
|
||||
preserve_time_min_seqno = std::max(preserve_time_min_seqno,
|
||||
storage_info->GetPreserveTimeMinSeqno());
|
||||
}
|
||||
|
||||
// Preserve sequence numbers for preserved write times and snapshots, though
|
||||
// the specific sequence number of the earliest snapshot can be zeroed.
|
||||
preserve_seqno_after_ =
|
||||
@@ -423,6 +441,17 @@ void CompactionJob::Prepare(
|
||||
c->GetKeepInLastLevelThroughSeqno());
|
||||
|
||||
options_file_number_ = versions_->options_file_number();
|
||||
|
||||
// For remote compaction hardening: capture the current MANIFEST position as a
|
||||
// floor for the worker's recovery. manifest_file_size() is a physical byte
|
||||
// offset that VersionSet::ProcessManifestWrites records under the DB mutex
|
||||
// only after the whole version-edit batch is written and sync'd, so it is a
|
||||
// durable, complete-batch offset -- never mid-record or mid-atomic-group --
|
||||
// at or past the compaction's (already committed) input version.
|
||||
if (mutable_db_options_copy_.remote_compaction_manifest_floor) {
|
||||
min_manifest_file_number_ = versions_->manifest_file_number();
|
||||
min_manifest_file_size_ = versions_->manifest_file_size();
|
||||
}
|
||||
}
|
||||
|
||||
void CompactionJob::MaybeAssignCompactionProgressAndWriter(
|
||||
@@ -2605,7 +2634,8 @@ Status CompactionJob::OpenCompactionOutputFile(SubcompactionState* sub_compact,
|
||||
std::move(writable_file), fname, fo_copy, db_options_.clock, io_tracer_,
|
||||
db_options_.stats, Histograms::SST_WRITE_MICROS, listeners,
|
||||
db_options_.file_checksum_gen_factory.get(),
|
||||
tmp_set.Contains(FileType::kTableFile), false));
|
||||
tmp_set.Contains(FileType::kTableFile),
|
||||
tmp_set.Contains(FileType::kTableFile)));
|
||||
|
||||
// TODO(hx235): pass in the correct `oldest_key_time` instead of `0`
|
||||
const ReadOptions read_options(Env::IOActivity::kCompaction);
|
||||
|
||||
@@ -524,6 +524,13 @@ class CompactionJob {
|
||||
// Setting this requires DBMutex.
|
||||
uint64_t options_file_number_ = 0;
|
||||
|
||||
// MANIFEST position (file number and size, in bytes) captured in Prepare()
|
||||
// (mutex held) to send to the remote worker as a floor. Zero when
|
||||
// DBOptions::remote_compaction_manifest_floor is off (or this is not a remote
|
||||
// compaction), meaning no floor is sent. Setting these requires DBMutex.
|
||||
uint64_t min_manifest_file_number_ = 0;
|
||||
uint64_t min_manifest_file_size_ = 0;
|
||||
|
||||
// Writer for persisting compaction progress during compaction
|
||||
log::Writer* compaction_progress_writer_ = nullptr;
|
||||
|
||||
@@ -584,6 +591,7 @@ class CompactionJob {
|
||||
struct CompactionServiceInput {
|
||||
std::string cf_name;
|
||||
|
||||
// Snapshot sequence numbers in strictly increasing order.
|
||||
std::vector<SequenceNumber> snapshots;
|
||||
|
||||
// SST files for compaction, it should already be expended to include all the
|
||||
@@ -603,6 +611,18 @@ struct CompactionServiceInput {
|
||||
|
||||
uint64_t options_file_number = 0;
|
||||
|
||||
// MANIFEST position (file number and size, in bytes) the primary scheduled
|
||||
// this compaction from, used as a floor by the remote worker: it refuses to
|
||||
// reconstruct the compaction against an older MANIFEST view (an older file
|
||||
// number, or the same file number read to a smaller size) and falls back to
|
||||
// local compaction. A non-zero min_manifest_file_number also signals the
|
||||
// worker to use "trust the MANIFEST" recovery. Zero means the primary did not
|
||||
// provide a floor (kill switch off, or an older primary); the worker then
|
||||
// uses the original recovery with no floor check.
|
||||
// See DBOptions::remote_compaction_manifest_floor.
|
||||
uint64_t min_manifest_file_number = 0;
|
||||
uint64_t min_manifest_file_size = 0;
|
||||
|
||||
// serialization interface to read and write the object
|
||||
static Status Read(const std::string& data_str, CompactionServiceInput* obj);
|
||||
Status Write(std::string* output);
|
||||
|
||||
@@ -1572,6 +1572,10 @@ TEST_F(CompactionJobTest, InputSerialization) {
|
||||
while (!rnd.OneIn(10)) {
|
||||
input.snapshots.emplace_back(rnd64.Uniform(UINT64_MAX));
|
||||
}
|
||||
std::sort(input.snapshots.begin(), input.snapshots.end());
|
||||
input.snapshots.erase(
|
||||
std::unique(input.snapshots.begin(), input.snapshots.end()),
|
||||
input.snapshots.end());
|
||||
while (!rnd.OneIn(10)) {
|
||||
input.input_files.emplace_back(rnd.RandomString(
|
||||
rnd.Uniform(kStrMaxLen - 1) +
|
||||
@@ -1641,6 +1645,25 @@ TEST_F(CompactionJobTest, InputSerialization) {
|
||||
ASSERT_TRUE(s.IsNotSupported());
|
||||
}
|
||||
|
||||
TEST_F(CompactionJobTest, OpenAndCompactRejectsUnorderedSnapshots) {
|
||||
for (const std::vector<SequenceNumber>& snapshots :
|
||||
{std::vector<SequenceNumber>{2, 1}, std::vector<SequenceNumber>{1, 1}}) {
|
||||
CompactionServiceInput input;
|
||||
input.snapshots = snapshots;
|
||||
|
||||
std::string serialized_input;
|
||||
ASSERT_OK(input.Write(&serialized_input));
|
||||
|
||||
std::string serialized_result;
|
||||
Status s =
|
||||
DB::OpenAndCompact("", "output", serialized_input, &serialized_result,
|
||||
CompactionServiceOptionsOverride());
|
||||
ASSERT_TRUE(s.IsInvalidArgument());
|
||||
ASSERT_NE(s.ToString().find("snapshots must be strictly increasing"),
|
||||
std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(CompactionJobTest, ResultSerialization) {
|
||||
// Setup a random CompactionServiceResult
|
||||
CompactionServiceResult result;
|
||||
|
||||
@@ -267,11 +267,22 @@ bool CompactionOutputs::ShouldStopBefore(const CompactionIterator& c_iter) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// If there's user defined partitioner, check that first
|
||||
if (partitioner_ && partitioner_->ShouldPartition(PartitionerRequest(
|
||||
last_key_for_partitioner_, c_iter.user_key(),
|
||||
current_output_file_size_)) == kRequired) {
|
||||
return true;
|
||||
if (partitioner_) {
|
||||
if (partitioner_prefix_valid_) {
|
||||
// Fast path: no per-key copy or virtual call. A partition is requested at
|
||||
// the first key that no longer shares the established prefix. An empty
|
||||
// prefix means no further partitions (every key "starts with" it).
|
||||
if (UNLIKELY(!c_iter.user_key().starts_with(partitioner_prefix_))) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// Slow path: per-key callback (and copy elsewhere)
|
||||
if (UNLIKELY(partitioner_->ShouldPartition(PartitionerRequest(
|
||||
last_key_for_partitioner_, c_iter.user_key(),
|
||||
current_output_file_size_)) == kRequired)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// files output to Level 0 won't be split
|
||||
@@ -401,11 +412,27 @@ Status CompactionOutputs::AddToOutput(
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
// A new output file starts a fresh partition-tracking window. Establish the
|
||||
// partitioner fast path from this file's first key: if the partitioner can
|
||||
// express its next boundary as "first key not sharing this prefix", cache
|
||||
// that prefix; otherwise fall back to per-key ShouldPartition(). This is
|
||||
// the only place the prefix needs to be (re)derived, since a partition
|
||||
// boundary always forces a file cut, and re-deriving on non-partition cuts
|
||||
// just recomputes the same still-valid prefix.
|
||||
if (partitioner_) {
|
||||
OptSlice prefix =
|
||||
partitioner_->ShouldPartitionByPrefix(c_iter.user_key());
|
||||
partitioner_prefix_valid_ = prefix.has_value();
|
||||
if (partitioner_prefix_valid_) {
|
||||
partitioner_prefix_.assign(prefix->data(), prefix->size());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// c_iter may emit range deletion keys, so update `last_key_for_partitioner_`
|
||||
// here before returning below when `is_range_del` is true
|
||||
if (partitioner_) {
|
||||
// here before returning below when `is_range_del` is true. Only needed in the
|
||||
// fallback mode; the prefix fast path does not use it.
|
||||
if (partitioner_ && !partitioner_prefix_valid_) {
|
||||
last_key_for_partitioner_.assign(c_iter.user_key().data_,
|
||||
c_iter.user_key().size_);
|
||||
}
|
||||
|
||||
@@ -349,8 +349,19 @@ class CompactionOutputs {
|
||||
const bool is_proximal_level_;
|
||||
|
||||
// partitioner information
|
||||
std::string last_key_for_partitioner_;
|
||||
std::unique_ptr<SstPartitioner> partitioner_;
|
||||
// Previous user key, only maintained (copied per key) in the fallback mode
|
||||
// (see below).
|
||||
std::string last_key_for_partitioner_;
|
||||
// Fast path for partitioners that support ShouldPartitionByPrefix(): when
|
||||
// partitioner_prefix_valid_ is true, all keys in the current output file
|
||||
// share partitioner_prefix_, and a partition boundary is requested at the
|
||||
// first future key that does not start with that prefix. This is (re)derived
|
||||
// once per output file from its first key, avoiding a per-key copy and
|
||||
// virtual ShouldPartition() call. When false, we fall back to comparing
|
||||
// against last_key_for_partitioner_ via ShouldPartition() on every key.
|
||||
std::string partitioner_prefix_;
|
||||
bool partitioner_prefix_valid_ = false;
|
||||
|
||||
// A flag determines if this subcompaction has been split by the cursor
|
||||
// for RoundRobin compaction
|
||||
|
||||
@@ -319,21 +319,22 @@ bool CompactionPicker::ExpandInputsToCleanCut(const std::string& /*cf_name*/,
|
||||
}
|
||||
|
||||
bool CompactionPicker::RangeOverlapWithCompaction(
|
||||
const Slice& smallest_user_key, const Slice& largest_user_key,
|
||||
int level) const {
|
||||
const Slice& smallest_user_key, const Slice& largest_user_key, int level,
|
||||
bool range_limit_exclusive) const {
|
||||
const Comparator* ucmp = icmp_->user_comparator();
|
||||
for (Compaction* c : compactions_in_progress_) {
|
||||
const int largest_cmp = ucmp->CompareWithoutTimestamp(
|
||||
largest_user_key, c->GetSmallestUserKey());
|
||||
if (c->output_level() == level &&
|
||||
ucmp->CompareWithoutTimestamp(smallest_user_key,
|
||||
c->GetLargestUserKey()) <= 0 &&
|
||||
ucmp->CompareWithoutTimestamp(largest_user_key,
|
||||
c->GetSmallestUserKey()) >= 0) {
|
||||
(range_limit_exclusive ? largest_cmp > 0 : largest_cmp >= 0)) {
|
||||
// Overlap
|
||||
return true;
|
||||
}
|
||||
if (c->SupportsPerKeyPlacement()) {
|
||||
if (c->OverlapProximalLevelOutputRange(smallest_user_key,
|
||||
largest_user_key)) {
|
||||
if (c->OverlapProximalLevelOutputRange(
|
||||
smallest_user_key, largest_user_key, range_limit_exclusive)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -364,7 +365,8 @@ bool CompactionPicker::FilesRangeOverlapWithCompaction(
|
||||
if (proximal_level != Compaction::kInvalidLevel) {
|
||||
if (ioptions_.compaction_style == kCompactionStyleUniversal) {
|
||||
if (RangeOverlapWithCompaction(smallest.user_key(), largest.user_key(),
|
||||
proximal_level)) {
|
||||
proximal_level,
|
||||
/*range_limit_exclusive=*/false)) {
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
@@ -372,14 +374,16 @@ bool CompactionPicker::FilesRangeOverlapWithCompaction(
|
||||
GetRange(inputs, &proximal_smallest, &proximal_largest, level);
|
||||
if (RangeOverlapWithCompaction(proximal_smallest.user_key(),
|
||||
proximal_largest.user_key(),
|
||||
proximal_level)) {
|
||||
proximal_level,
|
||||
/*range_limit_exclusive=*/false)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return RangeOverlapWithCompaction(smallest.user_key(), largest.user_key(),
|
||||
level);
|
||||
level,
|
||||
/*range_limit_exclusive=*/false);
|
||||
}
|
||||
|
||||
// Returns true if any one of specified files are being compacted
|
||||
|
||||
@@ -154,8 +154,8 @@ class CompactionPicker {
|
||||
// Return true if the passed key range overlap with a compaction output
|
||||
// that is currently running.
|
||||
bool RangeOverlapWithCompaction(const Slice& smallest_user_key,
|
||||
const Slice& largest_user_key,
|
||||
int level) const;
|
||||
const Slice& largest_user_key, int level,
|
||||
bool range_limit_exclusive) const;
|
||||
|
||||
// Stores the minimal range that covers all entries in inputs in
|
||||
// *smallest, *largest.
|
||||
|
||||
@@ -90,7 +90,8 @@ class CompactionPickerTestBase : public testing::Test {
|
||||
&icmp_, ucmp_, options_.num_levels, style, nullptr, false,
|
||||
EpochNumberRequirement::kMustPresent, ioptions_.clock,
|
||||
options_.bottommost_file_compaction_delay,
|
||||
OffpeakTimeOption(mutable_db_options_.daily_offpeak_time_utc)));
|
||||
OffpeakTimeOption(mutable_db_options_.daily_offpeak_time_utc),
|
||||
PeriodicCompactionPhaseParams{}));
|
||||
vstorage_->PrepareForVersionAppend(ioptions_, mutable_cf_options_);
|
||||
}
|
||||
|
||||
@@ -101,7 +102,8 @@ class CompactionPickerTestBase : public testing::Test {
|
||||
&icmp_, ucmp_, options_.num_levels, ioptions_.compaction_style,
|
||||
vstorage_.get(), false, EpochNumberRequirement::kMustPresent,
|
||||
ioptions_.clock, options_.bottommost_file_compaction_delay,
|
||||
OffpeakTimeOption(mutable_db_options_.daily_offpeak_time_utc)));
|
||||
OffpeakTimeOption(mutable_db_options_.daily_offpeak_time_utc),
|
||||
PeriodicCompactionPhaseParams{}));
|
||||
}
|
||||
|
||||
void DeleteVersionStorage() {
|
||||
@@ -4162,6 +4164,18 @@ TEST_F(CompactionPickerU64TsTest, Overlap) {
|
||||
CompactionOptions(), input_files, level, vstorage_.get(),
|
||||
mutable_cf_options_, mutable_db_options_, /*output_path_id=*/0));
|
||||
|
||||
{
|
||||
std::string range_start("400");
|
||||
PutFixed64(&range_start, ts_of_smallest);
|
||||
std::string range_limit(smallest);
|
||||
PutFixed64(&range_limit, ts_of_smallest);
|
||||
ASSERT_FALSE(level_compaction_picker.RangeOverlapWithCompaction(
|
||||
range_start, range_limit, level, /*range_limit_exclusive=*/true));
|
||||
ASSERT_TRUE(level_compaction_picker.RangeOverlapWithCompaction(
|
||||
range_start, range_limit, level,
|
||||
/*range_limit_exclusive=*/false));
|
||||
}
|
||||
|
||||
{
|
||||
// [600, ts=50000] to [600, ts=50000] is the range to check.
|
||||
// ucmp->Compare(smallest_user_key, c->GetLargestUserKey()) > 0, but
|
||||
@@ -4173,7 +4187,8 @@ TEST_F(CompactionPickerU64TsTest, Overlap) {
|
||||
std::string user_key_with_ts2(largest);
|
||||
PutFixed64(&user_key_with_ts2, ts_of_largest - 1);
|
||||
ASSERT_TRUE(level_compaction_picker.RangeOverlapWithCompaction(
|
||||
user_key_with_ts1, user_key_with_ts2, level));
|
||||
user_key_with_ts1, user_key_with_ts2, level,
|
||||
/*range_limit_exclusive=*/false));
|
||||
}
|
||||
{
|
||||
// [500, ts=60000] to [500, ts=60000] is the range to check.
|
||||
@@ -4186,7 +4201,8 @@ TEST_F(CompactionPickerU64TsTest, Overlap) {
|
||||
std::string user_key_with_ts2(smallest);
|
||||
PutFixed64(&user_key_with_ts2, ts_of_smallest + 1);
|
||||
ASSERT_TRUE(level_compaction_picker.RangeOverlapWithCompaction(
|
||||
user_key_with_ts1, user_key_with_ts2, level));
|
||||
user_key_with_ts1, user_key_with_ts2, level,
|
||||
/*range_limit_exclusive=*/false));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5058,6 +5074,41 @@ TEST_F(CompactionPickerTest, StandaloneRangeDeletionOnlyPicksOlderFiles) {
|
||||
ASSERT_EQ(10U, compaction->input(1, 0)->fd.GetNumber());
|
||||
}
|
||||
|
||||
// Tests for the seqno->time preserve window controlling bottommost file
|
||||
// marking. A bottommost file whose largest seqno is still within the preserve
|
||||
// window cannot have its seqno zeroed, so it must not be marked (otherwise the
|
||||
// rewrite makes no progress and the file is re-marked forever -- an infinite
|
||||
// loop).
|
||||
TEST_F(CompactionPickerTest, BottommostFileNotMarkedWithinPreserveWindow) {
|
||||
NewVersionStorage(6, kCompactionStyleLevel);
|
||||
Add(5, 1U, "100", "200", /*file_size=*/1000, /*path_id=*/0,
|
||||
/*smallest_seq=*/90, /*largest_seq=*/100);
|
||||
// preserve_time_min_seqno = 100 => preserve_seqno_after = 99; largest_seqno
|
||||
// 100 is within the window (> 99) and cannot be zeroed.
|
||||
vstorage_->SetPreserveTimeMinSeqno(100);
|
||||
UpdateVersionStorageInfo();
|
||||
vstorage_->UpdateOldestSnapshot(/*oldest_snapshot_seqnum=*/200,
|
||||
/*allow_ingest_behind=*/false, /*ucmp=*/ucmp_,
|
||||
/*full_history_ts_low=*/"");
|
||||
ASSERT_EQ(0U, vstorage_->BottommostFilesMarkedForCompaction().size());
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerTest, BottommostFileMarkedBelowPreserveWindow) {
|
||||
NewVersionStorage(6, kCompactionStyleLevel);
|
||||
Add(5, 1U, "100", "200", /*file_size=*/1000, /*path_id=*/0,
|
||||
/*smallest_seq=*/90, /*largest_seq=*/100);
|
||||
// preserve_time_min_seqno = 150 => preserve_seqno_after = 149; largest_seqno
|
||||
// 100 <= 149 so it can be zeroed and the file is marked as usual.
|
||||
vstorage_->SetPreserveTimeMinSeqno(150);
|
||||
UpdateVersionStorageInfo();
|
||||
vstorage_->UpdateOldestSnapshot(/*oldest_snapshot_seqnum=*/200,
|
||||
/*allow_ingest_behind=*/false, /*ucmp=*/ucmp_,
|
||||
/*full_history_ts_low=*/"");
|
||||
ASSERT_EQ(1U, vstorage_->BottommostFilesMarkedForCompaction().size());
|
||||
ASSERT_EQ(1U, vstorage_->BottommostFilesMarkedForCompaction()[0]
|
||||
.second->fd.GetNumber());
|
||||
}
|
||||
|
||||
// Tests for full_history_ts_low parameter in compaction picker.
|
||||
// The full_history_ts_low parameter is used to control bottommost file marking
|
||||
// for compaction when user-defined timestamps (UDT) are enabled.
|
||||
@@ -5137,6 +5188,90 @@ TEST_F(CompactionPickerU64TsTest,
|
||||
ASSERT_EQ(0U, vstorage_->BottommostFilesMarkedForCompaction().size());
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerU64TsTest,
|
||||
BottommostNotMarkedWithEmptyFullHistoryTsLowAndUnknownMaxTs) {
|
||||
// Regression test for an infinite bottommost compaction loop. A bottommost
|
||||
// UDT file whose max_timestamp was not recorded in its FileMetaData (e.g. a
|
||||
// file written by an older version, or one whose timestamp property is
|
||||
// otherwise absent) must NOT be marked for compaction when
|
||||
// full_history_ts_low is unset. Under UDT the only progress a bottommost
|
||||
// compaction can make here is zeroing sequence numbers, which
|
||||
// CompactionIterator does only when full_history_ts_low is set. With it unset
|
||||
// the compaction cannot advance the file, so the rewritten output
|
||||
// re-qualifies forever.
|
||||
NewVersionStorage(6, kCompactionStyleLevel);
|
||||
|
||||
std::string ts_small = MakeU64Timestamp(50);
|
||||
std::string ts_large = MakeU64Timestamp(100);
|
||||
|
||||
// Add a bottommost file with seqno < oldest_snapshot.
|
||||
Add(5, 1U, "100", "200", /*file_size=*/1000, /*path_id=*/0,
|
||||
/*smallest_seq=*/10, /*largest_seq=*/40,
|
||||
/*compensated_file_size=*/1000,
|
||||
/*marked_for_compact=*/false, Temperature::kUnknown,
|
||||
kUnknownOldestAncesterTime, kUnknownNewestKeyTime, ts_small, ts_large);
|
||||
|
||||
// Simulate a file whose min/max timestamp was never recorded.
|
||||
ASSERT_EQ(1U, vstorage_->LevelFiles(5).size());
|
||||
FileMetaData* f = vstorage_->LevelFiles(5)[0];
|
||||
f->min_timestamp.clear();
|
||||
f->max_timestamp.clear();
|
||||
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
vstorage_->UpdateOldestSnapshot(
|
||||
/*oldest_snapshot_seqnum=*/50,
|
||||
/*allow_ingest_behind=*/false,
|
||||
/*ucmp=*/ucmp_,
|
||||
/*full_history_ts_low=*/"");
|
||||
|
||||
// No bottommost file can make progress with empty full_history_ts_low, so
|
||||
// nothing is marked even though max_timestamp is unknown. Before the fix this
|
||||
// file was marked, producing an infinite compaction loop.
|
||||
ASSERT_EQ(0U, vstorage_->BottommostFilesMarkedForCompaction().size());
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerU64TsTest,
|
||||
BottommostMarkedWithSetFullHistoryTsLowAndUnknownMaxTs) {
|
||||
// A bottommost UDT file whose max_timestamp was not recorded (e.g. written by
|
||||
// an older version) must still be marked for a one-time backfill compaction
|
||||
// when full_history_ts_low IS set: that compaction can collapse timestamp
|
||||
// history / zero seqnos and record max_timestamp, so it makes progress
|
||||
// (bounded, not a loop). This complements
|
||||
// BottommostNotMarkedWithEmptyFullHistoryTsLowAndUnknownMaxTs, which covers
|
||||
// the unset case where no progress is possible.
|
||||
NewVersionStorage(6, kCompactionStyleLevel);
|
||||
|
||||
std::string ts_small = MakeU64Timestamp(50);
|
||||
std::string ts_large = MakeU64Timestamp(100);
|
||||
|
||||
Add(5, 1U, "100", "200", /*file_size=*/1000, /*path_id=*/0,
|
||||
/*smallest_seq=*/10, /*largest_seq=*/40,
|
||||
/*compensated_file_size=*/1000,
|
||||
/*marked_for_compact=*/false, Temperature::kUnknown,
|
||||
kUnknownOldestAncesterTime, kUnknownNewestKeyTime, ts_small, ts_large);
|
||||
|
||||
// Simulate a file whose min/max timestamp was never recorded.
|
||||
ASSERT_EQ(1U, vstorage_->LevelFiles(5).size());
|
||||
FileMetaData* f = vstorage_->LevelFiles(5)[0];
|
||||
f->min_timestamp.clear();
|
||||
f->max_timestamp.clear();
|
||||
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
std::string full_history_ts_low = MakeU64Timestamp(500);
|
||||
vstorage_->UpdateOldestSnapshot(
|
||||
/*oldest_snapshot_seqnum=*/50,
|
||||
/*allow_ingest_behind=*/false,
|
||||
/*ucmp=*/ucmp_, full_history_ts_low);
|
||||
|
||||
// With full_history_ts_low set, the unknown-max_timestamp file is marked so a
|
||||
// bounded compaction can backfill its metadata and reclaim obsolete data.
|
||||
ASSERT_EQ(1U, vstorage_->BottommostFilesMarkedForCompaction().size());
|
||||
ASSERT_EQ(1U, vstorage_->BottommostFilesMarkedForCompaction()[0]
|
||||
.second->fd.GetNumber());
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerU64TsTest, LevelPickCompactionWithFullHistoryTsLow) {
|
||||
// Test that level compaction correctly passes full_history_ts_low
|
||||
// and picks compaction appropriately
|
||||
|
||||
@@ -50,6 +50,8 @@ CompactionJob::ProcessKeyValueCompactionWithCompactionService(
|
||||
compaction_input.end =
|
||||
compaction_input.has_end ? sub_compact->end->ToString() : "";
|
||||
compaction_input.options_file_number = options_file_number_;
|
||||
compaction_input.min_manifest_file_number = min_manifest_file_number_;
|
||||
compaction_input.min_manifest_file_size = min_manifest_file_size_;
|
||||
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"CompactionServiceJob::ProcessKeyValueCompactionWithCompactionService",
|
||||
@@ -82,7 +84,6 @@ CompactionJob::ProcessKeyValueCompactionWithCompactionService(
|
||||
compaction->is_full_compaction(), compaction->is_manual_compaction(),
|
||||
compaction->bottommost_level(), compaction->start_level(),
|
||||
compaction->output_level());
|
||||
|
||||
CompactionServiceScheduleResponse response =
|
||||
db_options_.compaction_service->Schedule(info, compaction_input_binary);
|
||||
switch (response.status) {
|
||||
@@ -564,6 +565,14 @@ static std::unordered_map<std::string, OptionTypeInfo> cs_input_type_info = {
|
||||
{offsetof(struct CompactionServiceInput, options_file_number),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"min_manifest_file_number",
|
||||
{offsetof(struct CompactionServiceInput, min_manifest_file_number),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"min_manifest_file_size",
|
||||
{offsetof(struct CompactionServiceInput, min_manifest_file_size),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
};
|
||||
|
||||
static std::unordered_map<std::string, OptionTypeInfo>
|
||||
@@ -1075,9 +1084,19 @@ Status CompactionServiceInput::Read(const std::string& data_str,
|
||||
ConfigOptions cf;
|
||||
cf.invoke_prepare_options = false;
|
||||
cf.ignore_unknown_options = true;
|
||||
return OptionTypeInfo::ParseType(
|
||||
Status s = OptionTypeInfo::ParseType(
|
||||
cf, data_str.substr(sizeof(BinaryFormatVersion)), cs_input_type_info,
|
||||
obj);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
for (size_t i = 1; i < obj->snapshots.size(); ++i) {
|
||||
if (obj->snapshots[i - 1] >= obj->snapshots[i]) {
|
||||
return Status::InvalidArgument(
|
||||
"CompactionServiceInput snapshots must be strictly increasing");
|
||||
}
|
||||
}
|
||||
return Status::OK();
|
||||
} else {
|
||||
return Status::NotSupported(
|
||||
"Compaction Service Input data version not supported: " +
|
||||
|
||||
@@ -5,9 +5,12 @@
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "db/db_test_util.h"
|
||||
#include "file/file_util.h"
|
||||
#include "file/filename.h"
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/advanced_compression.h"
|
||||
#include "rocksdb/filter_policy.h"
|
||||
@@ -81,6 +84,82 @@ size_t CountFirstCountsFieldElements(const std::string& s) {
|
||||
return count;
|
||||
}
|
||||
|
||||
// Simulates a filesystem without POSIX read-after-unlink semantics (a remote
|
||||
// or object store): once the primary renames a freshly written CURRENT over
|
||||
// the old one, a reader that already opened the previous CURRENT object can no
|
||||
// longer read it and the filesystem reports the path as gone rather than
|
||||
// serving the unlinked object. Fails the first `fail_times` reads of a CURRENT
|
||||
// file that way; later reads succeed, as they do once the reader picks up the
|
||||
// replacement.
|
||||
class VanishingCurrentFileFS : public FileSystemWrapper {
|
||||
public:
|
||||
explicit VanishingCurrentFileFS(const std::shared_ptr<FileSystem>& base)
|
||||
: FileSystemWrapper(base) {}
|
||||
|
||||
static const char* kClassName() { return "VanishingCurrentFileFS"; }
|
||||
const char* Name() const override { return kClassName(); }
|
||||
|
||||
IOStatus NewSequentialFile(const std::string& fname, const FileOptions& opts,
|
||||
std::unique_ptr<FSSequentialFile>* result,
|
||||
IODebugContext* dbg) override {
|
||||
IOStatus io_s = target()->NewSequentialFile(fname, opts, result, dbg);
|
||||
if (io_s.ok() && IsCurrentFileName(fname)) {
|
||||
*result = std::make_unique<MaybeVanishedFile>(std::move(*result), this);
|
||||
}
|
||||
return io_s;
|
||||
}
|
||||
|
||||
void SetFailuresToInject(int failures) {
|
||||
failures_to_inject_.store(failures);
|
||||
}
|
||||
|
||||
int injected_failures() const { return injected_failures_.load(); }
|
||||
|
||||
private:
|
||||
static bool IsCurrentFileName(const std::string& fname) {
|
||||
const std::string suffix = "/" + kCurrentFileName;
|
||||
return fname.size() > suffix.size() &&
|
||||
fname.compare(fname.size() - suffix.size(), suffix.size(), suffix) ==
|
||||
0;
|
||||
}
|
||||
|
||||
// Returns true (consuming one injection) while failures remain.
|
||||
bool ConsumeFailure() {
|
||||
int remaining = failures_to_inject_.load();
|
||||
while (remaining > 0) {
|
||||
if (failures_to_inject_.compare_exchange_weak(remaining, remaining - 1)) {
|
||||
injected_failures_.fetch_add(1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
class MaybeVanishedFile : public FSSequentialFileOwnerWrapper {
|
||||
public:
|
||||
MaybeVanishedFile(std::unique_ptr<FSSequentialFile>&& target,
|
||||
VanishingCurrentFileFS* fs)
|
||||
: FSSequentialFileOwnerWrapper(std::move(target)), fs_(fs) {}
|
||||
|
||||
IOStatus Read(size_t n, const IOOptions& options, Slice* result,
|
||||
char* scratch, IODebugContext* dbg) override {
|
||||
if (fs_->ConsumeFailure()) {
|
||||
return IOStatus::PathNotFound(
|
||||
"Failed to read CURRENT: the object was replaced and is no longer "
|
||||
"readable");
|
||||
}
|
||||
return FSSequentialFileOwnerWrapper::Read(n, options, result, scratch,
|
||||
dbg);
|
||||
}
|
||||
|
||||
private:
|
||||
VanishingCurrentFileFS* fs_;
|
||||
};
|
||||
|
||||
std::atomic<int> failures_to_inject_{0};
|
||||
std::atomic<int> injected_failures_{0};
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
class MyTestCompactionService : public CompactionService {
|
||||
@@ -113,6 +192,7 @@ class MyTestCompactionService : public CompactionService {
|
||||
start_info_ = info;
|
||||
assert(info.db_name == db_path_);
|
||||
std::string unique_id = Env::Default()->GenerateUniqueId();
|
||||
last_output_directory_name_ = unique_id;
|
||||
jobs_.emplace(unique_id, compaction_service_input);
|
||||
infos_.emplace(unique_id, info);
|
||||
CompactionServiceScheduleResponse response(
|
||||
@@ -149,10 +229,10 @@ class MyTestCompactionService : public CompactionService {
|
||||
|
||||
OpenAndCompactOptions options;
|
||||
options.canceled = &canceled_;
|
||||
options.max_secondary_open_retries = max_secondary_open_retries_;
|
||||
|
||||
Status s =
|
||||
DB::OpenAndCompact(options, db_path_, GetOutputPath(scheduled_job_id),
|
||||
compaction_input, result, options_override);
|
||||
Status s = DB::OpenAndCompact(options, db_path_, scheduled_job_id,
|
||||
compaction_input, result, options_override);
|
||||
{
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
if (is_override_wait_result_) {
|
||||
@@ -163,6 +243,14 @@ class MyTestCompactionService : public CompactionService {
|
||||
compaction_num_.fetch_add(1);
|
||||
if (s.ok()) {
|
||||
return CompactionServiceJobStatus::kSuccess;
|
||||
} else if (s.IsIncomplete() && !s.IsManualCompactionPaused()) {
|
||||
// The remote worker declined the job (e.g. the MANIFEST floor check
|
||||
// rejected an older/stale filesystem view). Mapping that decline to
|
||||
// kUseLocal (retry locally) vs. kFailure (surface it) is the integrator's
|
||||
// choice; this example picks kUseLocal to exercise the fallback path,
|
||||
// while DbStressCompactionService deliberately picks kFailure. Real
|
||||
// cancellation (kManualCompactionPaused) is handled as a failure.
|
||||
return CompactionServiceJobStatus::kUseLocal;
|
||||
} else {
|
||||
return CompactionServiceJobStatus::kFailure;
|
||||
}
|
||||
@@ -204,6 +292,11 @@ class MyTestCompactionService : public CompactionService {
|
||||
|
||||
int GetCompactionNum() { return compaction_num_.load(); }
|
||||
|
||||
std::string GetLastOutputDirectoryName() {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
return last_output_directory_name_;
|
||||
}
|
||||
|
||||
CompactionServiceJobInfo GetCompactionInfoForStart() { return start_info_; }
|
||||
CompactionServiceJobInfo GetCompactionInfoForWait() { return wait_info_; }
|
||||
|
||||
@@ -231,6 +324,10 @@ class MyTestCompactionService : public CompactionService {
|
||||
void SetCanceled(bool canceled) { canceled_ = canceled; }
|
||||
bool GetCanceled() { return canceled_; }
|
||||
|
||||
void SetMaxSecondaryOpenRetries(uint32_t retries) {
|
||||
max_secondary_open_retries_ = retries;
|
||||
}
|
||||
|
||||
void GetResult(CompactionServiceResult* deserialized) {
|
||||
CompactionServiceResult::Read(result_, deserialized).PermitUncheckedError();
|
||||
}
|
||||
@@ -248,9 +345,12 @@ class MyTestCompactionService : public CompactionService {
|
||||
std::map<std::string, std::string> jobs_;
|
||||
std::map<std::string, CompactionServiceJobInfo> infos_;
|
||||
std::string result_;
|
||||
std::string last_output_directory_name_;
|
||||
|
||||
std::string GetOutputPath(const std::string& scheduled_job_id) {
|
||||
return db_path_ + "/" + scheduled_job_id;
|
||||
return RemoteCompactionJobDir(
|
||||
db_path_, options_.use_session_tmp_dir_for_remote_compaction,
|
||||
scheduled_job_id);
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -272,6 +372,8 @@ class MyTestCompactionService : public CompactionService {
|
||||
std::atomic_int installation_callback_count_{0};
|
||||
std::atomic<CompactionServiceJobStatus> final_updated_status_{
|
||||
CompactionServiceJobStatus::kUseLocal};
|
||||
uint32_t max_secondary_open_retries_ =
|
||||
OpenAndCompactOptions().max_secondary_open_retries;
|
||||
|
||||
protected:
|
||||
std::atomic_bool canceled_{false};
|
||||
@@ -399,8 +501,12 @@ TEST_F(CompactionServiceTest, BasicCompactions) {
|
||||
ASSERT_EQ(primary_statistics->getTickerCount(REMOTE_COMPACT_WRITE_BYTES),
|
||||
compactor_statistics->getTickerCount(COMPACT_WRITE_BYTES));
|
||||
ASSERT_GE(primary_statistics->getTickerCount(REMOTE_COMPACT_READ_BYTES), 1);
|
||||
ASSERT_GT(primary_statistics->getTickerCount(COMPACT_READ_BYTES),
|
||||
primary_statistics->getTickerCount(REMOTE_COMPACT_READ_BYTES));
|
||||
// The remote worker reads the full compaction inputs on demand, while the
|
||||
// primary only re-reads the (smaller) output files for verify_table(), so
|
||||
// remote read bytes dominate. (The worker no longer eagerly opens every file
|
||||
// during secondary recovery, so its input reads are attributed accurately.)
|
||||
ASSERT_GT(primary_statistics->getTickerCount(REMOTE_COMPACT_READ_BYTES),
|
||||
primary_statistics->getTickerCount(COMPACT_READ_BYTES));
|
||||
// compactor is already the remote side, which doesn't have remote
|
||||
ASSERT_EQ(compactor_statistics->getTickerCount(REMOTE_COMPACT_READ_BYTES), 0);
|
||||
ASSERT_EQ(compactor_statistics->getTickerCount(REMOTE_COMPACT_WRITE_BYTES),
|
||||
@@ -438,6 +544,27 @@ TEST_F(CompactionServiceTest, BasicCompactions) {
|
||||
}
|
||||
ASSERT_TRUE(s.IsAborted());
|
||||
|
||||
CompactionServiceResult result;
|
||||
my_cs->GetResult(&result);
|
||||
ASSERT_NOK(result.status);
|
||||
|
||||
// The aborted job's output files were never installed, so they are still in
|
||||
// the job's staging directory. Size them before reopening: the reopen starts
|
||||
// a new DB session, and DB::Open() deletes staging directories belonging to
|
||||
// any other session.
|
||||
uint64_t total_size = 0;
|
||||
Env* const env = options.env;
|
||||
ASSERT_TRUE(options.env != nullptr);
|
||||
for (const auto& output_file : result.output_files) {
|
||||
std::string file_name = result.output_path + "/" + output_file.file_name;
|
||||
|
||||
uint64_t file_size = 0;
|
||||
ASSERT_OK(env->GetFileSize(file_name, &file_size));
|
||||
ASSERT_GT(file_size, 0);
|
||||
total_size += file_size;
|
||||
}
|
||||
ASSERT_EQ(total_size, result.internal_stats.TotalBytesWritten());
|
||||
|
||||
// Test re-open and successful unique id verification
|
||||
std::atomic_int verify_passed{0};
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
@@ -452,13 +579,6 @@ TEST_F(CompactionServiceTest, BasicCompactions) {
|
||||
ReopenWithColumnFamilies({kDefaultColumnFamilyName, "cf_1", "cf_2", "cf_3"},
|
||||
options);
|
||||
ASSERT_GT(verify_passed, 0);
|
||||
CompactionServiceResult result;
|
||||
my_cs->GetResult(&result);
|
||||
if (s.IsAborted()) {
|
||||
ASSERT_NOK(result.status);
|
||||
} else {
|
||||
ASSERT_OK(result.status);
|
||||
}
|
||||
ASSERT_GE(result.internal_stats.output_level_stats.micros, 1);
|
||||
ASSERT_GE(result.internal_stats.output_level_stats.cpu_micros, 1);
|
||||
|
||||
@@ -466,17 +586,6 @@ TEST_F(CompactionServiceTest, BasicCompactions) {
|
||||
ASSERT_EQ(result.output_files.size(),
|
||||
result.internal_stats.output_level_stats.num_output_files);
|
||||
|
||||
uint64_t total_size = 0;
|
||||
for (auto output_file : result.output_files) {
|
||||
std::string file_name = result.output_path + "/" + output_file.file_name;
|
||||
|
||||
uint64_t file_size = 0;
|
||||
ASSERT_OK(options.env->GetFileSize(file_name, &file_size));
|
||||
ASSERT_GT(file_size, 0);
|
||||
total_size += file_size;
|
||||
}
|
||||
ASSERT_EQ(total_size, result.internal_stats.TotalBytesWritten());
|
||||
|
||||
ASSERT_TRUE(result.stats.is_remote_compaction);
|
||||
ASSERT_TRUE(result.stats.is_manual_compaction);
|
||||
ASSERT_FALSE(result.stats.is_full_compaction);
|
||||
@@ -2169,6 +2278,489 @@ TEST_F(CompactionServiceTest, ConcurrentCompaction) {
|
||||
ASSERT_EQ(FilesPerLevel(), "0,0,10");
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, RemoteCompactionMissingLowerFileTrustsManifest) {
|
||||
// A remote compaction reconstructs its view of the LSM (which files sit below
|
||||
// the output level) from the secondary's recovered MANIFEST version. That
|
||||
// view drives correctness-critical decisions like whether a deletion
|
||||
// tombstone can be dropped (Compaction::KeyNotExistsBeyondOutputLevel /
|
||||
// IsBottommostLevel, which read input_version_'s VersionStorageInfo).
|
||||
//
|
||||
// If a non-input file that is still referenced by the MANIFEST is momentarily
|
||||
// missing (as on a live primary), the OLD point-in-time recovery rolls back
|
||||
// to an earlier version. Here that rollback drops the (later, whole-DB)
|
||||
// manifest record entirely, so the remote worker can no longer find the
|
||||
// compaction input and the job fails ("cannot find matched SST files") --
|
||||
// i.e. it fails safe rather than mis-compacting. The fix makes recovery TRUST
|
||||
// the manifest: it keeps the current version, the job completes, and the
|
||||
// deleted key correctly stays deleted (the below-output tombstone is
|
||||
// retained).
|
||||
Options options = CurrentOptions();
|
||||
options.num_levels = 7;
|
||||
// -1 keeps the primary's descriptor for the (unlinked) lower file valid, so
|
||||
// the final read below is served from the primary regardless of the deletion.
|
||||
options.max_open_files = -1;
|
||||
options.level0_file_num_compaction_trigger = 100; // no auto compaction
|
||||
ReopenWithCompactionService(&options);
|
||||
|
||||
// Old value for "k", pushed down to L2 (below the compaction output level).
|
||||
ASSERT_OK(Put("k", "OLD"));
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(2); // L2 file contains k=OLD
|
||||
|
||||
// Newer tombstone for "k" at L0 (the compaction input).
|
||||
ASSERT_OK(Delete("k"));
|
||||
ASSERT_OK(Flush()); // L0 file contains Delete(k)
|
||||
|
||||
// Reopen so the MANIFEST is rewritten as current state before we perturb it.
|
||||
ReopenWithColumnFamilies({kDefaultColumnFamilyName, "cf_1", "cf_2", "cf_3"},
|
||||
options);
|
||||
// Closing on reopen cancels pending remote jobs; re-arm the service.
|
||||
GetCompactionService()->SetCanceled(false);
|
||||
|
||||
ColumnFamilyMetaData meta;
|
||||
db_->GetColumnFamilyMetaData(&meta); // default CF
|
||||
ASSERT_EQ(meta.levels[0].files.size(), 1);
|
||||
ASSERT_EQ(meta.levels[2].files.size(), 1);
|
||||
const auto& f_del = meta.levels[0].files[0];
|
||||
const auto& f_old = meta.levels[2].files[0];
|
||||
|
||||
// Make the lower (L2) file momentarily unavailable to a fresh open, as a live
|
||||
// primary would if it had compacted it away. It is NOT a compaction input.
|
||||
ASSERT_OK(env_->DeleteFile(f_old.db_path + "/" + f_old.name));
|
||||
|
||||
// Remote-compact the L0 tombstone into L1. L2 (with k=OLD) is below the
|
||||
// output level, so a correct compaction must retain the tombstone. Without
|
||||
// the fix, recovery rolls back and this job fails; with the fix it completes.
|
||||
std::string fname = f_del.db_path + "/" + f_del.name;
|
||||
ASSERT_OK(db_->CompactFiles(CompactionOptions(), {fname}, 1));
|
||||
|
||||
// Deleted key stays deleted (tombstone retained above the still-present L2
|
||||
// value, which the primary can still read via its open descriptor).
|
||||
ASSERT_EQ("NOT_FOUND", Get("k"));
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, RemoteCompactionManifestFloorSendsPosition) {
|
||||
// By default (remote_compaction_manifest_floor=true), the primary attaches a
|
||||
// non-zero MANIFEST floor to every remote compaction request, and the worker
|
||||
// runs its floor check. Here the worker recovers the same-or-later MANIFEST
|
||||
// position the primary scheduled from, so the check accepts (the
|
||||
// later-manifest positive case).
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
ReopenWithCompactionService(&options);
|
||||
ASSERT_TRUE(db_->GetDBOptions().remote_compaction_manifest_floor);
|
||||
GenerateTestData();
|
||||
|
||||
uint64_t floor_number = 0;
|
||||
uint64_t floor_size = 0;
|
||||
int floor_check_count = 0;
|
||||
bool floor_check_rejected = false;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionServiceJob::ProcessKeyValueCompactionWithCompactionService",
|
||||
[&](void* arg) {
|
||||
auto* input = static_cast<CompactionServiceInput*>(arg);
|
||||
floor_number = input->min_manifest_file_number;
|
||||
floor_size = input->min_manifest_file_size;
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImplSecondary::OpenAndCompact::ManifestFloorCheck", [&](void* arg) {
|
||||
floor_check_count++;
|
||||
if (*static_cast<bool*>(arg)) {
|
||||
floor_check_rejected = true;
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
std::string start_str = Key(15);
|
||||
std::string end_str = Key(45);
|
||||
Slice start(start_str);
|
||||
Slice end(end_str);
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), &start, &end));
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
// A floor was populated and the worker accepted its (later-or-equal) view.
|
||||
ASSERT_GT(floor_number, 0U);
|
||||
ASSERT_GT(floor_size, 0U);
|
||||
ASSERT_GE(floor_check_count, 1);
|
||||
ASSERT_FALSE(floor_check_rejected);
|
||||
VerifyTestData();
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, RemoteCompactionManifestFloorRejectsStaleView) {
|
||||
// If the worker recovers an OLDER MANIFEST view than the primary scheduled
|
||||
// from (simulated here by inflating the floor above the real position), the
|
||||
// floor check rejects the job and the primary falls back to a correct local
|
||||
// compaction rather than compacting against a stale/older LSM shape.
|
||||
Options options = CurrentOptions();
|
||||
options.num_levels = 7;
|
||||
options.level0_file_num_compaction_trigger = 100; // no auto compaction
|
||||
ReopenWithCompactionService(&options);
|
||||
|
||||
// Old value for "k", pushed down to L2 (below the compaction output level).
|
||||
ASSERT_OK(Put("k", "OLD"));
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(2); // L2: k=OLD
|
||||
|
||||
// Newer tombstone for "k" at L0 (the compaction input).
|
||||
ASSERT_OK(Delete("k"));
|
||||
ASSERT_OK(Flush()); // L0: Delete(k)
|
||||
|
||||
int floor_check_rejected = 0;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionServiceJob::ProcessKeyValueCompactionWithCompactionService",
|
||||
[&](void* arg) {
|
||||
auto* input = static_cast<CompactionServiceInput*>(arg);
|
||||
// Simulate the worker seeing an older MANIFEST than the primary
|
||||
// scheduled from by raising the floor above the real position.
|
||||
input->min_manifest_file_number += 1000000;
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImplSecondary::OpenAndCompact::ManifestFloorCheck", [&](void* arg) {
|
||||
if (*static_cast<bool*>(arg)) {
|
||||
floor_check_rejected++;
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
ColumnFamilyMetaData meta;
|
||||
db_->GetColumnFamilyMetaData(&meta); // default CF
|
||||
ASSERT_EQ(meta.levels[0].files.size(), 1);
|
||||
const auto& f_del = meta.levels[0].files[0];
|
||||
std::string fname = f_del.db_path + "/" + f_del.name;
|
||||
|
||||
// The worker rejects the stale view -> local fallback -> the compaction still
|
||||
// completes correctly (CompactFiles returns OK).
|
||||
ASSERT_OK(db_->CompactFiles(CompactionOptions(), {fname}, 1));
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
ASSERT_GE(floor_check_rejected, 1);
|
||||
// Deleted key stays deleted: the local fallback retained the below-output
|
||||
// tombstone. A silent mis-compaction against the older view would resurrect
|
||||
// "OLD".
|
||||
ASSERT_EQ("NOT_FOUND", Get("k"));
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, RemoteCompactionManifestFloorKillSwitchOff) {
|
||||
// With the kill switch off, the primary sends no floor, so the worker uses
|
||||
// the original point-in-time recovery (no trust-manifest, no floor check). A
|
||||
// transiently-missing non-input file then rolls recovery back and fails the
|
||||
// job -- confirming the escape hatch reverts to the pre-fix behavior.
|
||||
Options options = CurrentOptions();
|
||||
options.num_levels = 7;
|
||||
options.max_open_files = -1;
|
||||
options.level0_file_num_compaction_trigger = 100; // no auto compaction
|
||||
options.remote_compaction_manifest_floor = false; // kill switch OFF
|
||||
ReopenWithCompactionService(&options);
|
||||
|
||||
ASSERT_OK(Put("k", "OLD"));
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(2); // L2: k=OLD
|
||||
|
||||
ASSERT_OK(Delete("k"));
|
||||
ASSERT_OK(Flush()); // L0: Delete(k)
|
||||
|
||||
// Reopen so the MANIFEST is rewritten as current state before we perturb it.
|
||||
ReopenWithColumnFamilies({kDefaultColumnFamilyName, "cf_1", "cf_2", "cf_3"},
|
||||
options);
|
||||
GetCompactionService()->SetCanceled(false);
|
||||
|
||||
bool floor_provided_seen = true;
|
||||
int floor_check_count = 0;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionServiceJob::ProcessKeyValueCompactionWithCompactionService",
|
||||
[&](void* arg) {
|
||||
auto* input = static_cast<CompactionServiceInput*>(arg);
|
||||
floor_provided_seen = (input->min_manifest_file_number != 0);
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImplSecondary::OpenAndCompact::ManifestFloorCheck",
|
||||
[&](void* /*arg*/) { floor_check_count++; });
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
ColumnFamilyMetaData meta;
|
||||
db_->GetColumnFamilyMetaData(&meta);
|
||||
ASSERT_EQ(meta.levels[0].files.size(), 1);
|
||||
ASSERT_EQ(meta.levels[2].files.size(), 1);
|
||||
const auto& f_del = meta.levels[0].files[0];
|
||||
const auto& f_old = meta.levels[2].files[0];
|
||||
|
||||
// Make the lower (L2) non-input file momentarily unavailable, as a live
|
||||
// primary would if it had compacted it away.
|
||||
ASSERT_OK(env_->DeleteFile(f_old.db_path + "/" + f_old.name));
|
||||
|
||||
std::string fname = f_del.db_path + "/" + f_del.name;
|
||||
Status s = db_->CompactFiles(CompactionOptions(), {fname}, 1);
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
// No floor was sent and the worker skipped the floor check; the original
|
||||
// recovery rolled back and failed the job (reverted behavior).
|
||||
ASSERT_FALSE(floor_provided_seen);
|
||||
ASSERT_EQ(0, floor_check_count);
|
||||
ASSERT_NOK(s);
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, RemoteCompactionManifestFloorMutable) {
|
||||
// remote_compaction_manifest_floor is a mutable DB option: flipping it via
|
||||
// SetDBOptions changes whether the primary attaches a floor to subsequent
|
||||
// remote compaction requests (and thus whether the worker runs the floor
|
||||
// check).
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
ReopenWithCompactionService(&options);
|
||||
GenerateTestData();
|
||||
|
||||
bool floor_provided = false;
|
||||
int floor_check_count = 0;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionServiceJob::ProcessKeyValueCompactionWithCompactionService",
|
||||
[&](void* arg) {
|
||||
auto* input = static_cast<CompactionServiceInput*>(arg);
|
||||
floor_provided = (input->min_manifest_file_number != 0);
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImplSecondary::OpenAndCompact::ManifestFloorCheck",
|
||||
[&](void* /*arg*/) { floor_check_count++; });
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
std::string start_str = Key(15);
|
||||
std::string end_str = Key(45);
|
||||
Slice start(start_str);
|
||||
Slice end(end_str);
|
||||
|
||||
// Default on: floor attached, floor check runs.
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), &start, &end));
|
||||
ASSERT_TRUE(floor_provided);
|
||||
ASSERT_GE(floor_check_count, 1);
|
||||
|
||||
// Turn the kill switch off dynamically.
|
||||
ASSERT_OK(db_->SetDBOptions({{"remote_compaction_manifest_floor", "false"}}));
|
||||
ASSERT_FALSE(db_->GetDBOptions().remote_compaction_manifest_floor);
|
||||
|
||||
floor_provided = true;
|
||||
floor_check_count = 0;
|
||||
|
||||
// Create a fresh compaction target overlapping the range and re-compact.
|
||||
ASSERT_OK(Put(Key(20), "v"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), &start, &end));
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
// Now no floor is attached and the worker skips the floor check.
|
||||
ASSERT_FALSE(floor_provided);
|
||||
ASSERT_EQ(0, floor_check_count);
|
||||
}
|
||||
|
||||
namespace {
|
||||
// A remote compaction service whose output directory already contains an SST
|
||||
// left behind by a previously interrupted (canceled) fresh attempt, then runs
|
||||
// OpenAndCompact with allow_resumption=true. Used to verify that when
|
||||
// resumption is internally disabled, the output directory is cleaned before the
|
||||
// fresh compaction starts.
|
||||
class LeftoverOutputCompactionService : public MyTestCompactionService {
|
||||
public:
|
||||
using MyTestCompactionService::MyTestCompactionService;
|
||||
|
||||
CompactionServiceJobStatus Wait(const std::string& scheduled_job_id,
|
||||
std::string* result) override {
|
||||
std::string compaction_input;
|
||||
{
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
auto it = jobs_.find(scheduled_job_id);
|
||||
if (it == jobs_.end()) {
|
||||
return CompactionServiceJobStatus::kFailure;
|
||||
}
|
||||
compaction_input = it->second;
|
||||
jobs_.erase(it);
|
||||
}
|
||||
|
||||
CompactionServiceOptionsOverride options_override = GetOptionsOverride();
|
||||
Env* env = options_override.env;
|
||||
const std::string output_dir = GetOutputPath(scheduled_job_id);
|
||||
EXPECT_OK(env->CreateDirIfMissing(output_dir));
|
||||
|
||||
// An output SST left behind by a previously interrupted fresh attempt. Uses
|
||||
// a high file number the fresh compaction will not reuse, so whether it is
|
||||
// cleaned up is directly observable.
|
||||
leftover_file_ = MakeTableFileName(output_dir, 999999);
|
||||
{
|
||||
std::unique_ptr<WritableFile> f;
|
||||
EXPECT_OK(env->NewWritableFile(leftover_file_, &f, EnvOptions()));
|
||||
EXPECT_OK(f->Append("leftover"));
|
||||
EXPECT_OK(f->Close());
|
||||
}
|
||||
|
||||
OpenAndCompactOptions open_options;
|
||||
open_options.allow_resumption = true;
|
||||
Status s = DB::OpenAndCompact(open_options, db_path_, scheduled_job_id,
|
||||
compaction_input, result, options_override);
|
||||
{
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
result_ = *result;
|
||||
}
|
||||
if (!s.ok()) {
|
||||
return CompactionServiceJobStatus::kFailure;
|
||||
}
|
||||
leftover_cleaned_ = env->FileExists(leftover_file_).IsNotFound();
|
||||
return CompactionServiceJobStatus::kSuccess;
|
||||
}
|
||||
|
||||
bool leftover_cleaned() const { return leftover_cleaned_; }
|
||||
|
||||
private:
|
||||
std::string leftover_file_;
|
||||
bool leftover_cleaned_ = false;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
TEST_F(CompactionServiceTest, CleansOutputDirWhenResumptionDisabled) {
|
||||
// Regression test: allow_resumption=true is requested, but resumption is
|
||||
// internally disabled because paranoid_file_checks=true is incompatible with
|
||||
// the resumption path's output-hash verification. OpenAndCompact then starts
|
||||
// a fresh compaction, which must clean the output directory first (honoring
|
||||
// the documented OpenAndCompactOptions::allow_resumption fallback contract),
|
||||
// so an output file left by a previously interrupted attempt does not collide
|
||||
// with the file numbers the fresh compaction reuses. Before the fix the
|
||||
// leftover file was kept, which under a no-reopen filesystem (as in the crash
|
||||
// test) fails the fresh compaction's NewWritableFile with a
|
||||
// "no-reopen-for-write contract" violation.
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
options.paranoid_file_checks = true; // disables resumption on the worker
|
||||
options.use_session_tmp_dir_for_remote_compaction = true;
|
||||
options.env = env_;
|
||||
auto statistics = CreateDBStatistics();
|
||||
|
||||
auto cs = std::make_shared<LeftoverOutputCompactionService>(
|
||||
dbname_, options, statistics,
|
||||
std::vector<std::shared_ptr<EventListener>>{},
|
||||
std::vector<std::shared_ptr<TablePropertiesCollectorFactory>>{});
|
||||
options.compaction_service = cs;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Two overlapping flushed files so the manual compaction has real work.
|
||||
ASSERT_OK(Put(Key(1), "v1"));
|
||||
ASSERT_OK(Put(Key(3), "v3"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put(Key(2), "v2"));
|
||||
ASSERT_OK(Put(Key(3), "v3new"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
|
||||
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
|
||||
|
||||
// The fresh compaction cleaned the leftover output before starting.
|
||||
ASSERT_TRUE(cs->leftover_cleaned());
|
||||
ASSERT_EQ(Get(Key(1)), "v1");
|
||||
ASSERT_EQ(Get(Key(2)), "v2");
|
||||
ASSERT_EQ(Get(Key(3)), "v3new");
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, RetriesSecondaryOpenWhenCurrentFileVanishes) {
|
||||
// The remote compaction worker opens the live primary's DB directory
|
||||
// read-only inside OpenAndCompact. The primary replaces CURRENT on every
|
||||
// MANIFEST rotation, and on a filesystem without POSIX read-after-unlink
|
||||
// semantics the CURRENT object the worker already opened becomes unreadable.
|
||||
// That is a transient race, so the secondary open must be retried; otherwise
|
||||
// a CompactionService that does not fall back to a local compaction
|
||||
// (db_stress with remote_compaction_failure_fall_back_to_local=false) turns
|
||||
// it into a primary background error that fails every later flush.
|
||||
auto fs = std::make_shared<VanishingCurrentFileFS>(env_->GetFileSystem());
|
||||
std::unique_ptr<Env> vanishing_env(NewCompositeEnv(fs));
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
options.env = vanishing_env.get();
|
||||
auto statistics = CreateDBStatistics();
|
||||
|
||||
auto cs = std::make_shared<MyTestCompactionService>(
|
||||
dbname_, options, statistics,
|
||||
std::vector<std::shared_ptr<EventListener>>{},
|
||||
std::vector<std::shared_ptr<TablePropertiesCollectorFactory>>{});
|
||||
cs->SetMaxSecondaryOpenRetries(3);
|
||||
options.compaction_service = cs;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Two overlapping flushed files so the manual compaction has real work.
|
||||
ASSERT_OK(Put(Key(1), "v1"));
|
||||
ASSERT_OK(Put(Key(3), "v3"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put(Key(2), "v2"));
|
||||
ASSERT_OK(Put(Key(3), "v3new"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Arm the injection only now: the primary is done opening, so the failed
|
||||
// CURRENT reads below happen in the worker's secondary open. Three failures
|
||||
// exercise a non-default retry limit and require the fourth attempt.
|
||||
fs->SetFailuresToInject(3);
|
||||
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
|
||||
const Status compact_s = db_->CompactRange(cro, nullptr, nullptr);
|
||||
|
||||
// Collect everything before closing: the DB must be closed while the
|
||||
// injecting Env is still alive, so no assertion may return early above it.
|
||||
const int injected_failures = fs->injected_failures();
|
||||
const int compactions = cs->GetCompactionNum();
|
||||
const std::string v1 = Get(Key(1));
|
||||
const std::string v2 = Get(Key(2));
|
||||
const std::string v3 = Get(Key(3));
|
||||
Close();
|
||||
|
||||
ASSERT_OK(compact_s);
|
||||
ASSERT_EQ(3, injected_failures);
|
||||
ASSERT_GE(compactions, 1);
|
||||
ASSERT_EQ(v1, "v1");
|
||||
ASSERT_EQ(v2, "v2");
|
||||
ASSERT_EQ(v3, "v3new");
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, CanDisableSecondaryOpenRetry) {
|
||||
auto fs = std::make_shared<VanishingCurrentFileFS>(env_->GetFileSystem());
|
||||
std::unique_ptr<Env> vanishing_env(NewCompositeEnv(fs));
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
options.env = vanishing_env.get();
|
||||
auto statistics = CreateDBStatistics();
|
||||
|
||||
auto cs = std::make_shared<MyTestCompactionService>(
|
||||
dbname_, options, statistics,
|
||||
std::vector<std::shared_ptr<EventListener>>{},
|
||||
std::vector<std::shared_ptr<TablePropertiesCollectorFactory>>{});
|
||||
cs->SetMaxSecondaryOpenRetries(0);
|
||||
options.compaction_service = cs;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(Put(Key(1), "v1"));
|
||||
ASSERT_OK(Put(Key(3), "v3"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put(Key(2), "v2"));
|
||||
ASSERT_OK(Put(Key(3), "v3new"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
fs->SetFailuresToInject(1);
|
||||
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
|
||||
const Status compact_s = db_->CompactRange(cro, nullptr, nullptr);
|
||||
const int injected_failures = fs->injected_failures();
|
||||
const int compactions = cs->GetCompactionNum();
|
||||
Close();
|
||||
|
||||
ASSERT_TRUE(compact_s.IsIncomplete());
|
||||
ASSERT_EQ(1, injected_failures);
|
||||
ASSERT_EQ(1, compactions);
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, CompactionInfo) {
|
||||
Options options = CurrentOptions();
|
||||
ReopenWithCompactionService(&options);
|
||||
@@ -2854,7 +3446,7 @@ class ResumableCompactionService : public MyTestCompactionService {
|
||||
EXPECT_OK(statistics_->Reset());
|
||||
|
||||
Status s =
|
||||
DB::OpenAndCompact(options, db_path_, GetOutputPath(scheduled_job_id),
|
||||
DB::OpenAndCompact(options, db_path_, scheduled_job_id,
|
||||
compaction_input, &temp_result, override_options);
|
||||
|
||||
EXPECT_TRUE(s.IsManualCompactionPaused());
|
||||
@@ -2871,9 +3463,8 @@ class ResumableCompactionService : public MyTestCompactionService {
|
||||
std::string* result) {
|
||||
EXPECT_OK(statistics_->Reset());
|
||||
|
||||
Status s =
|
||||
DB::OpenAndCompact(options, db_path_, GetOutputPath(scheduled_job_id),
|
||||
compaction_input, result, override_options);
|
||||
Status s = DB::OpenAndCompact(options, db_path_, scheduled_job_id,
|
||||
compaction_input, result, override_options);
|
||||
|
||||
EXPECT_TRUE(s.ok());
|
||||
|
||||
@@ -3378,6 +3969,182 @@ TEST_F(ResumableCompactionKeyTypeTest, CancelAndResumeWithTimedPut) {
|
||||
|
||||
VerifyResumeBytes();
|
||||
}
|
||||
|
||||
// Covers the DB session temporary directory lifecycle when remote compaction
|
||||
// opts in to using it.
|
||||
class SessionTmpDirTest : public DBTestBase {
|
||||
public:
|
||||
SessionTmpDirTest()
|
||||
: DBTestBase("session_tmp_dir_test",
|
||||
/*env_do_fsync=*/true) {}
|
||||
|
||||
protected:
|
||||
// Opens with a CompactionService opted in to the session temporary directory.
|
||||
Options OptionsWithCompactionService() {
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
options.use_session_tmp_dir_for_remote_compaction = true;
|
||||
worker_statistics_ = CreateDBStatistics();
|
||||
compaction_service_ = std::make_shared<MyTestCompactionService>(
|
||||
dbname_, options, worker_statistics_,
|
||||
std::vector<std::shared_ptr<EventListener>>{},
|
||||
std::vector<std::shared_ptr<TablePropertiesCollectorFactory>>{});
|
||||
options.compaction_service = compaction_service_;
|
||||
return options;
|
||||
}
|
||||
|
||||
std::string SessionTmpPath() { return SessionTmpDir(dbname_); }
|
||||
|
||||
// Creates and closes a DB session before planting temporary output for the
|
||||
// next session to delete.
|
||||
void CreateDbAndQuiesce(const Options& options) {
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_TRUE(Exists(SessionTmpPath()));
|
||||
Close();
|
||||
}
|
||||
|
||||
// Creates `parent/name/` holding one file, mimicking a leaked job directory.
|
||||
void PlantDir(const std::string& parent, const std::string& name) {
|
||||
SpecialEnv* const env = env_;
|
||||
if (env == nullptr) {
|
||||
ADD_FAILURE() << "Test environment is not initialized";
|
||||
return;
|
||||
}
|
||||
ASSERT_OK(env->CreateDirIfMissing(parent));
|
||||
const std::string dir = parent + "/" + name;
|
||||
ASSERT_OK(env->CreateDirIfMissing(dir));
|
||||
ASSERT_OK(WriteStringToFile(env, "leftover", dir + "/000123.sst"));
|
||||
}
|
||||
|
||||
bool Exists(const std::string& path) {
|
||||
return env_ != nullptr && env_->FileExists(path).ok();
|
||||
}
|
||||
|
||||
// Reopens after planting stale output. Cleanup completes before DB::Open()
|
||||
// schedules compactions or returns.
|
||||
void ReopenAndDeleteStaleOutput(const Options& options) { Reopen(options); }
|
||||
|
||||
std::shared_ptr<Statistics> worker_statistics_;
|
||||
std::shared_ptr<CompactionService> compaction_service_;
|
||||
};
|
||||
|
||||
TEST_F(SessionTmpDirTest, ResolvesJobDirectory) {
|
||||
const std::string job_dir_name = "SESSION01_4294967296";
|
||||
const std::string legacy_output_path = dbname_ + "/" + job_dir_name;
|
||||
|
||||
ASSERT_EQ(RemoteCompactionJobDir(dbname_, false, job_dir_name),
|
||||
legacy_output_path);
|
||||
ASSERT_EQ(RemoteCompactionJobDir(dbname_, false, legacy_output_path),
|
||||
legacy_output_path);
|
||||
ASSERT_EQ(RemoteCompactionJobDir(dbname_, true, job_dir_name),
|
||||
SessionTmpPath() + "/" + job_dir_name);
|
||||
}
|
||||
|
||||
TEST_F(SessionTmpDirTest, DeletesDirsFromPreviousSessions) {
|
||||
Options options = OptionsWithCompactionService();
|
||||
CreateDbAndQuiesce(options);
|
||||
|
||||
// Two leaked job directories, one of them with a nested subdirectory to prove
|
||||
// the removal is recursive.
|
||||
PlantDir(SessionTmpPath(), "SESSIONFROMDEADHOST01_4294967296");
|
||||
PlantDir(SessionTmpPath() + "/SESSIONFROMDEADHOST01_4294967296", "nested");
|
||||
PlantDir(SessionTmpPath(), "ANOTHERDEADSESSION02_8589934592");
|
||||
|
||||
ReopenAndDeleteStaleOutput(options);
|
||||
|
||||
ASSERT_TRUE(Exists(SessionTmpPath()));
|
||||
ASSERT_FALSE(Exists(SessionTmpPath() + "/SESSIONFROMDEADHOST01_4294967296"));
|
||||
ASSERT_FALSE(Exists(SessionTmpPath() + "/ANOTHERDEADSESSION02_8589934592"));
|
||||
}
|
||||
|
||||
TEST_F(SessionTmpDirTest, IgnoresDirsOutsideSessionTmpDir) {
|
||||
Options options = OptionsWithCompactionService();
|
||||
CreateDbAndQuiesce(options);
|
||||
|
||||
// A CompactionService that stages output outside the configured root owns
|
||||
// cleaning it up; the DB must not touch it.
|
||||
const std::string elsewhere = "service_owned_staging_dir";
|
||||
PlantDir(dbname_, elsewhere);
|
||||
PlantDir(SessionTmpPath(), "SESSIONFROMDEADHOST01_4294967296");
|
||||
|
||||
ReopenAndDeleteStaleOutput(options);
|
||||
|
||||
ASSERT_TRUE(Exists(dbname_ + "/" + elsewhere));
|
||||
ASSERT_FALSE(Exists(SessionTmpPath() + "/SESSIONFROMDEADHOST01_4294967296"));
|
||||
|
||||
// Nothing in RocksDB owns this directory, so nothing else will remove it --
|
||||
// including the DestroyDB that the next test's setup runs against the shared
|
||||
// per-thread DB path.
|
||||
ASSERT_OK(DestroyDir(env_, dbname_ + "/" + elsewhere));
|
||||
}
|
||||
|
||||
TEST_F(SessionTmpDirTest, DisabledOptionSkipsCleanup) {
|
||||
if (Exists(SessionTmpPath())) {
|
||||
ASSERT_OK(DestroyDir(env_, SessionTmpPath()));
|
||||
}
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_FALSE(Exists(SessionTmpPath()));
|
||||
|
||||
PlantDir(SessionTmpPath(), "SESSIONFROMDEADHOST01_4294967296");
|
||||
|
||||
Reopen(options);
|
||||
Close();
|
||||
|
||||
ASSERT_TRUE(Exists(SessionTmpPath() + "/SESSIONFROMDEADHOST01_4294967296"));
|
||||
ASSERT_OK(DestroyDir(env_, SessionTmpPath()));
|
||||
}
|
||||
|
||||
TEST_F(SessionTmpDirTest, EnabledSessionTmpDirIsCreated) {
|
||||
Options options = CurrentOptions();
|
||||
options.use_session_tmp_dir_for_remote_compaction = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_TRUE(Exists(SessionTmpPath()));
|
||||
}
|
||||
|
||||
TEST_F(SessionTmpDirTest, DestroyDBRemovesSessionTmpDir) {
|
||||
Options options = OptionsWithCompactionService();
|
||||
CreateDbAndQuiesce(options);
|
||||
PlantDir(SessionTmpPath(), "SESSIONFROMDEADHOST01_4294967296");
|
||||
|
||||
// DestroyDB's final DeleteDir(dbname) is non-recursive, so it can only remove
|
||||
// the DB directory if the session temporary directory is gone first.
|
||||
ASSERT_OK(DestroyDB(dbname_, options));
|
||||
ASSERT_FALSE(Exists(dbname_));
|
||||
}
|
||||
|
||||
TEST_F(SessionTmpDirTest, CompactionUsesSessionTmpDir) {
|
||||
Options options = OptionsWithCompactionService();
|
||||
options.disable_auto_compactions = true;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
for (int i = 0; i < 20; i++) {
|
||||
ASSERT_OK(Put(Key(i), "value" + std::to_string(i)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
for (int i = 0; i < 20; i += 2) {
|
||||
ASSERT_OK(Put(Key(i), "value_new" + std::to_string(i)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
|
||||
auto* my_cs = static_cast_with_check<MyTestCompactionService>(
|
||||
compaction_service_.get());
|
||||
ASSERT_GE(my_cs->GetCompactionNum(), 1);
|
||||
CompactionServiceResult result;
|
||||
my_cs->GetResult(&result);
|
||||
ASSERT_OK(result.status);
|
||||
ASSERT_EQ(result.output_path,
|
||||
RemoteCompactionJobDir(dbname_, true,
|
||||
my_cs->GetLastOutputDirectoryName()));
|
||||
|
||||
for (int i = 0; i < 20; i++) {
|
||||
ASSERT_EQ(Get(Key(i)), (i % 2 ? "value" : "value_new") + std::to_string(i));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
@@ -25,6 +25,18 @@ SstPartitionerFixedPrefixFactory::SstPartitionerFixedPrefixFactory(size_t len)
|
||||
RegisterOptions("Length", &len_, &sst_fixed_prefix_type_info);
|
||||
}
|
||||
|
||||
OptSlice SstPartitionerFixedPrefix::ShouldPartitionByPrefix(
|
||||
const Slice& user_key) {
|
||||
if (user_key.size() >= len_) {
|
||||
return {user_key.data(), len_};
|
||||
} else {
|
||||
// ShouldPartition() cuts a partition for EVERY UNIQUE KEY outside the
|
||||
// prefix length domain, for better or worse. We can't directly represent
|
||||
// that so we defer to ShouldPartition().
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
PartitionerResult SstPartitionerFixedPrefix::ShouldPartition(
|
||||
const PartitionerRequest& request) {
|
||||
Slice last_key_fixed(*request.prev_user_key);
|
||||
|
||||
@@ -1339,9 +1339,13 @@ TEST_F(TieredCompactionTest, SequenceBasedTieredStorageLevel) {
|
||||
// This test was essentially for a hacked-up version on future functionality.
|
||||
// It can be resurrected if/when a form of range-based tiering is properly
|
||||
// implemented.
|
||||
// FIXME: aside from that, this test reproduces a near-endless compaction
|
||||
// cycle that needs to be reproduced independently and fixed before
|
||||
// leveled compaction can be used with the preclude feature in production.
|
||||
// The near-endless compaction cycle this used to reproduce (a bottommost file
|
||||
// whose seqno could not be zeroed within the preserve window being re-marked
|
||||
// forever) is now prevented by sharing the seqno-zeroability decision between
|
||||
// the compaction output path and bottommost-file marking (see
|
||||
// BottommostSeqnoCanBeZeroed and CompactionPickerTest.BottommostFile*
|
||||
// PreserveWindow). This test remains disabled only because range-based tiering
|
||||
// is not implemented.
|
||||
TEST_F(TieredCompactionTest, DISABLED_RangeBasedTieredStorageLevel) {
|
||||
const int kNumTrigger = 4;
|
||||
const int kNumLevels = 7;
|
||||
@@ -1558,6 +1562,188 @@ class PrecludeLastLevelTest : public PrecludeLastLevelTestBase,
|
||||
}
|
||||
};
|
||||
|
||||
// Regression test for an infinite kBottommostFiles compaction loop on a
|
||||
// leveled column family that has the seqno->time preserve feature enabled. A
|
||||
// bottommost file whose largest sequence number is still within the preserve
|
||||
// window cannot have its seqno zeroed by compaction, so it must not be marked
|
||||
// for bottommost compaction; otherwise each rewrite makes no progress and the
|
||||
// file is re-marked forever. This exercises the DBImpl plumbing that feeds
|
||||
// VersionStorageInfo::preserve_time_min_seqno_ (DBImpl::
|
||||
// MaybeUpdatePreserveTimeMinSeqno) end to end, complementing the isolated
|
||||
// marking-predicate coverage in compaction_picker_test.cc.
|
||||
class PrecludeBottommostLoopTest : public PrecludeLastLevelTestBase {};
|
||||
|
||||
TEST_F(PrecludeBottommostLoopTest, LeveledPreserveTimeNoBottommostLoop) {
|
||||
const int kNumLevels = 7;
|
||||
Options options = CurrentOptions();
|
||||
options.compaction_style = kCompactionStyleLevel;
|
||||
options.preserve_internal_time_seconds = 10000;
|
||||
options.env = mock_env_.get();
|
||||
options.num_levels = kNumLevels;
|
||||
// Prevent the extra L0 file below from auto-triggering an L0 compaction.
|
||||
options.level0_file_num_compaction_trigger = 4;
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Populate the seqno->time mapping with recent samples so the preserve window
|
||||
// maps the file's (recent) largest seqno as non-zeroable. Total elapsed mock
|
||||
// time stays far below preserve_internal_time_seconds, so nothing ages out.
|
||||
for (int i = 0; i < 10; i++) {
|
||||
ASSERT_OK(Put("infinite", "v" + std::to_string(i)));
|
||||
dbfull()->TEST_WaitForPeriodicTaskRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(10); });
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
// Push the file with the hot key down to the bottommost level.
|
||||
MoveFilesToLevel(kNumLevels - 1);
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(kNumLevels - 1));
|
||||
|
||||
// Confirm the bottommost file's largest seqno was not zeroed (it is within
|
||||
// the preserve window), so it is a genuine marking candidate that only the
|
||||
// preserve-window guard should protect -- this keeps the test from passing
|
||||
// vacuously.
|
||||
{
|
||||
std::vector<LiveFileMetaData> files;
|
||||
db_->GetLiveFilesMetaData(&files);
|
||||
bool found_bottommost = false;
|
||||
for (const auto& f : files) {
|
||||
if (f.level == kNumLevels - 1) {
|
||||
ASSERT_GT(f.largest_seqno, 0U);
|
||||
found_bottommost = true;
|
||||
}
|
||||
}
|
||||
ASSERT_TRUE(found_bottommost);
|
||||
}
|
||||
|
||||
// Bump the sequence number above the bottommost file so that releasing a
|
||||
// snapshot advances oldest_snapshot_seqnum_ past the file's largest seqno,
|
||||
// which makes the file a candidate for bottommost marking (the snapshot gate
|
||||
// passes). Only the preserve-window guard should keep it from being marked.
|
||||
ASSERT_OK(Put("bumpseqnum", ""));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
std::atomic<int> bottommost_compactions{0};
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"LevelCompactionPicker::PickCompaction:Return", [&](void* arg) {
|
||||
auto* c = static_cast<Compaction*>(arg);
|
||||
if (c != nullptr &&
|
||||
c->compaction_reason() == CompactionReason::kBottommostFiles) {
|
||||
bottommost_compactions.fetch_add(1);
|
||||
}
|
||||
});
|
||||
|
||||
const Snapshot* snapshot = db_->GetSnapshot();
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
|
||||
// The bottommost file's largest seqno is within the preserve window, so it is
|
||||
// not marked and the DB quiesces. Before the fix the file was marked and the
|
||||
// rewrite re-qualified it forever (this wait would hang).
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
ASSERT_EQ(0, bottommost_compactions.load());
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
// Close explicitly because mock_env_ is destroyed before the DB otherwise.
|
||||
Close();
|
||||
}
|
||||
|
||||
// Regression test for the empty-mapping variant of the same loop: a bottommost
|
||||
// file that predates a preserve/preclude rollout has an EMPTY persisted
|
||||
// seqno->time mapping. Once the live, DB-wide mapping ages the file out of the
|
||||
// preserve window, the marker (which reads the live mapping) marks it. The
|
||||
// compaction that rewrites it must use the same version-carried preserve
|
||||
// boundary rather than the file's own empty mapping; otherwise it computes
|
||||
// preserve_seqno_after_ = 0, zeroes nothing, and re-marks the file forever.
|
||||
// With the fix the file's seqno is zeroed in one bounded compaction.
|
||||
TEST_F(PrecludeBottommostLoopTest, EmptyMappingAgedOutNoBottommostLoop) {
|
||||
const int kNumLevels = 7;
|
||||
Options options = CurrentOptions();
|
||||
options.compaction_style = kCompactionStyleLevel;
|
||||
options.env = mock_env_.get();
|
||||
options.num_levels = kNumLevels;
|
||||
options.level0_file_num_compaction_trigger = 4;
|
||||
// Start with preserve/preclude disabled so the bottommost file below is
|
||||
// written with an EMPTY persisted seqno->time mapping, mimicking a file that
|
||||
// predates a preserve/preclude rollout.
|
||||
options.preserve_internal_time_seconds = 0;
|
||||
options.preclude_last_level_data_seconds = 0;
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Hold a snapshot below the "aged" key so moving it to the bottommost level
|
||||
// does not zero its seqno (there is no preserve window yet to protect it).
|
||||
ASSERT_OK(Put("keeplow", "v"));
|
||||
const Snapshot* protect = db_->GetSnapshot();
|
||||
ASSERT_OK(Put("aged", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(kNumLevels - 1);
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(kNumLevels - 1));
|
||||
|
||||
SequenceNumber aged_seqno = 0;
|
||||
{
|
||||
std::vector<LiveFileMetaData> files;
|
||||
db_->GetLiveFilesMetaData(&files);
|
||||
for (const auto& f : files) {
|
||||
if (f.level == kNumLevels - 1) {
|
||||
aged_seqno = f.largest_seqno;
|
||||
}
|
||||
}
|
||||
}
|
||||
ASSERT_GT(aged_seqno, 0U);
|
||||
|
||||
// Roll out the preserve feature dynamically and let the live, DB-wide
|
||||
// seqno->time mapping accumulate samples while mock time advances well past
|
||||
// preserve_internal_time_seconds, so the pre-existing file's seqno ages out
|
||||
// of the preserve window per the live mapping (even though the file's own
|
||||
// persisted mapping is empty). The protecting snapshot keeps the file from
|
||||
// being marked during this loop (oldest_snapshot_seqnum_ stays below it).
|
||||
ASSERT_OK(db_->SetOptions({{"preserve_internal_time_seconds", "10000"}}));
|
||||
for (int i = 0; i < 100; i++) {
|
||||
ASSERT_OK(Put("hot", "v" + std::to_string(i)));
|
||||
dbfull()->TEST_WaitForPeriodicTaskRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(200); });
|
||||
}
|
||||
|
||||
std::atomic<int> bottommost_compactions{0};
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"LevelCompactionPicker::PickCompaction:Return", [&](void* arg) {
|
||||
auto* c = static_cast<Compaction*>(arg);
|
||||
if (c != nullptr &&
|
||||
c->compaction_reason() == CompactionReason::kBottommostFiles) {
|
||||
bottommost_compactions.fetch_add(1);
|
||||
}
|
||||
});
|
||||
|
||||
// Release the protecting snapshot so oldest_snapshot_seqnum_ advances past
|
||||
// the file's largest seqno, making it a bottommost marking candidate. The
|
||||
// marker (using the live mapping) marks it; the compaction that rewrites it
|
||||
// must use the same version-carried preserve boundary (not the file's empty
|
||||
// mapping) to actually zero the seqno, otherwise it makes no progress and
|
||||
// loops.
|
||||
db_->ReleaseSnapshot(protect);
|
||||
|
||||
// With the fix the file's seqno is zeroed in a bounded number of bottommost
|
||||
// compactions and the DB quiesces. Before the fix this wait would hang.
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
|
||||
{
|
||||
std::vector<LiveFileMetaData> files;
|
||||
db_->GetLiveFilesMetaData(&files);
|
||||
for (const auto& f : files) {
|
||||
if (f.level == kNumLevels - 1) {
|
||||
ASSERT_EQ(0U, f.largest_seqno);
|
||||
}
|
||||
}
|
||||
}
|
||||
// The file was genuinely marked and rewritten (not vacuously skipped), and
|
||||
// the rewrite made progress instead of looping.
|
||||
ASSERT_GE(bottommost_compactions.load(), 1);
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_P(PrecludeLastLevelTest, MigrationFromPreserveTimeManualCompaction) {
|
||||
const int kNumTrigger = 4;
|
||||
const int kNumLevels = 7;
|
||||
|
||||
+212
-6
@@ -10,6 +10,7 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "folly/Executor.h"
|
||||
@@ -18,7 +19,10 @@
|
||||
#include "folly/io/async/EventBase.h"
|
||||
#include "rocksdb/coro_db.h"
|
||||
#include "rocksdb/file_system.h"
|
||||
#include "rocksdb/sst_file_reader.h"
|
||||
#include "table/multiget_context.h"
|
||||
#include "table/sst_file_reader_impl.h"
|
||||
#include "util/coro_stats_util.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
@@ -33,14 +37,28 @@ folly::coro::Task<Status> CoroDB::CoGet(DB* db, const ReadOptions& options,
|
||||
file_system == nullptr ? nullptr : file_system->GetReadExecutor();
|
||||
auto* read_event_base =
|
||||
read_executor == nullptr ? nullptr : read_executor->getEventBase();
|
||||
CoroutineStatsConfig stats_config = CaptureAndDisableCoroutineStatsConfig();
|
||||
|
||||
Status status;
|
||||
if (coro_db == nullptr || read_event_base == nullptr) {
|
||||
CoroutineStatsContextScope stats_scope(std::move(stats_config),
|
||||
db->GetEnv());
|
||||
status = db->Get(options, column_family, key, value, timestamp);
|
||||
} else {
|
||||
auto task = [](CoroutineStatsConfig task_stats_config, Env* task_env,
|
||||
CoroDB* task_db, const ReadOptions& task_options,
|
||||
ColumnFamilyHandle* task_column_family, Slice task_key,
|
||||
PinnableSlice* task_value,
|
||||
std::string* task_timestamp) -> folly::coro::Task<Status> {
|
||||
CoroutineStatsContextScope stats_scope(std::move(task_stats_config),
|
||||
task_env);
|
||||
co_return co_await folly::coro::co_nothrow(
|
||||
task_db->GetCoroutine(task_options, task_column_family, task_key,
|
||||
task_value, task_timestamp));
|
||||
}(std::move(stats_config), db->GetEnv(), coro_db, options, column_family,
|
||||
key, value, timestamp);
|
||||
status = co_await folly::coro::co_nothrow(folly::coro::co_withExecutor(
|
||||
folly::Executor::getKeepAliveToken(read_event_base),
|
||||
coro_db->GetCoroutine(options, column_family, key, value, timestamp)));
|
||||
folly::Executor::getKeepAliveToken(read_event_base), std::move(task)));
|
||||
}
|
||||
co_return status;
|
||||
}
|
||||
@@ -73,6 +91,131 @@ folly::coro::Task<Status> CoroDB::CoGet(DB* db, const ReadOptions& options,
|
||||
return CoGet(db, options, db->DefaultColumnFamily(), key, value, timestamp);
|
||||
}
|
||||
|
||||
folly::coro::Task<Status> CoroDB::CoGet(SstFileReader* reader,
|
||||
const ReadOptions& options,
|
||||
const Slice& key,
|
||||
PinnableSlice* value) {
|
||||
assert(reader != nullptr);
|
||||
SstFileReader::Rep* rep = reader->rep_.get();
|
||||
auto* read_executor = rep->read_executor;
|
||||
auto* read_event_base =
|
||||
read_executor == nullptr ? nullptr : read_executor->getEventBase();
|
||||
CoroutineStatsConfig stats_config = CaptureAndDisableCoroutineStatsConfig();
|
||||
|
||||
Status status;
|
||||
if (read_event_base == nullptr) {
|
||||
CoroutineStatsContextScope stats_scope(std::move(stats_config),
|
||||
rep->options.env);
|
||||
status = reader->Get(options, key, value);
|
||||
} else {
|
||||
auto task = [](CoroutineStatsConfig task_stats_config, Env* task_env,
|
||||
SstFileReader* task_reader, const ReadOptions& task_options,
|
||||
Slice task_key,
|
||||
PinnableSlice* task_value) -> folly::coro::Task<Status> {
|
||||
CoroutineStatsContextScope stats_scope(std::move(task_stats_config),
|
||||
task_env);
|
||||
co_return co_await folly::coro::co_nothrow(
|
||||
task_reader->rep_->GetCoroutine(task_options, task_key, task_value));
|
||||
}(std::move(stats_config), rep->options.env, reader, options, key, value);
|
||||
status = co_await folly::coro::co_nothrow(folly::coro::co_withExecutor(
|
||||
folly::Executor::getKeepAliveToken(read_event_base), std::move(task)));
|
||||
}
|
||||
co_return status;
|
||||
}
|
||||
|
||||
folly::coro::Task<Status> CoroDB::CoGet(SstFileReader* reader,
|
||||
const ReadOptions& options,
|
||||
const Slice& key, std::string* value) {
|
||||
assert(value != nullptr);
|
||||
PinnableSlice pinnable_value(value);
|
||||
assert(!pinnable_value.IsPinned());
|
||||
Status status = co_await folly::coro::co_nothrow(
|
||||
CoGet(reader, options, key, &pinnable_value));
|
||||
if (status.ok() && pinnable_value.IsPinned()) {
|
||||
value->assign(pinnable_value.data(), pinnable_value.size());
|
||||
}
|
||||
co_return status;
|
||||
}
|
||||
|
||||
folly::coro::Task<Status> CoroDB::CoGetEntity(DB* db,
|
||||
const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
const Slice& key,
|
||||
PinnableWideColumns* columns) {
|
||||
assert(db != nullptr);
|
||||
CoroDB* coro_db = db->GetCoroDB();
|
||||
auto* file_system = db->GetFileSystem();
|
||||
auto* read_executor =
|
||||
file_system == nullptr ? nullptr : file_system->GetReadExecutor();
|
||||
auto* read_event_base =
|
||||
read_executor == nullptr ? nullptr : read_executor->getEventBase();
|
||||
CoroutineStatsConfig stats_config = CaptureAndDisableCoroutineStatsConfig();
|
||||
|
||||
Status status;
|
||||
if (coro_db == nullptr || read_event_base == nullptr) {
|
||||
CoroutineStatsContextScope stats_scope(std::move(stats_config),
|
||||
db->GetEnv());
|
||||
status = db->GetEntity(options, column_family, key, columns);
|
||||
} else {
|
||||
auto task =
|
||||
[](CoroutineStatsConfig task_stats_config, Env* task_env,
|
||||
CoroDB* task_db, const ReadOptions& task_options,
|
||||
ColumnFamilyHandle* task_column_family, Slice task_key,
|
||||
PinnableWideColumns* task_columns) -> folly::coro::Task<Status> {
|
||||
CoroutineStatsContextScope stats_scope(std::move(task_stats_config),
|
||||
task_env);
|
||||
co_return co_await folly::coro::co_nothrow(task_db->GetEntityCoroutine(
|
||||
task_options, task_column_family, task_key, task_columns));
|
||||
}(std::move(stats_config), db->GetEnv(), coro_db, options, column_family,
|
||||
key, columns);
|
||||
status = co_await folly::coro::co_nothrow(folly::coro::co_withExecutor(
|
||||
folly::Executor::getKeepAliveToken(read_event_base), std::move(task)));
|
||||
}
|
||||
co_return status;
|
||||
}
|
||||
|
||||
folly::coro::Task<Status> CoroDB::CoGetEntity(DB* db,
|
||||
const ReadOptions& options,
|
||||
const Slice& key,
|
||||
PinnableWideColumns* columns) {
|
||||
assert(db != nullptr);
|
||||
return CoGetEntity(db, options, db->DefaultColumnFamily(), key, columns);
|
||||
}
|
||||
|
||||
folly::coro::Task<Status> CoroDB::CoGetEntity(DB* db,
|
||||
const ReadOptions& options,
|
||||
const Slice& key,
|
||||
PinnableAttributeGroups* result) {
|
||||
assert(db != nullptr);
|
||||
CoroDB* coro_db = db->GetCoroDB();
|
||||
auto* file_system = db->GetFileSystem();
|
||||
auto* read_executor =
|
||||
file_system == nullptr ? nullptr : file_system->GetReadExecutor();
|
||||
auto* read_event_base =
|
||||
read_executor == nullptr ? nullptr : read_executor->getEventBase();
|
||||
CoroutineStatsConfig stats_config = CaptureAndDisableCoroutineStatsConfig();
|
||||
|
||||
Status status;
|
||||
if (coro_db == nullptr || read_event_base == nullptr) {
|
||||
CoroutineStatsContextScope stats_scope(std::move(stats_config),
|
||||
db->GetEnv());
|
||||
status = db->GetEntity(options, key, result);
|
||||
} else {
|
||||
auto task =
|
||||
[](CoroutineStatsConfig task_stats_config, Env* task_env,
|
||||
CoroDB* task_db, const ReadOptions& task_options, Slice task_key,
|
||||
PinnableAttributeGroups* task_result) -> folly::coro::Task<Status> {
|
||||
CoroutineStatsContextScope stats_scope(std::move(task_stats_config),
|
||||
task_env);
|
||||
co_return co_await folly::coro::co_nothrow(
|
||||
task_db->GetEntityCoroutine(task_options, task_key, task_result));
|
||||
}(std::move(stats_config), db->GetEnv(), coro_db, options, key, result);
|
||||
status = co_await folly::coro::co_nothrow(folly::coro::co_withExecutor(
|
||||
folly::Executor::getKeepAliveToken(read_event_base), std::move(task)));
|
||||
}
|
||||
co_return status;
|
||||
}
|
||||
|
||||
folly::coro::Task<std::vector<Status>> CoroDB::CoMultiGet(
|
||||
DB* db, const ReadOptions& options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
@@ -110,6 +253,54 @@ folly::coro::Task<std::vector<Status>> CoroDB::CoMultiGet(
|
||||
CoMultiGet(db, options, column_families, keys, values, timestamps));
|
||||
}
|
||||
|
||||
folly::coro::Task<std::vector<Status>> CoroDB::CoMultiGet(
|
||||
SstFileReader* reader, const ReadOptions& options,
|
||||
const std::vector<Slice>& keys, std::vector<PinnableSlice>* values) {
|
||||
assert(reader != nullptr);
|
||||
SstFileReader::Rep* rep = reader->rep_.get();
|
||||
auto* read_executor = rep->read_executor;
|
||||
auto* read_event_base =
|
||||
read_executor == nullptr ? nullptr : read_executor->getEventBase();
|
||||
CoroutineStatsConfig stats_config = CaptureAndDisableCoroutineStatsConfig();
|
||||
|
||||
std::vector<Status> statuses;
|
||||
if (read_event_base == nullptr) {
|
||||
CoroutineStatsContextScope stats_scope(std::move(stats_config),
|
||||
rep->options.env);
|
||||
statuses = reader->MultiGet(options, keys, values);
|
||||
} else {
|
||||
auto task = [](CoroutineStatsConfig task_stats_config, Env* task_env,
|
||||
SstFileReader* task_reader, const ReadOptions& task_options,
|
||||
const std::vector<Slice>& task_keys,
|
||||
std::vector<PinnableSlice>* task_values)
|
||||
-> folly::coro::Task<std::vector<Status>> {
|
||||
CoroutineStatsContextScope stats_scope(std::move(task_stats_config),
|
||||
task_env);
|
||||
co_return co_await folly::coro::co_nothrow(
|
||||
task_reader->rep_->MultiGetCoroutine(task_options, task_keys,
|
||||
task_values));
|
||||
}(std::move(stats_config), rep->options.env, reader, options, keys, values);
|
||||
statuses = co_await folly::coro::co_nothrow(folly::coro::co_withExecutor(
|
||||
folly::Executor::getKeepAliveToken(read_event_base), std::move(task)));
|
||||
}
|
||||
co_return statuses;
|
||||
}
|
||||
|
||||
folly::coro::Task<std::vector<Status>> CoroDB::CoMultiGet(
|
||||
SstFileReader* reader, const ReadOptions& options,
|
||||
const std::vector<Slice>& keys, std::vector<std::string>* values) {
|
||||
std::vector<PinnableSlice> pinnable_values;
|
||||
std::vector<Status> statuses = co_await folly::coro::co_nothrow(
|
||||
CoMultiGet(reader, options, keys, &pinnable_values));
|
||||
values->resize(keys.size());
|
||||
for (size_t i = 0; i < keys.size(); ++i) {
|
||||
if (statuses[i].ok()) {
|
||||
(*values)[i].assign(pinnable_values[i].data(), pinnable_values[i].size());
|
||||
}
|
||||
}
|
||||
co_return statuses;
|
||||
}
|
||||
|
||||
folly::coro::Task<void> CoroDB::CoMultiGet(
|
||||
DB* db, const ReadOptions& options, size_t num_keys,
|
||||
ColumnFamilyHandle** column_families, const Slice* keys,
|
||||
@@ -122,16 +313,31 @@ folly::coro::Task<void> CoroDB::CoMultiGet(
|
||||
file_system == nullptr ? nullptr : file_system->GetReadExecutor();
|
||||
auto* read_event_base =
|
||||
read_executor == nullptr ? nullptr : read_executor->getEventBase();
|
||||
CoroutineStatsConfig stats_config = CaptureAndDisableCoroutineStatsConfig();
|
||||
|
||||
if (coro_db == nullptr || read_event_base == nullptr) {
|
||||
CoroutineStatsContextScope stats_scope(std::move(stats_config),
|
||||
db->GetEnv());
|
||||
db->MultiGet(options, num_keys, column_families, keys, values, timestamps,
|
||||
statuses, sorted_input);
|
||||
} else {
|
||||
auto task = [](CoroutineStatsConfig task_stats_config, Env* task_env,
|
||||
CoroDB* task_db, const ReadOptions& task_options,
|
||||
size_t task_num_keys,
|
||||
ColumnFamilyHandle** task_column_families,
|
||||
const Slice* task_keys, PinnableSlice* task_values,
|
||||
std::string* task_timestamps, Status* task_statuses,
|
||||
bool task_sorted_input) -> folly::coro::Task<void> {
|
||||
CoroutineStatsContextScope stats_scope(std::move(task_stats_config),
|
||||
task_env);
|
||||
co_await folly::coro::co_nothrow(task_db->MultiGetCoroutine(
|
||||
task_options, task_num_keys, task_column_families, task_keys,
|
||||
task_values, task_timestamps, task_statuses, task_sorted_input));
|
||||
}(std::move(stats_config), db->GetEnv(), coro_db, options, num_keys,
|
||||
column_families, keys, values,
|
||||
timestamps, statuses, sorted_input);
|
||||
co_await folly::coro::co_nothrow(folly::coro::co_withExecutor(
|
||||
folly::Executor::getKeepAliveToken(read_event_base),
|
||||
coro_db->MultiGetCoroutine(options, num_keys, column_families, keys,
|
||||
values, timestamps, statuses,
|
||||
sorted_input)));
|
||||
folly::Executor::getKeepAliveToken(read_event_base), std::move(task)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+757
-32
@@ -11,6 +11,7 @@
|
||||
|
||||
#include "db/db_test_util.h"
|
||||
#include "db/log_writer.h"
|
||||
#include "db/manifest_ops.h"
|
||||
#include "db/version_edit.h"
|
||||
#include "file/writable_file_writer.h"
|
||||
#include "options/options_helper.h"
|
||||
@@ -20,10 +21,13 @@
|
||||
#include "rocksdb/merge_operator.h"
|
||||
#include "rocksdb/perf_context.h"
|
||||
#include "rocksdb/table.h"
|
||||
#include "rocksdb/utilities/checkpoint.h"
|
||||
#include "rocksdb/utilities/debug.h"
|
||||
#include "table/block_based/block_based_table_reader.h"
|
||||
#include "table/block_based/block_builder.h"
|
||||
#include "table/format.h"
|
||||
#include "test_util/sync_point.h"
|
||||
#include "util/defer.h"
|
||||
#include "util/file_checksum_helper.h"
|
||||
#include "util/random.h"
|
||||
#include "utilities/counted_fs.h"
|
||||
@@ -34,6 +38,19 @@
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
namespace {
|
||||
|
||||
OutputMetadata NewerVersionOutputMetadata() {
|
||||
OutputMetadata output_metadata;
|
||||
output_metadata.WantNewerVersionPresent();
|
||||
return output_metadata;
|
||||
}
|
||||
|
||||
MultiGetOutputMetadata NewerVersionMultiGetOutputMetadata() {
|
||||
MultiGetOutputMetadata output_metadata;
|
||||
output_metadata.WantNewerVersionPresent();
|
||||
return output_metadata;
|
||||
}
|
||||
|
||||
class MyFlushBlockPolicy : public FlushBlockPolicy {
|
||||
public:
|
||||
explicit MyFlushBlockPolicy(const int num_keys_in_block,
|
||||
@@ -141,6 +158,7 @@ struct RecoveryOptimizationCounters {
|
||||
sp->ClearAllCallBacks();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// optimize_manifest_for_recovery=true: a clean reopen of a flushed DB must
|
||||
@@ -351,6 +369,82 @@ TEST_F(DBBasicTest,
|
||||
synthetic_number);
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest,
|
||||
OptimizeManifestForRecoveryKeepsCurrentOptionsFileWithStaleOptions) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
options.avoid_unnecessary_blocking_io = false;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
const uint64_t next_before_close =
|
||||
dbfull()->GetVersionSet()->current_next_file_number();
|
||||
Close();
|
||||
|
||||
const uint64_t stale_options_number = next_before_close + 100;
|
||||
ASSERT_OK(WriteStringToFile(
|
||||
env_, "" /*data*/, dbname_ + "/" + OptionsFileName(stale_options_number),
|
||||
/*should_sync=*/true));
|
||||
ASSERT_OK(WriteStringToFile(
|
||||
env_, "" /*data*/,
|
||||
dbname_ + "/" + OptionsFileName(stale_options_number + 1),
|
||||
/*should_sync=*/true));
|
||||
|
||||
RecoveryOptimizationCounters counters;
|
||||
counters.Install();
|
||||
Reopen(options);
|
||||
counters.Uninstall();
|
||||
|
||||
const uint64_t current_options_number =
|
||||
dbfull()->GetVersionSet()->options_file_number();
|
||||
ASSERT_EQ(1, counters.next_file_number.load());
|
||||
ASSERT_GT(dbfull()->GetVersionSet()->current_next_file_number(),
|
||||
stale_options_number + 1);
|
||||
ASSERT_GT(current_options_number, 0u);
|
||||
ASSERT_OK(env_->FileExists(OptionsFileName(dbname_, current_options_number)));
|
||||
|
||||
const std::string checkpoint_dir = dbname_ + "_checkpoint";
|
||||
ASSERT_OK(DestroyDir(env_, checkpoint_dir));
|
||||
Checkpoint* checkpoint = nullptr;
|
||||
ASSERT_OK(Checkpoint::Create(db_.get(), &checkpoint));
|
||||
std::unique_ptr<Checkpoint> checkpoint_guard(checkpoint);
|
||||
ASSERT_OK(checkpoint_guard->CreateCheckpoint(checkpoint_dir));
|
||||
ASSERT_OK(DestroyDir(env_, checkpoint_dir));
|
||||
ASSERT_EQ("v", Get("k"));
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest,
|
||||
OptimizeManifestForRecoveryReservesTempOptionsFileNumbersInMemory) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
const uint64_t next_before_close =
|
||||
dbfull()->GetVersionSet()->current_next_file_number();
|
||||
Close();
|
||||
|
||||
const uint64_t stale_temp_options_number = next_before_close + 100;
|
||||
ASSERT_OK(
|
||||
WriteStringToFile(env_, "" /*data*/,
|
||||
TempOptionsFileName(dbname_, stale_temp_options_number),
|
||||
/*should_sync=*/true));
|
||||
|
||||
RecoveryOptimizationCounters counters;
|
||||
counters.Install();
|
||||
Reopen(options);
|
||||
counters.Uninstall();
|
||||
|
||||
ASSERT_EQ(1, counters.next_file_number.load());
|
||||
ASSERT_GT(dbfull()->GetVersionSet()->current_next_file_number(),
|
||||
stale_temp_options_number);
|
||||
ASSERT_EQ("v", Get("k"));
|
||||
}
|
||||
|
||||
// optimize_manifest_for_recovery=true: after a clean Put + Flush + Close, the
|
||||
// next Open's min_log_number_to_keep equals (max_wal_before_close + 1),
|
||||
// proving the close-time MANIFEST write took effect.
|
||||
@@ -978,27 +1072,19 @@ TEST_F(DBBasicTest, WritableFileWriterInitialFileSizeAdoptsExistingSize) {
|
||||
TEST_F(DBBasicTest, ReuseManifestOnOpenSkipsOnTailCorruption) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
options.reuse_manifest_on_open = true;
|
||||
options.write_dbid_to_manifest = false;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
Close();
|
||||
|
||||
// Find the MANIFEST file and append garbage to it.
|
||||
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());
|
||||
uint64_t manifest_file_number = 0;
|
||||
ASSERT_OK(GetCurrentManifestPath(dbname_, env_->GetFileSystem().get(),
|
||||
/*is_retry=*/false, &manifest_path,
|
||||
&manifest_file_number));
|
||||
{
|
||||
std::string contents;
|
||||
ASSERT_OK(ReadFileToString(env_, manifest_path, &contents));
|
||||
@@ -1007,9 +1093,13 @@ TEST_F(DBBasicTest, ReuseManifestOnOpenSkipsOnTailCorruption) {
|
||||
}
|
||||
|
||||
std::atomic<int> reopened{0};
|
||||
std::atomic<int> new_manifest{0};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"VersionSet::ReopenManifestForAppend:Reopened",
|
||||
[&](void* /*arg*/) { reopened.fetch_add(1); });
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"VersionSet::ProcessManifestWrites:BeforeNewManifest",
|
||||
[&](void* /*arg*/) { new_manifest.fetch_add(1); });
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
Reopen(options);
|
||||
@@ -1018,6 +1108,12 @@ TEST_F(DBBasicTest, ReuseManifestOnOpenSkipsOnTailCorruption) {
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
ASSERT_EQ(0, reopened.load());
|
||||
ASSERT_EQ(1, new_manifest.load());
|
||||
std::string manifest_path_after;
|
||||
ASSERT_OK(GetCurrentManifestPath(dbname_, env_->GetFileSystem().get(),
|
||||
/*is_retry=*/false, &manifest_path_after,
|
||||
&manifest_file_number));
|
||||
ASSERT_NE(manifest_path, manifest_path_after);
|
||||
ASSERT_EQ("v", Get("k"));
|
||||
}
|
||||
|
||||
@@ -1030,27 +1126,19 @@ TEST_F(DBBasicTest, ReuseManifestOnOpenSkipsOnTailCorruption) {
|
||||
TEST_F(DBBasicTest, ReuseManifestOnOpenIncompleteAtomicGroupAtTail) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
options.reuse_manifest_on_open = true;
|
||||
options.write_dbid_to_manifest = false;
|
||||
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());
|
||||
uint64_t manifest_file_number = 0;
|
||||
ASSERT_OK(GetCurrentManifestPath(dbname_, env_->GetFileSystem().get(),
|
||||
/*is_retry=*/false, &manifest_path,
|
||||
&manifest_file_number));
|
||||
|
||||
// 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
|
||||
@@ -1088,11 +1176,29 @@ TEST_F(DBBasicTest, ReuseManifestOnOpenIncompleteAtomicGroupAtTail) {
|
||||
// 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).
|
||||
std::atomic<int> reopened{0};
|
||||
std::atomic<int> new_manifest{0};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"VersionSet::ReopenManifestForAppend:Reopened",
|
||||
[&](void* /*arg*/) { reopened.fetch_add(1); });
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"VersionSet::ProcessManifestWrites:BeforeNewManifest",
|
||||
[&](void* /*arg*/) { new_manifest.fetch_add(1); });
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
// Reopen: recovery should succeed, but ReopenManifestForAppend must not
|
||||
// reuse a MANIFEST ending with an incomplete atomic group.
|
||||
Reopen(options);
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
ASSERT_EQ(0, reopened.load());
|
||||
ASSERT_EQ(1, new_manifest.load());
|
||||
std::string manifest_path_after;
|
||||
ASSERT_OK(GetCurrentManifestPath(dbname_, env_->GetFileSystem().get(),
|
||||
/*is_retry=*/false, &manifest_path_after,
|
||||
&manifest_file_number));
|
||||
ASSERT_NE(manifest_path, manifest_path_after);
|
||||
ASSERT_EQ("v", Get("k"));
|
||||
|
||||
// Write new data to create new MANIFEST records.
|
||||
@@ -1228,6 +1334,24 @@ TEST_F(DBBasicTest, ReadOnlyDB) {
|
||||
ASSERT_OK(EnforcedReadOnlyReopen(options));
|
||||
ASSERT_EQ("v3", Get("foo"));
|
||||
ASSERT_EQ("v2", Get("bar"));
|
||||
std::string metadata_value;
|
||||
ReadOptions metadata_read_options;
|
||||
OutputMetadata output_metadata = NewerVersionOutputMetadata();
|
||||
ASSERT_OK(db_->GetWithMetadata(metadata_read_options, "foo", &metadata_value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ("v3", metadata_value);
|
||||
ASSERT_FALSE(*output_metadata.newer_version_present);
|
||||
std::vector<Slice> metadata_keys{Slice("foo")};
|
||||
std::vector<std::string> metadata_values;
|
||||
MultiGetOutputMetadata multiget_output_metadata =
|
||||
NewerVersionMultiGetOutputMetadata();
|
||||
std::vector<Status> metadata_statuses =
|
||||
db_->MultiGetWithMetadata(metadata_read_options, metadata_keys,
|
||||
&metadata_values, &multiget_output_metadata);
|
||||
ASSERT_EQ(1, metadata_statuses.size());
|
||||
ASSERT_OK(metadata_statuses[0]);
|
||||
ASSERT_EQ("v3", metadata_values[0]);
|
||||
ASSERT_FALSE((*multiget_output_metadata.newer_version_present)[0]);
|
||||
verify_all_iters();
|
||||
ASSERT_EQ(db_->SyncWAL().code(), Status::Code::kNotSupported);
|
||||
|
||||
@@ -1409,6 +1533,35 @@ TEST_F(DBBasicTest, CompactedDB) {
|
||||
ASSERT_EQ(DummyString(kFileSize / 2, 'j'), Get("jjj"));
|
||||
ASSERT_EQ("NOT_FOUND", Get("kkk"));
|
||||
|
||||
std::string metadata_value;
|
||||
ReadOptions metadata_read_options;
|
||||
const Snapshot* metadata_snapshot = db_->GetSnapshot();
|
||||
metadata_read_options.snapshot = metadata_snapshot;
|
||||
OutputMetadata output_metadata = NewerVersionOutputMetadata();
|
||||
ASSERT_OK(db_->GetWithMetadata(metadata_read_options, "aaa", &metadata_value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ(DummyString(kFileSize / 2, 'a'), metadata_value);
|
||||
ASSERT_FALSE(*output_metadata.newer_version_present);
|
||||
std::vector<Slice> metadata_keys{Slice("aaa"), Slice("ccc")};
|
||||
std::vector<std::string> metadata_values;
|
||||
MultiGetOutputMetadata multiget_output_metadata =
|
||||
NewerVersionMultiGetOutputMetadata();
|
||||
std::vector<Status> metadata_statuses =
|
||||
db_->MultiGetWithMetadata(metadata_read_options, metadata_keys,
|
||||
&metadata_values, &multiget_output_metadata);
|
||||
ASSERT_EQ(metadata_keys.size(), metadata_statuses.size());
|
||||
ASSERT_OK(metadata_statuses[0]);
|
||||
ASSERT_EQ(DummyString(kFileSize / 2, 'a'), metadata_values[0]);
|
||||
ASSERT_FALSE((*multiget_output_metadata.newer_version_present)[0]);
|
||||
ASSERT_TRUE(metadata_statuses[1].IsNotFound());
|
||||
ASSERT_FALSE((*multiget_output_metadata.newer_version_present)[1]);
|
||||
db_->ReleaseSnapshot(metadata_snapshot);
|
||||
|
||||
ASSERT_TRUE(db_->GetWithMetadata(ReadOptions(), db_->DefaultColumnFamily(),
|
||||
"aaa", static_cast<PinnableSlice*>(nullptr),
|
||||
&output_metadata)
|
||||
.IsInvalidArgument());
|
||||
|
||||
// TODO: validate that other write ops return NotImplemented
|
||||
// (CompactedDB is missing some overrides)
|
||||
|
||||
@@ -1596,6 +1749,430 @@ TEST_P(DBBasicGetWithParam, GetSnapshot) {
|
||||
} while (ChangeOptions());
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, GetNewerVersionPresent) {
|
||||
// Snapshot reads report later point and range-delete writes without changing
|
||||
// the snapshot-visible value.
|
||||
Options options = CurrentOptions();
|
||||
options.merge_operator = MergeOperators::CreateStringAppendOperator();
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
ASSERT_OK(Put(1, "key", "v1"));
|
||||
ASSERT_OK(Put(1, "merge_key", "base"));
|
||||
ASSERT_OK(Put(1, "stable", "s1"));
|
||||
ASSERT_OK(Put(1, "range_key", "r1"));
|
||||
const Snapshot* snapshot = db_->GetSnapshot();
|
||||
|
||||
ReadOptions snapshot_read;
|
||||
snapshot_read.snapshot = snapshot;
|
||||
|
||||
std::string value;
|
||||
OutputMetadata output_metadata = NewerVersionOutputMetadata();
|
||||
ASSERT_OK(db_->GetWithMetadata(snapshot_read, handles_[1], "key", &value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ("v1", value);
|
||||
ASSERT_FALSE(*output_metadata.newer_version_present);
|
||||
|
||||
ASSERT_OK(Put(1, "other", "v2"));
|
||||
ASSERT_OK(Put(1, "created_after_put", "new"));
|
||||
ASSERT_OK(Merge(1, "created_after_merge", "new"));
|
||||
ASSERT_OK(
|
||||
db_->DeleteRange(WriteOptions(), handles_[1], "range_key", "range_key~"));
|
||||
ASSERT_OK(Merge(1, "merge_key", "new"));
|
||||
value.clear();
|
||||
*output_metadata.newer_version_present = true;
|
||||
ASSERT_OK(db_->GetWithMetadata(snapshot_read, handles_[1], "stable", &value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ("s1", value);
|
||||
ASSERT_FALSE(*output_metadata.newer_version_present);
|
||||
|
||||
value.clear();
|
||||
*output_metadata.newer_version_present = true;
|
||||
ASSERT_TRUE(db_->GetWithMetadata(snapshot_read, handles_[1],
|
||||
"created_after_put", &value,
|
||||
&output_metadata)
|
||||
.IsNotFound());
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
value.clear();
|
||||
*output_metadata.newer_version_present = false;
|
||||
ASSERT_TRUE(db_->GetWithMetadata(snapshot_read, handles_[1],
|
||||
"created_after_merge", &value,
|
||||
&output_metadata)
|
||||
.IsNotFound());
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
value.clear();
|
||||
*output_metadata.newer_version_present = false;
|
||||
ASSERT_OK(db_->GetWithMetadata(snapshot_read, handles_[1], "range_key",
|
||||
&value, &output_metadata));
|
||||
ASSERT_EQ("r1", value);
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
value.clear();
|
||||
*output_metadata.newer_version_present = false;
|
||||
ASSERT_OK(db_->GetWithMetadata(snapshot_read, handles_[1], "merge_key",
|
||||
&value, &output_metadata));
|
||||
ASSERT_EQ("base", value);
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
ASSERT_OK(Put(1, "key", "v2"));
|
||||
value.clear();
|
||||
ASSERT_OK(db_->GetWithMetadata(snapshot_read, handles_[1], "key", &value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ("v1", value);
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
ASSERT_OK(Flush(1));
|
||||
value.clear();
|
||||
*output_metadata.newer_version_present = false;
|
||||
ASSERT_OK(db_->GetWithMetadata(snapshot_read, handles_[1], "key", &value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ("v1", value);
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
value.clear();
|
||||
*output_metadata.newer_version_present = false;
|
||||
ASSERT_OK(db_->GetWithMetadata(snapshot_read, handles_[1], "range_key",
|
||||
&value, &output_metadata));
|
||||
ASSERT_EQ("r1", value);
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
ReadOptions latest_read;
|
||||
value.clear();
|
||||
*output_metadata.newer_version_present = true;
|
||||
ASSERT_OK(db_->GetWithMetadata(latest_read, handles_[1], "key", &value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ("v2", value);
|
||||
ASSERT_FALSE(*output_metadata.newer_version_present);
|
||||
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, NewerVersionPresentRequestedByOutputMetadata) {
|
||||
// Newer-version tracking is opt-in through OutputMetadata and
|
||||
// MultiGetOutputMetadata.
|
||||
Options options = CurrentOptions();
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
ASSERT_OK(Put(1, "key", "old"));
|
||||
const Snapshot* snapshot = db_->GetSnapshot();
|
||||
ASSERT_OK(Put(1, "key", "new"));
|
||||
|
||||
ReadOptions read_options;
|
||||
read_options.snapshot = snapshot;
|
||||
|
||||
std::string value;
|
||||
OutputMetadata output_metadata = NewerVersionOutputMetadata();
|
||||
ASSERT_OK(db_->GetWithMetadata(read_options, handles_[1], "key", &value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ("old", value);
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
std::vector<Slice> keys{"key"};
|
||||
std::vector<ColumnFamilyHandle*> cfs(keys.size(), handles_[1]);
|
||||
std::vector<std::string> values;
|
||||
MultiGetOutputMetadata multiget_output_metadata =
|
||||
NewerVersionMultiGetOutputMetadata();
|
||||
std::vector<Status> statuses = db_->MultiGetWithMetadata(
|
||||
read_options, cfs, keys, &values, &multiget_output_metadata);
|
||||
ASSERT_EQ(1, statuses.size());
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ("old", values[0]);
|
||||
ASSERT_EQ(1, multiget_output_metadata.newer_version_present->size());
|
||||
ASSERT_TRUE((*multiget_output_metadata.newer_version_present)[0]);
|
||||
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, NewerVersionPresentIgnoreRangeDeletions) {
|
||||
// ignore_range_deletions affects the returned value, not newer-version
|
||||
// metadata about covering range tombstones.
|
||||
Options options = CurrentOptions();
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
ASSERT_OK(Put(1, "range_key", "old"));
|
||||
const Snapshot* snapshot = db_->GetSnapshot();
|
||||
ASSERT_OK(
|
||||
db_->DeleteRange(WriteOptions(), handles_[1], "range_key", "range_key~"));
|
||||
|
||||
ReadOptions read_options;
|
||||
read_options.snapshot = snapshot;
|
||||
read_options.ignore_range_deletions = true;
|
||||
|
||||
std::string value;
|
||||
OutputMetadata output_metadata = NewerVersionOutputMetadata();
|
||||
ASSERT_OK(db_->GetWithMetadata(read_options, handles_[1], "range_key", &value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ("old", value);
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
std::vector<Slice> keys{"range_key"};
|
||||
std::vector<ColumnFamilyHandle*> cfs(keys.size(), handles_[1]);
|
||||
std::vector<std::string> values;
|
||||
MultiGetOutputMetadata multiget_output_metadata =
|
||||
NewerVersionMultiGetOutputMetadata();
|
||||
std::vector<Status> statuses = db_->MultiGetWithMetadata(
|
||||
read_options, cfs, keys, &values, &multiget_output_metadata);
|
||||
ASSERT_EQ(1, statuses.size());
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ("old", values[0]);
|
||||
ASSERT_TRUE((*multiget_output_metadata.newer_version_present)[0]);
|
||||
|
||||
ASSERT_OK(Flush(1));
|
||||
value.clear();
|
||||
*output_metadata.newer_version_present = false;
|
||||
ASSERT_OK(db_->GetWithMetadata(read_options, handles_[1], "range_key", &value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ("old", value);
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
values.clear();
|
||||
multiget_output_metadata.newer_version_present->clear();
|
||||
statuses = db_->MultiGetWithMetadata(read_options, cfs, keys, &values,
|
||||
&multiget_output_metadata);
|
||||
ASSERT_EQ(1, statuses.size());
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ("old", values[0]);
|
||||
ASSERT_TRUE((*multiget_output_metadata.newer_version_present)[0]);
|
||||
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, NewerVersionPresentFromImmutableMemTable) {
|
||||
Options options = CurrentOptions();
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
ASSERT_OK(Put(1, "point_key", "old_point"));
|
||||
ASSERT_OK(Put(1, "range_key", "old_range"));
|
||||
ManagedSnapshot snapshot(db_.get());
|
||||
|
||||
ASSERT_OK(Put(1, "point_key", "new_point"));
|
||||
ASSERT_OK(
|
||||
db_->DeleteRange(WriteOptions(), handles_[1], "range_key", "range_key~"));
|
||||
|
||||
auto* cfd =
|
||||
static_cast_with_check<ColumnFamilyHandleImpl>(handles_[1])->cfd();
|
||||
Status resume_status;
|
||||
{
|
||||
ASSERT_OK(db_->PauseBackgroundWork());
|
||||
Defer resume_background(
|
||||
[&] { resume_status = db_->ContinueBackgroundWork(); });
|
||||
ASSERT_OK(dbfull()->TEST_SwitchMemtable(cfd));
|
||||
|
||||
ReadOptions read_options;
|
||||
read_options.snapshot = snapshot.snapshot();
|
||||
|
||||
std::string value;
|
||||
OutputMetadata output_metadata = NewerVersionOutputMetadata();
|
||||
ASSERT_OK(db_->GetWithMetadata(read_options, handles_[1], "point_key",
|
||||
&value, &output_metadata));
|
||||
ASSERT_EQ("old_point", value);
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
value.clear();
|
||||
*output_metadata.newer_version_present = false;
|
||||
ASSERT_OK(db_->GetWithMetadata(read_options, handles_[1], "range_key",
|
||||
&value, &output_metadata));
|
||||
ASSERT_EQ("old_range", value);
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
std::vector<Slice> keys{"point_key", "range_key"};
|
||||
std::vector<ColumnFamilyHandle*> cfs(keys.size(), handles_[1]);
|
||||
std::vector<std::string> values;
|
||||
MultiGetOutputMetadata multiget_output_metadata =
|
||||
NewerVersionMultiGetOutputMetadata();
|
||||
const std::vector<Status> statuses = db_->MultiGetWithMetadata(
|
||||
read_options, cfs, keys, &values, &multiget_output_metadata);
|
||||
ASSERT_EQ(2, statuses.size());
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_OK(statuses[1]);
|
||||
ASSERT_EQ(std::vector<std::string>({"old_point", "old_range"}), values);
|
||||
ASSERT_TRUE((*multiget_output_metadata.newer_version_present)[0]);
|
||||
ASSERT_TRUE((*multiget_output_metadata.newer_version_present)[1]);
|
||||
}
|
||||
ASSERT_OK(resume_status);
|
||||
ASSERT_OK(Flush(1));
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, NewerVersionPresentBypassesRowCacheWhileTracking) {
|
||||
// Prime row-cache entries for the snapshot values, then verify metadata
|
||||
// reads still inspect SST sequence numbers instead of replaying those
|
||||
// entries.
|
||||
option_config_ = kRowCache;
|
||||
Options options = CurrentOptions();
|
||||
options.statistics = ROCKSDB_NAMESPACE::CreateDBStatistics();
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
ASSERT_OK(Put(1, "get_key", "old_get"));
|
||||
ASSERT_OK(Put(1, "multiget_key", "old_multiget"));
|
||||
ASSERT_OK(Flush(1));
|
||||
ManagedSnapshot snapshot(db_.get());
|
||||
ASSERT_OK(Put(1, "get_key", "new_get"));
|
||||
ASSERT_OK(Put(1, "multiget_key", "new_multiget"));
|
||||
ASSERT_OK(Flush(1));
|
||||
|
||||
ReadOptions read_options;
|
||||
read_options.snapshot = snapshot.snapshot();
|
||||
|
||||
std::string primed_value;
|
||||
ASSERT_OK(db_->Get(read_options, handles_[1], "get_key", &primed_value));
|
||||
ASSERT_EQ("old_get", primed_value);
|
||||
std::vector<Slice> primed_keys{"multiget_key"};
|
||||
std::vector<ColumnFamilyHandle*> primed_cfs(primed_keys.size(), handles_[1]);
|
||||
std::vector<std::string> primed_values;
|
||||
const std::vector<Status> primed_statuses =
|
||||
db_->MultiGet(read_options, primed_cfs, primed_keys, &primed_values);
|
||||
ASSERT_OK(primed_statuses[0]);
|
||||
ASSERT_EQ("old_multiget", primed_values[0]);
|
||||
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
SCOPED_TRACE(i);
|
||||
std::string value;
|
||||
OutputMetadata output_metadata = NewerVersionOutputMetadata();
|
||||
ASSERT_OK(db_->GetWithMetadata(read_options, handles_[1], "get_key", &value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ("old_get", value);
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
}
|
||||
|
||||
std::vector<Slice> keys{"multiget_key"};
|
||||
std::vector<ColumnFamilyHandle*> cfs(keys.size(), handles_[1]);
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
SCOPED_TRACE(i);
|
||||
std::vector<std::string> values;
|
||||
MultiGetOutputMetadata multiget_output_metadata =
|
||||
NewerVersionMultiGetOutputMetadata();
|
||||
std::vector<Status> statuses = db_->MultiGetWithMetadata(
|
||||
read_options, cfs, keys, &values, &multiget_output_metadata);
|
||||
ASSERT_EQ(1, statuses.size());
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ("old_multiget", values[0]);
|
||||
ASSERT_TRUE((*multiget_output_metadata.newer_version_present)[0]);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, NewerVersionPresentUsesRowCacheAfterObservation) {
|
||||
option_config_ = kRowCache;
|
||||
Options options = CurrentOptions();
|
||||
options.statistics = ROCKSDB_NAMESPACE::CreateDBStatistics();
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
ASSERT_OK(Put(1, "key", "old"));
|
||||
ASSERT_OK(Flush(1));
|
||||
ManagedSnapshot snapshot(db_.get());
|
||||
|
||||
ReadOptions read_options;
|
||||
read_options.snapshot = snapshot.snapshot();
|
||||
std::vector<Slice> keys{"key"};
|
||||
std::vector<ColumnFamilyHandle*> cfs(keys.size(), handles_[1]);
|
||||
std::vector<std::string> values;
|
||||
std::vector<Status> statuses =
|
||||
db_->MultiGet(read_options, cfs, keys, &values);
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ("old", values[0]);
|
||||
|
||||
ASSERT_OK(Put(1, "key", "new"));
|
||||
const uint64_t row_cache_hits = TestGetTickerCount(options, ROW_CACHE_HIT);
|
||||
|
||||
values.clear();
|
||||
MultiGetOutputMetadata output_metadata = NewerVersionMultiGetOutputMetadata();
|
||||
statuses = db_->MultiGetWithMetadata(read_options, cfs, keys, &values,
|
||||
&output_metadata);
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ("old", values[0]);
|
||||
ASSERT_TRUE((*output_metadata.newer_version_present)[0]);
|
||||
ASSERT_EQ(row_cache_hits + 1, TestGetTickerCount(options, ROW_CACHE_HIT));
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, NewerVersionPresentPersistedTierNotSupported) {
|
||||
// kPersistedTier is incompatible with snapshot newer-version tracking but not
|
||||
// with untracked latest-version reads.
|
||||
Options options = CurrentOptions();
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
ASSERT_OK(Put(1, "key", "old"));
|
||||
const Snapshot* snapshot = db_->GetSnapshot();
|
||||
ASSERT_OK(Put(1, "key", "new"));
|
||||
|
||||
ReadOptions read_options;
|
||||
read_options.snapshot = snapshot;
|
||||
read_options.read_tier = kPersistedTier;
|
||||
|
||||
std::string value;
|
||||
OutputMetadata output_metadata = NewerVersionOutputMetadata();
|
||||
ASSERT_TRUE(db_->GetWithMetadata(read_options, handles_[1], "key", &value,
|
||||
&output_metadata)
|
||||
.IsNotSupported());
|
||||
|
||||
std::vector<Slice> keys{"key"};
|
||||
std::vector<ColumnFamilyHandle*> cfs(keys.size(), handles_[1]);
|
||||
std::vector<std::string> values;
|
||||
MultiGetOutputMetadata multiget_output_metadata =
|
||||
NewerVersionMultiGetOutputMetadata();
|
||||
std::vector<Status> statuses = db_->MultiGetWithMetadata(
|
||||
read_options, cfs, keys, &values, &multiget_output_metadata);
|
||||
ASSERT_EQ(1, statuses.size());
|
||||
ASSERT_TRUE(statuses[0].IsNotSupported());
|
||||
ASSERT_FALSE((*multiget_output_metadata.newer_version_present)[0]);
|
||||
|
||||
read_options.snapshot = nullptr;
|
||||
value.clear();
|
||||
*output_metadata.newer_version_present = true;
|
||||
ASSERT_OK(db_->GetWithMetadata(read_options, handles_[1], "key", &value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ("new", value);
|
||||
ASSERT_FALSE(*output_metadata.newer_version_present);
|
||||
|
||||
values.clear();
|
||||
multiget_output_metadata.newer_version_present->clear();
|
||||
statuses = db_->MultiGetWithMetadata(read_options, cfs, keys, &values,
|
||||
&multiget_output_metadata);
|
||||
ASSERT_EQ(1, statuses.size());
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ("new", values[0]);
|
||||
ASSERT_FALSE((*multiget_output_metadata.newer_version_present)[0]);
|
||||
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, ReadOnlyNewerVersionPresentReturnsFalse) {
|
||||
// Read-only DBs do not widen snapshot reads, so requested metadata remains
|
||||
// false.
|
||||
Options options = CurrentOptions();
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
ASSERT_OK(Put(1, "key", "value"));
|
||||
ASSERT_OK(Flush(1));
|
||||
ASSERT_OK(
|
||||
TryReopenReadOnlyWithColumnFamilies({"default", "pikachu"}, options));
|
||||
|
||||
const Snapshot* snapshot = db_->GetSnapshot();
|
||||
ReadOptions read_options;
|
||||
read_options.snapshot = snapshot;
|
||||
|
||||
std::string value;
|
||||
OutputMetadata output_metadata = NewerVersionOutputMetadata();
|
||||
ASSERT_OK(db_->GetWithMetadata(read_options, handles_[1], "key", &value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ("value", value);
|
||||
ASSERT_FALSE(*output_metadata.newer_version_present);
|
||||
|
||||
std::vector<Slice> keys{"key"};
|
||||
std::vector<ColumnFamilyHandle*> cfs(keys.size(), handles_[1]);
|
||||
std::vector<std::string> values;
|
||||
MultiGetOutputMetadata multiget_output_metadata =
|
||||
NewerVersionMultiGetOutputMetadata();
|
||||
std::vector<Status> statuses = db_->MultiGetWithMetadata(
|
||||
read_options, cfs, keys, &values, &multiget_output_metadata);
|
||||
ASSERT_EQ(1, statuses.size());
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ("value", values[0]);
|
||||
ASSERT_EQ(1, multiget_output_metadata.newer_version_present->size());
|
||||
ASSERT_FALSE((*multiget_output_metadata.newer_version_present)[0]);
|
||||
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, CheckLock) {
|
||||
do {
|
||||
std::unique_ptr<DB> localdb;
|
||||
@@ -2188,6 +2765,151 @@ TEST_F(DBBasicTest, MultiGetSimple) {
|
||||
} while (ChangeCompactOptions());
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, MultiGetNewerVersionPresent) {
|
||||
// A mixed MultiGet batch reports newer writes independently for each key in
|
||||
// input order.
|
||||
Options options = CurrentOptions();
|
||||
options.merge_operator = MergeOperators::CreateStringAppendOperator();
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
ASSERT_OK(Put(1, "updated", "old"));
|
||||
ASSERT_OK(Put(1, "unchanged", "same"));
|
||||
ASSERT_OK(Put(1, "deleted", "old"));
|
||||
ASSERT_OK(Put(1, "merged", "old"));
|
||||
ASSERT_OK(Put(1, "range_deleted", "old"));
|
||||
const Snapshot* snapshot = db_->GetSnapshot();
|
||||
|
||||
ASSERT_OK(Put(1, "updated", "new"));
|
||||
ASSERT_OK(Put(1, "created_after_put", "new"));
|
||||
ASSERT_OK(Merge(1, "created_after_merge", "new"));
|
||||
ASSERT_OK(Delete(1, "deleted"));
|
||||
ASSERT_OK(Merge(1, "merged", "new"));
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), handles_[1], "range_deleted",
|
||||
"range_deleted~"));
|
||||
|
||||
ReadOptions read_options;
|
||||
read_options.snapshot = snapshot;
|
||||
std::vector<Slice> keys({"updated", "unchanged", "deleted", "merged",
|
||||
"range_deleted", "created_after_put",
|
||||
"created_after_merge", "missing"});
|
||||
std::vector<ColumnFamilyHandle*> cfs(keys.size(), handles_[1]);
|
||||
std::vector<std::string> values;
|
||||
MultiGetOutputMetadata output_metadata = NewerVersionMultiGetOutputMetadata();
|
||||
|
||||
std::vector<Status> statuses = db_->MultiGetWithMetadata(
|
||||
read_options, cfs, keys, &values, &output_metadata);
|
||||
|
||||
ASSERT_EQ(keys.size(), statuses.size());
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ("old", values[0]);
|
||||
ASSERT_TRUE((*output_metadata.newer_version_present)[0]);
|
||||
|
||||
ASSERT_OK(statuses[1]);
|
||||
ASSERT_EQ("same", values[1]);
|
||||
ASSERT_FALSE((*output_metadata.newer_version_present)[1]);
|
||||
|
||||
ASSERT_OK(statuses[2]);
|
||||
ASSERT_EQ("old", values[2]);
|
||||
ASSERT_TRUE((*output_metadata.newer_version_present)[2]);
|
||||
|
||||
ASSERT_OK(statuses[3]);
|
||||
ASSERT_EQ("old", values[3]);
|
||||
ASSERT_TRUE((*output_metadata.newer_version_present)[3]);
|
||||
|
||||
ASSERT_OK(statuses[4]);
|
||||
ASSERT_EQ("old", values[4]);
|
||||
ASSERT_TRUE((*output_metadata.newer_version_present)[4]);
|
||||
|
||||
ASSERT_TRUE(statuses[5].IsNotFound());
|
||||
ASSERT_TRUE((*output_metadata.newer_version_present)[5]);
|
||||
|
||||
ASSERT_TRUE(statuses[6].IsNotFound());
|
||||
ASSERT_TRUE((*output_metadata.newer_version_present)[6]);
|
||||
|
||||
ASSERT_TRUE(statuses[7].IsNotFound());
|
||||
ASSERT_FALSE((*output_metadata.newer_version_present)[7]);
|
||||
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, MultiGetNewerVersionPresentAcrossColumnFamilies) {
|
||||
// Unsorted mixed-CF reads keep metadata aligned with input order after each
|
||||
// CF is processed independently.
|
||||
Options options = CurrentOptions();
|
||||
CreateAndReopenWithCF({"one", "two"}, options);
|
||||
|
||||
ASSERT_OK(Put(1, "updated_one", "old_one"));
|
||||
ASSERT_OK(Put(2, "updated_two", "old_two"));
|
||||
ASSERT_OK(Put(2, "stable_two", "stable"));
|
||||
const Snapshot* snapshot = db_->GetSnapshot();
|
||||
ASSERT_OK(Put(1, "updated_one", "new_one"));
|
||||
ASSERT_OK(Put(2, "updated_two", "new_two"));
|
||||
ASSERT_OK(Flush(1));
|
||||
ASSERT_OK(Flush(2));
|
||||
|
||||
ReadOptions read_options;
|
||||
read_options.snapshot = snapshot;
|
||||
std::vector<Slice> keys{"updated_two", "updated_one", "stable_two"};
|
||||
std::vector<ColumnFamilyHandle*> column_families{handles_[2], handles_[1],
|
||||
handles_[2]};
|
||||
std::vector<std::string> values;
|
||||
MultiGetOutputMetadata output_metadata = NewerVersionMultiGetOutputMetadata();
|
||||
const std::vector<Status> statuses = db_->MultiGetWithMetadata(
|
||||
read_options, column_families, keys, &values, &output_metadata);
|
||||
|
||||
for (const Status& status : statuses) {
|
||||
ASSERT_OK(status);
|
||||
}
|
||||
ASSERT_EQ(std::vector<std::string>({"old_two", "old_one", "stable"}), values);
|
||||
ASSERT_TRUE((*output_metadata.newer_version_present)[0]);
|
||||
ASSERT_TRUE((*output_metadata.newer_version_present)[1]);
|
||||
ASSERT_FALSE((*output_metadata.newer_version_present)[2]);
|
||||
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, MultiGetWithMetadataColumnFamiliesKeysSizeMismatch) {
|
||||
CreateAndReopenWithCF({"pikachu"}, CurrentOptions());
|
||||
|
||||
std::vector<Slice> keys({"k1", "k2"});
|
||||
std::vector<ColumnFamilyHandle*> cfs({handles_[1]});
|
||||
std::vector<std::string> values({"stale"});
|
||||
MultiGetOutputMetadata output_metadata;
|
||||
output_metadata.timestamps.emplace();
|
||||
output_metadata.newer_version_present = std::vector<uint8_t>({1});
|
||||
|
||||
std::vector<Status> statuses = db_->MultiGetWithMetadata(
|
||||
ReadOptions(), cfs, keys, &values, &output_metadata);
|
||||
|
||||
ASSERT_EQ(keys.size(), statuses.size());
|
||||
ASSERT_EQ(keys.size(), values.size());
|
||||
for (const Status& status : statuses) {
|
||||
ASSERT_TRUE(status.IsInvalidArgument());
|
||||
}
|
||||
ASSERT_EQ(keys.size(), output_metadata.timestamps->size());
|
||||
ASSERT_EQ(keys.size(), output_metadata.newer_version_present->size());
|
||||
ASSERT_FALSE((*output_metadata.newer_version_present)[0]);
|
||||
ASSERT_FALSE((*output_metadata.newer_version_present)[1]);
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, GetWithMetadataNullValueInvalidArgument) {
|
||||
CreateAndReopenWithCF({"pikachu"}, CurrentOptions());
|
||||
|
||||
OutputMetadata output_metadata = NewerVersionOutputMetadata();
|
||||
ASSERT_TRUE(db_->GetWithMetadata(ReadOptions(), handles_[1], "key",
|
||||
static_cast<PinnableSlice*>(nullptr),
|
||||
&output_metadata)
|
||||
.IsInvalidArgument());
|
||||
ASSERT_FALSE(*output_metadata.newer_version_present);
|
||||
|
||||
*output_metadata.newer_version_present = true;
|
||||
ASSERT_TRUE(db_->GetWithMetadata(ReadOptions(), "key",
|
||||
static_cast<std::string*>(nullptr),
|
||||
&output_metadata)
|
||||
.IsInvalidArgument());
|
||||
ASSERT_FALSE(*output_metadata.newer_version_present);
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, MultiGetEmpty) {
|
||||
do {
|
||||
CreateAndReopenWithCF({"pikachu"}, CurrentOptions());
|
||||
@@ -2227,6 +2949,9 @@ INSTANTIATE_TEST_CASE_P(FormatVersions, DBBlockChecksumTest,
|
||||
TEST_P(DBBlockChecksumTest, BlockChecksumTest) {
|
||||
BlockBasedTableOptions table_options;
|
||||
table_options.format_version = GetParam();
|
||||
// kFooterFormatVersionsToTest includes the unpublished draft format_version
|
||||
// 8; writing it requires this opt-in.
|
||||
SaveAndRestore<bool> allow_draft(&TEST_AllowUnsupportedFormatVersion(), true);
|
||||
Options options = CurrentOptions();
|
||||
const int kNumPerFile = 2;
|
||||
|
||||
|
||||
@@ -2542,9 +2542,13 @@ TEST_P(DBBloomFilterTestVaryPrefixAndFormatVer, PartitionedMultiGet) {
|
||||
values[i] = PinnableSlice();
|
||||
}
|
||||
|
||||
get_perf_context()->Reset();
|
||||
const PerfLevel previous_perf_level = GetPerfLevel();
|
||||
SetPerfLevel(PerfLevel::kEnableTime);
|
||||
db_->MultiGet(ropts, Q, column_families.data(), key_slices.data(),
|
||||
values.data(),
|
||||
/*timestamps=*/nullptr, statuses.data(), true);
|
||||
SetPerfLevel(previous_perf_level);
|
||||
|
||||
// Confirm correct status results
|
||||
uint32_t number_not_found = 0;
|
||||
@@ -2574,6 +2578,8 @@ TEST_P(DBBloomFilterTestVaryPrefixAndFormatVer, PartitionedMultiGet) {
|
||||
// Confirm no duplicate loading same filter partition
|
||||
uint64_t filter_accesses = PopTicker(options, BLOCK_CACHE_FILTER_HIT) +
|
||||
PopTicker(options, BLOCK_CACHE_FILTER_MISS);
|
||||
// Partition reads must contribute to the filter-block timing metric.
|
||||
EXPECT_GT(get_perf_context()->read_filter_block_nanos, 0);
|
||||
if (stride == 1) {
|
||||
EXPECT_EQ(filter_accesses, 1);
|
||||
} else {
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "db/blob/blob_index.h"
|
||||
#include "db/db_test_util.h"
|
||||
#include "db/dbformat.h"
|
||||
#include "db/periodic_compaction_phaser.h"
|
||||
#include "db/table_cache.h"
|
||||
#include "env/mock_env.h"
|
||||
#include "file/filename.h"
|
||||
@@ -29,6 +30,8 @@
|
||||
#include "test_util/sync_point.h"
|
||||
#include "test_util/testutil.h"
|
||||
#include "util/concurrent_task_limiter_impl.h"
|
||||
#include "util/fastrange.h"
|
||||
#include "util/hash.h"
|
||||
#include "util/random.h"
|
||||
#include "utilities/fault_injection_env.h"
|
||||
#include "utilities/fault_injection_fs.h"
|
||||
@@ -1334,6 +1337,118 @@ TEST_F(DBCompactionTest, CompactionSstPartitionerNonTrivial) {
|
||||
ASSERT_EQ("B", Get("bbbb1"));
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionTest, CompactionSstPartitionerPrefixFastPath) {
|
||||
// Exercises the ShouldPartitionByPrefix() fast path in CompactionOutputs:
|
||||
// multiple keys sharing a fixed prefix must stay together in one output file
|
||||
// (no spurious cuts), with boundaries landing exactly at prefix changes.
|
||||
Options options = CurrentOptions();
|
||||
options.compaction_style = kCompactionStyleLevel;
|
||||
options.level0_file_num_compaction_trigger = 3;
|
||||
options.sst_partitioner_factory = NewSstPartitionerFixedPrefixFactory(4);
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Three prefixes with multiple keys each, spread across two flushed files so
|
||||
// that a non-trivial compaction merges them.
|
||||
ASSERT_OK(Put("aaaa1", "v"));
|
||||
ASSERT_OK(Put("aaaa3", "v"));
|
||||
ASSERT_OK(Put("bbbb1", "v"));
|
||||
ASSERT_OK(Put("cccc2", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
|
||||
|
||||
ASSERT_OK(Put("aaaa2", "v"));
|
||||
ASSERT_OK(Put("bbbb2", "v"));
|
||||
ASSERT_OK(Put("cccc1", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
|
||||
|
||||
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
|
||||
// Exactly one output file per distinct 4-byte prefix.
|
||||
std::vector<LiveFileMetaData> files;
|
||||
dbfull()->GetLiveFilesMetaData(&files);
|
||||
ASSERT_EQ(3, files.size());
|
||||
// Each file holds a whole prefix group (multiple keys => the fast path
|
||||
// correctly did NOT cut within a shared prefix).
|
||||
for (const auto& f : files) {
|
||||
ASSERT_EQ(f.smallestkey.substr(0, 4), f.largestkey.substr(0, 4));
|
||||
}
|
||||
for (const std::string k :
|
||||
{"aaaa1", "aaaa2", "aaaa3", "bbbb1", "bbbb2", "cccc1", "cccc2"}) {
|
||||
ASSERT_EQ("v", Get(k));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionTest, CompactionSstPartitionerPrefixShortKeyFallback) {
|
||||
// Keys shorter than the fixed prefix length cannot be represented as a
|
||||
// prefix, so ShouldPartitionByPrefix() returns no value and CompactionOutputs
|
||||
// falls back to per-key ShouldPartition(). This also exercises short->long
|
||||
// and long->short mode transitions across output file boundaries.
|
||||
Options options = CurrentOptions();
|
||||
options.compaction_style = kCompactionStyleLevel;
|
||||
options.level0_file_num_compaction_trigger = 3;
|
||||
options.sst_partitioner_factory = NewSstPartitionerFixedPrefixFactory(4);
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// "a" and "bb" are shorter than len 4 (fallback, each in its own file);
|
||||
// "cccc1"/"cccc2" share prefix "cccc" (fast path, one file); "dd" is short
|
||||
// again (fallback, own file).
|
||||
ASSERT_OK(Put("a", "v"));
|
||||
ASSERT_OK(Put("bb", "v"));
|
||||
ASSERT_OK(Put("cccc1", "v"));
|
||||
ASSERT_OK(Put("dd", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
|
||||
|
||||
// Second file overlaps to force a non-trivial (rewriting) compaction.
|
||||
ASSERT_OK(Put("cccc2", "v"));
|
||||
ASSERT_OK(Put("a", "v2"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
|
||||
|
||||
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
|
||||
// "a" | "bb" | {"cccc1","cccc2"} | "dd" => 4 files.
|
||||
std::vector<LiveFileMetaData> files;
|
||||
dbfull()->GetLiveFilesMetaData(&files);
|
||||
ASSERT_EQ(4, files.size());
|
||||
ASSERT_EQ("v2", Get("a"));
|
||||
for (const std::string k : {"bb", "cccc1", "cccc2", "dd"}) {
|
||||
ASSERT_EQ("v", Get(k));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionTest, CompactionSstPartitionerZeroLenNoSplit) {
|
||||
// A zero-length fixed prefix yields an empty (but present) prefix from
|
||||
// ShouldPartitionByPrefix(), which means "no further partitions": every key
|
||||
// "starts with" the empty prefix, so the fast path never cuts.
|
||||
Options options = CurrentOptions();
|
||||
options.compaction_style = kCompactionStyleLevel;
|
||||
options.level0_file_num_compaction_trigger = 3;
|
||||
options.sst_partitioner_factory = NewSstPartitionerFixedPrefixFactory(0);
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(Put("aaaa1", "v1"));
|
||||
ASSERT_OK(Put("bbbb1", "v1"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
|
||||
|
||||
// Overlapping key forces a non-trivial compaction (not a trivial move).
|
||||
ASSERT_OK(Put("aaaa1", "v2"));
|
||||
ASSERT_OK(Put("cccc1", "v2"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
|
||||
|
||||
ASSERT_OK(dbfull()->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
|
||||
std::vector<LiveFileMetaData> files;
|
||||
dbfull()->GetLiveFilesMetaData(&files);
|
||||
ASSERT_EQ(1, files.size());
|
||||
ASSERT_EQ("v2", Get("aaaa1"));
|
||||
ASSERT_EQ("v1", Get("bbbb1"));
|
||||
ASSERT_EQ("v2", Get("cccc1"));
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionTest, ZeroSeqIdCompaction) {
|
||||
Options options = CurrentOptions();
|
||||
options.compaction_style = kCompactionStyleLevel;
|
||||
@@ -5371,6 +5486,7 @@ TEST_F(DBCompactionTest, LevelPeriodicCompaction) {
|
||||
for (bool if_open_all_files : {false, true}) {
|
||||
Options options = CurrentOptions();
|
||||
options.periodic_compaction_seconds = 48 * 60 * 60; // 2 days
|
||||
options.periodic_compaction_phase_recovery_percent = 0;
|
||||
if (if_open_all_files) {
|
||||
options.max_open_files = -1; // needed for ttl compaction
|
||||
} else {
|
||||
@@ -5466,6 +5582,169 @@ TEST_F(DBCompactionTest, LevelPeriodicCompaction) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionTest, PeriodicCompactionPhaseTriggerTime) {
|
||||
// Unit-tests PeriodicCompactionPhaser::TriggerTime, the core of
|
||||
// preferred-phase periodic compaction (see
|
||||
// DBOptions::periodic_compaction_phase_recovery_percent).
|
||||
using Params = PeriodicCompactionPhaseParams;
|
||||
const uint64_t kN = 1000; // periodic_compaction_seconds
|
||||
|
||||
// The trigger-time helper treats seed_hash as an opaque value; use a fixed
|
||||
// constant and derive the resulting preferred-phase target the same way
|
||||
// production does (FastRange64 of the seed over the interval).
|
||||
const uint64_t kSeed = 0x9e3779b97f4a7c15ULL;
|
||||
const uint64_t target = FastRange64(kSeed, kN);
|
||||
ASSERT_LT(target, kN);
|
||||
|
||||
// recovery_percent == 0 => classic behavior: trigger at file_mod + N.
|
||||
{
|
||||
Params p;
|
||||
p.seed_hash = kSeed;
|
||||
p.recovery_percent = 0;
|
||||
for (uint64_t fm : {uint64_t{1}, uint64_t{7}, uint64_t{12345}}) {
|
||||
ASSERT_EQ(PeriodicCompactionPhaser::TriggerTime(fm, kN, p), fm + kN);
|
||||
}
|
||||
}
|
||||
|
||||
// Steady state (anchor in the distant past): trigger is pulled earlier by
|
||||
// recovery_percent% of the phase offset, and never later than the deadline.
|
||||
{
|
||||
Params p;
|
||||
p.seed_hash = kSeed;
|
||||
p.recovery_percent = 50;
|
||||
p.anchor_time = 0;
|
||||
const uint64_t fm = 500;
|
||||
const uint64_t natural = fm + kN;
|
||||
const uint64_t offset = (fm % kN + kN - target) % kN;
|
||||
const uint64_t expected = natural - offset * 50 / 100;
|
||||
ASSERT_EQ(PeriodicCompactionPhaser::TriggerTime(fm, kN, p), expected);
|
||||
ASSERT_LE(expected, natural);
|
||||
}
|
||||
|
||||
// Geometric convergence: repeatedly re-stamp the file at its trigger time and
|
||||
// confirm the phase error is non-increasing and converges to (near) zero.
|
||||
for (int recovery_percent : {50, 100}) {
|
||||
Params p;
|
||||
p.seed_hash = kSeed;
|
||||
p.recovery_percent = recovery_percent;
|
||||
p.anchor_time = 0;
|
||||
uint64_t fm = 123;
|
||||
uint64_t prev_error = kN;
|
||||
for (int cycle = 0; cycle < 64; ++cycle) {
|
||||
const uint64_t trig = PeriodicCompactionPhaser::TriggerTime(fm, kN, p);
|
||||
ASSERT_LE(trig, fm + kN); // never later than the deadline
|
||||
const uint64_t error = (trig % kN + kN - target) % kN;
|
||||
ASSERT_LE(error, prev_error);
|
||||
prev_error = error;
|
||||
fm = trig;
|
||||
}
|
||||
// rp=100 pulls the whole gap so it reaches the phase exactly; smaller rates
|
||||
// decay geometrically and leave a tiny integer-rounding residue.
|
||||
if (recovery_percent == 100) {
|
||||
ASSERT_EQ(prev_error, uint64_t{0});
|
||||
} else {
|
||||
ASSERT_LE(prev_error, uint64_t{1});
|
||||
}
|
||||
}
|
||||
|
||||
// Already past the hard deadline when phasing took effect (e.g. the DB was
|
||||
// down, or periodic_compaction_seconds was turned down): instead of firing
|
||||
// the whole cohort immediately, spread it over the first quarter-period after
|
||||
// the anchor on an absolute (epoch-aligned) grid of period N/4.
|
||||
{
|
||||
Params p;
|
||||
p.seed_hash = kSeed;
|
||||
p.recovery_percent = 50;
|
||||
p.anchor_time = 200;
|
||||
const uint64_t n = 100;
|
||||
const uint64_t grid = n / 4; // 25
|
||||
const uint64_t phase = FastRange64(kSeed, grid);
|
||||
// natural deadline (10 + 100 = 110) is before the anchor (200).
|
||||
const uint64_t expected = 200 + (phase + grid - 200 % grid) % grid;
|
||||
const uint64_t trig = PeriodicCompactionPhaser::TriggerTime(10, n, p);
|
||||
ASSERT_EQ(trig, expected);
|
||||
ASSERT_GE(trig, uint64_t{200}); // never before the anchor
|
||||
ASSERT_LT(trig, 200 + grid); // within N/4 of the anchor
|
||||
}
|
||||
|
||||
// Past due against the recovery target but not yet at the deadline => spread
|
||||
// across [anchor, deadline] using the phase; never later than the deadline.
|
||||
{
|
||||
const uint64_t n = 100;
|
||||
const uint64_t small_target = FastRange64(kSeed, n);
|
||||
const uint64_t fm = small_target + 50 + 3 * n; // phase offset == 50
|
||||
const uint64_t natural = fm + n;
|
||||
const uint64_t anchor =
|
||||
natural - 25; // recovery target (natural-50) < anchor
|
||||
Params p;
|
||||
p.seed_hash = kSeed;
|
||||
p.recovery_percent = 100;
|
||||
p.anchor_time = anchor;
|
||||
ASSERT_EQ((fm % n + n - small_target) % n, uint64_t{50});
|
||||
const uint64_t expected = anchor + FastRange64(kSeed, natural - anchor);
|
||||
const uint64_t trig = PeriodicCompactionPhaser::TriggerTime(fm, n, p);
|
||||
ASSERT_EQ(trig, expected);
|
||||
ASSERT_GE(trig, anchor);
|
||||
ASSERT_LT(trig, natural);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionTest, PeriodicCompactionPhaseCfSpread) {
|
||||
// Unit-tests PeriodicCompactionPhaser::CfPhaseSeedHash: a DB's column
|
||||
// families are spread quasi-uniformly around the DB base phase via the
|
||||
// golden-ratio recurrence, for any base and any number of CFs.
|
||||
using Phaser = PeriodicCompactionPhaser;
|
||||
|
||||
// cf_id 0 leaves the base phase unchanged; distinct cf_ids -> distinct
|
||||
// phases.
|
||||
for (uint64_t base : {uint64_t{0}, uint64_t{0x9e3779b97f4a7c15ULL},
|
||||
uint64_t{12345678901234567ULL}, ~uint64_t{0}}) {
|
||||
ASSERT_EQ(Phaser::CfPhaseSeedHash(base, 0), base);
|
||||
std::vector<uint64_t> hs;
|
||||
for (uint32_t id = 0; id < 64; ++id) {
|
||||
hs.push_back(Phaser::CfPhaseSeedHash(base, id));
|
||||
}
|
||||
std::sort(hs.begin(), hs.end());
|
||||
for (size_t i = 1; i < hs.size(); ++i) {
|
||||
ASSERT_NE(hs[i], hs[i - 1]) << "duplicate CF phase seed at base=" << base;
|
||||
}
|
||||
}
|
||||
|
||||
// Low-discrepancy: for N CFs the phases over an interval are well spread --
|
||||
// no gap larger than ~2x the average (a hash would allow chance clustering).
|
||||
for (uint64_t base : {uint64_t{0}, uint64_t{0xdeadbeefcafef00dULL}}) {
|
||||
for (uint32_t N : {uint32_t{3}, uint32_t{8}, uint32_t{16}, uint32_t{40}}) {
|
||||
const uint64_t n = 100000;
|
||||
std::vector<uint64_t> phases;
|
||||
for (uint32_t id = 0; id < N; ++id) {
|
||||
phases.push_back(FastRange64(Phaser::CfPhaseSeedHash(base, id), n));
|
||||
}
|
||||
std::sort(phases.begin(), phases.end());
|
||||
uint64_t max_gap = n - phases.back() + phases.front(); // wrap-around gap
|
||||
for (size_t i = 1; i < phases.size(); ++i) {
|
||||
max_gap = std::max(max_gap, phases[i] - phases[i - 1]);
|
||||
}
|
||||
ASSERT_LE(max_gap, 2 * (n / N))
|
||||
<< "base=" << base << " N=" << N << " max_gap=" << max_gap;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionTest, PeriodicCompactionPhaseOptions) {
|
||||
// Verifies periodic_compaction_phase_recovery_percent is plumbed and
|
||||
// dynamically settable.
|
||||
Options options = CurrentOptions();
|
||||
options.periodic_compaction_phase_recovery_percent = 25;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_EQ(dbfull()->GetDBOptions().periodic_compaction_phase_recovery_percent,
|
||||
25);
|
||||
|
||||
ASSERT_OK(dbfull()->SetDBOptions(
|
||||
{{"periodic_compaction_phase_recovery_percent", "0"}}));
|
||||
ASSERT_EQ(dbfull()->GetDBOptions().periodic_compaction_phase_recovery_percent,
|
||||
0);
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionTest, LevelPeriodicCompactionOffpeak) {
|
||||
// This test simply checks if offpeak adjustment works in Leveled
|
||||
// Compactions. For testing offpeak periodic compactions in various
|
||||
@@ -5483,6 +5762,7 @@ TEST_F(DBCompactionTest, LevelPeriodicCompactionOffpeak) {
|
||||
Options options = CurrentOptions();
|
||||
options.ttl = 0;
|
||||
options.periodic_compaction_seconds = 5 * kSecondsPerDay; // 5 days
|
||||
options.periodic_compaction_phase_recovery_percent = 0;
|
||||
// In the case where all files are opened and doing DB restart
|
||||
// forcing the file creation time in manifest file to be 0 to
|
||||
// simulate the case of reading from an old version.
|
||||
@@ -5630,6 +5910,7 @@ TEST_F(DBCompactionTest, LevelPeriodicCompactionWithOldDB) {
|
||||
// Forward the clock by 2 days.
|
||||
env_->MockSleepForSeconds(2 * 24 * 60 * 60);
|
||||
options.periodic_compaction_seconds = 1 * 24 * 60 * 60; // 1 day
|
||||
options.periodic_compaction_phase_recovery_percent = 0;
|
||||
|
||||
Reopen(options);
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
@@ -5648,6 +5929,7 @@ TEST_F(DBCompactionTest, LevelPeriodicAndTtlCompaction) {
|
||||
Options options = CurrentOptions();
|
||||
options.ttl = 10 * 60 * 60; // 10 hours
|
||||
options.periodic_compaction_seconds = 48 * 60 * 60; // 2 days
|
||||
options.periodic_compaction_phase_recovery_percent = 0;
|
||||
options.max_open_files = -1; // needed for both periodic and ttl compactions
|
||||
env_->SetMockSleep();
|
||||
options.env = env_;
|
||||
@@ -5730,6 +6012,7 @@ TEST_F(DBCompactionTest, LevelTtlBooster) {
|
||||
Options options = CurrentOptions();
|
||||
options.ttl = 10 * 60 * 60; // 10 hours
|
||||
options.periodic_compaction_seconds = 480 * 60 * 60; // very long
|
||||
options.periodic_compaction_phase_recovery_percent = 0;
|
||||
options.level0_file_num_compaction_trigger = 2;
|
||||
options.max_bytes_for_level_base = 5 * uint64_t{kNumKeysPerFile * kValueSize};
|
||||
options.max_open_files = -1; // needed for both periodic and ttl compactions
|
||||
@@ -12279,6 +12562,7 @@ TEST_F(DBCompactionTest, PeriodicTask) {
|
||||
options.statistics = CreateDBStatistics();
|
||||
int kPeriodicCompactionSeconds = 7 * 24 * 60 * 60; // 1 week
|
||||
options.periodic_compaction_seconds = kPeriodicCompactionSeconds;
|
||||
options.periodic_compaction_phase_recovery_percent = 0;
|
||||
options.num_levels = 50;
|
||||
auto listener = std::make_shared<PeriodicCompactionListener>();
|
||||
options.listeners.push_back(listener);
|
||||
|
||||
@@ -415,7 +415,14 @@ Status DBImpl::GetLiveFilesStorageInfoImpl(
|
||||
|
||||
// Capture some final info before releasing mutex
|
||||
const uint64_t manifest_number = versions_->manifest_file_number();
|
||||
const uint64_t manifest_size = versions_->manifest_file_size();
|
||||
uint64_t manifest_size = versions_->manifest_file_size();
|
||||
if (cf_subset && !excluded_cf_ids->empty()) {
|
||||
Status manifest_s = versions_->GetManifestAppendBoundary(&manifest_size);
|
||||
if (!manifest_s.ok()) {
|
||||
mutex_.Unlock();
|
||||
return manifest_s;
|
||||
}
|
||||
}
|
||||
const uint64_t options_number = versions_->options_file_number();
|
||||
const uint64_t options_size = versions_->options_file_size_;
|
||||
const uint64_t min_log_num = MinLogNumberToKeep();
|
||||
|
||||
@@ -20,9 +20,11 @@
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/utilities/transaction_db.h"
|
||||
#include "table/block_based/block_based_table_builder.h"
|
||||
#include "table/format.h"
|
||||
#include "test_util/sync_point.h"
|
||||
#include "test_util/testutil.h"
|
||||
#include "util/cast_util.h"
|
||||
#include "util/defer.h"
|
||||
#include "util/mutexlock.h"
|
||||
#include "utilities/fault_injection_env.h"
|
||||
#include "utilities/fault_injection_fs.h"
|
||||
@@ -3723,6 +3725,108 @@ TEST_F(DBFlushTest, NonAtomicNormalFlushAbortWhenBGError) {
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
}
|
||||
|
||||
TEST_F(DBFlushTest, NonRecoveryFlushDoesNotStarveErrorRecovery) {
|
||||
constexpr uint64_t kRecoveryTimeoutMicros = 15 * 1000 * 1000;
|
||||
constexpr int kLateWriteCount = 32;
|
||||
|
||||
Options opts = CurrentOptions();
|
||||
opts.atomic_flush = true;
|
||||
opts.memtable_factory.reset(test::NewSpecialSkipListFactory(1));
|
||||
opts.max_write_buffer_number = 64;
|
||||
opts.max_background_flushes = 1;
|
||||
DestroyAndReopen(opts);
|
||||
env_->SetBackgroundThreads(1, Env::HIGH);
|
||||
|
||||
std::atomic_int flush_write_table_count{0};
|
||||
port::Mutex wait_mutex;
|
||||
port::CondVar wait_cv(&wait_mutex);
|
||||
bool recovery_wait_started = false;
|
||||
bool recovery_succeeded = false;
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"FlushJob::WriteLevel0Table:s", [&](void* s_ptr) {
|
||||
if (flush_write_table_count.fetch_add(1) == 0) {
|
||||
Status* s = static_cast<Status*>(s_ptr);
|
||||
IOStatus io_error = IOStatus::IOError("injected foobar");
|
||||
io_error.SetRetryable(true);
|
||||
*s = io_error;
|
||||
TEST_SYNC_POINT(
|
||||
"DBFlushTest::NonRecoveryFlushDoesNotStarveErrorRecovery:"
|
||||
"FirstFlushFailed");
|
||||
TEST_SYNC_POINT(
|
||||
"DBFlushTest::NonRecoveryFlushDoesNotStarveErrorRecovery:"
|
||||
"ContinueFirstFlush");
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::ResumeImpl:BeforeWaitForBackgroundWork", [&](void*) {
|
||||
MutexLock l(&wait_mutex);
|
||||
recovery_wait_started = true;
|
||||
wait_cv.SignalAll();
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"RecoverFromRetryableBGIOError:RecoverSuccess", [&](void*) {
|
||||
MutexLock l(&wait_mutex);
|
||||
recovery_succeeded = true;
|
||||
wait_cv.SignalAll();
|
||||
});
|
||||
SyncPoint::GetInstance()->LoadDependency(
|
||||
{{"DBFlushTest::NonRecoveryFlushDoesNotStarveErrorRecovery:"
|
||||
"FirstFlushFailed",
|
||||
"DBFlushTest::NonRecoveryFlushDoesNotStarveErrorRecovery:"
|
||||
"FailureObserved"},
|
||||
{"DBFlushTest::NonRecoveryFlushDoesNotStarveErrorRecovery:"
|
||||
"ReleaseFirstFlush",
|
||||
"DBFlushTest::NonRecoveryFlushDoesNotStarveErrorRecovery:"
|
||||
"ContinueFirstFlush"}});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
auto wait_until = [&](const auto& predicate) {
|
||||
const uint64_t abs_time =
|
||||
SystemClock::Default()->NowMicros() + kRecoveryTimeoutMicros;
|
||||
MutexLock l(&wait_mutex);
|
||||
while (!predicate()) {
|
||||
if (wait_cv.TimedWait(abs_time)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
ASSERT_OK(Put(Key(1), "val1"));
|
||||
ASSERT_OK(Put(Key(2), "val2"));
|
||||
TEST_SYNC_POINT(
|
||||
"DBFlushTest::NonRecoveryFlushDoesNotStarveErrorRecovery:"
|
||||
"FailureObserved");
|
||||
TEST_SYNC_POINT(
|
||||
"DBFlushTest::NonRecoveryFlushDoesNotStarveErrorRecovery:"
|
||||
"ReleaseFirstFlush");
|
||||
|
||||
const bool recovery_wait_reached =
|
||||
wait_until([&] { return recovery_wait_started; });
|
||||
Status late_write_status;
|
||||
if (recovery_wait_reached) {
|
||||
for (int key = 3; key < 3 + kLateWriteCount && late_write_status.ok();
|
||||
++key) {
|
||||
late_write_status = Put(Key(key), "val");
|
||||
}
|
||||
}
|
||||
const bool recovered = wait_until([&] { return recovery_succeeded; });
|
||||
|
||||
if (!recovered) {
|
||||
IOStatus cleanup_error = IOStatus::IOError("stop stuck error recovery");
|
||||
dbfull()->TEST_SetBGError(cleanup_error, BackgroundErrorReason::kFlush);
|
||||
dbfull()->TEST_SignalAllBgCv();
|
||||
}
|
||||
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
ASSERT_TRUE(recovery_wait_reached);
|
||||
ASSERT_OK(late_write_status);
|
||||
ASSERT_TRUE(recovered);
|
||||
}
|
||||
|
||||
TEST_F(DBFlushTest, DBStuckAfterAtomicFlushError) {
|
||||
// Test for a bug with atomic flush where DB can become stuck
|
||||
// after a flush error. A repro timeline:
|
||||
@@ -4007,6 +4111,112 @@ INSTANTIATE_TEST_CASE_P(
|
||||
// larger than the max allowed padding size
|
||||
testing::Values(4, kLowSpaceOverheadRatio)));
|
||||
|
||||
// super_block_alignment pads data blocks, giving the padded block's index entry
|
||||
// a non-contiguous handle. At format_version <= 7 the writer forces that index
|
||||
// entry to shared==0 (full key AND full value). format_version 8 instead uses
|
||||
// the index value-delta escape (see IndexValue::EncodeTo), keeping the key
|
||||
// delta-encoded and the restart cadence intact. This verifies that fv8 returns
|
||||
// byte-identical read results to fv7 for the same keys (also with
|
||||
// separate_key_value_in_data_block, the intersection that motivated the
|
||||
// escape), that padding -- and hence the escape -- is actually exercised, and
|
||||
// that fv8 index values are delta encoded.
|
||||
TEST_F(DBFlushTest, SuperBlockAlignmentValueDeltaEscape) {
|
||||
// Writing the unpublished draft format_version 8 requires this opt-in.
|
||||
SaveAndRestore<bool> allow_draft(&TEST_AllowUnsupportedFormatVersion(), true);
|
||||
|
||||
constexpr int kKeyCount = 5000;
|
||||
auto format_key = [](int i) {
|
||||
char buf[16];
|
||||
snprintf(buf, sizeof(buf), "%010d", i);
|
||||
return std::string(buf);
|
||||
};
|
||||
|
||||
// Deterministic values so fv7 and fv8 see identical input.
|
||||
Random rnd(test::RandomSeed());
|
||||
std::vector<std::string> values;
|
||||
values.reserve(kKeyCount);
|
||||
for (int i = 0; i < kKeyCount; ++i) {
|
||||
values.push_back(rnd.RandomString(static_cast<int>(rnd.Uniform(1000))));
|
||||
}
|
||||
|
||||
auto build_and_read = [&](uint32_t format_version, bool separate_kv,
|
||||
std::vector<std::string>* got_get,
|
||||
std::vector<std::string>* got_iter, int* pad_count,
|
||||
bool* index_delta_encoded) {
|
||||
Options options = CurrentOptions();
|
||||
options.compression = kNoCompression;
|
||||
BlockBasedTableOptions bbto;
|
||||
bbto.format_version = format_version;
|
||||
bbto.index_block_restart_interval = 3; // > 1 so value delta encoding is on
|
||||
bbto.super_block_alignment_size = 16 * 1024;
|
||||
bbto.super_block_alignment_space_overhead_ratio = 8;
|
||||
bbto.separate_key_value_in_data_block = separate_kv;
|
||||
options.table_factory.reset(NewBlockBasedTableFactory(bbto));
|
||||
DestroyAndReopen(options);
|
||||
|
||||
int local_pad = 0;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"BlockBasedTableBuilder::WriteMaybeCompressedBlock:SuperBlockAlignment",
|
||||
[&local_pad](void*) { local_pad++; });
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
for (int i = 0; i < kKeyCount; ++i) {
|
||||
ASSERT_OK(Put(format_key(i), values[i]));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
*pad_count = local_pad;
|
||||
|
||||
got_get->clear();
|
||||
for (int i = 0; i < kKeyCount; ++i) {
|
||||
PinnableSlice v;
|
||||
ASSERT_OK(Get(format_key(i), &v));
|
||||
got_get->push_back(v.ToString());
|
||||
}
|
||||
got_iter->clear();
|
||||
{
|
||||
std::unique_ptr<Iterator> it(db_->NewIterator(ReadOptions()));
|
||||
for (it->SeekToFirst(); it->Valid(); it->Next()) {
|
||||
ASSERT_OK(it->status());
|
||||
got_iter->push_back(it->value().ToString());
|
||||
}
|
||||
ASSERT_OK(it->status());
|
||||
}
|
||||
|
||||
// Confirm the index value encoding of the produced SST(s).
|
||||
TablePropertiesCollection props;
|
||||
ASSERT_OK(db_->GetPropertiesOfAllTables(&props));
|
||||
ASSERT_FALSE(props.empty());
|
||||
*index_delta_encoded = true;
|
||||
for (const auto& kv : props) {
|
||||
if (kv.second->index_value_is_delta_encoded == 0) {
|
||||
*index_delta_encoded = false;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (bool separate_kv : {false, true}) {
|
||||
SCOPED_TRACE("separate_kv=" + std::to_string(separate_kv));
|
||||
std::vector<std::string> get7, iter7, get8, iter8;
|
||||
int pad7 = 0, pad8 = 0;
|
||||
bool delta7 = false, delta8 = false;
|
||||
build_and_read(7, separate_kv, &get7, &iter7, &pad7, &delta7);
|
||||
build_and_read(/*format_version=*/8, separate_kv, &get8, &iter8, &pad8,
|
||||
&delta8);
|
||||
|
||||
// Reads must be identical across versions.
|
||||
ASSERT_EQ(get7, get8);
|
||||
ASSERT_EQ(iter7, iter8);
|
||||
ASSERT_EQ(static_cast<int>(iter8.size()), kKeyCount);
|
||||
// Padding (the non-contiguity that drives the escape) actually happened.
|
||||
EXPECT_GT(pad8, 0);
|
||||
// fv8 index values are delta encoded, so the escape path was exercised.
|
||||
EXPECT_TRUE(delta8);
|
||||
}
|
||||
}
|
||||
|
||||
// Test that when the table builder's io_status becomes bad during flush
|
||||
// (simulating write fault injection), BuildTable properly propagates the
|
||||
// builder's IO error instead of producing a misleading Corruption from the
|
||||
|
||||
@@ -204,6 +204,19 @@ class DBFollowerTest : public DBTestBase {
|
||||
return result;
|
||||
}
|
||||
|
||||
void VerifyFollowerBatchValue(const std::string& key,
|
||||
const std::string& expected) {
|
||||
std::vector<Iterator*> iterators;
|
||||
ASSERT_OK(follower()->NewIterators(
|
||||
ReadOptions(), {follower()->DefaultColumnFamily()}, &iterators));
|
||||
ASSERT_EQ(1U, iterators.size());
|
||||
std::unique_ptr<Iterator> iterator(iterators[0]);
|
||||
iterator->Seek(key);
|
||||
ASSERT_OK(iterator->status());
|
||||
ASSERT_TRUE(iterator->Valid());
|
||||
ASSERT_EQ(expected, iterator->value());
|
||||
}
|
||||
|
||||
DB* follower() { return follower_.get(); }
|
||||
DBFollowerTestFS* follower_fs() {
|
||||
return static_cast<DBFollowerTestFS*>(follower_env_->GetFileSystem().get());
|
||||
@@ -253,6 +266,32 @@ TEST_F(DBFollowerTest, Basic) {
|
||||
CheckDirs();
|
||||
}
|
||||
|
||||
TEST_F(DBFollowerTest, NewIteratorsRefreshesAfterCatchUp) {
|
||||
ASSERT_OK(Put("key", "v1"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
SyncPoint::GetInstance()->LoadDependency({
|
||||
{"DBImplFollower::TryCatchupWithLeader:Begin1", "Leader::Start"},
|
||||
{"Leader::Done", "DBImplFollower::TryCatchupWithLeader:Begin2"},
|
||||
{"DBImplFollower::TryCatchupWithLeader:End", "Follower::WaitForCatchup"},
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
ASSERT_OK(OpenAsFollower());
|
||||
VerifyFollowerBatchValue("key", "v1");
|
||||
|
||||
TEST_SYNC_POINT("Leader::Start");
|
||||
ASSERT_OK(Put("key", "v2"));
|
||||
ASSERT_OK(Flush());
|
||||
TEST_SYNC_POINT("Leader::Done");
|
||||
TEST_SYNC_POINT("Follower::WaitForCatchup");
|
||||
|
||||
VerifyFollowerBatchValue("key", "v2");
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearTrace();
|
||||
}
|
||||
|
||||
TEST_F(DBFollowerTest, Flush) {
|
||||
SyncPoint::GetInstance()->LoadDependency({
|
||||
{"DBImplFollower::TryCatchupWithLeader:Begin1", "Leader::Start"},
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include "db/blob/blob_fetcher.h"
|
||||
#include "db/db_impl/db_impl.h"
|
||||
#include "db/db_impl/db_impl_metadata.h"
|
||||
#include "db/version_set.h"
|
||||
#include "logging/logging.h"
|
||||
#include "table/get_context.h"
|
||||
@@ -34,6 +35,46 @@ size_t CompactedDBImpl::FindFile(const Slice& key) {
|
||||
files_.files);
|
||||
}
|
||||
|
||||
Status CompactedDBImpl::GetWithMetadata(const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableSlice* value,
|
||||
OutputMetadata* output_metadata) {
|
||||
std::string* timestamp = GetOutputTimestamp(output_metadata);
|
||||
bool* newer_version_present = GetOutputNewerVersionPresent(output_metadata);
|
||||
if (newer_version_present != nullptr) {
|
||||
*newer_version_present = false;
|
||||
}
|
||||
if (value == nullptr) {
|
||||
return Status::InvalidArgument(
|
||||
"Cannot call GetWithMetadata with a null value");
|
||||
}
|
||||
return Get(options, column_family, key, value, timestamp);
|
||||
}
|
||||
|
||||
void CompactedDBImpl::MultiGetWithMetadata(
|
||||
const ReadOptions& options, size_t num_keys,
|
||||
ColumnFamilyHandle* const* column_families, const Slice* keys,
|
||||
PinnableSlice* values, Status* statuses,
|
||||
MultiGetOutputMetadata* output_metadata, const bool sorted_input) {
|
||||
std::vector<std::string>* timestamps = GetOutputTimestamps(output_metadata);
|
||||
if (timestamps != nullptr) {
|
||||
timestamps->resize(num_keys);
|
||||
}
|
||||
std::vector<uint8_t>* newer_version_present =
|
||||
GetOutputNewerVersionPresent(output_metadata);
|
||||
if (newer_version_present != nullptr) {
|
||||
newer_version_present->assign(num_keys, false);
|
||||
}
|
||||
autovector<ColumnFamilyHandle*, MultiGetContext::MAX_BATCH_SIZE>
|
||||
stack_column_families;
|
||||
std::vector<ColumnFamilyHandle*> heap_column_families;
|
||||
ColumnFamilyHandle** mutable_column_families = MakeMutableCfHandles(
|
||||
column_families, num_keys, &stack_column_families, &heap_column_families);
|
||||
MultiGet(options, num_keys, mutable_column_families, keys, values,
|
||||
timestamps != nullptr ? timestamps->data() : nullptr, statuses,
|
||||
sorted_input);
|
||||
}
|
||||
|
||||
Status CompactedDBImpl::Init(const Options& options) {
|
||||
SuperVersionContext sv_context(/* create_superversion */ true);
|
||||
mutex_.Lock();
|
||||
|
||||
@@ -31,6 +31,12 @@ class CompactedDBImpl : public DBImpl {
|
||||
const Slice& key, PinnableSlice* value,
|
||||
std::string* timestamp);
|
||||
|
||||
using DB::GetWithMetadata;
|
||||
Status GetWithMetadata(const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
PinnableSlice* value,
|
||||
OutputMetadata* output_metadata) override;
|
||||
|
||||
using DB::MultiGet;
|
||||
// Note that CompactedDBImpl::MultiGet is not the optimized version of
|
||||
// MultiGet to use.
|
||||
@@ -42,6 +48,14 @@ class CompactedDBImpl : public DBImpl {
|
||||
std::string* timestamps, Status* statuses,
|
||||
const bool sorted_input);
|
||||
|
||||
using DB::MultiGetWithMetadata;
|
||||
void MultiGetWithMetadata(const ReadOptions& options, size_t num_keys,
|
||||
ColumnFamilyHandle* const* column_families,
|
||||
const Slice* keys, PinnableSlice* values,
|
||||
Status* statuses,
|
||||
MultiGetOutputMetadata* output_metadata,
|
||||
const bool sorted_input) override;
|
||||
|
||||
using DBImpl::Put;
|
||||
Status Put(const WriteOptions& /*options*/,
|
||||
ColumnFamilyHandle* /*column_family*/, const Slice& /*key*/,
|
||||
|
||||
@@ -6,10 +6,6 @@
|
||||
|
||||
#include "util/coro_utils.h"
|
||||
|
||||
#if defined(USE_COROUTINES) && defined(WITH_COROUTINES)
|
||||
#include "util/coro_stats_util.h"
|
||||
#endif // USE_COROUTINES && WITH_COROUTINES
|
||||
|
||||
#if defined(WITHOUT_COROUTINES) || \
|
||||
(defined(USE_COROUTINES) && defined(WITH_COROUTINES))
|
||||
|
||||
@@ -18,10 +14,6 @@ namespace ROCKSDB_NAMESPACE {
|
||||
DEFINE_SYNC_AND_ASYNC(Status, CompactedDBImpl::Get)
|
||||
(const ReadOptions& _read_options, ColumnFamilyHandle*, const Slice& key,
|
||||
PinnableSlice* value, std::string* timestamp) {
|
||||
#ifdef WITH_COROUTINES
|
||||
INSTALL_COROUTINE_STATS_CONTEXT_SCOPE(
|
||||
immutable_db_options_.fs->GetReadExecutor(), immutable_db_options_.env);
|
||||
#endif
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
_read_options.io_activity != Env::IOActivity::kGet) {
|
||||
CO_RETURN Status::InvalidArgument(
|
||||
@@ -110,10 +102,6 @@ DEFINE_SYNC_AND_ASYNC(void, CompactedDBImpl::MultiGet)
|
||||
ColumnFamilyHandle** /*column_families*/, const Slice* keys,
|
||||
PinnableSlice* values, std::string* timestamps, Status* statuses,
|
||||
const bool /*sorted_input*/) {
|
||||
#ifdef WITH_COROUTINES
|
||||
INSTALL_COROUTINE_STATS_CONTEXT_SCOPE(
|
||||
immutable_db_options_.fs->GetReadExecutor(), immutable_db_options_.env);
|
||||
#endif
|
||||
assert(user_comparator_);
|
||||
Status s;
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
|
||||
+651
-286
File diff suppressed because it is too large.
Load diff
+148
-15
@@ -290,12 +290,22 @@ class DBImpl : public DB
|
||||
const Slice& key, PinnableSlice* value,
|
||||
std::string* timestamp);
|
||||
|
||||
using DB::GetWithMetadata;
|
||||
Status GetWithMetadata(const ReadOptions& _read_options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
PinnableSlice* value,
|
||||
OutputMetadata* output_metadata) override;
|
||||
|
||||
using DB::GetEntity;
|
||||
Status GetEntity(const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
PinnableWideColumns* columns) override;
|
||||
Status GetEntity(const ReadOptions& options, const Slice& key,
|
||||
PinnableAttributeGroups* result) override;
|
||||
DECLARE_SYNC_AND_ASYNC_OVERRIDE(Status, GetEntity,
|
||||
const ReadOptions& _read_options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
const Slice& key,
|
||||
PinnableWideColumns* columns);
|
||||
DECLARE_SYNC_AND_ASYNC_OVERRIDE(Status, GetEntity,
|
||||
const ReadOptions& _read_options,
|
||||
const Slice& key,
|
||||
PinnableAttributeGroups* result);
|
||||
|
||||
Status GetEntityLazy(const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
@@ -332,6 +342,15 @@ class DBImpl : public DB
|
||||
std::string* timestamps, Status* statuses,
|
||||
const bool sorted_input = false);
|
||||
|
||||
using DB::MultiGetWithMetadata;
|
||||
void MultiGetWithMetadata(const ReadOptions& _read_options,
|
||||
const size_t num_keys,
|
||||
ColumnFamilyHandle* const* column_families,
|
||||
const Slice* keys, PinnableSlice* values,
|
||||
Status* statuses,
|
||||
MultiGetOutputMetadata* output_metadata,
|
||||
const bool sorted_input = false) override;
|
||||
|
||||
void MultiGetWithCallback(
|
||||
const ReadOptions& _read_options, ColumnFamilyHandle* column_family,
|
||||
ReadCallback* callback,
|
||||
@@ -419,6 +438,9 @@ class DBImpl : public DB
|
||||
Status NewIterators(const ReadOptions& _read_options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
std::vector<Iterator*>* iterators) override;
|
||||
Status NewIterators(const std::vector<ReadOptions>& read_options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
std::vector<Iterator*>* iterators) override;
|
||||
|
||||
using DB::NewMultiScan;
|
||||
std::unique_ptr<MultiScan> NewMultiScan(
|
||||
@@ -765,6 +787,7 @@ class DBImpl : public DB
|
||||
PinnableSlice* value = nullptr;
|
||||
PinnableWideColumns* columns = nullptr;
|
||||
std::string* timestamp = nullptr;
|
||||
bool* newer_version_present = nullptr;
|
||||
bool* value_found = nullptr;
|
||||
ReadCallback* callback = nullptr;
|
||||
bool* is_blob_index = nullptr;
|
||||
@@ -795,6 +818,18 @@ class DBImpl : public DB
|
||||
Cleanable* lazy_columns_pin = nullptr;
|
||||
const Version** lazy_columns_version = nullptr;
|
||||
const SameFileBlobReader** lazy_columns_same_file_reader = nullptr;
|
||||
|
||||
// Batched lazy read (MultiGetEntityLazy). When `lazy_columns_shared_sv` is
|
||||
// non-null, GetImpl reads against this externally-owned SuperVersion at
|
||||
// `lazy_columns_snapshot_seq` (the consistent per-batch sequence number)
|
||||
// instead of acquiring its own SuperVersion and deriving its own sequence
|
||||
// number, and it does NOT transfer a per-key pin -- the batch holds one
|
||||
// shared SuperVersion pin per column family. `lazy_columns_pin` must then
|
||||
// be null; `lazy_columns_version` / `lazy_columns_same_file_reader` are
|
||||
// still filled. (Single-key GetEntityLazy leaves this null and uses
|
||||
// `lazy_columns_pin`.)
|
||||
SuperVersion* lazy_columns_shared_sv = nullptr;
|
||||
SequenceNumber lazy_columns_snapshot_seq = kMaxSequenceNumber;
|
||||
};
|
||||
|
||||
DECLARE_SYNC_AND_ASYNC(Status, GetImpl, const ReadOptions& read_options,
|
||||
@@ -805,6 +840,11 @@ class DBImpl : public DB
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
PinnableSlice* value, std::string* timestamp);
|
||||
|
||||
Status GetImpl(const ReadOptions& read_options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
PinnableSlice* value, std::string* timestamp,
|
||||
bool* newer_version_present);
|
||||
|
||||
// Function that Get and KeyMayExist call with no_io true or false
|
||||
// Note: 'value_found' from KeyMayExist propagates here
|
||||
// This function is also called by GetMergeOperands
|
||||
@@ -827,6 +867,18 @@ class DBImpl : public DB
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
LazyWideColumns* result);
|
||||
|
||||
// Batched-read counterpart of GetEntityLazyImpl for one key of
|
||||
// MultiGetEntityLazy: performs the lazy point lookup against the batch's
|
||||
// shared `super_version` at the batch's consistent `snapshot_seq` (rather
|
||||
// than acquiring its own), and binds the entity's resolver to that Version
|
||||
// without taking a per-key pin (the batch owns one shared pin per column
|
||||
// family).
|
||||
Status GetEntityLazyForBatch(const ReadOptions& read_options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
SuperVersion* super_version,
|
||||
SequenceNumber snapshot_seq, const Slice& key,
|
||||
LazyWideColumns* result);
|
||||
|
||||
// If `snapshot` == kMaxSequenceNumber, set a recent one inside the file.
|
||||
ArenaWrappedDBIter* NewIteratorImpl(const ReadOptions& options,
|
||||
ColumnFamilyHandleImpl* cfh,
|
||||
@@ -1156,10 +1208,12 @@ class DBImpl : public DB
|
||||
}
|
||||
};
|
||||
|
||||
using RecoveredTransactionMap =
|
||||
std::unordered_map<std::string, RecoveredTransaction*>;
|
||||
|
||||
bool allow_2pc() const { return immutable_db_options_.allow_2pc; }
|
||||
|
||||
std::unordered_map<std::string, RecoveredTransaction*>
|
||||
recovered_transactions() {
|
||||
RecoveredTransactionMap recovered_transactions() {
|
||||
return recovered_transactions_;
|
||||
}
|
||||
|
||||
@@ -1190,16 +1244,24 @@ class DBImpl : public DB
|
||||
logs_with_prep_tracker_.MarkLogAsContainingPrepSection(log);
|
||||
}
|
||||
|
||||
void DeleteRecoveredTransaction(const std::string& name) {
|
||||
auto it = recovered_transactions_.find(name);
|
||||
// Deletes the recovered transaction `it` points to and returns the iterator
|
||||
// following it, like std::unordered_map::erase().
|
||||
RecoveredTransactionMap::iterator DeleteRecoveredTransaction(
|
||||
RecoveredTransactionMap::iterator it) {
|
||||
assert(it != recovered_transactions_.end());
|
||||
auto* trx = it->second;
|
||||
recovered_transactions_.erase(it);
|
||||
RecoveredTransactionMap::iterator next = recovered_transactions_.erase(it);
|
||||
for (const auto& info : trx->batches_) {
|
||||
logs_with_prep_tracker_.MarkLogAsHavingPrepSectionFlushed(
|
||||
info.second.log_number_);
|
||||
}
|
||||
delete trx;
|
||||
return next;
|
||||
}
|
||||
|
||||
void DeleteRecoveredTransaction(const std::string& name) {
|
||||
RecoveredTransactionMap::iterator it = recovered_transactions_.find(name);
|
||||
DeleteRecoveredTransaction(it);
|
||||
}
|
||||
|
||||
void DeleteAllRecoveredTransactions() {
|
||||
@@ -1318,6 +1380,8 @@ class DBImpl : public DB
|
||||
// Get the background error status
|
||||
Status TEST_GetBGError();
|
||||
|
||||
void TEST_SetBGError(const IOStatus& error, BackgroundErrorReason reason);
|
||||
|
||||
bool TEST_IsRecoveryInProgress();
|
||||
|
||||
Status TEST_ResumeImpl(DBRecoverContext context);
|
||||
@@ -1441,6 +1505,20 @@ class DBImpl : public DB
|
||||
// REQUIRES: DB mutex held or during open
|
||||
void EnsureSeqnoToTimeMapping(const MinAndMaxPreserveSeconds& preserve_secs);
|
||||
|
||||
// Computes the seqno->time preserve-window lower bound from
|
||||
// seqno_to_time_mapping_ and stores it on cfd's current version, so
|
||||
// bottommost file marking does not mark files whose largest seqno cannot be
|
||||
// zeroed yet (which would loop). No-op for column families without
|
||||
// preserve/preclude enabled. Returns true if the stored value changed, so
|
||||
// callers can recompute bottommost marking when the boundary moves.
|
||||
// Note: right after opening an existing DB, seqno_to_time_mapping_ may not be
|
||||
// fully reconstructed, so this bound can be imprecise until the first
|
||||
// periodic RecordSeqnoToTimeMapping. That is safe: CompactionJob folds this
|
||||
// same bound into kBottommostFiles compactions, so any marked file still
|
||||
// makes progress (never loops), and the periodic task self-corrects the
|
||||
// bound. REQUIRES: DB mutex held
|
||||
bool MaybeUpdatePreserveTimeMinSeqno(ColumnFamilyData* cfd);
|
||||
|
||||
// Only called during open
|
||||
void PrepopulateSeqnoToTimeMapping(
|
||||
const MinAndMaxPreserveSeconds& preserve_secs);
|
||||
@@ -1525,8 +1603,7 @@ class DBImpl : public DB
|
||||
FileSystemPtr fs_;
|
||||
MutableDBOptions mutable_db_options_;
|
||||
Statistics* stats_;
|
||||
std::unordered_map<std::string, RecoveredTransaction*>
|
||||
recovered_transactions_;
|
||||
RecoveredTransactionMap recovered_transactions_;
|
||||
std::unique_ptr<Tracer> tracer_;
|
||||
InstrumentedMutex trace_mutex_;
|
||||
BlockCacheTracer block_cache_tracer_;
|
||||
@@ -1627,6 +1704,15 @@ class DBImpl : public DB
|
||||
edit_lists_[i].emplace_back(new VersionEdit(edit));
|
||||
}
|
||||
|
||||
bool HasVersionEdits() const {
|
||||
for (const auto& edit_list : edit_lists_) {
|
||||
if (!edit_list.empty()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
std::unordered_map<uint32_t, uint32_t> map_; // cf_id to index;
|
||||
autovector<ColumnFamilyData*> cfds_;
|
||||
autovector<autovector<VersionEdit*>> edit_lists_;
|
||||
@@ -1996,7 +2082,7 @@ class DBImpl : public DB
|
||||
// LogAndApplyForRecovery should be called only once during recovery and it
|
||||
// should be called when RocksDB writes to a first new MANIFEST since this
|
||||
// recovery.
|
||||
Status LogAndApplyForRecovery(const RecoveryContext& recovery_ctx);
|
||||
Status LogAndApplyForRecovery(RecoveryContext& recovery_ctx);
|
||||
|
||||
// Schedule background work to open and validate SST files asynchronously.
|
||||
// Called when open_files_async is enabled.
|
||||
@@ -2774,7 +2860,11 @@ class DBImpl : public DB
|
||||
// equal to this per-column-family specified value, this flush request is
|
||||
// considered to have completed its work of flushing this column family.
|
||||
// After completing the work for all column families in this request, this
|
||||
// flush is considered complete.
|
||||
// flush is considered complete. EnqueuePendingFlush() acquires one
|
||||
// reference for each CFD when it successfully queues this request.
|
||||
// PopFirstFromFlushQueue() transfers responsibility for those references
|
||||
// to its caller, which must release each one with UnrefAndTryDelete() after
|
||||
// processing or discarding the request.
|
||||
std::unordered_map<ColumnFamilyData*, uint64_t>
|
||||
cfd_to_max_mem_id_to_persist;
|
||||
|
||||
@@ -2827,6 +2917,9 @@ class DBImpl : public DB
|
||||
Env::Priority thread_pri);
|
||||
void BackgroundCallFlush(Env::Priority thread_pri);
|
||||
void BackgroundCallPurge();
|
||||
// Recursively removes all children of the DB session temporary directory.
|
||||
// Best effort: errors are logged, not returned. REQUIRES: mutex_ not held.
|
||||
void CleanupSessionTmpDir();
|
||||
Status BackgroundCompaction(bool* madeProgress, JobContext* job_context,
|
||||
LogBuffer* log_buffer,
|
||||
PrepickedCompaction* prepicked_compaction,
|
||||
@@ -3092,7 +3185,9 @@ class DBImpl : public DB
|
||||
ColumnFamilyHandle** column_families,
|
||||
const Slice* keys, PinnableSlice* values,
|
||||
PinnableWideColumns* columns, std::string* timestamps,
|
||||
Status* statuses, bool sorted_input);
|
||||
Status* statuses,
|
||||
std::vector<uint8_t>* newer_version_present,
|
||||
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
|
||||
@@ -3776,9 +3871,47 @@ class GetWithTimestampReadCallback : public ReadCallback {
|
||||
public:
|
||||
explicit GetWithTimestampReadCallback(SequenceNumber seq)
|
||||
: ReadCallback(seq) {}
|
||||
// A null result is used by MultiGet, whose per-key results live in
|
||||
// KeyContext/GetContext.
|
||||
void EnableNewerVersionTracking(SequenceNumber read_snapshot_seq,
|
||||
SequenceNumber upper_bound_seq,
|
||||
bool* single_key_result) {
|
||||
metadata_read_bounds_.emplace(
|
||||
MetadataReadBounds{read_snapshot_seq, upper_bound_seq});
|
||||
newer_version_present_ = single_key_result;
|
||||
}
|
||||
const MetadataReadBounds* GetMetadataReadBounds() const override {
|
||||
return metadata_read_bounds_.has_value() ? &*metadata_read_bounds_
|
||||
: nullptr;
|
||||
}
|
||||
bool NeedToTrackNewerVersions(
|
||||
const bool* per_key_result = nullptr) const override {
|
||||
const bool* result =
|
||||
per_key_result != nullptr ? per_key_result : newer_version_present_;
|
||||
return metadata_read_bounds_.has_value() && result != nullptr && !*result;
|
||||
}
|
||||
bool IsVisibleFullCheck(SequenceNumber seq) override {
|
||||
return seq <= max_visible_seq_;
|
||||
}
|
||||
bool IsNewerVisibleForMetadataRead(SequenceNumber seq) override {
|
||||
const MetadataReadBounds* bounds = GetMetadataReadBounds();
|
||||
return bounds != nullptr && bounds->read_snapshot_seq < seq &&
|
||||
seq <= bounds->newer_version_upper_bound_seq;
|
||||
}
|
||||
void MaybeRecordNewerVersion(SequenceNumber seq, ValueType type,
|
||||
bool* per_key_result = nullptr) override {
|
||||
bool* result =
|
||||
per_key_result != nullptr ? per_key_result : newer_version_present_;
|
||||
if (result != nullptr && !*result &&
|
||||
(IsValueType(type) || type == kTypeRangeDeletion) &&
|
||||
IsNewerVisibleForMetadataRead(seq)) {
|
||||
*result = true;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::optional<MetadataReadBounds> metadata_read_bounds_;
|
||||
bool* newer_version_present_ = nullptr;
|
||||
};
|
||||
|
||||
Options SanitizeOptions(const std::string& db, const Options& src,
|
||||
|
||||
@@ -1339,7 +1339,7 @@ Status DBImpl::CompactRangeInternal(const CompactRangeOptions& options,
|
||||
SuperVersion* super_version = cfd->GetReferencedSuperVersion(this);
|
||||
s = cfd->RangesOverlapWithMemtables(
|
||||
{range}, super_version, immutable_db_options_.allow_data_in_errors,
|
||||
&flush_needed);
|
||||
&flush_needed, /*range_limit_exclusive=*/false);
|
||||
CleanupSuperVersion(super_version);
|
||||
}
|
||||
|
||||
@@ -2224,8 +2224,8 @@ Status DBImpl::ReFitLevel(ColumnFamilyData* cfd, int level, int target_level) {
|
||||
"Levels between source and target are not empty for a move.");
|
||||
}
|
||||
if (cfd->RangeOverlapWithCompaction(refit_level_smallest.user_key(),
|
||||
refit_level_largest.user_key(),
|
||||
l)) {
|
||||
refit_level_largest.user_key(), l,
|
||||
/*range_limit_exclusive=*/false)) {
|
||||
refitting_level_ = false;
|
||||
return Status::NotSupported(
|
||||
"Levels between source and target "
|
||||
@@ -2248,8 +2248,8 @@ Status DBImpl::ReFitLevel(ColumnFamilyData* cfd, int level, int target_level) {
|
||||
"Levels between source and target are not empty for a move.");
|
||||
}
|
||||
if (cfd->RangeOverlapWithCompaction(refit_level_smallest.user_key(),
|
||||
refit_level_largest.user_key(),
|
||||
l)) {
|
||||
refit_level_largest.user_key(), l,
|
||||
/*range_limit_exclusive=*/false)) {
|
||||
refitting_level_ = false;
|
||||
return Status::NotSupported(
|
||||
"Levels between source and target "
|
||||
@@ -3512,14 +3512,9 @@ void DBImpl::MaybeScheduleFlushOrCompaction() {
|
||||
return;
|
||||
} else if (error_handler_.IsBGWorkStopped() &&
|
||||
!error_handler_.IsRecoveryInProgress()) {
|
||||
// There has been a hard error and this call is not part of the recovery
|
||||
// sequence. Bail out here so we don't get into an endless loop of
|
||||
// scheduling BG work which will again call this function
|
||||
//
|
||||
// Note that a non-recovery flush can still be scheduled if
|
||||
// error_handler_.IsRecoveryInProgress() returns true. We rely on
|
||||
// BackgroundCallFlush() to check flush reason and drop non-recovery
|
||||
// flushes.
|
||||
// Recovery must be able to schedule its flushes while background work is
|
||||
// stopped. New non-recovery requests are rejected by EnqueuePendingFlush,
|
||||
// and BackgroundFlush drops any request queued before the error.
|
||||
return;
|
||||
} else if (shutting_down_.load(std::memory_order_acquire)) {
|
||||
// DB is being deleted; no more background compactions
|
||||
@@ -3817,6 +3812,10 @@ bool DBImpl::EnqueuePendingFlush(const FlushRequest& flush_req) {
|
||||
if (reject_new_background_jobs_) {
|
||||
return enqueued;
|
||||
}
|
||||
if (error_handler_.IsBGWorkStopped() &&
|
||||
!IsRecoveryFlush(flush_req.flush_reason)) {
|
||||
return enqueued;
|
||||
}
|
||||
if (flush_req.cfd_to_max_mem_id_to_persist.empty()) {
|
||||
return enqueued;
|
||||
}
|
||||
@@ -3973,8 +3972,8 @@ Status DBImpl::BackgroundFlush(bool* made_progress, JobContext* job_context,
|
||||
|
||||
Status status;
|
||||
*reason = FlushReason::kOthers;
|
||||
// If BG work is stopped due to an error, but a recovery is in progress,
|
||||
// that means this flush is part of the recovery. So allow it to go through
|
||||
// During recovery, allow flush workers to inspect the queue. Non-recovery
|
||||
// requests are dropped below.
|
||||
if (!error_handler_.IsBGWorkStopped()) {
|
||||
if (shutting_down_.load(std::memory_order_acquire)) {
|
||||
status = Status::ShutdownInProgress();
|
||||
@@ -3996,12 +3995,9 @@ Status DBImpl::BackgroundFlush(bool* made_progress, JobContext* job_context,
|
||||
FlushRequest flush_req = PopFirstFromFlushQueue();
|
||||
FlushReason flush_reason = flush_req.flush_reason;
|
||||
if (!error_handler_.GetBGError().ok() && error_handler_.IsBGWorkStopped() &&
|
||||
flush_reason != FlushReason::kErrorRecovery &&
|
||||
flush_reason != FlushReason::kErrorRecoveryRetryFlush) {
|
||||
// Stop non-recovery flush when bg work is stopped
|
||||
// Note that we drop the flush request here.
|
||||
// Recovery thread should schedule further flushes after bg error
|
||||
// is cleared.
|
||||
!IsRecoveryFlush(flush_reason)) {
|
||||
// A request queued before the error can reach a worker before recovery
|
||||
// clears the queue. Drop it; recovery rebuilds flush work for all CFs.
|
||||
status = error_handler_.GetBGError();
|
||||
assert(!status.ok());
|
||||
ROCKS_LOG_BUFFER(log_buffer,
|
||||
@@ -4764,7 +4760,8 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
|
||||
immutable_db_options_.stats, Histograms::SST_WRITE_MICROS,
|
||||
c->immutable_options().listeners,
|
||||
immutable_db_options_.file_checksum_gen_factory.get(),
|
||||
tmp_set.Contains(FileType::kTableFile), false));
|
||||
tmp_set.Contains(FileType::kTableFile),
|
||||
tmp_set.Contains(FileType::kTableFile)));
|
||||
}
|
||||
|
||||
ROCKS_LOG_BUFFER(
|
||||
@@ -5525,6 +5522,11 @@ void DBImpl::InstallSuperVersionAndScheduleWork(
|
||||
cfd->InstallSuperVersion(sv_context, &mutex_,
|
||||
std::move(new_seqno_to_time_mapping));
|
||||
|
||||
// Refresh the seqno->time preserve-window bound used to gate bottommost file
|
||||
// marking, so a file whose largest seqno is still within the preserve window
|
||||
// is not marked for a compaction that cannot zero it out (infinite loop).
|
||||
MaybeUpdatePreserveTimeMinSeqno(cfd);
|
||||
|
||||
// There may be a small data race here. The snapshot tricking bottommost
|
||||
// compaction may already be released here. But assuming there will always be
|
||||
// newer snapshot created and released frequently, the compaction will be
|
||||
|
||||
@@ -225,6 +225,12 @@ Status DBImpl::TEST_GetBGError() {
|
||||
return error_handler_.GetBGError();
|
||||
}
|
||||
|
||||
void DBImpl::TEST_SetBGError(const IOStatus& error,
|
||||
BackgroundErrorReason reason) {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
error_handler_.SetBGError(error, reason);
|
||||
}
|
||||
|
||||
bool DBImpl::TEST_IsRecoveryInProgress() {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
return error_handler_.IsRecoveryInProgress();
|
||||
|
||||
+20
-10
@@ -489,31 +489,33 @@ void DBImpl::DeleteObsoleteFileImpl(int job_id, const std::string& fname,
|
||||
&file_deletion_status);
|
||||
if (file_deletion_status.ok()) {
|
||||
ROCKS_LOG_DEBUG(immutable_db_options_.info_log,
|
||||
"[JOB %d] Delete %s type=%d #%" PRIu64 " -- %s\n", job_id,
|
||||
fname.c_str(), type, number,
|
||||
"[JOB %d] [db_session_id=%s] Delete %s type=%d #%" PRIu64
|
||||
" -- %s\n",
|
||||
job_id, db_session_id_.c_str(), fname.c_str(), type, number,
|
||||
file_deletion_status.ToString().c_str());
|
||||
} else if (env_->FileExists(fname).IsNotFound()) {
|
||||
ROCKS_LOG_INFO(
|
||||
immutable_db_options_.info_log,
|
||||
"[JOB %d] Tried to delete a non-existing file %s type=%d #%" PRIu64
|
||||
" -- %s\n",
|
||||
job_id, fname.c_str(), type, number,
|
||||
"[JOB %d] [db_session_id=%s] Tried to delete a non-existing file %s"
|
||||
" type=%d #%" PRIu64 " -- %s\n",
|
||||
job_id, db_session_id_.c_str(), fname.c_str(), type, number,
|
||||
file_deletion_status.ToString().c_str());
|
||||
} else {
|
||||
ROCKS_LOG_ERROR(immutable_db_options_.info_log,
|
||||
"[JOB %d] Failed to delete %s type=%d #%" PRIu64 " -- %s\n",
|
||||
job_id, fname.c_str(), type, number,
|
||||
"[JOB %d] [db_session_id=%s] Failed to delete %s type=%d"
|
||||
" #%" PRIu64 " -- %s\n",
|
||||
job_id, db_session_id_.c_str(), fname.c_str(), type, number,
|
||||
file_deletion_status.ToString().c_str());
|
||||
}
|
||||
if (type == kTableFile) {
|
||||
EventHelpers::LogAndNotifyTableFileDeletion(
|
||||
&event_logger_, job_id, number, fname, file_deletion_status, GetName(),
|
||||
immutable_db_options_.listeners);
|
||||
db_session_id_, immutable_db_options_.listeners);
|
||||
}
|
||||
if (type == kBlobFile) {
|
||||
EventHelpers::LogAndNotifyBlobFileDeletion(
|
||||
&event_logger_, immutable_db_options_.listeners, job_id, number, fname,
|
||||
file_deletion_status, GetName());
|
||||
file_deletion_status, GetName(), db_session_id_);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1192,6 +1194,7 @@ Status DBImpl::MaybeUpdateNextFileNumber(RecoveryContext* recovery_ctx) {
|
||||
const uint64_t prev_next_file_number = versions_->current_next_file_number();
|
||||
uint64_t largest_file_number = prev_next_file_number;
|
||||
bool on_disk_file_advanced_counter = false;
|
||||
bool manifest_relevant_file_advanced_counter = false;
|
||||
Status s;
|
||||
for (const auto& path : CollectAllDBPaths()) {
|
||||
std::vector<std::string> files;
|
||||
@@ -1213,6 +1216,9 @@ Status DBImpl::MaybeUpdateNextFileNumber(RecoveryContext* recovery_ctx) {
|
||||
// NewFileNumber() would collide with it.
|
||||
if (number >= prev_next_file_number) {
|
||||
on_disk_file_advanced_counter = true;
|
||||
if (type != kOptionsFile && type != kTempFile) {
|
||||
manifest_relevant_file_advanced_counter = true;
|
||||
}
|
||||
}
|
||||
largest_file_number = std::max(largest_file_number, number);
|
||||
if ((type == kTableFile || type == kBlobFile)) {
|
||||
@@ -1234,7 +1240,8 @@ Status DBImpl::MaybeUpdateNextFileNumber(RecoveryContext* recovery_ctx) {
|
||||
mutable_db_options_.optimize_manifest_for_recovery &&
|
||||
!immutable_db_options_.best_efforts_recovery;
|
||||
|
||||
if (!optimize_manifest_for_recovery || on_disk_file_advanced_counter) {
|
||||
if (!optimize_manifest_for_recovery ||
|
||||
manifest_relevant_file_advanced_counter) {
|
||||
if (largest_file_number >= prev_next_file_number) {
|
||||
versions_->next_file_number_.store(largest_file_number + 1);
|
||||
}
|
||||
@@ -1247,6 +1254,9 @@ Status DBImpl::MaybeUpdateNextFileNumber(RecoveryContext* recovery_ctx) {
|
||||
assert(default_cfd);
|
||||
recovery_ctx->UpdateVersionEdits(default_cfd, edit);
|
||||
} else {
|
||||
if (on_disk_file_advanced_counter) {
|
||||
versions_->next_file_number_.store(largest_file_number + 1);
|
||||
}
|
||||
TEST_SYNC_POINT("DBImpl::Recovery:SkippedNoopEdit:NextFileNumber");
|
||||
}
|
||||
return s;
|
||||
|
||||
@@ -131,21 +131,21 @@ Status DBImplFollower::TryCatchUpWithLeader() {
|
||||
for (auto cfd : cfds_changed) {
|
||||
if (cfd->mem()->GetEarliestSequenceNumber() <
|
||||
versions_->LastSequence()) {
|
||||
// Construct a new memtable with earliest sequence number set to the
|
||||
// last sequence number in the VersionSet. This matters when
|
||||
// DBImpl::MultiCFSnapshot tries to get consistent references
|
||||
// to super versions in a lock free manner, it checks the earliest
|
||||
// sequence number to detect if there was a change in version in
|
||||
// the meantime.
|
||||
MemTable* new_mem = cfd->ConstructNewMemtable(
|
||||
cfd->GetLatestMutableCFOptions(), versions_->LastSequence());
|
||||
cfd->mem()->SetNextLogNumber(cfd->GetLogNumber());
|
||||
cfd->mem()->ConstructFragmentedRangeTombstones();
|
||||
cfd->imm()->Add(cfd->mem(), &job_context.memtables_to_free);
|
||||
new_mem->Ref();
|
||||
cfd->SetMemtable(new_mem);
|
||||
// Seal the active memtable so that its replacement gets its earliest
|
||||
// sequence number set to the last sequence number in the VersionSet.
|
||||
// This matters when DBImpl::MultiCFSnapshot tries to get consistent
|
||||
// references to super versions in a lock free manner, it checks the
|
||||
// earliest sequence number to detect if there was a change in
|
||||
// version in the meantime.
|
||||
SealActiveMemtable(cfd, cfd->GetLogNumber(),
|
||||
versions_->LastSequence(), &job_context);
|
||||
}
|
||||
|
||||
// `cfd->GetLogNumber()` is a safe watermark here, unlike in
|
||||
// DBImplSecondary::TryCatchUpWithPrimary(): this catch-up path never
|
||||
// replays WALs, so the memtables it collects hold nothing that only a
|
||||
// not-yet-installed Version could make readable.
|
||||
//
|
||||
// This will check if the old memtable is still referenced
|
||||
cfd->imm()->RemoveOldMemTables(cfd->GetLogNumber(),
|
||||
&job_context.memtables_to_free);
|
||||
@@ -153,6 +153,7 @@ Status DBImplFollower::TryCatchUpWithLeader() {
|
||||
cfd->InstallSuperVersion(&sv_context, &mutex_);
|
||||
sv_context.NewSuperVersion();
|
||||
}
|
||||
PublishSecondaryReadView();
|
||||
}
|
||||
|
||||
for (auto& file : files_to_delete) {
|
||||
@@ -164,6 +165,7 @@ Status DBImplFollower::TryCatchUpWithLeader() {
|
||||
}
|
||||
}
|
||||
}
|
||||
CleanupRetiredSecondaryReadViews();
|
||||
job_context.Clean();
|
||||
|
||||
// Cleanup unused, obsolete files.
|
||||
@@ -322,9 +324,11 @@ Status DB::OpenAsFollower(
|
||||
sv_context.NewSuperVersion();
|
||||
cfd->InstallSuperVersion(&sv_context, &impl->mutex_);
|
||||
}
|
||||
impl->PublishSecondaryReadView();
|
||||
impl->MarkAsyncFileOpenNotNeeded();
|
||||
}
|
||||
impl->mutex_.Unlock();
|
||||
impl->CleanupRetiredSecondaryReadViews();
|
||||
sv_context.Clean();
|
||||
if (s.ok()) {
|
||||
dbptr->reset(impl);
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
// 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 <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "rocksdb/db.h"
|
||||
#include "table/multiget_context.h"
|
||||
#include "util/autovector.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
inline std::string* GetOutputTimestamp(OutputMetadata* output_metadata) {
|
||||
return output_metadata != nullptr && output_metadata->timestamp.has_value()
|
||||
? &*output_metadata->timestamp
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
inline std::vector<std::string>* GetOutputTimestamps(
|
||||
MultiGetOutputMetadata* output_metadata) {
|
||||
return output_metadata != nullptr && output_metadata->timestamps.has_value()
|
||||
? &*output_metadata->timestamps
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
inline bool* GetOutputNewerVersionPresent(OutputMetadata* output_metadata) {
|
||||
return output_metadata != nullptr &&
|
||||
output_metadata->newer_version_present.has_value()
|
||||
? &*output_metadata->newer_version_present
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
inline std::vector<uint8_t>* GetOutputNewerVersionPresent(
|
||||
MultiGetOutputMetadata* output_metadata) {
|
||||
return output_metadata != nullptr &&
|
||||
output_metadata->newer_version_present.has_value()
|
||||
? &*output_metadata->newer_version_present
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
inline ColumnFamilyHandle** MakeMutableCfHandles(
|
||||
ColumnFamilyHandle* const* column_families, size_t num_keys,
|
||||
autovector<ColumnFamilyHandle*, MultiGetContext::MAX_BATCH_SIZE>*
|
||||
stack_column_families,
|
||||
std::vector<ColumnFamilyHandle*>* heap_column_families) {
|
||||
ColumnFamilyHandle** mutable_column_families;
|
||||
if (num_keys <= MultiGetContext::MAX_BATCH_SIZE) {
|
||||
stack_column_families->resize(num_keys);
|
||||
mutable_column_families =
|
||||
num_keys == 0 ? nullptr : &(*stack_column_families)[0];
|
||||
} else {
|
||||
heap_column_families->resize(num_keys);
|
||||
mutable_column_families = heap_column_families->data();
|
||||
}
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
mutable_column_families[i] = column_families[i];
|
||||
}
|
||||
return mutable_column_families;
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -1034,7 +1034,7 @@ Status DBImpl::InitPersistStatsColumnFamily() {
|
||||
return s;
|
||||
}
|
||||
|
||||
Status DBImpl::LogAndApplyForRecovery(const RecoveryContext& recovery_ctx) {
|
||||
Status DBImpl::LogAndApplyForRecovery(RecoveryContext& recovery_ctx) {
|
||||
mutex_.AssertHeld();
|
||||
// descriptor_log_ is normally null after Recover, but when
|
||||
// reuse_manifest_on_open is set VersionSet::Recover may have already
|
||||
@@ -1044,9 +1044,21 @@ Status DBImpl::LogAndApplyForRecovery(const RecoveryContext& recovery_ctx) {
|
||||
const ReadOptions read_options(Env::IOActivity::kDBOpen);
|
||||
const WriteOptions write_options(Env::IOActivity::kDBOpen);
|
||||
|
||||
if (versions_->force_new_manifest_on_open_ &&
|
||||
!recovery_ctx.HasVersionEdits()) {
|
||||
VersionEdit edit;
|
||||
ColumnFamilyData* default_cfd =
|
||||
versions_->GetColumnFamilySet()->GetDefault();
|
||||
assert(default_cfd);
|
||||
recovery_ctx.UpdateVersionEdits(default_cfd, edit);
|
||||
}
|
||||
|
||||
Status s = versions_->LogAndApply(recovery_ctx.cfds_, read_options,
|
||||
write_options, recovery_ctx.edit_lists_,
|
||||
&mutex_, directories_.GetDbDir());
|
||||
if (s.ok()) {
|
||||
versions_->force_new_manifest_on_open_ = false;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -2699,6 +2711,12 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname,
|
||||
if (s.ok()) {
|
||||
s = impl->CreateArchivalDirectory();
|
||||
}
|
||||
if (s.ok() &&
|
||||
impl->immutable_db_options_.use_session_tmp_dir_for_remote_compaction) {
|
||||
// Create the session-scoped temporary directory before a CompactionService
|
||||
// can create any per-job directory under it.
|
||||
s = impl->env_->CreateDirIfMissing(SessionTmpDir(dbname));
|
||||
}
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -2913,6 +2931,13 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname,
|
||||
impl->immutable_db_options_.db_paths[0].path);
|
||||
}
|
||||
|
||||
if (s.ok() &&
|
||||
impl->immutable_db_options_.use_session_tmp_dir_for_remote_compaction) {
|
||||
// Finish deleting data from the previous DB session before this open can
|
||||
// schedule a new compaction in the directory.
|
||||
impl->CleanupSessionTmpDir();
|
||||
}
|
||||
|
||||
if (s.ok()) {
|
||||
// When the DB is stopped, it's possible that there are some .trash files
|
||||
// that were not deleted yet, when we open the DB we will find these .trash
|
||||
|
||||
+114
-43
@@ -11,6 +11,7 @@
|
||||
#include "db/blob/blob_fetcher.h"
|
||||
#include "db/db_impl/compacted_db_impl.h"
|
||||
#include "db/db_impl/db_impl.h"
|
||||
#include "db/db_impl/db_impl_metadata.h"
|
||||
#include "db/manifest_ops.h"
|
||||
#include "db/merge_context.h"
|
||||
#include "logging/logging.h"
|
||||
@@ -30,6 +31,30 @@ DBImplReadOnly::DBImplReadOnly(const DBOptions& db_options,
|
||||
|
||||
DBImplReadOnly::~DBImplReadOnly() = default;
|
||||
|
||||
void DBImplReadOnly::MultiGetWithMetadata(
|
||||
const ReadOptions& options, const size_t num_keys,
|
||||
ColumnFamilyHandle* const* column_families, const Slice* keys,
|
||||
PinnableSlice* values, Status* statuses,
|
||||
MultiGetOutputMetadata* output_metadata, const bool sorted_input) {
|
||||
std::vector<std::string>* timestamps = GetOutputTimestamps(output_metadata);
|
||||
if (timestamps != nullptr) {
|
||||
timestamps->resize(num_keys);
|
||||
}
|
||||
std::vector<uint8_t>* newer_version_present =
|
||||
GetOutputNewerVersionPresent(output_metadata);
|
||||
if (newer_version_present != nullptr) {
|
||||
newer_version_present->assign(num_keys, false);
|
||||
}
|
||||
autovector<ColumnFamilyHandle*, MultiGetContext::MAX_BATCH_SIZE>
|
||||
stack_column_families;
|
||||
std::vector<ColumnFamilyHandle*> heap_column_families;
|
||||
ColumnFamilyHandle** mutable_column_families = MakeMutableCfHandles(
|
||||
column_families, num_keys, &stack_column_families, &heap_column_families);
|
||||
DBImpl::MultiGet(options, num_keys, mutable_column_families, keys, values,
|
||||
timestamps != nullptr ? timestamps->data() : nullptr,
|
||||
statuses, sorted_input);
|
||||
}
|
||||
|
||||
Iterator* DBImplReadOnly::NewIterator(const ReadOptions& _read_options,
|
||||
ColumnFamilyHandle* column_family) {
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
@@ -82,65 +107,111 @@ Status DBImplReadOnly::NewIterators(
|
||||
const ReadOptions& read_options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
std::vector<Iterator*>* iterators) {
|
||||
if (read_options.timestamp) {
|
||||
for (auto* cf : column_families) {
|
||||
assert(cf);
|
||||
const Status s = FailIfTsMismatchCf(cf, *(read_options.timestamp));
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (auto* cf : column_families) {
|
||||
assert(cf);
|
||||
const Status s = FailIfCfHasTs(cf);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
return NewIterators(
|
||||
std::vector<ReadOptions>(column_families.size(), read_options),
|
||||
column_families, iterators);
|
||||
}
|
||||
|
||||
ReadCallback* read_callback = nullptr; // No read callback provided.
|
||||
Status DBImplReadOnly::NewIterators(
|
||||
const std::vector<ReadOptions>& read_options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
std::vector<Iterator*>* iterators) {
|
||||
if (read_options.size() != column_families.size()) {
|
||||
return Status::InvalidArgument(
|
||||
"read_options and column_families must have the same size");
|
||||
}
|
||||
if (iterators == nullptr) {
|
||||
return Status::InvalidArgument("iterators not allowed to be nullptr");
|
||||
}
|
||||
|
||||
if (column_families.empty()) {
|
||||
iterators->clear();
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
const Snapshot* const snapshot = read_options.front().snapshot;
|
||||
const bool tailing = read_options.front().tailing;
|
||||
std::vector<ReadOptions> normalized_read_options;
|
||||
normalized_read_options.reserve(read_options.size());
|
||||
for (size_t i = 0; i < read_options.size(); ++i) {
|
||||
const ReadOptions& options = read_options[i];
|
||||
if (options.io_activity != Env::IOActivity::kUnknown &&
|
||||
options.io_activity != Env::IOActivity::kDBIterator) {
|
||||
return Status::InvalidArgument(
|
||||
"Can only call NewIterators with `ReadOptions::io_activity` is "
|
||||
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kDBIterator`");
|
||||
}
|
||||
if (options.read_tier == kPersistedTier) {
|
||||
return Status::NotSupported(
|
||||
"ReadTier::kPersistedData is not yet supported in iterators.");
|
||||
}
|
||||
if (options.snapshot != snapshot) {
|
||||
return Status::InvalidArgument(
|
||||
"All ReadOptions must use the same snapshot");
|
||||
}
|
||||
if (options.tailing != tailing) {
|
||||
return Status::InvalidArgument(
|
||||
"All ReadOptions must use the same tailing setting");
|
||||
}
|
||||
|
||||
auto* cf = column_families[i];
|
||||
assert(cf);
|
||||
Status s;
|
||||
if (options.timestamp) {
|
||||
s = FailIfTsMismatchCf(cf, *(options.timestamp));
|
||||
} else {
|
||||
s = FailIfCfHasTs(cf);
|
||||
}
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
normalized_read_options.emplace_back(options);
|
||||
if (normalized_read_options.back().io_activity ==
|
||||
Env::IOActivity::kUnknown) {
|
||||
normalized_read_options.back().io_activity = Env::IOActivity::kDBIterator;
|
||||
}
|
||||
}
|
||||
|
||||
iterators->clear();
|
||||
iterators->reserve(column_families.size());
|
||||
SequenceNumber latest_snapshot = versions_->LastSequence();
|
||||
|
||||
SequenceNumber read_seq =
|
||||
read_options.snapshot != nullptr
|
||||
? static_cast<const SnapshotImpl*>(read_options.snapshot)->number_
|
||||
: latest_snapshot;
|
||||
|
||||
snapshot != nullptr ? static_cast<const SnapshotImpl*>(snapshot)->number_
|
||||
: versions_->LastSequence();
|
||||
autovector<std::tuple<ColumnFamilyHandleImpl*, SuperVersion*>> cfh_to_sv;
|
||||
for (auto* cf : column_families) {
|
||||
auto* cfh = static_cast_with_check<ColumnFamilyHandleImpl>(cf);
|
||||
cfh_to_sv.emplace_back(cfh, cfh->cfd()->GetSuperVersion()->Ref());
|
||||
}
|
||||
|
||||
const bool check_read_ts =
|
||||
read_options.timestamp && read_options.timestamp->size() > 0;
|
||||
for (auto cfh : column_families) {
|
||||
auto* cfd = static_cast_with_check<ColumnFamilyHandleImpl>(cfh)->cfd();
|
||||
auto* sv = cfd->GetSuperVersion()->Ref();
|
||||
cfh_to_sv.emplace_back(static_cast_with_check<ColumnFamilyHandleImpl>(cfh),
|
||||
sv);
|
||||
if (check_read_ts) {
|
||||
const auto unref_super_versions = [&]() {
|
||||
for (const auto& entry : cfh_to_sv) {
|
||||
std::get<1>(entry)->Unref();
|
||||
}
|
||||
};
|
||||
for (size_t i = 0; i < cfh_to_sv.size(); ++i) {
|
||||
const ReadOptions& options = normalized_read_options[i];
|
||||
if (options.timestamp && !options.timestamp->empty()) {
|
||||
auto* cfd = std::get<0>(cfh_to_sv[i])->cfd();
|
||||
auto* sv = std::get<1>(cfh_to_sv[i]);
|
||||
const Status s =
|
||||
FailIfReadCollapsedHistory(cfd, sv, *(read_options.timestamp));
|
||||
FailIfReadCollapsedHistory(cfd, sv, *(options.timestamp));
|
||||
if (!s.ok()) {
|
||||
for (auto prev_entry : cfh_to_sv) {
|
||||
std::get<1>(prev_entry)->Unref();
|
||||
}
|
||||
unref_super_versions();
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert(cfh_to_sv.size() == column_families.size());
|
||||
for (auto [cfh, sv] : cfh_to_sv) {
|
||||
auto* db_iter = NewArenaWrappedDbIterator(
|
||||
env_, read_options, cfh, sv, read_seq, read_callback, this,
|
||||
/*expose_blob_index=*/false, /*allow_refresh=*/false,
|
||||
/*allow_mark_memtable_for_flush=*/false);
|
||||
iterators->push_back(db_iter);
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < cfh_to_sv.size(); ++i) {
|
||||
auto* cfh = std::get<0>(cfh_to_sv[i]);
|
||||
auto* sv = std::get<1>(cfh_to_sv[i]);
|
||||
iterators->push_back(NewArenaWrappedDbIterator(
|
||||
env_, normalized_read_options[i], cfh, sv, read_seq,
|
||||
nullptr /* read_callback */, this, /* expose_blob_index */ false,
|
||||
/* allow_refresh */ false, /* allow_mark_memtable_for_flush */ false));
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,13 @@ class DBImplReadOnly : public DBImpl {
|
||||
const Slice& key,
|
||||
GetImplOptions& get_impl_options);
|
||||
|
||||
// TODO: Implement ReadOnly MultiGet?
|
||||
using DBImpl::MultiGetWithMetadata;
|
||||
void MultiGetWithMetadata(const ReadOptions& options, const size_t num_keys,
|
||||
ColumnFamilyHandle* const* column_families,
|
||||
const Slice* keys, PinnableSlice* values,
|
||||
Status* statuses,
|
||||
MultiGetOutputMetadata* output_metadata,
|
||||
const bool sorted_input = false) override;
|
||||
|
||||
using DBImpl::NewIterator;
|
||||
Iterator* NewIterator(const ReadOptions& _read_options,
|
||||
@@ -37,6 +43,9 @@ class DBImplReadOnly : public DBImpl {
|
||||
Status NewIterators(const ReadOptions& options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
std::vector<Iterator*>* iterators) override;
|
||||
Status NewIterators(const std::vector<ReadOptions>& read_options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
std::vector<Iterator*>* iterators) override;
|
||||
|
||||
using DBImpl::Put;
|
||||
Status Put(const WriteOptions& /*options*/,
|
||||
|
||||
@@ -20,6 +20,10 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImplReadOnly::GetImpl)
|
||||
get_impl_options.merge_operands != nullptr);
|
||||
assert(get_impl_options.column_family);
|
||||
|
||||
// This instance cannot observe writes after one of its snapshots, so the
|
||||
// false result initialized by GetWithMetadata is definitive.
|
||||
get_impl_options.newer_version_present = nullptr;
|
||||
|
||||
Status s;
|
||||
|
||||
if (read_options.timestamp) {
|
||||
@@ -49,7 +53,13 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImplReadOnly::GetImpl)
|
||||
|
||||
const Comparator* ucmp = get_impl_options.column_family->GetComparator();
|
||||
assert(ucmp);
|
||||
SequenceNumber snapshot = versions_->LastSequence();
|
||||
// For a batched lazy read (MultiGetEntityLazy), use the SuperVersion and
|
||||
// consistent sequence number the batch already fixed (see
|
||||
// GetImplOptions::lazy_columns_shared_sv); otherwise this read-only
|
||||
// instance's stable current SuperVersion and last sequence.
|
||||
SequenceNumber snapshot = get_impl_options.lazy_columns_shared_sv != nullptr
|
||||
? get_impl_options.lazy_columns_snapshot_seq
|
||||
: versions_->LastSequence();
|
||||
GetWithTimestampReadCallback read_cb(snapshot);
|
||||
auto cfh = static_cast_with_check<ColumnFamilyHandleImpl>(
|
||||
get_impl_options.column_family);
|
||||
@@ -77,7 +87,10 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImplReadOnly::GetImpl)
|
||||
|
||||
// In read-only mode Get(), no super version operation is needed (i.e.
|
||||
// GetAndRefSuperVersion and ReturnAndCleanupSuperVersion)
|
||||
SuperVersion* super_version = cfd->GetSuperVersion();
|
||||
SuperVersion* super_version =
|
||||
get_impl_options.lazy_columns_shared_sv != nullptr
|
||||
? get_impl_options.lazy_columns_shared_sv
|
||||
: cfd->GetSuperVersion();
|
||||
if (read_options.timestamp && read_options.timestamp->size() > 0) {
|
||||
s = FailIfReadCollapsedHistory(cfd, super_version,
|
||||
*(read_options.timestamp));
|
||||
@@ -157,15 +170,19 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImplReadOnly::GetImpl)
|
||||
PERF_COUNTER_ADD(get_read_bytes, size);
|
||||
}
|
||||
}
|
||||
if (get_impl_options.lazy_columns_version != nullptr && s.ok()) {
|
||||
// Lazy result (GetEntityLazy / MultiGetEntityLazy): hand back the Version
|
||||
// the entity's blob references must be resolved against (both the
|
||||
// single-key and batched paths).
|
||||
*get_impl_options.lazy_columns_version = super_version->current;
|
||||
}
|
||||
if (get_impl_options.lazy_columns_pin != nullptr && s.ok()) {
|
||||
// Lazy result (GetEntityLazy): hand back the Version the entity's blob
|
||||
// references must be resolved against, and transfer a SuperVersion pin into
|
||||
// the result so it -- and deferred blob reads -- stay valid after this call
|
||||
// (as an iterator's pin does). Read-only borrows the current SuperVersion
|
||||
// without a reference, so the pin takes its own.
|
||||
if (get_impl_options.lazy_columns_version != nullptr) {
|
||||
*get_impl_options.lazy_columns_version = super_version->current;
|
||||
}
|
||||
// Single-key GetEntityLazy: transfer a SuperVersion pin into the result so
|
||||
// it -- and deferred blob reads -- stay valid after this call (as an
|
||||
// iterator's pin does). Read-only borrows the current SuperVersion without
|
||||
// a reference, so the pin takes its own. The batched path passes no pin (it
|
||||
// uses lazy_columns_shared_sv); the batch holds one shared pin per column
|
||||
// family instead.
|
||||
TransferSuperVersionPin(super_version, get_impl_options.lazy_columns_pin);
|
||||
}
|
||||
CO_RETURN s;
|
||||
|
||||
+838
-122
File diff suppressed because it is too large.
Load diff
@@ -5,11 +5,19 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <condition_variable>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "db/db_impl/db_impl.h"
|
||||
#include "logging/logging.h"
|
||||
#include "port/port.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
@@ -101,6 +109,14 @@ class DBImplSecondary : public DBImpl {
|
||||
const Slice& key,
|
||||
GetImplOptions& get_impl_options);
|
||||
|
||||
using DBImpl::MultiGetWithMetadata;
|
||||
void MultiGetWithMetadata(const ReadOptions& options, const size_t num_keys,
|
||||
ColumnFamilyHandle* const* column_families,
|
||||
const Slice* keys, PinnableSlice* values,
|
||||
Status* statuses,
|
||||
MultiGetOutputMetadata* output_metadata,
|
||||
const bool sorted_input = false) override;
|
||||
|
||||
using DBImpl::NewIterator;
|
||||
// Operations on the created iterators can return IOError due to files being
|
||||
// deleted by the primary. To avoid IOError in this case, application can
|
||||
@@ -124,6 +140,9 @@ class DBImplSecondary : public DBImpl {
|
||||
Status NewIterators(const ReadOptions& _read_options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
std::vector<Iterator*>* iterators) override;
|
||||
Status NewIterators(const std::vector<ReadOptions>& read_options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
std::vector<Iterator*>* iterators) override;
|
||||
|
||||
using DBImpl::Put;
|
||||
Status Put(const WriteOptions& /*options*/,
|
||||
@@ -274,6 +293,8 @@ class DBImplSecondary : public DBImpl {
|
||||
#endif // NDEBUG
|
||||
|
||||
protected:
|
||||
Status CloseImpl() override;
|
||||
|
||||
Status FlushForGetLiveFiles(bool /*force_atomic_flush*/) override {
|
||||
// No-op for read-only DB
|
||||
return Status::OK();
|
||||
@@ -287,13 +308,76 @@ class DBImplSecondary : public DBImpl {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Seals `cfd`'s active memtable: adds it to the immutable memtable list and
|
||||
// puts a fresh, empty active memtable in its place. This is the counterpart
|
||||
// of DBImpl::SwitchMemtable(), which cannot be used here because there is no
|
||||
// writer queue and no WAL of this instance's own.
|
||||
//
|
||||
// `next_log_number` is recorded on the sealed memtable. A subsequent
|
||||
// MemTableList::RemoveOldMemTables(log_number) call considers the sealed
|
||||
// memtable collectible once `next_log_number <= log_number`, so pass a log
|
||||
// number above every WAL whose entries the memtable holds.
|
||||
// `new_mem_earliest_seq` becomes the replacement memtable's earliest
|
||||
// sequence number.
|
||||
//
|
||||
// REQUIRES: mutex_ held
|
||||
void SealActiveMemtable(ColumnFamilyData* cfd, uint64_t next_log_number,
|
||||
SequenceNumber new_mem_earliest_seq,
|
||||
JobContext* job_context);
|
||||
|
||||
std::unique_ptr<log::FragmentBufferedReader> manifest_reader_;
|
||||
std::unique_ptr<log::Reader::Reporter> manifest_reporter_;
|
||||
std::unique_ptr<Status> manifest_reader_status_;
|
||||
|
||||
// Publishes a view of every initialized, non-dropped column family while
|
||||
// mutex_ protects their SuperVersions. The previous view is retained for
|
||||
// cleanup outside mutex_ and outside reader threads. No-op once the view has
|
||||
// been torn down for close.
|
||||
// REQUIRES: mutex_ held
|
||||
void PublishSecondaryReadView();
|
||||
|
||||
// Reclaims retired views that have no readers. This must be called without
|
||||
// mutex_ because releasing a view's final SuperVersion can lock it.
|
||||
void CleanupRetiredSecondaryReadViews();
|
||||
|
||||
private:
|
||||
friend class DB;
|
||||
|
||||
struct SecondaryReadView {
|
||||
using SuperVersions = std::unordered_map<uint32_t, SuperVersion*>;
|
||||
|
||||
SecondaryReadView(DBImplSecondary* db_arg, SequenceNumber sequence_arg)
|
||||
: db(db_arg), sequence(sequence_arg) {}
|
||||
~SecondaryReadView();
|
||||
SecondaryReadView(const SecondaryReadView&) = delete;
|
||||
SecondaryReadView& operator=(const SecondaryReadView&) = delete;
|
||||
SecondaryReadView(SecondaryReadView&&) = delete;
|
||||
SecondaryReadView& operator=(SecondaryReadView&&) = delete;
|
||||
|
||||
DBImplSecondary* db;
|
||||
SequenceNumber sequence;
|
||||
SuperVersions super_versions;
|
||||
};
|
||||
|
||||
void RetireSecondaryReadView(
|
||||
std::shared_ptr<const SecondaryReadView> read_view);
|
||||
// REQUIRES: retired_secondary_read_views_mutex_ held
|
||||
bool HasReclaimableSecondaryReadView() const;
|
||||
// REQUIRES: retired_secondary_read_views_mutex_ held
|
||||
bool AllSecondaryReadViewsReclaimed() const;
|
||||
bool ReclaimRetiredSecondaryReadViews();
|
||||
void ReclaimRetiredSecondaryReadViewsLoop();
|
||||
void DrainRetiredSecondaryReadViews();
|
||||
void StopSecondaryReadViewReclaimer();
|
||||
void ReleaseSecondaryReadView(
|
||||
std::shared_ptr<const SecondaryReadView>* read_view);
|
||||
void ResetSecondaryReadView();
|
||||
|
||||
Status NewIteratorsImpl(
|
||||
const std::vector<ReadOptions>& read_options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
std::vector<Iterator*>* iterators);
|
||||
|
||||
// No copying allowed
|
||||
DBImplSecondary(const DBImplSecondary&);
|
||||
void operator=(const DBImplSecondary&);
|
||||
@@ -304,13 +388,63 @@ class DBImplSecondary : public DBImpl {
|
||||
std::unordered_set<ColumnFamilyData*>* cfds_changed,
|
||||
JobContext* job_context);
|
||||
Status FindNewLogNumbers(std::vector<uint64_t>* logs);
|
||||
// After manifest recovery, replay WALs and refresh log_readers_ if necessary
|
||||
// Replays WALs after manifest recovery, refreshes log_readers_ as needed, and
|
||||
// drops recovered transactions the primary resolved. Cleanup runs even when
|
||||
// `log_numbers` is empty.
|
||||
// REQUIRES: log_numbers are sorted in ascending order
|
||||
Status RecoverLogFiles(const std::vector<uint64_t>& log_numbers,
|
||||
SequenceNumber* next_sequence,
|
||||
std::unordered_set<ColumnFamilyData*>* cfds_changed,
|
||||
JobContext* job_context);
|
||||
|
||||
// Records `log_number` as the newest WAL whose entries `cf_id`'s active
|
||||
// memtable may hold, never lowering what is already recorded. Returns what
|
||||
// was recorded before this call, or nullopt if nothing was.
|
||||
//
|
||||
// REQUIRES: mutex_ held
|
||||
std::optional<uint64_t> RecordCurrentLog(uint32_t cf_id, uint64_t log_number);
|
||||
|
||||
// Seals the column family's active memtable if everything it holds is
|
||||
// already readable through the installed Version, so that a following
|
||||
// RemoveOldMemTables(installed_log_number) call drops it. Returns whether the
|
||||
// memtable was sealed; in the common case it is not, and this is a no-op.
|
||||
//
|
||||
// `installed_log_number` is the column family's log number as of the Version
|
||||
// most recently installed for it, from
|
||||
// ReactiveVersionSet::GetInstalledVersionLogNumber(). It gates the seal, and
|
||||
// the sealed memtable's next log number is at most that value.
|
||||
//
|
||||
// REQUIRES: mutex_ held
|
||||
// REQUIRES: installed_log_number <= cfd->GetLogNumber()
|
||||
// REQUIRES: cf_id_to_current_log_[cfd->GetID()] is at least as new as the
|
||||
// newest WAL whose entries the active memtable holds, i.e. any WAL replay for
|
||||
// this round has run. Too low a value drops a memtable the installed Version
|
||||
// does not cover; too high only retains it for longer. RecoverLogFiles()
|
||||
// records both before and after each insert to keep that true; see there.
|
||||
bool MaybeSealFullyFlushedActiveMemtable(ColumnFamilyData* cfd,
|
||||
uint64_t installed_log_number,
|
||||
JobContext* job_context);
|
||||
|
||||
// Warns while `installed_log_number` lags behind cfd->GetLogNumber() and the
|
||||
// column family is holding immutable memtables that RemoveOldMemTables()
|
||||
// cannot collect as a result. Nothing on a secondary flushes them, so their
|
||||
// number grows by one per WAL switch until the primary's flushed files become
|
||||
// readable or the instance is reopened.
|
||||
//
|
||||
// Warns only when what it would report has changed, since the condition can
|
||||
// persist across arbitrarily many catch-up calls.
|
||||
//
|
||||
// REQUIRES: mutex_ held
|
||||
void MaybeWarnAboutRetainedMemtables(ColumnFamilyData* cfd,
|
||||
uint64_t installed_log_number);
|
||||
|
||||
// Drops recovered transactions the primary resolved and flushed.
|
||||
//
|
||||
// REQUIRES: mutex_ held
|
||||
// REQUIRES: every WAL reader for the round opened successfully
|
||||
// REQUIRES: called before replaying records from the round
|
||||
void DeleteResolvedRecoveredTransactions();
|
||||
|
||||
// Run compaction without installation, the output files will be placed in the
|
||||
// secondary DB path. The LSM tree won't be changed, the secondary DB is still
|
||||
// in read-only mode.
|
||||
@@ -350,7 +484,8 @@ class DBImplSecondary : public DBImpl {
|
||||
};
|
||||
|
||||
Status InitializeCompactionWorkspace(
|
||||
bool allow_resumption, std::unique_ptr<FSDirectory>* output_dir,
|
||||
bool allow_resumption, bool resumption_requested,
|
||||
std::unique_ptr<FSDirectory>* output_dir,
|
||||
std::unique_ptr<log::Writer>* compaction_progress_writer);
|
||||
|
||||
Status PrepareCompactionProgressState();
|
||||
@@ -404,19 +539,49 @@ class DBImplSecondary : public DBImpl {
|
||||
// MANIFEST only. When recover_wal is true, WAL files are also replayed
|
||||
// (needed by DB::OpenAsSecondary). When false, WAL replay is skipped
|
||||
// (used by DB::OpenAndCompact which only needs LSM state).
|
||||
//
|
||||
// When trust_manifest_recovery is true (only DB::OpenAndCompact), MANIFEST
|
||||
// recovery trusts the manifest and does not stat/open SST or blob files, so
|
||||
// only the compaction's (deletion-protected) input files are opened later, on
|
||||
// demand. Must be false for normal secondary opens.
|
||||
static Status OpenAsSecondaryImpl(
|
||||
const DBOptions& db_options, const std::string& dbname,
|
||||
const std::string& secondary_path,
|
||||
const std::vector<ColumnFamilyDescriptor>& column_families,
|
||||
std::vector<ColumnFamilyHandle*>* handles, std::unique_ptr<DB>* dbptr,
|
||||
bool recover_wal);
|
||||
bool recover_wal, bool trust_manifest_recovery);
|
||||
|
||||
// Cache log readers for each log number, used for continue WAL replay
|
||||
// after recovery
|
||||
std::map<uint64_t, std::unique_ptr<LogReaderContainer>> log_readers_;
|
||||
|
||||
// Current WAL number replayed for each column family.
|
||||
std::unordered_map<ColumnFamilyData*, uint64_t> cfd_to_current_log_;
|
||||
// The newest WAL whose entries each column family's active memtable may
|
||||
// hold, keyed by column family id. An upper bound, not necessarily a WAL the
|
||||
// memtable took entries from. Column family ids are never reused, so an entry
|
||||
// left behind by a dropped column family can never be mistaken for a new
|
||||
// column family's.
|
||||
std::unordered_map<uint32_t, uint64_t> cf_id_to_current_log_;
|
||||
|
||||
// The installed Version's log number and retained memtable count last
|
||||
// reported by MaybeWarnAboutRetainedMemtables() for each column family.
|
||||
std::unordered_map<uint32_t, std::pair<uint64_t, int>>
|
||||
cf_id_to_retention_warning_;
|
||||
|
||||
// Mutated only under mutex_, so that load-then-store stays atomic between
|
||||
// publication and teardown. Readers load it without mutex_, so every access
|
||||
// must still go through AtomicSharedPtrLoad/Store.
|
||||
std::shared_ptr<const SecondaryReadView> secondary_read_view_;
|
||||
// Set by ResetSecondaryReadView() so a later catch-up cannot publish a view
|
||||
// that would outlive the drain close waits on.
|
||||
// Guarded by mutex_, which every mutation of secondary_read_view_ holds.
|
||||
bool secondary_read_view_closed_ = false;
|
||||
std::mutex retired_secondary_read_views_mutex_;
|
||||
std::condition_variable retired_secondary_read_views_cv_;
|
||||
std::vector<std::shared_ptr<const SecondaryReadView>>
|
||||
retired_secondary_read_views_;
|
||||
size_t secondary_read_view_reclaims_in_flight_ = 0;
|
||||
bool secondary_read_view_reclaimer_stopping_ = false;
|
||||
std::unique_ptr<port::Thread> secondary_read_view_reclaimer_;
|
||||
|
||||
const std::string secondary_path_;
|
||||
|
||||
|
||||
@@ -19,6 +19,14 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImplSecondary::GetImpl)
|
||||
get_impl_options.merge_operands != nullptr);
|
||||
assert(get_impl_options.column_family);
|
||||
|
||||
if (get_impl_options.newer_version_present != nullptr) {
|
||||
if (read_options.snapshot != nullptr) {
|
||||
CO_RETURN Status::NotSupported(
|
||||
"GetWithMetadata is not supported in secondary DB mode");
|
||||
}
|
||||
get_impl_options.newer_version_present = nullptr;
|
||||
}
|
||||
|
||||
Status s;
|
||||
|
||||
if (read_options.timestamp) {
|
||||
@@ -48,7 +56,12 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImplSecondary::GetImpl)
|
||||
|
||||
const Comparator* ucmp = get_impl_options.column_family->GetComparator();
|
||||
assert(ucmp);
|
||||
SequenceNumber snapshot = versions_->LastSequence();
|
||||
// For a batched lazy read (MultiGetEntityLazy) use the sequence number the
|
||||
// batch fixed (see GetImplOptions::lazy_columns_shared_sv); otherwise this
|
||||
// instance's last sequence.
|
||||
SequenceNumber snapshot = get_impl_options.lazy_columns_shared_sv != nullptr
|
||||
? get_impl_options.lazy_columns_snapshot_seq
|
||||
: versions_->LastSequence();
|
||||
GetWithTimestampReadCallback read_cb(snapshot);
|
||||
auto cfh = static_cast_with_check<ColumnFamilyHandleImpl>(
|
||||
get_impl_options.column_family);
|
||||
@@ -74,21 +87,32 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImplSecondary::GetImpl)
|
||||
}
|
||||
}
|
||||
|
||||
// Acquire SuperVersion
|
||||
// Acquire SuperVersion, or use the batch's shared one for a batched lazy read
|
||||
// (see GetImplOptions::lazy_columns_shared_sv), skipping the per-key
|
||||
// reference and its release.
|
||||
const bool own_super_version =
|
||||
get_impl_options.lazy_columns_shared_sv == nullptr;
|
||||
SuperVersion* super_version;
|
||||
if (own_super_version) {
|
||||
#if defined(WITH_COROUTINES)
|
||||
SuperVersion* super_version = cfd->GetReferencedSuperVersion(this);
|
||||
super_version = cfd->GetReferencedSuperVersion(this);
|
||||
#else
|
||||
SuperVersion* super_version = GetAndRefSuperVersion(cfd);
|
||||
super_version = GetAndRefSuperVersion(cfd);
|
||||
#endif
|
||||
} else {
|
||||
super_version = get_impl_options.lazy_columns_shared_sv;
|
||||
}
|
||||
if (read_options.timestamp && read_options.timestamp->size() > 0) {
|
||||
s = FailIfReadCollapsedHistory(cfd, super_version,
|
||||
*(read_options.timestamp));
|
||||
if (!s.ok()) {
|
||||
if (own_super_version) {
|
||||
#if defined(WITH_COROUTINES)
|
||||
CleanupSuperVersion(super_version);
|
||||
CleanupSuperVersion(super_version);
|
||||
#else
|
||||
ReturnAndCleanupSuperVersion(cfd, super_version);
|
||||
ReturnAndCleanupSuperVersion(cfd, super_version);
|
||||
#endif
|
||||
}
|
||||
CO_RETURN s;
|
||||
}
|
||||
}
|
||||
@@ -169,11 +193,13 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImplSecondary::GetImpl)
|
||||
}
|
||||
if (!s.ok() && !s.IsMergeInProgress() && !s.IsNotFound()) {
|
||||
assert(done);
|
||||
if (own_super_version) {
|
||||
#if defined(WITH_COROUTINES)
|
||||
CleanupSuperVersion(super_version);
|
||||
CleanupSuperVersion(super_version);
|
||||
#else
|
||||
ReturnAndCleanupSuperVersion(cfd, super_version);
|
||||
ReturnAndCleanupSuperVersion(cfd, super_version);
|
||||
#endif
|
||||
}
|
||||
CO_RETURN s;
|
||||
}
|
||||
if (!done) {
|
||||
@@ -191,21 +217,27 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImplSecondary::GetImpl)
|
||||
}
|
||||
{
|
||||
PERF_TIMER_GUARD(get_post_process_time);
|
||||
if (get_impl_options.lazy_columns_version != nullptr && s.ok()) {
|
||||
// Lazy result (GetEntityLazy / MultiGetEntityLazy): hand back the Version
|
||||
// the entity's blob references must be resolved against (both the
|
||||
// single-key and batched paths).
|
||||
*get_impl_options.lazy_columns_version = super_version->current;
|
||||
}
|
||||
if (get_impl_options.lazy_columns_pin != nullptr && s.ok()) {
|
||||
// Lazy result (GetEntityLazy): hand back the resolution Version and take
|
||||
// a SuperVersion pin for the result before the borrowed reference below
|
||||
// is released, so the result -- and deferred blob reads -- stay valid
|
||||
// after this call (as an iterator's pin does).
|
||||
if (get_impl_options.lazy_columns_version != nullptr) {
|
||||
*get_impl_options.lazy_columns_version = super_version->current;
|
||||
}
|
||||
// Single-key GetEntityLazy: take a SuperVersion pin for the result before
|
||||
// the borrowed reference below is released, so the result -- and deferred
|
||||
// blob reads -- stay valid after this call (as an iterator's pin does).
|
||||
// The batched path passes no pin (it uses lazy_columns_shared_sv); the
|
||||
// batch holds one shared pin per column family instead.
|
||||
TransferSuperVersionPin(super_version, get_impl_options.lazy_columns_pin);
|
||||
}
|
||||
if (own_super_version) {
|
||||
#if defined(WITH_COROUTINES)
|
||||
CleanupSuperVersion(super_version);
|
||||
CleanupSuperVersion(super_version);
|
||||
#else
|
||||
ReturnAndCleanupSuperVersion(cfd, super_version);
|
||||
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
|
||||
|
||||
@@ -6,10 +6,6 @@
|
||||
|
||||
#include "util/coro_utils.h"
|
||||
|
||||
#if defined(USE_COROUTINES) && defined(WITH_COROUTINES)
|
||||
#include "util/coro_stats_util.h"
|
||||
#endif // USE_COROUTINES && WITH_COROUTINES
|
||||
|
||||
#if defined(WITHOUT_COROUTINES) || \
|
||||
(defined(USE_COROUTINES) && defined(WITH_COROUTINES))
|
||||
|
||||
@@ -25,10 +21,6 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
DEFINE_SYNC_AND_ASYNC(Status, DBImpl::Get)
|
||||
(const ReadOptions& _read_options, ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableSlice* value, std::string* timestamp) {
|
||||
#ifdef WITH_COROUTINES
|
||||
INSTALL_COROUTINE_STATS_CONTEXT_SCOPE(
|
||||
immutable_db_options_.fs->GetReadExecutor(), immutable_db_options_.env);
|
||||
#endif
|
||||
assert(value != nullptr);
|
||||
value->Reset();
|
||||
|
||||
@@ -48,6 +40,106 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::Get)
|
||||
timestamp);
|
||||
}
|
||||
|
||||
DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetEntity)
|
||||
(const ReadOptions& _read_options, ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableWideColumns* columns) {
|
||||
if (!column_family) {
|
||||
CO_RETURN Status::InvalidArgument(
|
||||
"Cannot call GetEntity without a column family handle");
|
||||
}
|
||||
if (!columns) {
|
||||
CO_RETURN Status::InvalidArgument(
|
||||
"Cannot call GetEntity without a PinnableWideColumns object");
|
||||
}
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
_read_options.io_activity != Env::IOActivity::kGetEntity) {
|
||||
CO_RETURN Status::InvalidArgument(
|
||||
"Can only call GetEntity with `ReadOptions::io_activity` set to "
|
||||
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kGetEntity`");
|
||||
}
|
||||
ReadOptions read_options(_read_options);
|
||||
if (read_options.io_activity == Env::IOActivity::kUnknown) {
|
||||
read_options.io_activity = Env::IOActivity::kGetEntity;
|
||||
}
|
||||
columns->Reset();
|
||||
|
||||
GetImplOptions get_impl_options;
|
||||
get_impl_options.column_family = column_family;
|
||||
get_impl_options.columns = columns;
|
||||
|
||||
CO_RETURN CO_AWAIT(GetImpl, read_options, key, get_impl_options);
|
||||
}
|
||||
|
||||
DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetEntity)
|
||||
(const ReadOptions& _read_options, const Slice& key,
|
||||
PinnableAttributeGroups* result) {
|
||||
if (!result) {
|
||||
CO_RETURN Status::InvalidArgument(
|
||||
"Cannot call GetEntity without PinnableAttributeGroups object");
|
||||
}
|
||||
Status s;
|
||||
const size_t num_column_families = result->size();
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
_read_options.io_activity != Env::IOActivity::kGetEntity) {
|
||||
s = Status::InvalidArgument(
|
||||
"Can only call GetEntity with `ReadOptions::io_activity` set to "
|
||||
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kGetEntity`");
|
||||
for (size_t i = 0; i < num_column_families; ++i) {
|
||||
(*result)[i].SetStatus(s);
|
||||
}
|
||||
CO_RETURN s;
|
||||
}
|
||||
// return early if no CF was passed in
|
||||
if (num_column_families == 0) {
|
||||
CO_RETURN s;
|
||||
}
|
||||
ReadOptions read_options(_read_options);
|
||||
if (read_options.io_activity == Env::IOActivity::kUnknown) {
|
||||
read_options.io_activity = Env::IOActivity::kGetEntity;
|
||||
}
|
||||
std::vector<Slice> keys;
|
||||
std::vector<ColumnFamilyHandle*> column_families;
|
||||
for (size_t i = 0; i < num_column_families; ++i) {
|
||||
// If any of the CFH is null, break early since the entire query will fail
|
||||
if (!(*result)[i].column_family()) {
|
||||
s = Status::InvalidArgument(
|
||||
"DB failed to query because one or more group(s) have null column "
|
||||
"family handle");
|
||||
(*result)[i].SetStatus(
|
||||
Status::InvalidArgument("Column family handle cannot be null"));
|
||||
break;
|
||||
}
|
||||
// Adding the same key slice for different CFs
|
||||
keys.emplace_back(key);
|
||||
column_families.emplace_back((*result)[i].column_family());
|
||||
}
|
||||
if (!s.ok()) {
|
||||
for (size_t i = 0; i < num_column_families; ++i) {
|
||||
if ((*result)[i].status().ok()) {
|
||||
(*result)[i].SetStatus(
|
||||
Status::Incomplete("DB not queried due to invalid argument(s) in "
|
||||
"one or more of the attribute groups"));
|
||||
}
|
||||
}
|
||||
CO_RETURN s;
|
||||
}
|
||||
std::vector<PinnableWideColumns> columns(num_column_families);
|
||||
std::vector<Status> statuses(num_column_families);
|
||||
CO_AWAIT(MultiGetCommon, read_options, num_column_families,
|
||||
column_families.data(), keys.data(),
|
||||
/* values */ nullptr, columns.data(),
|
||||
/* timestamps */ nullptr, statuses.data(),
|
||||
/* newer_version_present */ nullptr,
|
||||
/* sorted_input */ false);
|
||||
// Set results
|
||||
for (size_t i = 0; i < num_column_families; ++i) {
|
||||
(*result)[i].Reset();
|
||||
(*result)[i].SetStatus(statuses[i]);
|
||||
(*result)[i].SetColumns(std::move(columns[i]));
|
||||
}
|
||||
CO_RETURN s;
|
||||
}
|
||||
|
||||
DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
(const ReadOptions& read_options, ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableSlice* value, std::string* timestamp) {
|
||||
@@ -69,6 +161,13 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
|
||||
assert(get_impl_options.column_family);
|
||||
|
||||
if (get_impl_options.newer_version_present != nullptr &&
|
||||
read_options.snapshot != nullptr &&
|
||||
get_impl_options.callback != nullptr) {
|
||||
CO_RETURN Status::NotSupported(
|
||||
"Newer-version metadata is not supported with a read callback");
|
||||
}
|
||||
|
||||
if (read_options.timestamp) {
|
||||
const Status s = FailIfTsMismatchCf(get_impl_options.column_family,
|
||||
*(read_options.timestamp));
|
||||
@@ -118,13 +217,33 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
}
|
||||
}
|
||||
|
||||
// Acquire SuperVersion
|
||||
// Acquire SuperVersion (or, for a batched lazy read, use the shared one the
|
||||
// batch already referenced -- see GetImplOptions::lazy_columns_shared_sv).
|
||||
const bool own_super_version =
|
||||
get_impl_options.lazy_columns_shared_sv == nullptr;
|
||||
SuperVersion* sv;
|
||||
#if defined(WITH_COROUTINES)
|
||||
SuperVersion* sv = cfd->GetReferencedSuperVersion(this);
|
||||
Defer sv_cleanup([&] { CleanupSuperVersion(sv); });
|
||||
if (own_super_version) {
|
||||
sv = cfd->GetReferencedSuperVersion(this);
|
||||
} else {
|
||||
sv = get_impl_options.lazy_columns_shared_sv;
|
||||
}
|
||||
Defer sv_cleanup([&] {
|
||||
if (own_super_version) {
|
||||
CleanupSuperVersion(sv);
|
||||
}
|
||||
});
|
||||
#else
|
||||
SuperVersion* sv = GetAndRefSuperVersion(cfd);
|
||||
Defer sv_cleanup([&] { ReturnAndCleanupSuperVersion(cfd, sv); });
|
||||
if (own_super_version) {
|
||||
sv = GetAndRefSuperVersion(cfd);
|
||||
} else {
|
||||
sv = get_impl_options.lazy_columns_shared_sv;
|
||||
}
|
||||
Defer sv_cleanup([&] {
|
||||
if (own_super_version) {
|
||||
ReturnAndCleanupSuperVersion(cfd, sv);
|
||||
}
|
||||
});
|
||||
#endif
|
||||
if (read_options.timestamp && read_options.timestamp->size() > 0) {
|
||||
const Status s =
|
||||
@@ -139,7 +258,11 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
TEST_SYNC_POINT("DBImpl::GetImpl:2");
|
||||
|
||||
SequenceNumber snapshot;
|
||||
if (read_options.snapshot != nullptr) {
|
||||
if (!own_super_version) {
|
||||
// Batched lazy read: use the batch's consistent sequence number (derived
|
||||
// once, with the shared SuperVersion, by MultiCFSnapshot).
|
||||
snapshot = get_impl_options.lazy_columns_snapshot_seq;
|
||||
} else 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();
|
||||
@@ -173,6 +296,17 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
snapshot = get_impl_options.callback->max_visible_seq();
|
||||
}
|
||||
}
|
||||
SequenceNumber lookup_snapshot = snapshot;
|
||||
const bool newer_version_present_requested =
|
||||
get_impl_options.newer_version_present != nullptr &&
|
||||
read_options.snapshot != nullptr;
|
||||
const SequenceNumber newer_version_upper_bound_seq =
|
||||
newer_version_present_requested ? GetLastPublishedSequence() : snapshot;
|
||||
const bool track_newer_versions = newer_version_present_requested &&
|
||||
snapshot < newer_version_upper_bound_seq;
|
||||
if (track_newer_versions) {
|
||||
lookup_snapshot = newer_version_upper_bound_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
|
||||
@@ -184,6 +318,13 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
.callback); // timestamp with callback is not supported
|
||||
read_cb.Refresh(snapshot);
|
||||
get_impl_options.callback = &read_cb;
|
||||
} else if (track_newer_versions && get_impl_options.callback == nullptr) {
|
||||
read_cb.Refresh(snapshot);
|
||||
get_impl_options.callback = &read_cb;
|
||||
}
|
||||
if (track_newer_versions) {
|
||||
read_cb.EnableNewerVersionTracking(snapshot, newer_version_upper_bound_seq,
|
||||
get_impl_options.newer_version_present);
|
||||
}
|
||||
TEST_SYNC_POINT("DBImpl::GetImpl:3");
|
||||
TEST_SYNC_POINT("DBImpl::GetImpl:4");
|
||||
@@ -198,7 +339,7 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
// 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);
|
||||
LookupKey lkey(key, lookup_snapshot, read_options.timestamp);
|
||||
PERF_TIMER_STOP(get_snapshot_time);
|
||||
|
||||
bool skip_memtable = (read_options.read_tier == kPersistedTier &&
|
||||
@@ -403,17 +544,21 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
PERF_COUNTER_ADD(get_read_bytes, size);
|
||||
}
|
||||
|
||||
if (get_impl_options.lazy_columns_version != nullptr && s.ok()) {
|
||||
// Lazy result (GetEntityLazy / MultiGetEntityLazy): the entity's blob
|
||||
// references were left unresolved. Hand back the Version they must be
|
||||
// resolved against (for both the single-key and batched paths).
|
||||
*get_impl_options.lazy_columns_version = sv->current;
|
||||
}
|
||||
if (get_impl_options.lazy_columns_pin != nullptr && s.ok()) {
|
||||
// Lazy result (GetEntityLazy): the entity's blob references were left
|
||||
// unresolved. Hand back the Version they must be resolved against and
|
||||
// transfer an extra SuperVersion reference into the result's pin, so the
|
||||
// result -- and thus deferred blob reads -- stay valid after this call
|
||||
// returns (as an iterator's SuperVersion pin does). The reference is
|
||||
// released when the pin's cleanup runs (result destruction); the
|
||||
// borrowed reference for this call is dropped by the sv_cleanup Defer.
|
||||
if (get_impl_options.lazy_columns_version != nullptr) {
|
||||
*get_impl_options.lazy_columns_version = sv->current;
|
||||
}
|
||||
// Single-key GetEntityLazy: transfer an extra SuperVersion reference into
|
||||
// the result's pin, so the result -- and thus deferred blob reads -- stay
|
||||
// valid after this call returns (as an iterator's SuperVersion pin does).
|
||||
// The reference is released when the pin's cleanup runs (result
|
||||
// destruction); the borrowed reference for this call is dropped by the
|
||||
// sv_cleanup Defer. The batched path passes no pin here (it uses
|
||||
// lazy_columns_shared_sv) because the batch holds one shared pin per
|
||||
// column family instead.
|
||||
TransferSuperVersionPin(sv, get_impl_options.lazy_columns_pin);
|
||||
}
|
||||
RecordInHistogram(stats_, BYTES_PER_READ, size);
|
||||
@@ -635,7 +780,7 @@ 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) {
|
||||
std::vector<uint8_t>* newer_version_present, const bool sorted_input) {
|
||||
if (num_keys == 0) {
|
||||
CO_RETURN;
|
||||
}
|
||||
@@ -754,8 +899,26 @@ DEFINE_SYNC_AND_ASYNC(void, DBImpl::MultiGetCommon)
|
||||
|
||||
GetWithTimestampReadCallback timestamp_read_callback(0);
|
||||
ReadCallback* read_callback = nullptr;
|
||||
if (read_options.timestamp && read_options.timestamp->size() > 0) {
|
||||
SequenceNumber lookup_seqnum = consistent_seqnum;
|
||||
const bool newer_version_present_requested =
|
||||
newer_version_present != nullptr && read_options.snapshot != nullptr;
|
||||
const SequenceNumber newer_version_upper_bound_seq =
|
||||
newer_version_present_requested ? GetLastPublishedSequence()
|
||||
: consistent_seqnum;
|
||||
const bool track_newer_versions =
|
||||
newer_version_present_requested &&
|
||||
consistent_seqnum < newer_version_upper_bound_seq;
|
||||
if (track_newer_versions) {
|
||||
lookup_seqnum = newer_version_upper_bound_seq;
|
||||
}
|
||||
if ((read_options.timestamp && read_options.timestamp->size() > 0) ||
|
||||
track_newer_versions) {
|
||||
timestamp_read_callback.Refresh(consistent_seqnum);
|
||||
if (track_newer_versions) {
|
||||
timestamp_read_callback.EnableNewerVersionTracking(
|
||||
consistent_seqnum, newer_version_upper_bound_seq,
|
||||
/*single_key_result=*/nullptr);
|
||||
}
|
||||
read_callback = ×tamp_read_callback;
|
||||
}
|
||||
|
||||
@@ -766,8 +929,7 @@ DEFINE_SYNC_AND_ASYNC(void, DBImpl::MultiGetCommon)
|
||||
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);
|
||||
cf_sv_pair_iter->super_version, lookup_seqnum, read_callback);
|
||||
if (!s.ok()) {
|
||||
break;
|
||||
}
|
||||
@@ -795,16 +957,13 @@ DEFINE_SYNC_AND_ASYNC(void, DBImpl::MultiGetCommon)
|
||||
CleanupSuperVersion(cf_sv_pair.super_version);
|
||||
}
|
||||
}
|
||||
CopyNewerVersionPresent(key_context, newer_version_present);
|
||||
}
|
||||
|
||||
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) {
|
||||
#ifdef WITH_COROUTINES
|
||||
INSTALL_COROUTINE_STATS_CONTEXT_SCOPE(
|
||||
immutable_db_options_.fs->GetReadExecutor(), immutable_db_options_.env);
|
||||
#endif
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
_read_options.io_activity != Env::IOActivity::kMultiGet) {
|
||||
Status s = Status::InvalidArgument(
|
||||
@@ -823,7 +982,8 @@ DEFINE_SYNC_AND_ASYNC(void, DBImpl::MultiGet)
|
||||
}
|
||||
CO_AWAIT(MultiGetCommon, read_options, num_keys, column_families, keys,
|
||||
values,
|
||||
/* columns */ nullptr, timestamps, statuses, sorted_input);
|
||||
/* columns */ nullptr, timestamps, statuses,
|
||||
/* newer_version_present */ nullptr, sorted_input);
|
||||
CO_RETURN;
|
||||
}
|
||||
|
||||
|
||||
@@ -4308,6 +4308,109 @@ TEST_F(DBIteratorTest, IteratorsConsistentViewExplicitSnapshot) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBIteratorTest, NewIteratorsWithPerColumnFamilyReadOptions) {
|
||||
Options options = GetDefaultOptions();
|
||||
options.prefix_extractor.reset(NewFixedPrefixTransform(1));
|
||||
BlockBasedTableOptions table_options;
|
||||
table_options.filter_policy.reset(NewBloomFilterPolicy(10));
|
||||
table_options.whole_key_filtering = false;
|
||||
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
|
||||
CreateAndReopenWithCF({"cf_1"}, options);
|
||||
|
||||
ASSERT_OK(Put(0, "b1", "default_value"));
|
||||
ASSERT_OK(Put(1, "b1", "cf_1_value"));
|
||||
ASSERT_OK(Flush(0));
|
||||
ASSERT_OK(Flush(1));
|
||||
|
||||
ReadOptions prefix_read_options;
|
||||
prefix_read_options.total_order_seek = false;
|
||||
ReadOptions total_order_read_options;
|
||||
total_order_read_options.total_order_seek = true;
|
||||
std::vector<ReadOptions> read_options = {prefix_read_options,
|
||||
total_order_read_options};
|
||||
std::vector<Iterator*> iterators;
|
||||
ASSERT_OK(db_->NewIterators(read_options, handles_, &iterators));
|
||||
ASSERT_EQ(2U, iterators.size());
|
||||
|
||||
iterators[0]->Seek("a9");
|
||||
ASSERT_FALSE(iterators[0]->Valid());
|
||||
ASSERT_OK(iterators[0]->status());
|
||||
iterators[1]->Seek("a9");
|
||||
ASSERT_TRUE(iterators[1]->Valid());
|
||||
ASSERT_EQ("b1", iterators[1]->key());
|
||||
ASSERT_OK(iterators[1]->status());
|
||||
|
||||
for (auto* iterator : iterators) {
|
||||
delete iterator;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBIteratorTest, NewIteratorsPerColumnFamilyOptionsValidation) {
|
||||
Options options = GetDefaultOptions();
|
||||
CreateAndReopenWithCF({"cf_1"}, options);
|
||||
|
||||
std::vector<Iterator*> iterators;
|
||||
ASSERT_TRUE(
|
||||
db_->NewIterators(std::vector<ReadOptions>(1), handles_, &iterators)
|
||||
.IsInvalidArgument());
|
||||
|
||||
std::vector<ReadOptions> read_options(2);
|
||||
read_options[1].tailing = true;
|
||||
ASSERT_TRUE(db_->NewIterators(read_options, handles_, &iterators)
|
||||
.IsInvalidArgument());
|
||||
|
||||
read_options[1].tailing = false;
|
||||
const Snapshot* first_snapshot = db_->GetSnapshot();
|
||||
ASSERT_OK(Put(0, "key", "value"));
|
||||
const Snapshot* second_snapshot = db_->GetSnapshot();
|
||||
read_options[0].snapshot = second_snapshot;
|
||||
read_options[1].snapshot = second_snapshot;
|
||||
ASSERT_OK(db_->NewIterators(read_options, handles_, &iterators));
|
||||
for (auto* iterator : iterators) {
|
||||
delete iterator;
|
||||
}
|
||||
|
||||
read_options[0].snapshot = first_snapshot;
|
||||
read_options[1].snapshot = second_snapshot;
|
||||
ASSERT_TRUE(db_->NewIterators(read_options, handles_, &iterators)
|
||||
.IsInvalidArgument());
|
||||
db_->ReleaseSnapshot(first_snapshot);
|
||||
db_->ReleaseSnapshot(second_snapshot);
|
||||
}
|
||||
|
||||
TEST_F(DBIteratorTest,
|
||||
ReadOnlyNewIteratorsAllowsTableFilterWithRangeConversion) {
|
||||
Options options = CurrentOptions();
|
||||
options.min_tombstones_for_range_conversion = 2;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("key", "value"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(ReadOnlyReopen(options));
|
||||
|
||||
std::function<bool(const TableProperties&)> table_filter =
|
||||
[](const TableProperties&) { return true; };
|
||||
ReadOptions read_options;
|
||||
read_options.table_filter = &table_filter;
|
||||
const std::vector<ReadOptions> per_cf_read_options = {read_options};
|
||||
const std::vector<ColumnFamilyHandle*> column_families = {
|
||||
db_->DefaultColumnFamily()};
|
||||
for (bool use_per_cf_read_options : {false, true}) {
|
||||
std::vector<Iterator*> iterators;
|
||||
const Status s =
|
||||
use_per_cf_read_options
|
||||
? db_->NewIterators(per_cf_read_options, column_families,
|
||||
&iterators)
|
||||
: db_->NewIterators(read_options, column_families, &iterators);
|
||||
ASSERT_OK(s);
|
||||
ASSERT_EQ(1U, iterators.size());
|
||||
iterators[0]->SeekToFirst();
|
||||
ASSERT_OK(iterators[0]->status());
|
||||
ASSERT_TRUE(iterators[0]->Valid());
|
||||
ASSERT_EQ("key", iterators[0]->key());
|
||||
delete iterators[0];
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(DBIteratorTest, MemtableOpsScanFlushTriggerWithSeek) {
|
||||
// Tests that option memtable_op_scan_flush_trigger works when the limit
|
||||
// is reached during a Seek() operation.
|
||||
|
||||
+1323
-10
File diff suppressed because it is too large.
Load diff
@@ -17,6 +17,8 @@
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/advanced_compression.h"
|
||||
#include "rocksdb/db.h"
|
||||
#include "rocksdb/sst_file_reader.h"
|
||||
#include "rocksdb/sst_file_writer.h"
|
||||
#include "rocksdb/types.h"
|
||||
#include "rocksdb/utilities/object_registry.h"
|
||||
#include "rocksdb/utilities/table_properties_collectors.h"
|
||||
@@ -851,6 +853,156 @@ TEST_F(DBTablePropertiesTest, KeyLargestSmallestSeqno) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBTablePropertiesTest, LsmInfoAtCreationPacking) {
|
||||
using Applicability = LsmInfoAtCreation::Applicability;
|
||||
auto round_trip = [](const LsmInfoAtCreation& info) {
|
||||
return LsmInfoAtCreation::DecodeFrom(info.Encode());
|
||||
};
|
||||
|
||||
// Default is "unknown", which encodes to 0 and is distinct from every other
|
||||
// state.
|
||||
LsmInfoAtCreation unknown;
|
||||
EXPECT_EQ(unknown.applicability, Applicability::kUnknown);
|
||||
EXPECT_EQ(unknown.Encode(), 0U);
|
||||
EXPECT_EQ(round_trip(unknown), unknown);
|
||||
|
||||
// "not applicable" is non-zero (so it persists) and distinct from "unknown".
|
||||
LsmInfoAtCreation na = LsmInfoAtCreation::NotApplicable();
|
||||
EXPECT_EQ(na.applicability, Applicability::kNotApplicable);
|
||||
EXPECT_NE(na.Encode(), 0U);
|
||||
EXPECT_NE(na.Encode(), unknown.Encode());
|
||||
EXPECT_EQ(round_trip(na), na);
|
||||
|
||||
// "applicable" round-trips natural level values and bottommost. Level 0 / not
|
||||
// bottommost is still non-zero and thus distinct from "unknown"/"N/A".
|
||||
for (int level : {0, 1, 5, 63, 200}) {
|
||||
for (bool bottommost : {false, true}) {
|
||||
LsmInfoAtCreation info = LsmInfoAtCreation::Applicable(level, bottommost);
|
||||
EXPECT_NE(info.Encode(), 0U);
|
||||
LsmInfoAtCreation decoded = round_trip(info);
|
||||
EXPECT_EQ(decoded.applicability, Applicability::kApplicable);
|
||||
EXPECT_EQ(decoded.level_at_creation, level);
|
||||
EXPECT_EQ(decoded.is_bottommost, bottommost);
|
||||
EXPECT_EQ(decoded, info);
|
||||
}
|
||||
}
|
||||
|
||||
// Applicable but with an unknown level (e.g. the DB repairer): still
|
||||
// applicable, bottommost preserved, level decodes back to kUnknownLevel.
|
||||
LsmInfoAtCreation unknown_level = LsmInfoAtCreation::Applicable(
|
||||
LsmInfoAtCreation::kUnknownLevel, /*is_bottommost=*/true);
|
||||
LsmInfoAtCreation decoded = round_trip(unknown_level);
|
||||
EXPECT_EQ(decoded.applicability, Applicability::kApplicable);
|
||||
EXPECT_EQ(decoded.level_at_creation, LsmInfoAtCreation::kUnknownLevel);
|
||||
EXPECT_TRUE(decoded.is_bottommost);
|
||||
}
|
||||
|
||||
TEST_F(DBTablePropertiesTest, UncompressedDataSizeAndLsmInfoPersisted) {
|
||||
Options options = CurrentOptions();
|
||||
options.compression = kNoCompression;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "key1", "value1"));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "key2", "value2"));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "key3", "value3"));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
|
||||
// Freshly flushed L0 file. uncompressed_data_size is now persisted (it used
|
||||
// to always read back as 0), and LSM-at-creation info is populated.
|
||||
{
|
||||
TablePropertiesCollection props;
|
||||
ASSERT_OK(db_->GetPropertiesOfAllTables(&props));
|
||||
ASSERT_EQ(1U, props.size());
|
||||
auto table_props = props.begin()->second;
|
||||
|
||||
EXPECT_GT(table_props->uncompressed_data_size, 0U);
|
||||
LsmInfoAtCreation info =
|
||||
LsmInfoAtCreation::DecodeFrom(table_props->lsm_info_at_creation);
|
||||
EXPECT_EQ(info.applicability,
|
||||
LsmInfoAtCreation::Applicability::kApplicable);
|
||||
EXPECT_EQ(info.level_at_creation, 0);
|
||||
EXPECT_FALSE(info.is_bottommost);
|
||||
}
|
||||
|
||||
// After forcing a bottommost compaction, the (re)created file records
|
||||
// is_bottommost = true and a deeper creation level.
|
||||
{
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
|
||||
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
|
||||
|
||||
TablePropertiesCollection props;
|
||||
ASSERT_OK(db_->GetPropertiesOfAllTables(&props));
|
||||
ASSERT_EQ(1U, props.size());
|
||||
auto table_props = props.begin()->second;
|
||||
|
||||
EXPECT_GT(table_props->uncompressed_data_size, 0U);
|
||||
LsmInfoAtCreation info =
|
||||
LsmInfoAtCreation::DecodeFrom(table_props->lsm_info_at_creation);
|
||||
EXPECT_EQ(info.applicability,
|
||||
LsmInfoAtCreation::Applicability::kApplicable);
|
||||
EXPECT_TRUE(info.is_bottommost);
|
||||
EXPECT_GT(info.level_at_creation, 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBTablePropertiesTest, LsmInfoNotApplicableForSstFileWriter) {
|
||||
// A file written by SstFileWriter has no LSM position, so its
|
||||
// lsm_info_at_creation records "not applicable" (distinct from "unknown").
|
||||
Options options = CurrentOptions();
|
||||
options.compression = kNoCompression;
|
||||
std::string file = dbname_ + "/for_ingestion.sst";
|
||||
{
|
||||
SstFileWriter writer(EnvOptions(), options);
|
||||
ASSERT_OK(writer.Open(file));
|
||||
ASSERT_OK(writer.Put("a", "1"));
|
||||
ASSERT_OK(writer.Put("b", "2"));
|
||||
ASSERT_OK(writer.Finish());
|
||||
}
|
||||
|
||||
SstFileReader reader(options);
|
||||
ASSERT_OK(reader.Open(file));
|
||||
ASSERT_OK(reader.VerifyChecksum());
|
||||
std::shared_ptr<const TableProperties> table_props =
|
||||
reader.GetTableProperties();
|
||||
ASSERT_NE(table_props, nullptr);
|
||||
|
||||
LsmInfoAtCreation info =
|
||||
LsmInfoAtCreation::DecodeFrom(table_props->lsm_info_at_creation);
|
||||
EXPECT_EQ(info.applicability,
|
||||
LsmInfoAtCreation::Applicability::kNotApplicable);
|
||||
EXPECT_EQ(info.level_at_creation, LsmInfoAtCreation::kUnknownLevel);
|
||||
EXPECT_FALSE(info.is_bottommost);
|
||||
// uncompressed_data_size is persisted for SstFileWriter files too.
|
||||
EXPECT_GT(table_props->uncompressed_data_size, 0U);
|
||||
}
|
||||
|
||||
TEST_F(DBTablePropertiesTest, CompressionManagerFullDetailsPersisted) {
|
||||
// With an explicit CompressionManager, compression_options records the
|
||||
// manager's full details via ToString() (id plus any configured options),
|
||||
// not just GetId(). This manager has no options, so ToString() is its id.
|
||||
Options options = CurrentOptions();
|
||||
options.compression = kNoCompression;
|
||||
options.compression_manager =
|
||||
std::make_shared<ParseCompressionDisplayNameManager>();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "k", "v"));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
|
||||
TablePropertiesCollection props;
|
||||
ASSERT_OK(db_->GetPropertiesOfAllTables(&props));
|
||||
ASSERT_EQ(1U, props.size());
|
||||
auto table_props = props.begin()->second;
|
||||
|
||||
EXPECT_NE(
|
||||
table_props->compression_options.find(
|
||||
"_compression_manager=" +
|
||||
std::string(ParseCompressionDisplayNameManager::kCompatibilityName)),
|
||||
std::string::npos)
|
||||
<< table_props->compression_options;
|
||||
}
|
||||
|
||||
TEST_F(DBTablePropertiesTest, ParseCompressionNameForDisplay) {
|
||||
auto custom_mgr = std::make_shared<ParseCompressionDisplayNameManager>();
|
||||
|
||||
|
||||
@@ -3600,6 +3600,11 @@ class ModelDB : public DB {
|
||||
std::vector<Iterator*>* /*iterators*/) override {
|
||||
return Status::NotSupported("Not supported yet");
|
||||
}
|
||||
Status NewIterators(const std::vector<ReadOptions>& /*options*/,
|
||||
const std::vector<ColumnFamilyHandle*>& /*column_family*/,
|
||||
std::vector<Iterator*>* /*iterators*/) override {
|
||||
return Status::NotSupported("Not supported yet");
|
||||
}
|
||||
|
||||
std::unique_ptr<Iterator> NewCoalescingIterator(
|
||||
const ReadOptions& /*options*/,
|
||||
|
||||
@@ -32,6 +32,46 @@ class DBBasicTestWithTimestamp : public DBBasicTestWithTimestampBase {
|
||||
: DBBasicTestWithTimestampBase("db_basic_test_with_timestamp") {}
|
||||
};
|
||||
|
||||
TEST_F(DBBasicTestWithTimestamp, GetWithMetadataTimestampAndNewerVersion) {
|
||||
Options options = CurrentOptions();
|
||||
options.comparator = test::BytewiseComparatorWithU64TsWrapper();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string old_timestamp = EncodeAsUint64(1);
|
||||
const std::string new_timestamp = EncodeAsUint64(2);
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "key", old_timestamp, "old"));
|
||||
const Snapshot* snapshot = db_->GetSnapshot();
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "key", new_timestamp, "new"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
Slice read_timestamp(new_timestamp);
|
||||
ReadOptions read_options;
|
||||
read_options.snapshot = snapshot;
|
||||
read_options.timestamp = &read_timestamp;
|
||||
|
||||
OutputMetadata output_metadata;
|
||||
output_metadata.WantTimestamp().WantNewerVersionPresent();
|
||||
std::string value;
|
||||
ASSERT_OK(
|
||||
db_->GetWithMetadata(read_options, "key", &value, &output_metadata));
|
||||
ASSERT_EQ("old", value);
|
||||
ASSERT_EQ(old_timestamp, *output_metadata.timestamp);
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
MultiGetOutputMetadata multiget_output_metadata;
|
||||
multiget_output_metadata.WantTimestamps().WantNewerVersionPresent();
|
||||
std::vector<Slice> keys{"key"};
|
||||
std::vector<std::string> values;
|
||||
const std::vector<Status> statuses = db_->MultiGetWithMetadata(
|
||||
read_options, keys, &values, &multiget_output_metadata);
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ("old", values[0]);
|
||||
ASSERT_EQ(old_timestamp, (*multiget_output_metadata.timestamps)[0]);
|
||||
ASSERT_TRUE((*multiget_output_metadata.newer_version_present)[0]);
|
||||
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTestWithTimestamp, SanityChecks) {
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
@@ -4101,6 +4141,93 @@ TEST_F(DBBasicTestWithTimestamp, FullHistoryTsLowSanityCheckFail) {
|
||||
Close();
|
||||
}
|
||||
|
||||
// Verifies that reading with a snapshot and a FRESH timestamp succeeds even
|
||||
// after full_history_ts_low advances past the snapshot's original timestamp.
|
||||
// This models the db_stress scenario where a long-running snapshot's saved
|
||||
// timestamp becomes stale due to concurrent flushes advancing
|
||||
// full_history_ts_low (persist_user_defined_timestamps=false).
|
||||
TEST_F(DBBasicTestWithTimestamp,
|
||||
SnapshotReadWithFreshTimestampAfterTsLowAdvances) {
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
options.comparator = test::BytewiseComparatorWithU64TsWrapper();
|
||||
options.persist_user_defined_timestamps = false;
|
||||
options.allow_concurrent_memtable_write = false;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Write a key at timestamp 100.
|
||||
std::string write_ts;
|
||||
PutFixed64(&write_ts, 100);
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "key1", write_ts, "value1"));
|
||||
|
||||
// Take a snapshot (pins the data at this sequence number).
|
||||
const Snapshot* snap = db_->GetSnapshot();
|
||||
ASSERT_NE(snap, nullptr);
|
||||
|
||||
// Simulate what happens in the stress test: the original read timestamp at
|
||||
// snapshot time would have been ~100. Now advance full_history_ts_low well
|
||||
// past 100 by writing more data and flushing.
|
||||
std::string write_ts2;
|
||||
PutFixed64(&write_ts2, 500);
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "key1", write_ts2, "value2"));
|
||||
|
||||
// Advance full_history_ts_low to 400 (past the original timestamp 100).
|
||||
std::string ts_low;
|
||||
PutFixed64(&ts_low, 400);
|
||||
ASSERT_OK(db_->IncreaseFullHistoryTsLow(db_->DefaultColumnFamily(), ts_low));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Omitting ReadOptions.timestamp is not a valid way to bypass the stale
|
||||
// timestamp check because this column family enables user-defined timestamps.
|
||||
{
|
||||
ReadOptions read_opts;
|
||||
read_opts.snapshot = snap;
|
||||
std::string value;
|
||||
Status s = db_->Get(read_opts, "key1", &value);
|
||||
ASSERT_TRUE(s.IsInvalidArgument()) << s.ToString();
|
||||
}
|
||||
|
||||
// With persist_user_defined_timestamps=false and the flush,
|
||||
// full_history_ts_low auto-advances past 500 (the highest write). Reading
|
||||
// with the snapshot but the OLD (stale) timestamp of 100, which is below the
|
||||
// auto-advanced threshold, should fail with InvalidArgument because
|
||||
// read_ts < full_history_ts_low.
|
||||
{
|
||||
ReadOptions read_opts;
|
||||
read_opts.snapshot = snap;
|
||||
std::string stale_ts;
|
||||
PutFixed64(&stale_ts, 100);
|
||||
Slice stale_ts_slice = stale_ts;
|
||||
read_opts.timestamp = &stale_ts_slice;
|
||||
std::string value;
|
||||
Status s = db_->Get(read_opts, "key1", &value);
|
||||
ASSERT_TRUE(s.IsInvalidArgument()) << s.ToString();
|
||||
}
|
||||
|
||||
// Reading with the snapshot but a FRESH timestamp (>= full_history_ts_low)
|
||||
// should succeed. The snapshot pins data visibility by sequence number, so
|
||||
// the fresh timestamp only needs to pass the full_history_ts_low check.
|
||||
// When persist_user_defined_timestamps=false and we flush,
|
||||
// full_history_ts_low automatically advances to just above the newest write
|
||||
// timestamp in the flushed memtable (i.e. just above 500). So we use 1000 to
|
||||
// have comfortable margin above 500.
|
||||
{
|
||||
ReadOptions read_opts;
|
||||
read_opts.snapshot = snap;
|
||||
std::string fresh_ts;
|
||||
PutFixed64(&fresh_ts, 1000);
|
||||
Slice fresh_ts_slice = fresh_ts;
|
||||
read_opts.timestamp = &fresh_ts_slice;
|
||||
std::string value;
|
||||
Status s = db_->Get(read_opts, "key1", &value);
|
||||
ASSERT_OK(s);
|
||||
ASSERT_EQ(value, "value1");
|
||||
}
|
||||
|
||||
db_->ReleaseSnapshot(snap);
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTestWithTimestamp,
|
||||
GCPreserveRangeTombstoneWhenNoOrSmallFullHistoryLow) {
|
||||
Options options = CurrentOptions();
|
||||
|
||||
@@ -131,6 +131,43 @@ static const SequenceNumber kMaxSequenceNumber = ((0x1ull << 56) - 1);
|
||||
static const SequenceNumber kDisableGlobalSequenceNumber =
|
||||
std::numeric_limits<uint64_t>::max();
|
||||
|
||||
// Single source of truth for whether a bottommost entry's sequence number can
|
||||
// be zeroed out (the "seqno squashing" compaction optimization), for the two
|
||||
// conditions that gate it independently of snapshot visibility: the seqno->time
|
||||
// "preserve" window and the user-defined-timestamp (UDT) history cutoff.
|
||||
//
|
||||
// Callers still apply their own snapshot-visibility gate (which differs: the
|
||||
// compaction iterator is transaction/snapshot-checker aware, the version marker
|
||||
// only compares against the oldest snapshot). This shared predicate exists so
|
||||
// the entry-level decision in CompactionIterator::PrepareOutput and the
|
||||
// file-level decision in
|
||||
// VersionStorageInfo::ComputeBottommostFilesMarkedForCompaction cannot drift
|
||||
// apart -- a mismatch causes a bottommost file to be marked for a compaction
|
||||
// that can make no progress, i.e. an infinite compaction loop.
|
||||
//
|
||||
// - `seq`: the entry's sequence number (or a file's largest sequence number).
|
||||
// - `preserve_seqno_after`: max seqno that may be zeroed given the preserve
|
||||
// window; a seqno above it is too recent to zero.
|
||||
// - `ts_sz`: user-defined timestamp size (0 means UDT disabled).
|
||||
// - `full_history_ts_low_set`: whether full_history_ts_low has ever been set.
|
||||
// - `ts_below_full_history_ts_low`: whether the entry's (or file's max) user
|
||||
// timestamp is strictly below full_history_ts_low. Only consulted when UDT is
|
||||
// enabled; callers pass false when the timestamp is unknown so an
|
||||
// indeterminate file is treated as not-zeroable (conservative, no loop).
|
||||
inline bool BottommostSeqnoCanBeZeroed(SequenceNumber seq,
|
||||
SequenceNumber preserve_seqno_after,
|
||||
size_t ts_sz,
|
||||
bool full_history_ts_low_set,
|
||||
bool ts_below_full_history_ts_low) {
|
||||
if (seq > preserve_seqno_after) {
|
||||
return false;
|
||||
}
|
||||
if (ts_sz == 0) {
|
||||
return true;
|
||||
}
|
||||
return full_history_ts_low_set && ts_below_full_history_ts_low;
|
||||
}
|
||||
|
||||
constexpr uint64_t kNumInternalBytes = 8;
|
||||
|
||||
// Defined in dbformat.cc
|
||||
@@ -649,6 +686,19 @@ class IterKey {
|
||||
key_size_ = total_size;
|
||||
}
|
||||
|
||||
// Sets the key to `prefix` immediately followed by `suffix`, always copying
|
||||
// into buf_. Used by the data-block common-prefix feature to reconstruct a
|
||||
// restart-point key from the block's stored common user-key prefix plus the
|
||||
// key's stored (prefix-stripped) bytes. is_user_key_ is left unchanged.
|
||||
void SetKeyPrependingPrefix(const Slice& prefix, const Slice& suffix) {
|
||||
const size_t total_size = prefix.size() + suffix.size();
|
||||
EnlargeBufferIfNeeded(total_size);
|
||||
memcpy(buf_, prefix.data(), prefix.size());
|
||||
memcpy(buf_ + prefix.size(), suffix.data(), suffix.size());
|
||||
key_ = buf_;
|
||||
key_size_ = total_size;
|
||||
}
|
||||
|
||||
// A version of `TrimAppend` assuming the last bytes of length `ts_sz` in the
|
||||
// user key part of `key_` is not counted towards shared bytes. And the
|
||||
// decoded key needed a min timestamp of length `ts_sz` pad to the user key.
|
||||
|
||||
@@ -262,8 +262,8 @@ TEST_F(DeleteFileTest, BackgroundPurgeIteratorTest) {
|
||||
// Regression test for a use-after-free where an obsolete-file purge started
|
||||
// DURING DB close -- after CloseHelper's early purge drain -- continued using
|
||||
// DBImpl after ~DBImpl destroyed mutex_. Seen as a vhost-user-blk SIGABRT when
|
||||
// many RocksDB instances closed concurrently under a Warm Storage fault, with
|
||||
// avoid_unnecessary_blocking_io=true on a shared, process-wide Env.
|
||||
// many RocksDB instances closed concurrently under a remote file-system fault
|
||||
// while using avoid_unnecessary_blocking_io=true on a shared, process-wide Env.
|
||||
//
|
||||
// Here we make the straggler deterministic: keep a dropped column family handle
|
||||
// alive, delete it only after CloseHelper has passed its early purge drain,
|
||||
|
||||
+22
-28
@@ -433,13 +433,12 @@ void ErrorHandler::SetBGError(const Status& bg_status,
|
||||
// it can directly overwrite any existing bg_error_.
|
||||
bool auto_recovery = false;
|
||||
Status bg_err(new_bg_io_err, Status::Severity::kUnrecoverableError);
|
||||
CheckAndSetRecoveryAndBGError(bg_err);
|
||||
CheckAndSetRecoveryAndBGError(bg_err, context);
|
||||
ROCKS_LOG_INFO(
|
||||
db_options_.info_log,
|
||||
"ErrorHandler: Set background IO error as unrecoverable error\n");
|
||||
EventHelpers::NotifyOnBackgroundError(db_options_.listeners, reason,
|
||||
&bg_err, db_mutex_, &auto_recovery);
|
||||
recover_context_ = context;
|
||||
return;
|
||||
}
|
||||
if (wal_related) {
|
||||
@@ -457,13 +456,12 @@ void ErrorHandler::SetBGError(const Status& bg_status,
|
||||
// from WAL write failures.
|
||||
bool auto_recovery = false;
|
||||
Status bg_err(new_bg_io_err, Status::Severity::kFatalError);
|
||||
CheckAndSetRecoveryAndBGError(bg_err);
|
||||
CheckAndSetRecoveryAndBGError(bg_err, context);
|
||||
ROCKS_LOG_WARN(db_options_.info_log,
|
||||
"ErrorHandler: A potentially WAL error happened, set "
|
||||
"background IO error as fatal error\n");
|
||||
EventHelpers::NotifyOnBackgroundError(db_options_.listeners, reason,
|
||||
&bg_err, db_mutex_, &auto_recovery);
|
||||
recover_context_ = context;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -519,8 +517,7 @@ void ErrorHandler::SetBGError(const Status& bg_status,
|
||||
severity = Status::Severity::kHardError;
|
||||
}
|
||||
Status bg_err(new_bg_io_err, severity);
|
||||
CheckAndSetRecoveryAndBGError(bg_err);
|
||||
recover_context_ = context;
|
||||
CheckAndSetRecoveryAndBGError(bg_err, context);
|
||||
bool auto_recovery = db_options_.max_bgerror_resume_count > 0;
|
||||
EventHelpers::NotifyOnBackgroundError(db_options_.listeners, reason,
|
||||
&new_bg_io_err, db_mutex_,
|
||||
@@ -632,7 +629,7 @@ Status ErrorHandler::ClearBGError() {
|
||||
|
||||
Status ErrorHandler::RecoverFromBGError(bool is_manual) {
|
||||
InstrumentedMutexLock l(db_mutex_);
|
||||
bool no_bg_work_original_flag = soft_error_no_bg_work_;
|
||||
const bool soft_error_no_bg_work_on_entry = soft_error_no_bg_work_;
|
||||
if (is_manual) {
|
||||
// If its a manual recovery and there's a background recovery in progress
|
||||
// return busy status
|
||||
@@ -640,25 +637,15 @@ Status ErrorHandler::RecoverFromBGError(bool is_manual) {
|
||||
return Status::Busy("Recovery already in progress");
|
||||
}
|
||||
recovery_in_prog_ = true;
|
||||
|
||||
// In manual resume, we allow the bg work to run. If it is a auto resume,
|
||||
// the bg work should follow this tag.
|
||||
soft_error_no_bg_work_ = false;
|
||||
|
||||
// In manual resume, if the bg error is a soft error and also requires
|
||||
// no bg work, the error must be recovered by call the flush with
|
||||
// flush reason: kErrorRecoveryRetryFlush. In other case, the flush
|
||||
// reason is set to kErrorRecovery.
|
||||
if (no_bg_work_original_flag) {
|
||||
recover_context_.flush_reason = FlushReason::kErrorRecoveryRetryFlush;
|
||||
} else {
|
||||
recover_context_.flush_reason = FlushReason::kErrorRecovery;
|
||||
}
|
||||
}
|
||||
|
||||
// kErrorRecovery can be the default context for a generic soft error that
|
||||
// needs no flush. It can also remain from an earlier error after a later
|
||||
// no-WAL soft error sets soft_error_no_bg_work_. Only the first case may be
|
||||
// cleared directly.
|
||||
if (bg_error_.severity() == Status::Severity::kSoftError &&
|
||||
recover_context_.flush_reason == FlushReason::kErrorRecovery) {
|
||||
// Simply clear the background error and return
|
||||
recover_context_.flush_reason == FlushReason::kErrorRecovery &&
|
||||
!soft_error_no_bg_work_on_entry) {
|
||||
recovery_error_ = IOStatus::OK();
|
||||
return ClearBGError();
|
||||
}
|
||||
@@ -668,11 +655,13 @@ 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_, Env::IOActivity::kFlush);
|
||||
DBRecoverContext context = recover_context_;
|
||||
context.flush_after_recovery = true;
|
||||
Status s = db_->ResumeImpl(context, Env::IOActivity::kFlush);
|
||||
if (s.ok()) {
|
||||
soft_error_no_bg_work_ = false;
|
||||
} else {
|
||||
soft_error_no_bg_work_ = no_bg_work_original_flag;
|
||||
soft_error_no_bg_work_ = soft_error_no_bg_work_on_entry;
|
||||
}
|
||||
|
||||
// For manual recover, shutdown, and fatal error cases, set
|
||||
@@ -746,8 +735,6 @@ void ErrorHandler::RecoverFromRetryableBGIOError() {
|
||||
recovery_in_prog_ = false;
|
||||
return;
|
||||
}
|
||||
DBRecoverContext context = recover_context_;
|
||||
context.flush_after_recovery = true;
|
||||
int resume_count = db_options_.max_bgerror_resume_count;
|
||||
uint64_t wait_interval = db_options_.bgerror_resume_retry_interval;
|
||||
uint64_t retry_count = 0;
|
||||
@@ -763,6 +750,9 @@ void ErrorHandler::RecoverFromRetryableBGIOError() {
|
||||
TEST_SYNC_POINT("RecoverFromRetryableBGIOError:BeforeResume0");
|
||||
TEST_SYNC_POINT("RecoverFromRetryableBGIOError:BeforeResume1");
|
||||
recovery_error_ = IOStatus::OK();
|
||||
// An error during the previous attempt can require stronger recovery.
|
||||
DBRecoverContext context = recover_context_;
|
||||
context.flush_after_recovery = true;
|
||||
retry_count++;
|
||||
Status s = db_->ResumeImpl(context, Env::IOActivity::kFlush);
|
||||
RecordStats({ERROR_HANDLER_AUTORESUME_RETRY_TOTAL_COUNT},
|
||||
@@ -822,12 +812,16 @@ void ErrorHandler::RecoverFromRetryableBGIOError() {
|
||||
{{ERROR_HANDLER_AUTORESUME_RETRY_COUNT, retry_count}});
|
||||
}
|
||||
|
||||
void ErrorHandler::CheckAndSetRecoveryAndBGError(const Status& bg_err) {
|
||||
void ErrorHandler::CheckAndSetRecoveryAndBGError(
|
||||
const Status& bg_err, const DBRecoverContext& context) {
|
||||
assert(context.flush_reason != FlushReason::kErrorRecoveryRetryFlush ||
|
||||
bg_err.severity() == Status::Severity::kSoftError);
|
||||
if (recovery_in_prog_ && recovery_error_.ok()) {
|
||||
recovery_error_ = status_to_io_status(Status(bg_err));
|
||||
}
|
||||
if (bg_err.severity() > bg_error_.severity()) {
|
||||
bg_error_ = bg_err;
|
||||
recover_context_ = context;
|
||||
}
|
||||
if (bg_error_.severity() >= Status::Severity::kHardError) {
|
||||
is_db_stopped_.store(true, std::memory_order_release);
|
||||
|
||||
+4
-4
@@ -156,10 +156,10 @@ class ErrorHandler {
|
||||
void RecoverFromNoSpace();
|
||||
void StartRecoverFromRetryableBGIOError(const IOStatus& io_error);
|
||||
void RecoverFromRetryableBGIOError();
|
||||
// First, if it is in recovery and the recovery_error is ok. Set the
|
||||
// recovery_error_ to bg_err. Second, if the severity is higher than the
|
||||
// current bg_error_, overwrite it.
|
||||
void CheckAndSetRecoveryAndBGError(const Status& bg_err);
|
||||
// If recovery is in progress, record the first new error for the current
|
||||
// attempt. If this error is more severe, retain it and its recovery context.
|
||||
void CheckAndSetRecoveryAndBGError(const Status& bg_err,
|
||||
const DBRecoverContext& context);
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -7,6 +7,9 @@
|
||||
// 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 <atomic>
|
||||
#include <chrono>
|
||||
#include <future>
|
||||
#include <memory>
|
||||
|
||||
#include "db/db_test_util.h"
|
||||
@@ -1999,6 +2002,139 @@ TEST_F(DBErrorHandlingFSTest, FlushWritNoWALRetryableErrorAutoRecover2) {
|
||||
Destroy(options);
|
||||
}
|
||||
|
||||
TEST_F(DBErrorHandlingFSTest, AutoRecoveryEscalationSwitchesWAL) {
|
||||
Options options = GetDefaultOptions();
|
||||
options.env = fault_env_.get();
|
||||
options.create_if_missing = true;
|
||||
options.disable_auto_compactions = true;
|
||||
options.track_and_verify_wals_in_manifest = true;
|
||||
options.max_bgerror_resume_count = 3;
|
||||
options.bgerror_resume_retry_interval = 1000;
|
||||
options.avoid_flush_during_shutdown = true;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
struct ErrorInjectionState {
|
||||
std::atomic<bool> fail_flush{true};
|
||||
std::atomic<bool> fail_manifest{false};
|
||||
std::atomic<bool> manifest_error_injected{false};
|
||||
std::atomic<bool> recovery_notified{false};
|
||||
std::promise<void> recovery_finished;
|
||||
};
|
||||
auto state = std::make_shared<ErrorInjectionState>();
|
||||
auto recovery_finished = state->recovery_finished.get_future();
|
||||
// Keep the failed flush counted as background work while the first recovery
|
||||
// attempt waits without the DB mutex. This lets the foreground write safely
|
||||
// escalate the retained recovery context.
|
||||
SyncPoint::GetInstance()->LoadDependency(
|
||||
{{"AutoRecoveryEscalationSwitchesWAL:ReleaseFailedFlush",
|
||||
"DBImpl::BackgroundCallFlush:FilesFound"},
|
||||
{"RecoverFromRetryableBGIOError:BeforeResume0",
|
||||
"AutoRecoveryEscalationSwitchesWAL:RecoveryStarted"}});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"FlushJob::Run:PostBuildTable", [state](void* arg) {
|
||||
if (state->fail_flush.exchange(false)) {
|
||||
IOStatus error = IOStatus::IOError("injected retryable flush error");
|
||||
error.SetRetryable(true);
|
||||
*static_cast<Status*>(arg) = error;
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"VersionSet::ProcessManifestWrites:AfterSyncManifest",
|
||||
[state](void* arg) {
|
||||
if (state->fail_manifest.exchange(false)) {
|
||||
IOStatus error =
|
||||
IOStatus::IOError("injected retryable MANIFEST error");
|
||||
error.SetRetryable(true);
|
||||
*static_cast<IOStatus*>(arg) = error;
|
||||
state->manifest_error_injected.store(true);
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"RecoverFromRetryableBGIOError:RecoverSuccess", [state](void*) {
|
||||
if (!state->recovery_notified.exchange(true)) {
|
||||
state->recovery_finished.set_value();
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
ASSERT_OK(Put(Key(0), "value"));
|
||||
const uint64_t initial_wal = dbfull()->TEST_GetCurrentLogNumber();
|
||||
Status flush_status = Flush();
|
||||
ASSERT_EQ(Status::Severity::kSoftError, flush_status.severity());
|
||||
ASSERT_FALSE(state->fail_flush.load());
|
||||
const uint64_t failed_write_wal = dbfull()->TEST_GetCurrentLogNumber();
|
||||
ASSERT_GT(failed_write_wal, initial_wal);
|
||||
TEST_SYNC_POINT("AutoRecoveryEscalationSwitchesWAL:RecoveryStarted");
|
||||
|
||||
const SequenceNumber sequence_before_delete = db_->GetLatestSequenceNumber();
|
||||
|
||||
state->fail_manifest.store(true);
|
||||
WriteOptions write_options;
|
||||
write_options.sync = true;
|
||||
Status delete_status = db_->Delete(write_options, Key(0));
|
||||
ASSERT_TRUE(delete_status.IsIOError()) << delete_status.ToString();
|
||||
ASSERT_TRUE(state->manifest_error_injected.load());
|
||||
ASSERT_EQ(sequence_before_delete, db_->GetLatestSequenceNumber());
|
||||
ASSERT_EQ(Status::Severity::kHardError,
|
||||
dbfull()->TEST_GetBGError().severity());
|
||||
ASSERT_TRUE(dbfull()->TEST_IsRecoveryInProgress());
|
||||
|
||||
TEST_SYNC_POINT("AutoRecoveryEscalationSwitchesWAL:ReleaseFailedFlush");
|
||||
ASSERT_EQ(std::future_status::ready,
|
||||
recovery_finished.wait_for(std::chrono::seconds(10)))
|
||||
<< "automatic recovery did not finish";
|
||||
|
||||
const uint64_t wal_after_recovery = dbfull()->TEST_GetCurrentLogNumber();
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
ASSERT_GT(wal_after_recovery, failed_write_wal)
|
||||
<< "hard MANIFEST recovery must switch away from the WAL containing the "
|
||||
"failed synced write";
|
||||
|
||||
ASSERT_OK(db_->Delete(write_options, Key(0)));
|
||||
ASSERT_EQ("NOT_FOUND", Get(Key(0)));
|
||||
Reopen(options);
|
||||
ASSERT_EQ("NOT_FOUND", Get(Key(0)));
|
||||
Destroy(options);
|
||||
}
|
||||
|
||||
TEST_F(DBErrorHandlingFSTest, ManualResumePreservesStrongerRecoveryContext) {
|
||||
auto listener = std::make_shared<ErrorHandlerFSListener>();
|
||||
Options options = GetDefaultOptions();
|
||||
options.env = fault_env_.get();
|
||||
options.create_if_missing = true;
|
||||
options.listeners.emplace_back(listener);
|
||||
options.max_bgerror_resume_count = 0;
|
||||
options.avoid_flush_during_shutdown = true;
|
||||
listener->EnableAutoRecovery(false);
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(Put(Key(0), "value"));
|
||||
const uint64_t wal_before_recovery = dbfull()->TEST_GetCurrentLogNumber();
|
||||
|
||||
IOStatus soft_error = IOStatus::IOError("injected soft error");
|
||||
dbfull()->TEST_SetBGError(soft_error, BackgroundErrorReason::kAsyncFileOpen);
|
||||
ASSERT_EQ(Status::Severity::kSoftError,
|
||||
dbfull()->TEST_GetBGError().severity());
|
||||
|
||||
IOStatus retryable_error = IOStatus::IOError("injected retryable error");
|
||||
retryable_error.SetRetryable(true);
|
||||
dbfull()->TEST_SetBGError(retryable_error,
|
||||
BackgroundErrorReason::kFlushNoWAL);
|
||||
ASSERT_EQ(Status::Severity::kSoftError,
|
||||
dbfull()->TEST_GetBGError().severity());
|
||||
|
||||
ASSERT_OK(dbfull()->Resume());
|
||||
const uint64_t wal_after_recovery = dbfull()->TEST_GetCurrentLogNumber();
|
||||
ASSERT_GT(wal_after_recovery, wal_before_recovery)
|
||||
<< "manual resume must execute the retained stronger recovery context";
|
||||
|
||||
Reopen(options);
|
||||
ASSERT_EQ("value", Get(Key(0)));
|
||||
Destroy(options);
|
||||
}
|
||||
|
||||
// Auto resume fromt the flush retryable IO error. Activate the FS before the
|
||||
// first resume. Resume is successful
|
||||
TEST_F(DBErrorHandlingFSTest, FlushWritRetryableErrorAutoRecover1) {
|
||||
|
||||
+6
-4
@@ -201,7 +201,7 @@ void EventHelpers::LogAndNotifyTableFileCreationFinished(
|
||||
void EventHelpers::LogAndNotifyTableFileDeletion(
|
||||
EventLogger* event_logger, int job_id, uint64_t file_number,
|
||||
const std::string& file_path, const Status& status,
|
||||
const std::string& dbname,
|
||||
const std::string& dbname, const std::string& db_session_id,
|
||||
const std::vector<std::shared_ptr<EventListener>>& listeners) {
|
||||
if (!event_logger && listeners.empty()) {
|
||||
status.PermitUncheckedError();
|
||||
@@ -213,7 +213,8 @@ void EventHelpers::LogAndNotifyTableFileDeletion(
|
||||
AppendCurrentTime(&jwriter);
|
||||
|
||||
jwriter << "job" << job_id << "event" << "table_file_deletion"
|
||||
<< "file_number" << file_number << "status" << status.ToString();
|
||||
<< "file_number" << file_number << "db_session_id" << db_session_id
|
||||
<< "status" << status.ToString();
|
||||
|
||||
jwriter.EndObject();
|
||||
|
||||
@@ -322,7 +323,7 @@ void EventHelpers::LogAndNotifyBlobFileDeletion(
|
||||
EventLogger* event_logger,
|
||||
const std::vector<std::shared_ptr<EventListener>>& listeners, int job_id,
|
||||
uint64_t file_number, const std::string& file_path, const Status& status,
|
||||
const std::string& dbname) {
|
||||
const std::string& dbname, const std::string& db_session_id) {
|
||||
if (!event_logger && listeners.empty()) {
|
||||
status.PermitUncheckedError();
|
||||
return;
|
||||
@@ -333,7 +334,8 @@ void EventHelpers::LogAndNotifyBlobFileDeletion(
|
||||
AppendCurrentTime(&jwriter);
|
||||
|
||||
jwriter << "job" << job_id << "event" << "blob_file_deletion"
|
||||
<< "file_number" << file_number << "status" << status.ToString();
|
||||
<< "file_number" << file_number << "db_session_id" << db_session_id
|
||||
<< "status" << status.ToString();
|
||||
|
||||
jwriter.EndObject();
|
||||
event_logger->Log(jwriter);
|
||||
|
||||
+2
-2
@@ -39,7 +39,7 @@ class EventHelpers {
|
||||
static void LogAndNotifyTableFileDeletion(
|
||||
EventLogger* event_logger, int job_id, uint64_t file_number,
|
||||
const std::string& file_path, const Status& status,
|
||||
const std::string& db_name,
|
||||
const std::string& db_name, const std::string& db_session_id,
|
||||
const std::vector<std::shared_ptr<EventListener>>& listeners);
|
||||
static void NotifyOnErrorRecoveryEnd(
|
||||
const std::vector<std::shared_ptr<EventListener>>& listeners,
|
||||
@@ -66,7 +66,7 @@ class EventHelpers {
|
||||
EventLogger* event_logger,
|
||||
const std::vector<std::shared_ptr<EventListener>>& listeners, int job_id,
|
||||
uint64_t file_number, const std::string& file_path, const Status& status,
|
||||
const std::string& db_name);
|
||||
const std::string& db_name, const std::string& db_session_id);
|
||||
|
||||
private:
|
||||
static void LogAndNotifyTableFileCreation(
|
||||
|
||||
@@ -224,6 +224,24 @@ class ExternalSSTFileBasicTest
|
||||
bool random_rwfile_supported_;
|
||||
};
|
||||
|
||||
TEST_F(ExternalSSTFileBasicTest, ReopenWithoutFinishIsRejected) {
|
||||
Options options = CurrentOptions();
|
||||
SstFileWriter sst_file_writer(EnvOptions(), options);
|
||||
|
||||
ASSERT_OK(sst_file_writer.Open(sst_files_dir_ + "reopen1.sst"));
|
||||
ASSERT_OK(sst_file_writer.Put(Key(0), "val"));
|
||||
|
||||
// Re-opening would drop a TableBuilder that has had neither Finish() nor
|
||||
// Abandon() called on it.
|
||||
ASSERT_TRUE(
|
||||
sst_file_writer.Open(sst_files_dir_ + "reopen2.sst").IsInvalidArgument());
|
||||
|
||||
// The first file is still the open one and can still be finished.
|
||||
ExternalSstFileInfo file_info;
|
||||
ASSERT_OK(sst_file_writer.Finish(&file_info));
|
||||
ASSERT_EQ(file_info.num_entries, 1);
|
||||
}
|
||||
|
||||
TEST_F(ExternalSSTFileBasicTest, Basic) {
|
||||
Options options = CurrentOptions();
|
||||
|
||||
@@ -2306,6 +2324,86 @@ TEST_F(ExternalSSTFileBasicTest,
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
}
|
||||
|
||||
TEST_F(ExternalSSTFileBasicTest,
|
||||
AtomicReplaceRangeGeneratesTombstoneForPartialOverlap) {
|
||||
Options options = CurrentOptions();
|
||||
options.compaction_style = CompactionStyle::kCompactionStyleUniversal;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
SstFileWriter sst_file_writer(EnvOptions(), options);
|
||||
std::vector<std::string> old_files;
|
||||
for (const auto& file_contents :
|
||||
std::vector<std::vector<std::pair<std::string, std::string>>>{
|
||||
{{"a", "old-a"}, {"c", "old-c"}},
|
||||
{{"d", "old-d"}},
|
||||
{{"e", "old-e"}, {"f", "old-f"}, {"h", "old-h"}}}) {
|
||||
std::string file_path =
|
||||
sst_files_dir_ + "old-" + std::to_string(old_files.size()) + ".sst";
|
||||
ASSERT_OK(sst_file_writer.Open(file_path));
|
||||
for (const auto& [key, value] : file_contents) {
|
||||
ASSERT_OK(sst_file_writer.Put(key, value));
|
||||
}
|
||||
ASSERT_OK(sst_file_writer.Finish());
|
||||
old_files.push_back(std::move(file_path));
|
||||
}
|
||||
ASSERT_OK(db_->IngestExternalFile(old_files, IngestExternalFileOptions()));
|
||||
ASSERT_EQ("0,0,0,0,0,0,3", FilesPerLevel());
|
||||
|
||||
const std::string replacement_file = sst_files_dir_ + "replacement.sst";
|
||||
ASSERT_OK(sst_file_writer.Open(replacement_file));
|
||||
ASSERT_OK(sst_file_writer.Put("b", "new-b"));
|
||||
ASSERT_OK(sst_file_writer.Put("e", "new-e"));
|
||||
ASSERT_OK(sst_file_writer.Finish());
|
||||
|
||||
IngestExternalFileArg arg;
|
||||
arg.column_family = db_->DefaultColumnFamily();
|
||||
arg.external_files = {replacement_file};
|
||||
arg.options.snapshot_consistency = false;
|
||||
arg.atomic_replace_range = {{"b", "f"}};
|
||||
|
||||
arg.options.allow_global_seqno = false;
|
||||
Status status = db_->IngestExternalFiles({arg});
|
||||
ASSERT_TRUE(status.IsInvalidArgument()) << status.ToString();
|
||||
|
||||
arg.options.allow_global_seqno = true;
|
||||
arg.options.fail_if_not_bottommost_level = true;
|
||||
status = db_->IngestExternalFiles({arg});
|
||||
ASSERT_TRUE(status.IsTryAgain()) << status.ToString();
|
||||
|
||||
arg.options.fail_if_not_bottommost_level = false;
|
||||
const Snapshot* snapshot = db_->GetSnapshot();
|
||||
const SequenceNumber seqno_before_ingestion = db_->GetLatestSequenceNumber();
|
||||
ASSERT_OK(db_->IngestExternalFiles({arg}));
|
||||
ASSERT_EQ(seqno_before_ingestion + 2, db_->GetLatestSequenceNumber());
|
||||
|
||||
ASSERT_EQ("old-a", Get("a"));
|
||||
ASSERT_EQ("new-b", Get("b"));
|
||||
ASSERT_EQ("NOT_FOUND", Get("c"));
|
||||
ASSERT_EQ("NOT_FOUND", Get("d"));
|
||||
ASSERT_EQ("new-e", Get("e"));
|
||||
ASSERT_EQ("old-f", Get("f"));
|
||||
ASSERT_EQ("old-h", Get("h"));
|
||||
ASSERT_EQ("0,0,0,0,1,1,2", FilesPerLevel());
|
||||
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
ASSERT_EQ("0,0,0,0,1,1,2", FilesPerLevel());
|
||||
|
||||
ReadOptions snapshot_read_options;
|
||||
snapshot_read_options.snapshot = snapshot;
|
||||
std::string value;
|
||||
ASSERT_OK(db_->Get(snapshot_read_options, "c", &value));
|
||||
ASSERT_EQ("old-c", value);
|
||||
ASSERT_OK(db_->Get(snapshot_read_options, "e", &value));
|
||||
ASSERT_EQ("old-e", value);
|
||||
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
ASSERT_EQ("0,0,0,0,1,0,1", FilesPerLevel());
|
||||
ASSERT_EQ("NOT_FOUND", Get("c"));
|
||||
ASSERT_EQ("new-e", Get("e"));
|
||||
ASSERT_EQ("old-f", Get("f"));
|
||||
}
|
||||
|
||||
TEST_F(ExternalSSTFileBasicTest,
|
||||
PartiallyReplaceDataWithMultipleStandaloneRangeDeletions) {
|
||||
Options options = CurrentOptions();
|
||||
@@ -2963,14 +3061,9 @@ TEST_F(ExternalSSTFileBasicTest, FailIfNotBottommostLevelAndDisallowMemtable) {
|
||||
s = db_->IngestExternalFiles({arg});
|
||||
ASSERT_EQ(s.code(), Status::Code::kInvalidArgument);
|
||||
|
||||
// FIXME: upper bound should be exclusive (DeleteRange semantics).
|
||||
// currently rejected because of documented bug
|
||||
// The upper bound is exclusive, so the memtable key "e" does not
|
||||
// overlap the replacement range.
|
||||
arg.atomic_replace_range = {{"a", "e"}};
|
||||
s = db_->IngestExternalFiles({arg});
|
||||
ASSERT_EQ(s.code(), Status::Code::kInvalidArgument);
|
||||
|
||||
// work-around ensuring no memtable overlap
|
||||
arg.atomic_replace_range = {{"a", "d2"}};
|
||||
ASSERT_OK(db_->IngestExternalFiles({arg}));
|
||||
|
||||
ASSERT_EQ(Get(1, "e"), "5");
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
#include "file/random_access_file_reader.h"
|
||||
#include "logging/logging.h"
|
||||
#include "monitoring/statistics_impl.h"
|
||||
#include "options/options_helper.h"
|
||||
#include "table/merging_iterator.h"
|
||||
#include "table/prepared_file_info.h"
|
||||
#include "table/sst_file_writer_collectors.h"
|
||||
@@ -28,6 +29,129 @@
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
bool ExternalSstFileIngestionJob::SupportsAtomicReplaceRangeTombstone() const {
|
||||
return cfd_->is_delete_range_supported() &&
|
||||
cfd_->ioptions().compaction_style == kCompactionStyleUniversal &&
|
||||
ingestion_options_.allow_global_seqno &&
|
||||
!ingestion_options_.allow_db_generated_files &&
|
||||
!ingestion_options_.snapshot_consistency &&
|
||||
ucmp_->timestamp_size() == 0;
|
||||
}
|
||||
|
||||
bool ExternalSstFileIngestionJob::CanUseAtomicReplaceRangeTombstone(
|
||||
const std::optional<RangeOpt>& atomic_replace_range) const {
|
||||
return atomic_replace_range.has_value() &&
|
||||
atomic_replace_range->start.has_value() &&
|
||||
atomic_replace_range->limit.has_value() &&
|
||||
SupportsAtomicReplaceRangeTombstone() &&
|
||||
!ingestion_options_.fail_if_not_bottommost_level;
|
||||
}
|
||||
|
||||
bool ExternalSstFileIngestionJob::HasPartialOverlap(
|
||||
const VersionStorageInfo* vstorage) const {
|
||||
assert(vstorage != nullptr);
|
||||
assert(atomic_replace_range_.has_value());
|
||||
assert(!atomic_replace_range_->unset());
|
||||
for (int level = 0; level < cfd_->NumberLevels(); ++level) {
|
||||
for (const auto* file : vstorage->LevelFiles(level)) {
|
||||
if (file_range_checker_.Overlaps(*atomic_replace_range_, file->smallest,
|
||||
file->largest) &&
|
||||
!file_range_checker_.Contains(*atomic_replace_range_, file->smallest,
|
||||
file->largest)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
size_t ExternalSstFileIngestionJob::NumFilesToPrepare(
|
||||
size_t num_external_files,
|
||||
const std::optional<RangeOpt>& atomic_replace_range) const {
|
||||
return num_external_files +
|
||||
static_cast<size_t>(
|
||||
CanUseAtomicReplaceRangeTombstone(atomic_replace_range));
|
||||
}
|
||||
|
||||
Status ExternalSstFileIngestionJob::PrepareAtomicReplaceRangeTombstone(
|
||||
const Slice& start, const Slice& limit, uint64_t file_number,
|
||||
SuperVersion* super_version) {
|
||||
assert(cfd_->is_delete_range_supported());
|
||||
assert(cfd_->ioptions().compaction_style == kCompactionStyleUniversal);
|
||||
assert(ingestion_options_.allow_global_seqno);
|
||||
assert(!ingestion_options_.allow_db_generated_files);
|
||||
assert(!ingestion_options_.fail_if_not_bottommost_level);
|
||||
assert(!ingestion_options_.snapshot_consistency);
|
||||
assert(ucmp_->timestamp_size() == 0);
|
||||
|
||||
const uint32_t path_id = 0;
|
||||
const std::string file_path =
|
||||
TableFileName(cfd_->ioptions().cf_paths, file_number, path_id);
|
||||
ExternalSstFileInfo external_file_info;
|
||||
Status status;
|
||||
{
|
||||
Options options(
|
||||
BuildDBOptions(db_options_, mutable_db_options_),
|
||||
BuildColumnFamilyOptions(cfd_->initial_cf_options(),
|
||||
super_version->mutable_cf_options));
|
||||
SstFileWriter writer(env_options_, options, ucmp_);
|
||||
status = writer.Open(
|
||||
file_path, super_version->mutable_cf_options.default_write_temperature);
|
||||
if (status.ok()) {
|
||||
status = writer.DeleteRange(start, limit);
|
||||
}
|
||||
if (status.ok()) {
|
||||
status = writer.Finish(&external_file_info);
|
||||
}
|
||||
}
|
||||
if (!status.ok()) {
|
||||
fs_->DeleteFile(file_path, IOOptions(), nullptr).PermitUncheckedError();
|
||||
return status;
|
||||
}
|
||||
|
||||
IngestedFileInfo tombstone;
|
||||
tombstone.file_temperature =
|
||||
super_version->mutable_cf_options.default_write_temperature;
|
||||
tombstone.prefetch_lmax_index_and_filter_blocks =
|
||||
ingestion_options_.prefetch_lmax_index_and_filter_blocks;
|
||||
const PreparedFileInfo* prepared_file_info =
|
||||
ingestion_options_.write_global_seqno
|
||||
? nullptr
|
||||
: external_file_info.prepared_file_info.get();
|
||||
status = GetIngestedFileInfo(file_path, file_number, prepared_file_info,
|
||||
&tombstone, super_version);
|
||||
if (!status.ok()) {
|
||||
fs_->DeleteFile(file_path, IOOptions(), nullptr).PermitUncheckedError();
|
||||
return status;
|
||||
}
|
||||
|
||||
tombstone.internal_file_path = file_path;
|
||||
tombstone.copy_file = true;
|
||||
tombstone.generated_for_ingestion = true;
|
||||
tombstone.file_checksum = external_file_info.file_checksum;
|
||||
tombstone.file_checksum_func_name =
|
||||
external_file_info.file_checksum_func_name;
|
||||
atomic_replace_range_tombstone_.emplace(std::move(tombstone));
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
void ExternalSstFileIngestionJob::ActivateAtomicReplaceRangeTombstone() {
|
||||
assert(atomic_replace_range_tombstone_.has_value());
|
||||
// Prepare rejects an empty external file list and verifies every input file
|
||||
// is contained in the replacement range. The covering tombstone must
|
||||
// therefore overlap at least one input file.
|
||||
assert(!files_to_ingest_.empty());
|
||||
file_batches_to_ingest_.clear();
|
||||
files_to_ingest_.emplace_back(std::move(*atomic_replace_range_tombstone_));
|
||||
std::rotate(files_to_ingest_.begin(), files_to_ingest_.end() - 1,
|
||||
files_to_ingest_.end());
|
||||
atomic_replace_range_tombstone_.reset();
|
||||
atomic_replace_range_tombstone_active_ = true;
|
||||
files_overlap_ = ComputeFilesOverlap(files_to_ingest_);
|
||||
assert(files_overlap_);
|
||||
DivideInputFilesIntoBatches();
|
||||
}
|
||||
|
||||
Status ExternalSstFileIngestionJob::Prepare(
|
||||
const std::vector<std::string>& external_files_paths,
|
||||
const std::vector<std::string>& files_checksums,
|
||||
@@ -92,6 +216,11 @@ Status ExternalSstFileIngestionJob::Prepare(
|
||||
atomic_replace_range_.emplace();
|
||||
|
||||
if (atomic_replace_range->start && atomic_replace_range->limit) {
|
||||
if (ucmp_->CompareWithoutTimestamp(*atomic_replace_range->start,
|
||||
*atomic_replace_range->limit) >= 0) {
|
||||
return Status::InvalidArgument(
|
||||
"Atomic replace range limit must be greater than start");
|
||||
}
|
||||
// User keys to internal keys (with timestamps)
|
||||
const size_t ts_sz = ucmp_->timestamp_size();
|
||||
std::string start_with_ts, limit_with_ts;
|
||||
@@ -102,8 +231,8 @@ Status ExternalSstFileIngestionJob::Prepare(
|
||||
assert(limit.has_value());
|
||||
atomic_replace_range_->smallest_internal_key.Set(
|
||||
*start, kMaxSequenceNumber, kValueTypeForSeek);
|
||||
atomic_replace_range_->largest_internal_key.Set(
|
||||
*limit, kMaxSequenceNumber, kValueTypeForSeek);
|
||||
atomic_replace_range_->largest_internal_key =
|
||||
RangeTombstone(*start, *limit, kMaxSequenceNumber).SerializeEndKey();
|
||||
// Check files to ingest against replace range
|
||||
for (size_t i = 0; i < num_files; i++) {
|
||||
if (!file_range_checker_.Contains(*atomic_replace_range_,
|
||||
@@ -232,6 +361,20 @@ Status ExternalSstFileIngestionJob::Prepare(
|
||||
ingestion_path_ids.insert(f.fd.GetPathId());
|
||||
}
|
||||
|
||||
if (status.ok() && CanUseAtomicReplaceRangeTombstone(atomic_replace_range) &&
|
||||
HasPartialOverlap(sv->current->storage_info())) {
|
||||
assert(atomic_replace_range->start.has_value());
|
||||
assert(atomic_replace_range->limit.has_value());
|
||||
status = PrepareAtomicReplaceRangeTombstone(*atomic_replace_range->start,
|
||||
*atomic_replace_range->limit,
|
||||
next_file_number, sv);
|
||||
if (status.ok()) {
|
||||
assert(atomic_replace_range_tombstone_.has_value());
|
||||
ingestion_path_ids.insert(
|
||||
atomic_replace_range_tombstone_->fd.GetPathId());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SYNC_POINT("ExternalSstFileIngestionJob::BeforeSyncDir");
|
||||
if (status.ok()) {
|
||||
for (auto path_id : ingestion_path_ids) {
|
||||
@@ -493,9 +636,7 @@ Status ExternalSstFileIngestionJob::NeedsFlush(bool* flush_needed,
|
||||
assert(!atomic_replace_range_->smallest_internal_key.unset());
|
||||
assert(!atomic_replace_range_->largest_internal_key.unset());
|
||||
// NOTE: we already checked in Prepare() that the atomic_replace_range
|
||||
// covers all the files_to_ingest
|
||||
// FIXME: need to make upper bound key exclusive (not easy here because
|
||||
// the existing internal APIs deal in inclusive upper bound user keys)
|
||||
// covers all the files_to_ingest.
|
||||
ranges.emplace_back(
|
||||
atomic_replace_range_->smallest_internal_key.user_key(),
|
||||
atomic_replace_range_->largest_internal_key.user_key());
|
||||
@@ -507,7 +648,8 @@ Status ExternalSstFileIngestionJob::NeedsFlush(bool* flush_needed,
|
||||
}
|
||||
}
|
||||
status = cfd_->RangesOverlapWithMemtables(
|
||||
ranges, super_version, db_options_.allow_data_in_errors, flush_needed);
|
||||
ranges, super_version, db_options_.allow_data_in_errors, flush_needed,
|
||||
atomic_replace_range_.has_value() && !atomic_replace_range_->unset());
|
||||
if (!status.ok()) {
|
||||
ROCKS_LOG_WARN(db_options_.info_log,
|
||||
"Failed to check ranges overlap with memtables: %s",
|
||||
@@ -586,10 +728,12 @@ Status ExternalSstFileIngestionJob::Run() {
|
||||
} else {
|
||||
assert(!atomic_replace_range_->smallest_internal_key.unset());
|
||||
assert(!atomic_replace_range_->largest_internal_key.unset());
|
||||
bool has_partial_overlap = false;
|
||||
for (int lvl = 0; lvl < cfd_->NumberLevels(); lvl++) {
|
||||
if (cfd_->RangeOverlapWithCompaction(
|
||||
atomic_replace_range_->smallest_internal_key.user_key(),
|
||||
atomic_replace_range_->largest_internal_key.user_key(), lvl)) {
|
||||
atomic_replace_range_->largest_internal_key.user_key(), lvl,
|
||||
/*range_limit_exclusive=*/true)) {
|
||||
return Status::InvalidArgument(
|
||||
"Atomic replace range overlaps with pending compaction");
|
||||
}
|
||||
@@ -601,20 +745,36 @@ Status ExternalSstFileIngestionJob::Run() {
|
||||
// Set up to delete file to be replaced
|
||||
edit_.DeleteFile(lvl, file->fd.GetNumber());
|
||||
} else {
|
||||
// TODO: generate and ingest a tombstone file also
|
||||
return Status::InvalidArgument(
|
||||
"Atomic replace range partially overlaps with existing file");
|
||||
has_partial_overlap = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (has_partial_overlap) {
|
||||
if (ingestion_options_.fail_if_not_bottommost_level &&
|
||||
SupportsAtomicReplaceRangeTombstone()) {
|
||||
return Status::TryAgain(
|
||||
"Atomic replace range partially overlaps with an existing file, "
|
||||
"so replacement files cannot all be ingested to Lmax");
|
||||
}
|
||||
if (!atomic_replace_range_tombstone_.has_value()) {
|
||||
if (SupportsAtomicReplaceRangeTombstone()) {
|
||||
return Status::TryAgain(
|
||||
"Atomic replace range acquired a partial overlap after "
|
||||
"preparation; retry ingestion");
|
||||
}
|
||||
return Status::InvalidArgument(
|
||||
"Atomic replace range partially overlaps with existing file");
|
||||
}
|
||||
ActivateAtomicReplaceRangeTombstone();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Find levels to ingest into
|
||||
std::optional<int> prev_batch_uppermost_level;
|
||||
// batches at the front of file_batches_to_ingest_ contains older updates and
|
||||
// are placed in smaller levels.
|
||||
// Batches at the front contain older updates and are placed deeper in the
|
||||
// LSM tree than later overlapping batches.
|
||||
for (auto& batch : file_batches_to_ingest_) {
|
||||
int batch_uppermost_level = 0;
|
||||
status = AssignLevelsForOneBatch(batch, super_version, force_global_seqno,
|
||||
@@ -927,9 +1087,27 @@ void ExternalSstFileIngestionJob::Cleanup(const Status& status) {
|
||||
// remove all the files we copied
|
||||
DeleteInternalFiles();
|
||||
files_overlap_ = false;
|
||||
} else if (status.ok() && ingestion_options_.move_files) {
|
||||
} else {
|
||||
if (atomic_replace_range_tombstone_.has_value()) {
|
||||
Status s =
|
||||
fs_->DeleteFile(atomic_replace_range_tombstone_->internal_file_path,
|
||||
io_opts, nullptr);
|
||||
if (!s.ok()) {
|
||||
ROCKS_LOG_WARN(
|
||||
db_options_.info_log,
|
||||
"Failed to remove unused atomic replace range tombstone %s: %s",
|
||||
atomic_replace_range_tombstone_->internal_file_path.c_str(),
|
||||
s.ToString().c_str());
|
||||
}
|
||||
atomic_replace_range_tombstone_.reset();
|
||||
}
|
||||
}
|
||||
if (status.ok() && ingestion_options_.move_files) {
|
||||
// The files were moved and added successfully, remove original file links
|
||||
for (IngestedFileInfo& f : files_to_ingest_) {
|
||||
if (f.generated_for_ingestion) {
|
||||
continue;
|
||||
}
|
||||
Status s = fs_->DeleteFile(f.external_file_path, io_opts, nullptr);
|
||||
if (!s.ok()) {
|
||||
ROCKS_LOG_WARN(
|
||||
@@ -955,6 +1133,19 @@ void ExternalSstFileIngestionJob::DeleteInternalFiles() {
|
||||
f.internal_file_path.c_str(), s.ToString().c_str());
|
||||
}
|
||||
}
|
||||
if (atomic_replace_range_tombstone_.has_value() &&
|
||||
!atomic_replace_range_tombstone_->internal_file_path.empty()) {
|
||||
Status s = fs_->DeleteFile(
|
||||
atomic_replace_range_tombstone_->internal_file_path, io_opts, nullptr);
|
||||
if (!s.ok()) {
|
||||
ROCKS_LOG_WARN(
|
||||
db_options_.info_log,
|
||||
"AddFile() clean up for generated tombstone file %s failed : %s",
|
||||
atomic_replace_range_tombstone_->internal_file_path.c_str(),
|
||||
s.ToString().c_str());
|
||||
}
|
||||
atomic_replace_range_tombstone_.reset();
|
||||
}
|
||||
}
|
||||
|
||||
Status ExternalSstFileIngestionJob::ResetTableReader(
|
||||
@@ -965,6 +1156,7 @@ Status ExternalSstFileIngestionJob::ResetTableReader(
|
||||
std::unique_ptr<FSRandomAccessFile> sst_file;
|
||||
FileOptions fo{env_options_};
|
||||
fo.temperature = file_to_ingest->file_temperature;
|
||||
fo.file_checksum_func_name = kNoFileChecksumFuncName;
|
||||
Status status =
|
||||
fs_->NewRandomAccessFile(external_file, fo, &sst_file, nullptr);
|
||||
if (!status.ok()) {
|
||||
@@ -1511,17 +1703,23 @@ Status ExternalSstFileIngestionJob::AssignLevelAndSeqnoForIngestedFile(
|
||||
ingestion_options_.allow_db_generated_files
|
||||
? cfd_->NumberLevels()
|
||||
: assigned_level_exclusive_end;
|
||||
const bool is_generated_range_tombstone =
|
||||
atomic_replace_range_tombstone_active_ &&
|
||||
file_to_ingest->generated_for_ingestion;
|
||||
for (int lvl = 0; lvl < overlap_checking_exclusive_end; lvl++) {
|
||||
if (lvl > 0 && lvl < vstorage->base_level()) {
|
||||
continue;
|
||||
}
|
||||
if (lvl < assigned_level_exclusive_end &&
|
||||
atomic_replace_range_.has_value()) {
|
||||
atomic_replace_range_.has_value() &&
|
||||
!atomic_replace_range_tombstone_active_) {
|
||||
target_level = lvl;
|
||||
continue;
|
||||
}
|
||||
if (cfd_->RangeOverlapWithCompaction(file_to_ingest->start_ukey,
|
||||
file_to_ingest->limit_ukey, lvl)) {
|
||||
file_to_ingest->limit_ukey, lvl,
|
||||
/*range_limit_exclusive=*/
|
||||
is_generated_range_tombstone)) {
|
||||
// We must use L0 or any level higher than `lvl` to be able to overwrite
|
||||
// the compaction output keys that we overlap with in this level, We also
|
||||
// need to assign this file a seqno to overwrite the compaction output
|
||||
|
||||
@@ -82,12 +82,12 @@ class ExternalFileRangeChecker {
|
||||
range2_largest) <= 0;
|
||||
}
|
||||
|
||||
bool Contains(const KeyRangeInfo& range1, const KeyRangeInfo& range2) {
|
||||
bool Contains(const KeyRangeInfo& range1, const KeyRangeInfo& range2) const {
|
||||
return Contains(range1, range2.smallest_internal_key,
|
||||
range2.largest_internal_key);
|
||||
}
|
||||
bool Contains(const KeyRangeInfo& range1, const InternalKey& range2_smallest,
|
||||
const InternalKey& range2_largest) {
|
||||
const InternalKey& range2_largest) const {
|
||||
bool any_unset =
|
||||
range1.unset() || range2_smallest.unset() || range2_largest.unset();
|
||||
if (any_unset) {
|
||||
@@ -184,6 +184,11 @@ struct IngestedFileInfo : public KeyRangeInfo {
|
||||
// Whether Lmax commit-time table opening should prefetch index/filter blocks.
|
||||
bool prefetch_lmax_index_and_filter_blocks = true;
|
||||
|
||||
// Whether RocksDB generated this file as part of the ingestion job. Such a
|
||||
// file is already in the DB directory and must not be unlinked as a caller
|
||||
// input when move_files is enabled.
|
||||
bool generated_for_ingestion = false;
|
||||
|
||||
SequenceNumber largest_seqno = kMaxSequenceNumber;
|
||||
SequenceNumber smallest_seqno = kMaxSequenceNumber;
|
||||
};
|
||||
@@ -311,6 +316,10 @@ class ExternalSstFileIngestionJob {
|
||||
return ingestion_options_.file_opening_threads;
|
||||
}
|
||||
|
||||
size_t NumFilesToPrepare(
|
||||
size_t num_external_files,
|
||||
const std::optional<RangeOpt>& atomic_replace_range) const;
|
||||
|
||||
// Merge another already-Prepare()d job for the SAME column family into this
|
||||
// one so both sets of files are committed by a single Run(). The other job's
|
||||
// files are appended after this job's, so for any overlapping keys the other
|
||||
@@ -320,6 +329,20 @@ class ExternalSstFileIngestionJob {
|
||||
Status MergeForSameColumnFamily(ExternalSstFileIngestionJob* other);
|
||||
|
||||
private:
|
||||
bool SupportsAtomicReplaceRangeTombstone() const;
|
||||
|
||||
bool CanUseAtomicReplaceRangeTombstone(
|
||||
const std::optional<RangeOpt>& atomic_replace_range) const;
|
||||
|
||||
bool HasPartialOverlap(const VersionStorageInfo* vstorage) const;
|
||||
|
||||
Status PrepareAtomicReplaceRangeTombstone(const Slice& start,
|
||||
const Slice& limit,
|
||||
uint64_t file_number,
|
||||
SuperVersion* super_version);
|
||||
|
||||
void ActivateAtomicReplaceRangeTombstone();
|
||||
|
||||
Status ResetTableReader(const std::string& external_file,
|
||||
uint64_t new_file_number,
|
||||
bool user_defined_timestamps_persisted,
|
||||
@@ -451,6 +474,8 @@ class ExternalSstFileIngestionJob {
|
||||
std::vector<FileBatchInfo> file_batches_to_ingest_;
|
||||
const IngestExternalFileOptions ingestion_options_;
|
||||
std::optional<KeyRangeInfo> atomic_replace_range_;
|
||||
std::optional<IngestedFileInfo> atomic_replace_range_tombstone_;
|
||||
bool atomic_replace_range_tombstone_active_{false};
|
||||
Directories* directories_;
|
||||
EventLogger* event_logger_;
|
||||
VersionEdit edit_;
|
||||
|
||||
@@ -18,8 +18,10 @@
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/sst_file_reader.h"
|
||||
#include "rocksdb/sst_file_writer.h"
|
||||
#include "table/format.h"
|
||||
#include "table/prepared_file_info.h"
|
||||
#include "test_util/testutil.h"
|
||||
#include "util/defer.h"
|
||||
#include "util/random.h"
|
||||
#include "util/thread_guard.h"
|
||||
#include "utilities/fault_injection_env.h"
|
||||
@@ -431,6 +433,47 @@ TEST_F(ExternalSSTFileTest, PrepareThenCommit) {
|
||||
ASSERT_EQ("v3", Get("k3"));
|
||||
}
|
||||
|
||||
TEST_F(ExternalSSTFileTest,
|
||||
AtomicReplaceRangeRetriesIfPartialOverlapAppearsAfterPrepare) {
|
||||
Options options = CurrentOptions();
|
||||
options.compaction_style = kCompactionStyleUniversal;
|
||||
options.disable_auto_compactions = true;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(
|
||||
GenerateAndAddExternalFile(options, {{"c", "old-c"}, {"d", "old-d"}}));
|
||||
|
||||
std::string replacement_file;
|
||||
ASSERT_OK(GenerateExternalFileOnly(options, {{"c", "new-c"}, {"d", "new-d"}},
|
||||
&replacement_file));
|
||||
|
||||
IngestExternalFileArg arg;
|
||||
arg.column_family = db_->DefaultColumnFamily();
|
||||
arg.external_files = {replacement_file};
|
||||
arg.options.allow_global_seqno = true;
|
||||
arg.options.snapshot_consistency = false;
|
||||
arg.atomic_replace_range = {{"b", "e"}};
|
||||
|
||||
std::unique_ptr<FileIngestionHandle> handle;
|
||||
ASSERT_OK(db_->PrepareFileIngestion({arg}, &handle));
|
||||
|
||||
ASSERT_OK(
|
||||
GenerateAndAddExternalFile(options, {{"a", "old-a"}, {"b", "old-b"}}));
|
||||
|
||||
Status status = db_->CommitFileIngestionHandle(std::move(handle));
|
||||
ASSERT_TRUE(status.IsTryAgain()) << status.ToString();
|
||||
ASSERT_EQ("old-a", Get("a"));
|
||||
ASSERT_EQ("old-b", Get("b"));
|
||||
ASSERT_EQ("old-c", Get("c"));
|
||||
ASSERT_EQ("old-d", Get("d"));
|
||||
|
||||
ASSERT_OK(db_->IngestExternalFiles({arg}));
|
||||
ASSERT_EQ("old-a", Get("a"));
|
||||
ASSERT_EQ("NOT_FOUND", Get("b"));
|
||||
ASSERT_EQ("new-c", Get("c"));
|
||||
ASSERT_EQ("new-d", Get("d"));
|
||||
}
|
||||
|
||||
TEST_F(ExternalSSTFileTest, ParallelFileOpenWithFileOpeningThreads) {
|
||||
// Ingest many non-overlapping files with file_opening_threads > 1 so commit
|
||||
// opens table readers with multiple threads. max_open_files == -1 makes
|
||||
@@ -3528,6 +3571,9 @@ INSTANTIATE_TEST_CASE_P(FormatVersions, ExternalSSTBlockChecksumTest,
|
||||
TEST_P(ExternalSSTBlockChecksumTest, DISABLED_HugeBlockChecksum) {
|
||||
BlockBasedTableOptions table_options;
|
||||
table_options.format_version = GetParam();
|
||||
// kFooterFormatVersionsToTest includes the unpublished draft format_version
|
||||
// 8; writing it requires this opt-in.
|
||||
SaveAndRestore<bool> allow_draft(&TEST_AllowUnsupportedFormatVersion(), true);
|
||||
for (auto t : GetSupportedChecksums()) {
|
||||
table_options.checksum = t;
|
||||
Options options = CurrentOptions();
|
||||
|
||||
@@ -196,7 +196,8 @@ Status ImportColumnFamilyJob::Run() {
|
||||
nullptr /* src_vstorage */, cfd_->ioptions().force_consistency_checks,
|
||||
EpochNumberRequirement::kMightMissing, cfd_->ioptions().clock,
|
||||
cfd_->GetLatestMutableCFOptions().bottommost_file_compaction_delay,
|
||||
cfd_->current()->version_set()->offpeak_time_option());
|
||||
cfd_->current()->version_set()->offpeak_time_option(),
|
||||
PeriodicCompactionPhaseParams{});
|
||||
for (size_t j = 0; s.ok() && j < files_to_import_[i].size(); ++j) {
|
||||
const auto& f = files_to_import_[i][j];
|
||||
const auto& file_metadata = *metadatas_[i][j];
|
||||
|
||||
+152
-42
@@ -1307,6 +1307,8 @@ struct Saver {
|
||||
bool* is_blob_index;
|
||||
bool allow_data_in_errors;
|
||||
uint32_t protection_bytes_per_key;
|
||||
// MultiGet's per-key result. Single-key Get stores its result on callback_.
|
||||
bool* newer_version_present;
|
||||
bool CheckCallback(SequenceNumber _seq) {
|
||||
if (callback_) {
|
||||
return callback_->IsVisible(_seq);
|
||||
@@ -1366,6 +1368,12 @@ static bool SaveValue(void* arg, const char* entry) {
|
||||
ValueType type;
|
||||
SequenceNumber seq;
|
||||
UnPackSequenceAndType(tag, &seq, &type);
|
||||
if (UNLIKELY(
|
||||
s->callback_ != nullptr &&
|
||||
s->callback_->NeedToTrackNewerVersions(s->newer_version_present))) {
|
||||
s->callback_->MaybeRecordNewerVersion(seq, type,
|
||||
s->newer_version_present);
|
||||
}
|
||||
// If the value is not in the snapshot, skip it
|
||||
if (!s->CheckCallback(seq)) {
|
||||
return true; // to continue to the next seq
|
||||
@@ -1581,9 +1589,32 @@ bool MemTable::Get(const LookupKey& key, std::string* value,
|
||||
|
||||
PERF_TIMER_GUARD(get_from_memtable_time);
|
||||
|
||||
// Metadata reads widen the lookup seq to detect newer range tombstones. The
|
||||
// regular masking iterator must stay at the user's snapshot seq so such
|
||||
// tombstones only set metadata and do not hide snapshot-visible values.
|
||||
const SequenceNumber lookup_seq = GetInternalKeySeqno(key.internal_key());
|
||||
const MetadataReadBounds* metadata_read_bounds =
|
||||
callback != nullptr ? callback->GetMetadataReadBounds() : nullptr;
|
||||
const SequenceNumber range_del_read_seq =
|
||||
metadata_read_bounds != nullptr ? metadata_read_bounds->read_snapshot_seq
|
||||
: lookup_seq;
|
||||
assert(range_del_read_seq <= lookup_seq);
|
||||
if (callback != nullptr && callback->NeedToTrackNewerVersions() &&
|
||||
!is_range_del_table_empty_.LoadRelaxed()) {
|
||||
std::unique_ptr<FragmentedRangeTombstoneIterator> latest_range_del_iter(
|
||||
NewRangeTombstoneIteratorInternal(read_opts, lookup_seq,
|
||||
immutable_memtable));
|
||||
const SequenceNumber covering_seq =
|
||||
latest_range_del_iter != nullptr
|
||||
? latest_range_del_iter->MaxCoveringTombstoneSeqnum(key.user_key(),
|
||||
callback)
|
||||
: 0;
|
||||
if (covering_seq != 0) {
|
||||
callback->MaybeRecordNewerVersion(covering_seq, kTypeRangeDeletion);
|
||||
}
|
||||
}
|
||||
std::unique_ptr<FragmentedRangeTombstoneIterator> range_del_iter(
|
||||
NewRangeTombstoneIterator(read_opts,
|
||||
GetInternalKeySeqno(key.internal_key()),
|
||||
NewRangeTombstoneIterator(read_opts, range_del_read_seq,
|
||||
immutable_memtable));
|
||||
if (range_del_iter != nullptr) {
|
||||
SequenceNumber covering_seq =
|
||||
@@ -1646,15 +1677,13 @@ bool MemTable::Get(const LookupKey& key, std::string* value,
|
||||
return found_final_value;
|
||||
}
|
||||
|
||||
void MemTable::GetFromTable(const LookupKey& key,
|
||||
SequenceNumber max_covering_tombstone_seq,
|
||||
bool do_merge, ReadCallback* callback,
|
||||
bool* is_blob_index, std::string* value,
|
||||
PinnableWideColumns* columns,
|
||||
std::string* timestamp, Status* s,
|
||||
MergeContext* merge_context, SequenceNumber* seq,
|
||||
bool* found_final_value, bool* merge_in_progress,
|
||||
const BlobFetcher* blob_fetcher) {
|
||||
void MemTable::GetFromTable(
|
||||
const LookupKey& key, SequenceNumber max_covering_tombstone_seq,
|
||||
bool do_merge, ReadCallback* callback, bool* is_blob_index,
|
||||
std::string* value, PinnableWideColumns* columns, std::string* timestamp,
|
||||
Status* s, MergeContext* merge_context, SequenceNumber* seq,
|
||||
bool* found_final_value, bool* merge_in_progress,
|
||||
const BlobFetcher* blob_fetcher, bool* newer_version_present) {
|
||||
Saver saver;
|
||||
saver.status = s;
|
||||
saver.found_final_value = found_final_value;
|
||||
@@ -1678,6 +1707,7 @@ void MemTable::GetFromTable(const LookupKey& key,
|
||||
saver.do_merge = do_merge;
|
||||
saver.allow_data_in_errors = moptions_.allow_data_in_errors;
|
||||
saver.protection_bytes_per_key = moptions_.protection_bytes_per_key;
|
||||
saver.newer_version_present = newer_version_present;
|
||||
|
||||
if (!moptions_.paranoid_memory_checks &&
|
||||
!moptions_.memtable_verify_per_key_checksum_on_seek) {
|
||||
@@ -1711,13 +1741,25 @@ void MemTable::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
||||
}
|
||||
PERF_TIMER_GUARD(get_from_memtable_time);
|
||||
|
||||
// For now, memtable Bloom filter is effectively disabled if there are any
|
||||
// range tombstones. This is the simplest way to ensure range tombstones are
|
||||
// handled. TODO: allow Bloom checks where max_covering_tombstone_seq==0
|
||||
bool no_range_del = read_options.ignore_range_deletions ||
|
||||
is_range_del_table_empty_.LoadRelaxed();
|
||||
MultiGetRange temp_range(*range, range->begin(), range->end());
|
||||
if (bloom_filter_ && no_range_del) {
|
||||
const bool has_range_del = !is_range_del_table_empty_.LoadRelaxed();
|
||||
const bool apply_range_del =
|
||||
has_range_del && !read_options.ignore_range_deletions;
|
||||
bool track_range_del_metadata = false;
|
||||
if (has_range_del && callback != nullptr &&
|
||||
callback->GetMetadataReadBounds() != nullptr) {
|
||||
for (auto iter = temp_range.begin(); iter != temp_range.end(); ++iter) {
|
||||
if (callback->NeedToTrackNewerVersions(&iter->newer_version_present)) {
|
||||
track_range_del_metadata = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For now, memtable Bloom filter is effectively disabled if there are range
|
||||
// tombstones that must affect either read results or metadata.
|
||||
// TODO: allow Bloom checks where max_covering_tombstone_seq==0.
|
||||
if (bloom_filter_ && !apply_range_del && !track_range_del_metadata) {
|
||||
bool whole_key =
|
||||
!prefix_extractor_ || moptions_.memtable_whole_key_filtering;
|
||||
std::array<Slice, MultiGetContext::MAX_BATCH_SIZE> bloom_keys;
|
||||
@@ -1750,6 +1792,25 @@ void MemTable::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
||||
bool validate = moptions_.paranoid_memory_checks ||
|
||||
moptions_.memtable_verify_per_key_checksum_on_seek;
|
||||
|
||||
// For metadata-tracking reads, allocate one "latest" range tombstone
|
||||
// iterator per memtable batch (highest seq across the keys that requested
|
||||
// metadata that has not already observed newer data) instead of one per key.
|
||||
std::unique_ptr<FragmentedRangeTombstoneIterator> latest_range_del_iter;
|
||||
if (track_range_del_metadata) {
|
||||
SequenceNumber latest_range_del_read_seq = 0;
|
||||
for (auto iter = temp_range.begin(); iter != temp_range.end(); ++iter) {
|
||||
if (callback->NeedToTrackNewerVersions(&iter->newer_version_present)) {
|
||||
latest_range_del_read_seq =
|
||||
std::max(latest_range_del_read_seq,
|
||||
GetInternalKeySeqno(iter->lkey->internal_key()));
|
||||
}
|
||||
}
|
||||
if (latest_range_del_read_seq > 0) {
|
||||
latest_range_del_iter.reset(NewRangeTombstoneIteratorInternal(
|
||||
read_options, latest_range_del_read_seq, immutable_memtable));
|
||||
}
|
||||
}
|
||||
|
||||
if (use_batch_optimization) {
|
||||
// Phase 1: Handle range tombstones and set up Savers for batched lookup
|
||||
std::array<Saver, MultiGetContext::MAX_BATCH_SIZE> savers{};
|
||||
@@ -1760,18 +1821,39 @@ void MemTable::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
||||
size_t num_keys = 0;
|
||||
|
||||
for (auto iter = temp_range.begin(); iter != temp_range.end(); ++iter) {
|
||||
if (!no_range_del) {
|
||||
std::unique_ptr<FragmentedRangeTombstoneIterator> range_del_iter(
|
||||
NewRangeTombstoneIteratorInternal(
|
||||
read_options, GetInternalKeySeqno(iter->lkey->internal_key()),
|
||||
immutable_memtable));
|
||||
SequenceNumber covering_seq =
|
||||
range_del_iter->MaxCoveringTombstoneSeqnum(iter->lkey->user_key());
|
||||
if (covering_seq > iter->max_covering_tombstone_seq) {
|
||||
iter->max_covering_tombstone_seq = covering_seq;
|
||||
if (iter->timestamp) {
|
||||
iter->timestamp->assign(range_del_iter->timestamp().data(),
|
||||
range_del_iter->timestamp().size());
|
||||
if (apply_range_del || latest_range_del_iter != nullptr) {
|
||||
if (latest_range_del_iter != nullptr &&
|
||||
callback->NeedToTrackNewerVersions(&iter->newer_version_present)) {
|
||||
const SequenceNumber covering_seq =
|
||||
latest_range_del_iter->MaxCoveringTombstoneSeqnum(
|
||||
iter->lkey->user_key(), callback);
|
||||
if (covering_seq != 0) {
|
||||
callback->MaybeRecordNewerVersion(covering_seq, kTypeRangeDeletion,
|
||||
&iter->newer_version_present);
|
||||
}
|
||||
}
|
||||
if (apply_range_del) {
|
||||
const SequenceNumber lookup_seq =
|
||||
GetInternalKeySeqno(iter->lkey->internal_key());
|
||||
const MetadataReadBounds* metadata_read_bounds =
|
||||
callback != nullptr ? callback->GetMetadataReadBounds() : nullptr;
|
||||
const SequenceNumber range_del_read_seq =
|
||||
metadata_read_bounds != nullptr
|
||||
? metadata_read_bounds->read_snapshot_seq
|
||||
: lookup_seq;
|
||||
assert(range_del_read_seq <= lookup_seq);
|
||||
std::unique_ptr<FragmentedRangeTombstoneIterator> range_del_iter(
|
||||
NewRangeTombstoneIteratorInternal(
|
||||
read_options, range_del_read_seq, immutable_memtable));
|
||||
SequenceNumber covering_seq =
|
||||
range_del_iter->MaxCoveringTombstoneSeqnum(
|
||||
iter->lkey->user_key());
|
||||
if (covering_seq > iter->max_covering_tombstone_seq) {
|
||||
iter->max_covering_tombstone_seq = covering_seq;
|
||||
if (iter->timestamp) {
|
||||
iter->timestamp->assign(range_del_iter->timestamp().data(),
|
||||
range_del_iter->timestamp().size());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1801,6 +1883,10 @@ void MemTable::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
||||
saver.do_merge = true;
|
||||
saver.allow_data_in_errors = moptions_.allow_data_in_errors;
|
||||
saver.protection_bytes_per_key = moptions_.protection_bytes_per_key;
|
||||
saver.newer_version_present =
|
||||
callback != nullptr && callback->GetMetadataReadBounds() != nullptr
|
||||
? &iter->newer_version_present
|
||||
: nullptr;
|
||||
|
||||
memtable_keys[num_keys] = iter->lkey->memtable_key().data();
|
||||
callback_args[num_keys] = &savers[num_keys];
|
||||
@@ -1867,18 +1953,39 @@ void MemTable::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
||||
for (auto iter = temp_range.begin(); iter != temp_range.end(); ++iter) {
|
||||
bool found_final_value{false};
|
||||
bool merge_in_progress = iter->s->IsMergeInProgress();
|
||||
if (!no_range_del) {
|
||||
std::unique_ptr<FragmentedRangeTombstoneIterator> range_del_iter(
|
||||
NewRangeTombstoneIteratorInternal(
|
||||
read_options, GetInternalKeySeqno(iter->lkey->internal_key()),
|
||||
immutable_memtable));
|
||||
SequenceNumber covering_seq =
|
||||
range_del_iter->MaxCoveringTombstoneSeqnum(iter->lkey->user_key());
|
||||
if (covering_seq > iter->max_covering_tombstone_seq) {
|
||||
iter->max_covering_tombstone_seq = covering_seq;
|
||||
if (iter->timestamp) {
|
||||
iter->timestamp->assign(range_del_iter->timestamp().data(),
|
||||
range_del_iter->timestamp().size());
|
||||
if (apply_range_del || latest_range_del_iter != nullptr) {
|
||||
if (latest_range_del_iter != nullptr &&
|
||||
callback->NeedToTrackNewerVersions(&iter->newer_version_present)) {
|
||||
const SequenceNumber covering_seq =
|
||||
latest_range_del_iter->MaxCoveringTombstoneSeqnum(
|
||||
iter->lkey->user_key(), callback);
|
||||
if (covering_seq != 0) {
|
||||
callback->MaybeRecordNewerVersion(covering_seq, kTypeRangeDeletion,
|
||||
&iter->newer_version_present);
|
||||
}
|
||||
}
|
||||
if (apply_range_del) {
|
||||
const SequenceNumber lookup_seq =
|
||||
GetInternalKeySeqno(iter->lkey->internal_key());
|
||||
const MetadataReadBounds* metadata_read_bounds =
|
||||
callback != nullptr ? callback->GetMetadataReadBounds() : nullptr;
|
||||
const SequenceNumber range_del_read_seq =
|
||||
metadata_read_bounds != nullptr
|
||||
? metadata_read_bounds->read_snapshot_seq
|
||||
: lookup_seq;
|
||||
assert(range_del_read_seq <= lookup_seq);
|
||||
std::unique_ptr<FragmentedRangeTombstoneIterator> range_del_iter(
|
||||
NewRangeTombstoneIteratorInternal(
|
||||
read_options, range_del_read_seq, immutable_memtable));
|
||||
SequenceNumber covering_seq =
|
||||
range_del_iter->MaxCoveringTombstoneSeqnum(
|
||||
iter->lkey->user_key());
|
||||
if (covering_seq > iter->max_covering_tombstone_seq) {
|
||||
iter->max_covering_tombstone_seq = covering_seq;
|
||||
if (iter->timestamp) {
|
||||
iter->timestamp->assign(range_del_iter->timestamp().data(),
|
||||
range_del_iter->timestamp().size());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1887,7 +1994,10 @@ void MemTable::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
||||
*(iter->lkey), iter->max_covering_tombstone_seq, true, callback,
|
||||
&iter->is_blob_index, iter->value ? iter->value->GetSelf() : nullptr,
|
||||
iter->columns, iter->timestamp, iter->s, &(iter->merge_context),
|
||||
&dummy_seq, &found_final_value, &merge_in_progress, blob_fetcher);
|
||||
&dummy_seq, &found_final_value, &merge_in_progress, blob_fetcher,
|
||||
callback != nullptr && callback->GetMetadataReadBounds() != nullptr
|
||||
? &iter->newer_version_present
|
||||
: nullptr);
|
||||
|
||||
if (!found_final_value && merge_in_progress) {
|
||||
if (iter->s->ok()) {
|
||||
|
||||
+2
-1
@@ -1001,7 +1001,8 @@ class MemTable final : public ReadOnlyMemTable {
|
||||
std::string* timestamp, Status* s,
|
||||
MergeContext* merge_context, SequenceNumber* seq,
|
||||
bool* found_final_value, bool* merge_in_progress,
|
||||
const BlobFetcher* blob_fetcher);
|
||||
const BlobFetcher* blob_fetcher,
|
||||
bool* newer_version_present = nullptr);
|
||||
|
||||
// Always returns non-null and assumes certain pre-checks (e.g.,
|
||||
// is_range_del_table_empty_) are done. This is only valid during the lifetime
|
||||
|
||||
+5
-4
@@ -240,10 +240,11 @@ bool MemTableListVersion::GetFromList(
|
||||
assert(memtable->IsFragmentedRangeTombstonesConstructed());
|
||||
SequenceNumber current_seq = kMaxSequenceNumber;
|
||||
|
||||
bool done = memtable->Get(key, value, columns, timestamp, s, merge_context,
|
||||
max_covering_tombstone_seq, ¤t_seq,
|
||||
read_opts, true /* immutable_memtable */,
|
||||
callback, is_blob_index, true, blob_fetcher);
|
||||
bool done =
|
||||
memtable->Get(key, value, columns, timestamp, s, merge_context,
|
||||
max_covering_tombstone_seq, ¤t_seq, read_opts,
|
||||
true /* immutable_memtable */, callback, is_blob_index,
|
||||
true /* do_merge */, blob_fetcher);
|
||||
if (*seq == kMaxSequenceNumber) {
|
||||
// Store the most recent sequence number of any operation on this key.
|
||||
// Since we only care about the most recent change, we only need to
|
||||
|
||||
+17
-4
@@ -510,13 +510,26 @@ class MemTableList {
|
||||
}
|
||||
}
|
||||
|
||||
// Used only by DBImplSecondary during log replay.
|
||||
// Remove memtables whose data were written before the WAL with log_number
|
||||
// was created, i.e. mem->GetNextLogNumber() <= log_number. The memtables are
|
||||
// not freed, but put into a vector for future deref and reclamation.
|
||||
// Used only by secondary and follower instances, which collect memtables by
|
||||
// the log number that follows their contents rather than by flushing them.
|
||||
// Scanning from the oldest memtable, removes those with
|
||||
// mem->GetNextLogNumber() <= log_number and stops at the first one without,
|
||||
// so a memtable is kept while an older one is. The memtables are not freed,
|
||||
// but put into a vector for future deref and reclamation.
|
||||
void RemoveOldMemTables(uint64_t log_number,
|
||||
autovector<ReadOnlyMemTable*>* to_delete);
|
||||
|
||||
// Returns whether RemoveOldMemTables(log_number) would remove anything, so
|
||||
// that a caller can skip the call and the bookkeeping that follows it. O(1):
|
||||
// the scan stops at the oldest memtable that does not qualify, so only the
|
||||
// oldest one can decide this.
|
||||
//
|
||||
// REQUIRES: db mutex held.
|
||||
bool HasOldMemTablesToRemove(uint64_t log_number) const {
|
||||
const auto& memlist = current_->memlist_;
|
||||
return !memlist.empty() && memlist.back()->GetNextLogNumber() <= log_number;
|
||||
}
|
||||
|
||||
// This API is only used by atomic date replacement. To get an edit for
|
||||
// dropping the current `MemTableListVersion`.
|
||||
VersionEdit GetEditForDroppingCurrentVersion(
|
||||
|
||||
@@ -966,6 +966,65 @@ TEST_F(MemTableListTest, FlushRequestPersistsAfterPartialPick) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(MemTableListTest, RemoveOldMemTablesTest) {
|
||||
// Covers the predicate that lets a caller skip RemoveOldMemTables(), and
|
||||
// checks that it agrees with what the removal itself does. Both are used only
|
||||
// by the secondary instance, which collects memtables by the log number that
|
||||
// follows their contents rather than by flushing them.
|
||||
auto factory = std::make_shared<SkipListFactory>();
|
||||
options.memtable_factory = factory;
|
||||
ImmutableOptions ioptions(options);
|
||||
InternalKeyComparator cmp(BytewiseComparator());
|
||||
WriteBufferManager wb(options.db_write_buffer_size);
|
||||
MutableCFOptions mutable_cf_options(options);
|
||||
autovector<ReadOnlyMemTable*> to_delete;
|
||||
|
||||
MemTableList list(/*min_write_buffer_number_to_merge=*/1,
|
||||
/*max_write_buffer_size_to_maintain=*/0);
|
||||
|
||||
// An empty list has nothing to remove, whatever the log number.
|
||||
ASSERT_FALSE(list.HasOldMemTablesToRemove(0));
|
||||
ASSERT_FALSE(
|
||||
list.HasOldMemTablesToRemove(std::numeric_limits<uint64_t>::max()));
|
||||
|
||||
// Two sealed memtables, the older one followed by WAL 10 and the newer one by
|
||||
// WAL 20.
|
||||
SequenceNumber seq = 1;
|
||||
for (uint64_t next_log_number : {10, 20}) {
|
||||
MemTable* mem = new MemTable(cmp, ioptions, mutable_cf_options, &wb,
|
||||
kMaxSequenceNumber, 0 /* column_family_id */);
|
||||
mem->Ref();
|
||||
ASSERT_OK(mem->Add(++seq, kTypeValue, "key", "value",
|
||||
nullptr /* kv_prot_info */));
|
||||
mem->SetNextLogNumber(next_log_number);
|
||||
mem->ConstructFragmentedRangeTombstones();
|
||||
list.Add(mem, &to_delete);
|
||||
}
|
||||
ASSERT_EQ(2, list.NumNotFlushed());
|
||||
|
||||
// Neither memtable is superseded by a WAL older than the one following it.
|
||||
ASSERT_FALSE(list.HasOldMemTablesToRemove(9));
|
||||
list.RemoveOldMemTables(9, &to_delete);
|
||||
ASSERT_EQ(2, list.NumNotFlushed());
|
||||
|
||||
// The older memtable is collected at exactly its own next log number.
|
||||
ASSERT_TRUE(list.HasOldMemTablesToRemove(10));
|
||||
list.RemoveOldMemTables(10, &to_delete);
|
||||
ASSERT_EQ(1, list.NumNotFlushed());
|
||||
|
||||
ASSERT_FALSE(list.HasOldMemTablesToRemove(19));
|
||||
ASSERT_TRUE(list.HasOldMemTablesToRemove(20));
|
||||
list.RemoveOldMemTables(20, &to_delete);
|
||||
ASSERT_EQ(0, list.NumNotFlushed());
|
||||
ASSERT_FALSE(
|
||||
list.HasOldMemTablesToRemove(std::numeric_limits<uint64_t>::max()));
|
||||
|
||||
list.current()->Unref(&to_delete);
|
||||
for (ReadOnlyMemTable* m : to_delete) {
|
||||
delete m;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(MemTableListTest, EmptyAtomicFlushTest) {
|
||||
autovector<MemTableList*> lists;
|
||||
autovector<uint32_t> cf_ids;
|
||||
|
||||
+80
-25
@@ -37,6 +37,7 @@
|
||||
#include "folly/coro/CurrentExecutor.h"
|
||||
#include "folly/coro/Task.h"
|
||||
#include "folly/executors/IOThreadPoolExecutor.h"
|
||||
#include "folly/executors/ManualExecutor.h"
|
||||
#include "folly/io/async/EventBase.h"
|
||||
#include "folly/io/async/Request.h"
|
||||
#include "util/coro_stats_util.h"
|
||||
@@ -178,16 +179,16 @@ TEST_F(PerfContextTest, CoroutineStatsContextsRemainIsolatedWhenInterleaved) {
|
||||
get_perf_context()->EnablePerLevelPerfContext();
|
||||
get_iostats_context()->disable_iostats = true;
|
||||
CoroutineStatsConfig second_stats_config = CaptureCoroutineStatsConfig();
|
||||
CoroutineStatsConfig disabled_stats_config;
|
||||
disabled_stats_config.perf_level = PerfLevel::kDisable;
|
||||
disabled_stats_config.iostats_disabled = true;
|
||||
|
||||
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),
|
||||
folly::ManualExecutor executor;
|
||||
folly::coro::blockingWait(
|
||||
[first_stats_config = std::move(first_stats_config),
|
||||
second_stats_config = std::move(
|
||||
second_stats_config)]() mutable -> folly::coro::Task<void> {
|
||||
second_stats_config = std::move(second_stats_config),
|
||||
disabled_stats_config = std::move(
|
||||
disabled_stats_config)]() mutable -> folly::coro::Task<void> {
|
||||
get_perf_context()->Reset();
|
||||
get_perf_context()->ClearPerLevelPerfContext();
|
||||
get_perf_context()->block_read_count = 100;
|
||||
@@ -200,6 +201,7 @@ TEST_F(PerfContextTest, CoroutineStatsContextsRemainIsolatedWhenInterleaved) {
|
||||
const bool expected_per_level_perf_context_enabled =
|
||||
stats_config.per_level_perf_context_enabled;
|
||||
const bool expected_iostats_disabled = stats_config.iostats_disabled;
|
||||
const bool stats_enabled = IsCoroutineStatsEnabled(stats_config);
|
||||
{
|
||||
CoroutineStatsContextScope scope(std::move(stats_config),
|
||||
Env::Default());
|
||||
@@ -209,11 +211,12 @@ TEST_F(PerfContextTest, CoroutineStatsContextsRemainIsolatedWhenInterleaved) {
|
||||
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;
|
||||
if (stats_enabled) {
|
||||
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());
|
||||
@@ -221,6 +224,9 @@ TEST_F(PerfContextTest, CoroutineStatsContextsRemainIsolatedWhenInterleaved) {
|
||||
get_perf_context()->per_level_perf_context_enabled);
|
||||
EXPECT_EQ(expected_iostats_disabled,
|
||||
get_iostats_context()->disable_iostats);
|
||||
if (!stats_enabled) {
|
||||
co_return 0;
|
||||
}
|
||||
EXPECT_EQ(first, get_perf_context()->block_read_count);
|
||||
EXPECT_EQ(first, get_iostats_context()->bytes_read);
|
||||
get_perf_context()->block_read_count += second;
|
||||
@@ -231,18 +237,22 @@ TEST_F(PerfContextTest, CoroutineStatsContextsRemainIsolatedWhenInterleaved) {
|
||||
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);
|
||||
auto [disabled_result, first_result, second_result] =
|
||||
co_await folly::coro::collectAll(
|
||||
make_task(std::move(disabled_stats_config), 0, 0),
|
||||
make_task(std::move(first_stats_config), 1, 10),
|
||||
make_task(std::move(second_stats_config), 2, 20));
|
||||
EXPECT_EQ(0, disabled_result);
|
||||
EXPECT_EQ(11, first_result);
|
||||
EXPECT_EQ(22, second_result);
|
||||
|
||||
get_perf_context()->Reset();
|
||||
get_iostats_context()->Reset();
|
||||
co_return;
|
||||
}()));
|
||||
}(),
|
||||
&executor);
|
||||
EXPECT_EQ(PerfLevel::kDisable, GetPerfLevel());
|
||||
EXPECT_TRUE(get_iostats_context()->disable_iostats);
|
||||
}
|
||||
|
||||
TEST_F(PerfContextTest, CoroutineStatsContextScopeCollectsGetCpuNanos) {
|
||||
@@ -268,6 +278,50 @@ TEST_F(PerfContextTest, CoroutineStatsContextScopeCollectsGetCpuNanos) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(PerfContextTest, CoroutineStatsContextRestoredOnForeignThread) {
|
||||
SetPerfLevel(PerfLevel::kEnableTimeAndCPUTimeExceptForMutex);
|
||||
|
||||
folly::IOThreadPoolExecutor executor(1);
|
||||
folly::EventBase* event_base = executor.getEventBase();
|
||||
ASSERT_NE(nullptr, event_base);
|
||||
|
||||
uint64_t foreign_block_read_count = 0;
|
||||
uint64_t foreign_bytes_read = 0;
|
||||
folly::coro::blockingWait(folly::coro::co_withExecutor(
|
||||
folly::Executor::getKeepAliveToken(event_base),
|
||||
[&foreign_block_read_count, &foreign_bytes_read,
|
||||
stats_config =
|
||||
CaptureCoroutineStatsConfig()]() mutable -> folly::coro::Task<void> {
|
||||
CoroutineStatsContextScope scope(std::move(stats_config),
|
||||
Env::Default());
|
||||
get_perf_context()->block_read_count += 3;
|
||||
get_iostats_context()->bytes_read += 5;
|
||||
|
||||
std::shared_ptr<folly::RequestContext> captured_context =
|
||||
folly::RequestContext::saveContext();
|
||||
|
||||
std::thread foreign_thread([&] {
|
||||
get_perf_context()->Reset();
|
||||
get_iostats_context()->Reset();
|
||||
get_perf_context()->block_read_count = 41;
|
||||
get_iostats_context()->bytes_read = 43;
|
||||
{
|
||||
folly::RequestContextScopeGuard guard(captured_context);
|
||||
}
|
||||
foreign_block_read_count = get_perf_context()->block_read_count;
|
||||
foreign_bytes_read = get_iostats_context()->bytes_read;
|
||||
});
|
||||
foreign_thread.join();
|
||||
|
||||
EXPECT_EQ(3, get_perf_context()->block_read_count);
|
||||
EXPECT_EQ(5, get_iostats_context()->bytes_read);
|
||||
co_return;
|
||||
}()));
|
||||
|
||||
EXPECT_EQ(41, foreign_block_read_count);
|
||||
EXPECT_EQ(43, foreign_bytes_read);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
TEST_F(PerfContextTest, AsyncCallbackStatsStartEmptyOnSyncFallback) {
|
||||
@@ -282,10 +336,9 @@ TEST_F(PerfContextTest, AsyncCallbackStatsStartEmptyOnSyncFallback) {
|
||||
|
||||
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 {
|
||||
void OnComplete() override {
|
||||
const PerfContext* callback_perf_context = get_perf_context();
|
||||
const IOStatsContext* callback_iostats_context = get_iostats_context();
|
||||
ASSERT_NE(nullptr, callback_perf_context);
|
||||
ASSERT_NE(nullptr, callback_iostats_context);
|
||||
called = true;
|
||||
@@ -324,6 +377,8 @@ TEST_F(PerfContextTest, AsyncCallbackStatsStartEmptyOnSyncFallback) {
|
||||
|
||||
get_perf_context()->Reset();
|
||||
get_perf_context()->multiget_read_bytes = 29;
|
||||
SetPerfLevel(PerfLevel::kEnableCount);
|
||||
get_iostats_context()->disable_iostats = false;
|
||||
|
||||
StatsCallback multiget_callback;
|
||||
ColumnFamilyHandle* column_families[] = {db->DefaultColumnFamily(),
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#include "db/periodic_compaction_phaser.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "util/fastrange.h"
|
||||
#include "util/hash.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
bool PeriodicCompactionPhaser::SetConfig(int recovery_percent) {
|
||||
const bool changed = recovery_percent != recovery_percent_;
|
||||
recovery_percent_ = recovery_percent;
|
||||
return changed;
|
||||
}
|
||||
|
||||
PeriodicCompactionPhaseParams PeriodicCompactionPhaser::ParamsForCf(
|
||||
uint32_t cf_id) const {
|
||||
PeriodicCompactionPhaseParams params;
|
||||
if (recovery_percent_ <= 0) {
|
||||
// Phasing disabled; return default (disabled) params.
|
||||
return params;
|
||||
}
|
||||
// DB-level base phase (a fixed-point fraction * 2^64), hashed from the DB ID.
|
||||
// The DB ID is stable across re-open and physical cloning/migration but
|
||||
// distinct across DBs, so DBs configured alike do not herd. db_id_ is read
|
||||
// live because it is populated after construction (during DB::Open): versions
|
||||
// built before it is known hash an empty id and self-heal on later versions.
|
||||
const uint64_t db_base_phase = Hash64(db_id_.data(), db_id_.size());
|
||||
// Spread this CF quasi-uniformly around the DB base phase (golden ratio) so
|
||||
// CFs of one DB do not share a phase.
|
||||
params.seed_hash = CfPhaseSeedHash(db_base_phase, cf_id);
|
||||
params.anchor_time = anchor_time_;
|
||||
params.recovery_percent = std::min(recovery_percent_, 100);
|
||||
return params;
|
||||
}
|
||||
|
||||
uint64_t PeriodicCompactionPhaser::TriggerTime(
|
||||
uint64_t file_modification_time, uint64_t periodic_compaction_seconds,
|
||||
const PeriodicCompactionPhaseParams& params) {
|
||||
const uint64_t n = periodic_compaction_seconds;
|
||||
// Unphased deadline: file age reaches periodic_compaction_seconds. This is a
|
||||
// hard upper bound on age; phasing only ever triggers earlier.
|
||||
const uint64_t natural_trigger = file_modification_time + n;
|
||||
const int recovery_percent = params.recovery_percent;
|
||||
if (recovery_percent <= 0 || n == 0) {
|
||||
return natural_trigger;
|
||||
}
|
||||
// Preferred-phase time within each period, floor(p*n) with p == seed / 2^64.
|
||||
const uint64_t target = FastRange64(params.seed_hash, n);
|
||||
// Distance (in [0, n)) from the natural deadline back to the nearest
|
||||
// preferred-phase time at or before it.
|
||||
const uint64_t offset = (file_modification_time % n + n - target) % n;
|
||||
// Close recovery_percent% of that gap, triggering earlier. Over successive
|
||||
// re-stamped cycles the phase error decays geometrically toward the phase.
|
||||
// (A "snap to the exact phase" refinement was considered and rejected: under
|
||||
// unpredictable scheduling/queueing latency -- the delay between a file
|
||||
// becoming due and its periodic compaction actually running -- a snap can
|
||||
// split the fleet between the phase and a latency-induced fixed point
|
||||
// (re-herding, the opposite of the goal), whereas the plain geometric pull
|
||||
// degrades gracefully to a uniform, still well-spread shift.)
|
||||
const uint64_t early_pull =
|
||||
offset * static_cast<uint64_t>(std::min(recovery_percent, 100)) / 100;
|
||||
const uint64_t recovery_trigger = natural_trigger - early_pull;
|
||||
const uint64_t anchor = params.anchor_time;
|
||||
if (natural_trigger <= anchor) {
|
||||
// A whole cohort can be past its deadline when phasing took effect: after
|
||||
// the DB was down (e.g. DC power-outage recovery) or after
|
||||
// periodic_compaction_seconds was turned down. Instead of firing the cohort
|
||||
// immediately (a herd), spread it over the first quarter-period after the
|
||||
// anchor, on an absolute (epoch-aligned) grid of period n/4 with a stable
|
||||
// per-(DB, CF) phase: bounded lateness (<= ~n/4) in exchange for
|
||||
// anti-herding. The grid is absolute (not anchor + offset) so a rapidly
|
||||
// restarting DB is not perpetually re-anchored just short of its trigger
|
||||
// and thereby starved -- the grid point is a fixed wall-clock time that
|
||||
// arrives regardless of restarts.
|
||||
const uint64_t grid = n / 4;
|
||||
if (grid == 0) {
|
||||
return anchor;
|
||||
}
|
||||
const uint64_t phase = FastRange64(params.seed_hash, grid);
|
||||
// Smallest t >= anchor with t % grid == phase.
|
||||
return anchor + (phase + grid - anchor % grid) % grid;
|
||||
} else if (recovery_trigger >= anchor) {
|
||||
return recovery_trigger;
|
||||
}
|
||||
// Not yet past due, but the recovery target lands before the anchor: spread
|
||||
// the catch-up across [anchor, natural deadline] using the phase (no
|
||||
// recovery), so enabling phasing fleet-wide does not fire everything at once,
|
||||
// and never later than the natural deadline.
|
||||
return anchor + FastRange64(params.seed_hash, natural_trigger - anchor);
|
||||
}
|
||||
|
||||
uint64_t PeriodicCompactionPhaser::CfPhaseSeedHash(uint64_t db_base_phase,
|
||||
uint32_t cf_id) {
|
||||
// Golden-ratio conjugate ((sqrt(5)-1)/2) as a fixed-point fraction * 2^64.
|
||||
// The additive recurrence db_base + cf_id * kGolden (wrapping mod 2^64, i.e.
|
||||
// mod 1) is a low-discrepancy Weyl/Kronecker sequence, so a DB's CFs are
|
||||
// spread quasi-uniformly around the base phase for any number of CFs.
|
||||
constexpr uint64_t kGoldenRatioU64 = 0x9E3779B97F4A7C15ULL;
|
||||
return db_base_phase + static_cast<uint64_t>(cf_id) * kGoldenRatioU64;
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -0,0 +1,92 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
#include "rocksdb/rocksdb_namespace.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
// Parameters controlling randomized-but-stable phasing of periodic
|
||||
// (time-based) compaction, derived per Version from
|
||||
// DBOptions::periodic_compaction_phase_recovery_percent together with the DB ID
|
||||
// and column-family id. recovery_percent == 0 disables phasing (exact legacy
|
||||
// behavior).
|
||||
struct PeriodicCompactionPhaseParams {
|
||||
// Stable per-(DB, CF) hash seeding the preferred phase.
|
||||
uint64_t seed_hash = 0;
|
||||
// Unix time (seconds) at which phasing took effect for this DB (DB::Open, or
|
||||
// the last relevant SetDBOptions change). Used to spread the initial
|
||||
// catch-up burst across [anchor_time, natural deadline].
|
||||
uint64_t anchor_time = 0;
|
||||
// Percent [0, 100] of the phase gap closed per trigger; 0 disables phasing.
|
||||
int recovery_percent = 0;
|
||||
};
|
||||
|
||||
// De-herds periodic (time-based) compaction across a fleet: gives each (DB, CF)
|
||||
// a stable "preferred phase" within periodic_compaction_seconds and steers
|
||||
// compactions toward it, never past the deadline (enabled by
|
||||
// DBOptions::periodic_compaction_phase_recovery_percent). The DB-level base
|
||||
// phase is derived from the DB ID -- stable across re-open and physical
|
||||
// cloning/migration but distinct across DBs -- and each column family is spread
|
||||
// around it by cf id. This owns the DB-level recovery percent and the phasing
|
||||
// anchor; the per-file trigger math and phase derivation are stateless static
|
||||
// helpers. Lives on VersionSet, which caches the resulting per-CF params on
|
||||
// each Version.
|
||||
class PeriodicCompactionPhaser {
|
||||
public:
|
||||
// db_id is referenced live (it is populated after construction, during
|
||||
// DB::Open, and is read lazily) and must outlive this phaser -- it is owned
|
||||
// by the same VersionSet.
|
||||
explicit PeriodicCompactionPhaser(const std::string& db_id) : db_id_(db_id) {}
|
||||
|
||||
// Apply the (mutable) recovery percent. Returns true if it changed -- in
|
||||
// which case the caller should Reanchor() and refresh cached params. Does not
|
||||
// itself move the anchor.
|
||||
//
|
||||
// A setting of 25 to 33 will set this phaser to "stun"
|
||||
bool SetConfig(int recovery_percent);
|
||||
|
||||
// Move the phasing anchor to `now` (unix seconds).
|
||||
void Reanchor(uint64_t now) { anchor_time_ = now; }
|
||||
|
||||
int recovery_percent() const { return recovery_percent_; }
|
||||
|
||||
// Phasing params for a column family (by id): the DB base phase (hashed from
|
||||
// the DB ID) spread by cf_id (golden ratio), plus the current anchor and
|
||||
// recovery percent. Returns disabled params when phasing is off.
|
||||
PeriodicCompactionPhaseParams ParamsForCf(uint32_t cf_id) const;
|
||||
|
||||
// --- Stateless helpers (static; unit-tested directly) ---
|
||||
|
||||
// Wall-clock time (unix seconds) at which a file with the given modification
|
||||
// time becomes eligible for periodic compaction under `params`, NOT
|
||||
// accounting for the offpeak pull. When params.recovery_percent == 0 (phasing
|
||||
// disabled) this is file_modification_time + periodic_compaction_seconds
|
||||
// (classic behavior); otherwise it is pulled earlier toward the preferred
|
||||
// phase and is never later than the classic deadline.
|
||||
static uint64_t TriggerTime(uint64_t file_modification_time,
|
||||
uint64_t periodic_compaction_seconds,
|
||||
const PeriodicCompactionPhaseParams& params);
|
||||
|
||||
// Spreads a DB's column families quasi-uniformly around a DB-level base phase
|
||||
// via the golden-ratio additive (Weyl/Kronecker) recurrence: cf phase ==
|
||||
// db_base_phase + cf_id * (phi conjugate), all in fixed point (fraction *
|
||||
// 2^64) so the mod-1 wrap is the natural uint64 overflow. Low-discrepancy for
|
||||
// any number of CFs.
|
||||
static uint64_t CfPhaseSeedHash(uint64_t db_base_phase, uint32_t cf_id);
|
||||
|
||||
private:
|
||||
const std::string& db_id_;
|
||||
int recovery_percent_ = 0;
|
||||
// Unix time (seconds) at which phasing took effect for this DB (construction
|
||||
// or last relevant SetDBOptions change); spreads the initial catch-up burst.
|
||||
uint64_t anchor_time_ = 0;
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -296,17 +296,19 @@ bool RangeDelAggregator::StripeRep::ShouldDelete(
|
||||
}
|
||||
|
||||
bool RangeDelAggregator::StripeRep::IsRangeOverlapped(const Slice& start,
|
||||
const Slice& end) {
|
||||
const Slice& end,
|
||||
bool end_exclusive) {
|
||||
Invalidate();
|
||||
|
||||
// Set the internal start/end keys so that:
|
||||
// - if start_ikey has the same user key and sequence number as the
|
||||
// current end key, start_ikey will be considered greater; and
|
||||
// - if end_ikey has the same user key and sequence number as the current
|
||||
// start key, end_ikey will be considered greater.
|
||||
// - an exclusive end_ikey is considered less than a tombstone starting at
|
||||
// the same user key, while an inclusive end_ikey is considered greater.
|
||||
ParsedInternalKey start_ikey(start, kMaxSequenceNumber,
|
||||
static_cast<ValueType>(0));
|
||||
ParsedInternalKey end_ikey(end, 0, static_cast<ValueType>(0));
|
||||
ParsedInternalKey end_ikey(end, end_exclusive ? kMaxSequenceNumber : 0,
|
||||
static_cast<ValueType>(0));
|
||||
for (auto& iter : iters_) {
|
||||
bool checked_candidate_tombstones = false;
|
||||
for (iter->SeekForPrev(start);
|
||||
@@ -349,9 +351,10 @@ bool ReadRangeDelAggregator::ShouldDeleteImpl(const ParsedInternalKey& parsed,
|
||||
}
|
||||
|
||||
bool ReadRangeDelAggregator::IsRangeOverlapped(const Slice& start,
|
||||
const Slice& end) {
|
||||
const Slice& end,
|
||||
bool end_exclusive) {
|
||||
InvalidateRangeDelMapPositions();
|
||||
return rep_.IsRangeOverlapped(start, end);
|
||||
return rep_.IsRangeOverlapped(start, end, end_exclusive);
|
||||
}
|
||||
|
||||
void CompactionRangeDelAggregator::AddTombstones(
|
||||
|
||||
@@ -347,7 +347,8 @@ class RangeDelAggregator {
|
||||
|
||||
// If user-defined timestamp is enabled, `start` and `end` are user keys
|
||||
// with timestamp.
|
||||
bool IsRangeOverlapped(const Slice& start, const Slice& end);
|
||||
bool IsRangeOverlapped(const Slice& start, const Slice& end,
|
||||
bool end_exclusive);
|
||||
|
||||
private:
|
||||
bool InStripe(SequenceNumber seq) const {
|
||||
@@ -394,7 +395,8 @@ class ReadRangeDelAggregator final : public RangeDelAggregator {
|
||||
return ShouldDeleteImpl(parsed, mode);
|
||||
}
|
||||
|
||||
bool IsRangeOverlapped(const Slice& start, const Slice& end);
|
||||
bool IsRangeOverlapped(const Slice& start, const Slice& end,
|
||||
bool end_exclusive);
|
||||
|
||||
void InvalidateRangeDelMapPositions() override { rep_.Invalidate(); }
|
||||
|
||||
|
||||
@@ -164,7 +164,8 @@ void VerifyIsRangeOverlapped(
|
||||
const std::vector<IsRangeOverlappedTestCase>& test_cases) {
|
||||
for (const auto& test_case : test_cases) {
|
||||
EXPECT_EQ(test_case.result,
|
||||
range_del_agg->IsRangeOverlapped(test_case.start, test_case.end));
|
||||
range_del_agg->IsRangeOverlapped(test_case.start, test_case.end,
|
||||
/*end_exclusive=*/false));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,6 +387,7 @@ TEST_F(RangeDelAggregatorTest, SingleIterInAggregator) {
|
||||
{"a", "c", true},
|
||||
{"d", "f", true},
|
||||
{"g", "l", false}});
|
||||
EXPECT_FALSE(range_del_agg.IsRangeOverlapped("_", "a", true));
|
||||
}
|
||||
|
||||
TEST_F(RangeDelAggregatorTest, MultipleItersInAggregator) {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <functional>
|
||||
#include <set>
|
||||
|
||||
#include "db/read_callback.h"
|
||||
#include "util/autovector.h"
|
||||
#include "util/kv_map.h"
|
||||
#include "util/vector_iterator.h"
|
||||
@@ -490,6 +491,29 @@ SequenceNumber FragmentedRangeTombstoneIterator::MaxCoveringTombstoneSeqnum(
|
||||
: 0;
|
||||
}
|
||||
|
||||
SequenceNumber FragmentedRangeTombstoneIterator::MaxCoveringTombstoneSeqnum(
|
||||
const Slice& target_user_key, ReadCallback* callback) {
|
||||
assert(callback != nullptr);
|
||||
SeekToCoveringTombstone(target_user_key);
|
||||
if (!ValidPos() ||
|
||||
ucmp_->CompareWithoutTimestamp(start_key(), target_user_key) > 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto seq_end = tombstones_->seq_iter(pos_->seq_end_idx);
|
||||
// Sequence numbers are ordered newest to oldest and seq_pos_ is already
|
||||
// bounded by upper_bound_. Return the first tombstone the metadata callback
|
||||
// considers newer than the read snapshot.
|
||||
while (seq_pos_ != seq_end && *seq_pos_ >= lower_bound_) {
|
||||
const SequenceNumber candidate_seq = *seq_pos_;
|
||||
if (callback->IsNewerVisibleForMetadataRead(candidate_seq)) {
|
||||
return candidate_seq;
|
||||
}
|
||||
++seq_pos_;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::map<SequenceNumber, std::unique_ptr<FragmentedRangeTombstoneIterator>>
|
||||
FragmentedRangeTombstoneIterator::SplitBySnapshot(
|
||||
const std::vector<SequenceNumber>& snapshots) {
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
#include "table/internal_iterator.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
class ReadCallback;
|
||||
|
||||
struct FragmentedRangeTombstoneList {
|
||||
public:
|
||||
// A compact representation of a "stack" of range tombstone fragments, which
|
||||
@@ -236,6 +239,12 @@ class FragmentedRangeTombstoneIterator : public InternalIterator {
|
||||
// If there is no covering tombstone, then 0 is returned.
|
||||
SequenceNumber MaxCoveringTombstoneSeqnum(const Slice& user_key);
|
||||
|
||||
// Return the max sequence number of a covering range tombstone that should
|
||||
// be reported as newer-version metadata. If there is no such tombstone, then
|
||||
// 0 is returned.
|
||||
SequenceNumber MaxCoveringTombstoneSeqnum(const Slice& user_key,
|
||||
ReadCallback* callback);
|
||||
|
||||
// Splits the iterator into n+1 iterators (where n is the number of
|
||||
// snapshots), each providing a view over a "stripe" of sequence numbers. The
|
||||
// iterators are keyed by the upper bound of their ranges (the provided
|
||||
|
||||
Loaded 100 of 297 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user