mirror of
https://github.com/facebook/rocksdb.git
synced 2026-11-07 11:32:34 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a6ac302081 | ||
|
|
108d3bf062 | ||
|
|
d6cd1fc884 | ||
|
|
dfd356fa10 | ||
|
|
60d6f470e8 | ||
|
|
ea16e73a71 | ||
|
|
4c5486efec | ||
|
|
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",
|
||||
|
||||
@@ -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,7 +53,7 @@ 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
|
||||
|
||||
@@ -73,7 +79,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 +308,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 +323,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
|
||||
@@ -411,7 +424,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
|
||||
|
||||
+48
@@ -1,6 +1,54 @@
|
||||
# Rocksdb Change Log
|
||||
> NOTE: Entries for next release do not go here. Follow instructions in `unreleased_history/README.txt`
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -816,6 +816,16 @@ 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_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 +866,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;
|
||||
@@ -2514,6 +2534,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,13 @@ 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_lowest_used_cache_tier(
|
||||
rocksdb_options_t* opt, int v);
|
||||
|
||||
@@ -267,6 +274,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);
|
||||
@@ -1170,6 +1185,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);
|
||||
@@ -833,6 +839,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 +871,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);
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+17
-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
|
||||
|
||||
@@ -416,7 +416,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 +502,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 +603,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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -30,28 +31,28 @@ 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;
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -9883,6 +9883,16 @@ 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_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 +9933,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;
|
||||
@@ -11581,6 +11601,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;
|
||||
|
||||
+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(
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -603,6 +610,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);
|
||||
|
||||
@@ -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",
|
||||
@@ -564,6 +566,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>
|
||||
|
||||
@@ -163,6 +163,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;
|
||||
}
|
||||
@@ -399,8 +407,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),
|
||||
@@ -2169,6 +2181,286 @@ 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);
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, CompactionInfo) {
|
||||
Options options = CurrentOptions();
|
||||
ReopenWithCompactionService(&options);
|
||||
|
||||
@@ -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;
|
||||
|
||||
+91
-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"
|
||||
@@ -141,6 +145,7 @@ struct RecoveryOptimizationCounters {
|
||||
sp->ClearAllCallBacks();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
// optimize_manifest_for_recovery=true: a clean reopen of a flushed DB must
|
||||
@@ -351,6 +356,45 @@ 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));
|
||||
|
||||
Reopen(options);
|
||||
const uint64_t current_options_number =
|
||||
dbfull()->GetVersionSet()->options_file_number();
|
||||
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"));
|
||||
}
|
||||
|
||||
// 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 +1022,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 +1043,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 +1058,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 +1076,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 +1126,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.
|
||||
@@ -2227,6 +2283,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;
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+93
-16
@@ -347,6 +347,22 @@ Status DBImpl::ResumeImpl(DBRecoverContext context,
|
||||
const ReadOptions read_options(io_activity);
|
||||
const WriteOptions write_options(io_activity);
|
||||
|
||||
assert(static_cast<size_t>(unscheduled_flushes_) <= flush_queue_.size());
|
||||
// Recovery rebuilds flush requests for every column family with pending
|
||||
// immutable data after scheduled workers exit, so every existing request is
|
||||
// redundant and safe to remove. A worker can exit on the background error
|
||||
// without popping a request, leaving it out of unscheduled_flushes_.
|
||||
while (!flush_queue_.empty()) {
|
||||
FlushRequest flush_req = PopFirstFromFlushQueue();
|
||||
for (const auto& item : flush_req.cfd_to_max_mem_id_to_persist) {
|
||||
ColumnFamilyData* cfd = item.first;
|
||||
assert(cfd);
|
||||
cfd->UnrefAndTryDelete();
|
||||
}
|
||||
}
|
||||
unscheduled_flushes_ = 0;
|
||||
|
||||
TEST_SYNC_POINT("DBImpl::ResumeImpl:BeforeWaitForBackgroundWork");
|
||||
WaitForBackgroundWork();
|
||||
|
||||
TEST_SYNC_POINT("DBImpl::ResumeImpl:Start");
|
||||
@@ -474,9 +490,8 @@ Status DBImpl::ResumeImpl(DBRecoverContext context,
|
||||
s = Status::ShutdownInProgress();
|
||||
}
|
||||
if (s.ok() && context.flush_after_recovery) {
|
||||
// Since we drop all non-recovery flush requests during recovery,
|
||||
// and new memtable may fill up during recovery,
|
||||
// schedule one more round of flush.
|
||||
// Normal flush requests are discarded during recovery, and a new memtable
|
||||
// may fill while recovery releases the DB mutex. Schedule a catch-up flush.
|
||||
Status status = RetryFlushesForErrorRecovery(
|
||||
FlushReason::kCatchUpAfterErrorRecovery, false /* wait */);
|
||||
if (!status.ok()) {
|
||||
@@ -1662,6 +1677,21 @@ Status DBImpl::SetOptions(
|
||||
VersionEdit dummy_edit;
|
||||
dummy_edit.MarkNoManifestWriteDummy();
|
||||
TEST_SYNC_POINT_CALLBACK("DBImpl::SetOptions:dummy_edit", &dummy_edit);
|
||||
// If any CF is changing periodic_compaction_seconds, (re)anchor phasing to
|
||||
// now BEFORE the new Version is built below, so the new Version's scoring
|
||||
// spreads a turn-down's newly past-due cohort over the phase grid (within
|
||||
// ~N/4 of now) instead of firing it all at once (a herd). Anchoring is
|
||||
// DB-level and benign to CFs that are not changing their interval.
|
||||
bool changing_periodic_compaction_seconds = false;
|
||||
for (const auto& cfd_opts : column_family_datas) {
|
||||
if (cfd_opts.second->count("periodic_compaction_seconds") > 0) {
|
||||
changing_periodic_compaction_seconds = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (changing_periodic_compaction_seconds) {
|
||||
versions_->ReanchorCompactionPhase();
|
||||
}
|
||||
for (const auto& cfd_opts : column_family_datas) {
|
||||
auto* cfd = cfd_opts.first;
|
||||
const auto* options_map_ptr = cfd_opts.second;
|
||||
@@ -6538,12 +6568,23 @@ Status DBImpl::PrepareFileIngestion(
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<ExternalSstFileIngestionJob> ingestion_jobs;
|
||||
ingestion_jobs.reserve(num_cfs);
|
||||
for (const auto& arg : args) {
|
||||
auto* cfd = static_cast<ColumnFamilyHandleImpl*>(arg.column_family)->cfd();
|
||||
ingestion_jobs.emplace_back(versions_.get(), cfd, immutable_db_options_,
|
||||
mutable_db_options_, file_options_, &snapshots_,
|
||||
arg.options, &directories_, &event_logger_,
|
||||
io_tracer_);
|
||||
}
|
||||
|
||||
// TODO (yanqin) maybe handle the case in which column_families have
|
||||
// duplicates
|
||||
std::unique_ptr<std::list<uint64_t>::iterator> pending_output_elem;
|
||||
size_t total = 0;
|
||||
for (const auto& arg : args) {
|
||||
total += arg.external_files.size();
|
||||
for (size_t i = 0; i != num_cfs; ++i) {
|
||||
total += ingestion_jobs[i].NumFilesToPrepare(args[i].external_files.size(),
|
||||
args[i].atomic_replace_range);
|
||||
}
|
||||
uint64_t next_file_number = 0;
|
||||
Status status = ReserveFileNumbersBeforeIngestion(
|
||||
@@ -6555,20 +6596,11 @@ Status DBImpl::PrepareFileIngestion(
|
||||
return status;
|
||||
}
|
||||
|
||||
std::vector<ExternalSstFileIngestionJob> ingestion_jobs;
|
||||
ingestion_jobs.reserve(num_cfs);
|
||||
for (const auto& arg : args) {
|
||||
auto* cfd = static_cast<ColumnFamilyHandleImpl*>(arg.column_family)->cfd();
|
||||
ingestion_jobs.emplace_back(versions_.get(), cfd, immutable_db_options_,
|
||||
mutable_db_options_, file_options_, &snapshots_,
|
||||
arg.options, &directories_, &event_logger_,
|
||||
io_tracer_);
|
||||
}
|
||||
|
||||
// TODO(yanqin) maybe make jobs run in parallel
|
||||
uint64_t start_file_number = next_file_number;
|
||||
for (size_t i = 1; i != num_cfs; ++i) {
|
||||
start_file_number += args[i - 1].external_files.size();
|
||||
start_file_number += ingestion_jobs[i - 1].NumFilesToPrepare(
|
||||
args[i - 1].external_files.size(), args[i - 1].atomic_replace_range);
|
||||
SuperVersion* super_version =
|
||||
ingestion_jobs[i].GetColumnFamilyData()->GetReferencedSuperVersion(
|
||||
this);
|
||||
@@ -7688,6 +7720,7 @@ void DBImpl::RecordSeqnoToTimeMapping() {
|
||||
new_seqno_to_time_mapping->CopyFrom(seqno_to_time_mapping_);
|
||||
|
||||
// Update in SV of all applicable CFs
|
||||
bool enqueued_any = false;
|
||||
for (ColumnFamilyData* cfd : *versions_->GetColumnFamilySet()) {
|
||||
if (cfd->IsDropped()) {
|
||||
continue;
|
||||
@@ -7697,8 +7730,26 @@ void DBImpl::RecordSeqnoToTimeMapping() {
|
||||
sv_context.NewSuperVersion();
|
||||
cfd->InstallSuperVersion(&sv_context, &mutex_,
|
||||
new_seqno_to_time_mapping);
|
||||
// Recording a new sample can move the preserve-window boundary, aging a
|
||||
// bottommost file out of it. When that happens, recompute bottommost
|
||||
// marking and enqueue now: on a quiet DB nothing else would create a
|
||||
// new Version to pick up the change, so the compaction would otherwise
|
||||
// be delayed until unrelated activity.
|
||||
if (MaybeUpdatePreserveTimeMinSeqno(cfd) && !cfd->AllowIngestBehind()) {
|
||||
VersionStorageInfo* vstorage = cfd->current()->storage_info();
|
||||
vstorage->ComputeBottommostFilesMarkedForCompaction(
|
||||
/*allow_ingest_behind=*/false, cfd->ioptions().user_comparator,
|
||||
cfd->GetFullHistoryTsLow());
|
||||
if (!vstorage->BottommostFilesMarkedForCompaction().empty()) {
|
||||
EnqueuePendingCompaction(cfd);
|
||||
enqueued_any = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (enqueued_any) {
|
||||
MaybeScheduleFlushOrCompaction();
|
||||
}
|
||||
bg_cv_.SignalAll();
|
||||
}
|
||||
|
||||
@@ -7706,6 +7757,32 @@ void DBImpl::RecordSeqnoToTimeMapping() {
|
||||
sv_context.Clean();
|
||||
}
|
||||
|
||||
bool DBImpl::MaybeUpdatePreserveTimeMinSeqno(ColumnFamilyData* cfd) {
|
||||
mutex_.AssertHeld();
|
||||
VersionStorageInfo* vstorage = cfd->current()->storage_info();
|
||||
const SequenceNumber prev = vstorage->GetPreserveTimeMinSeqno();
|
||||
const MutableCFOptions& mopts = cfd->GetLatestMutableCFOptions();
|
||||
MinAndMaxPreserveSeconds preserve_info{mopts};
|
||||
if (!preserve_info.IsEnabled()) {
|
||||
// Preserve/preclude disabled: no restriction on bottommost seqno zeroing.
|
||||
vstorage->SetPreserveTimeMinSeqno(kMaxSequenceNumber);
|
||||
return prev != kMaxSequenceNumber;
|
||||
}
|
||||
int64_t current_time = 0;
|
||||
if (!immutable_db_options_.clock->GetCurrentTime(¤t_time).ok()) {
|
||||
// Leave the previous value in place; being stale is safe (a hot key's
|
||||
// largest seqno stays above any past boundary and remains unmarked).
|
||||
return false;
|
||||
}
|
||||
SequenceNumber preserve_time_min_seqno = kMaxSequenceNumber;
|
||||
seqno_to_time_mapping_.GetCurrentTieringCutoffSeqnos(
|
||||
static_cast<uint64_t>(current_time), mopts.preserve_internal_time_seconds,
|
||||
mopts.preclude_last_level_data_seconds, &preserve_time_min_seqno,
|
||||
/*preclude_last_level_min_seqno=*/nullptr);
|
||||
vstorage->SetPreserveTimeMinSeqno(preserve_time_min_seqno);
|
||||
return prev != preserve_time_min_seqno;
|
||||
}
|
||||
|
||||
void DBImpl::TriggerPeriodicCompaction() {
|
||||
TEST_SYNC_POINT("DBImpl::TriggerPeriodicCompaction:StartRunning");
|
||||
{
|
||||
|
||||
+47
-9
@@ -1156,10 +1156,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 +1192,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 +1328,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 +1453,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 +1551,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 +1652,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 +2030,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 +2808,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;
|
||||
|
||||
|
||||
@@ -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,
|
||||
@@ -5525,6 +5521,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();
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
+365
-46
@@ -7,6 +7,7 @@
|
||||
|
||||
#include <cinttypes>
|
||||
#include <optional>
|
||||
#include <unordered_set>
|
||||
|
||||
#include "db/arena_wrapped_db_iter.h"
|
||||
#include "db/blob/blob_fetcher.h"
|
||||
@@ -81,7 +82,9 @@ Status DBImplSecondary::FindAndRecoverLogFiles(
|
||||
Status s;
|
||||
std::vector<uint64_t> logs;
|
||||
s = FindNewLogNumbers(&logs);
|
||||
if (s.ok() && !logs.empty()) {
|
||||
if (s.ok()) {
|
||||
// Empty recovery rounds still prune resolved transactions and completed
|
||||
// prep tracking.
|
||||
SequenceNumber next_sequence(kMaxSequenceNumber);
|
||||
s = RecoverLogFiles(logs, &next_sequence, cfds_changed, job_context);
|
||||
}
|
||||
@@ -204,6 +207,8 @@ Status DBImplSecondary::RecoverLogFiles(
|
||||
assert(reader != nullptr);
|
||||
}
|
||||
|
||||
DeleteResolvedRecoveredTransactions();
|
||||
|
||||
const UnorderedMap<uint32_t, size_t>& running_ts_sz =
|
||||
versions_->GetRunningColumnFamiliesTimestampSize();
|
||||
for (auto log_number : log_numbers) {
|
||||
@@ -220,6 +225,10 @@ Status DBImplSecondary::RecoverLogFiles(
|
||||
std::string scratch;
|
||||
Slice record;
|
||||
WriteBatch batch;
|
||||
// Hoisted alongside `scratch`, `record` and `batch`: the loop below runs
|
||||
// once per replayed WAL record, and clear() keeps the bucket array that a
|
||||
// set constructed there would allocate again for every record.
|
||||
std::unordered_set<uint32_t> selected_cf_ids;
|
||||
|
||||
while (reader->ReadRecord(&record, &scratch,
|
||||
immutable_db_options_.wal_recovery_mode) &&
|
||||
@@ -253,42 +262,62 @@ Status DBImplSecondary::RecoverLogFiles(
|
||||
continue;
|
||||
}
|
||||
cfds_changed->insert(cfd);
|
||||
// The return value is what the seal below compares against
|
||||
// `log_number`, so the call has to stay. The write also names a WAL
|
||||
// for column families whose inserts are skipped as already flushed
|
||||
// and never reach the set recorded after the insert.
|
||||
const std::optional<uint64_t> prev_log_number =
|
||||
RecordCurrentLog(id, log_number);
|
||||
const std::vector<FileMetaData*>& l0_files =
|
||||
cfd->current()->storage_info()->LevelFiles(0);
|
||||
SequenceNumber seq =
|
||||
l0_files.empty() ? 0 : l0_files.back()->fd.largest_seqno;
|
||||
// If the write batch's sequence number is smaller than the last
|
||||
// sequence number of the largest sequence persisted for this column
|
||||
// family, then its data must reside in an SST that has already been
|
||||
// added in the prior MANIFEST replay.
|
||||
// `l0_files` is ordered newest first, so `seq` is the largest
|
||||
// sequence number in the oldest L0 file. A write batch at or below it
|
||||
// must reside in an SST that a prior MANIFEST replay already added.
|
||||
if (seq_of_batch <= seq) {
|
||||
continue;
|
||||
}
|
||||
auto curr_log_num = std::numeric_limits<uint64_t>::max();
|
||||
if (cfd_to_current_log_.count(cfd) > 0) {
|
||||
curr_log_num = cfd_to_current_log_[cfd];
|
||||
}
|
||||
// If the active memtable contains records added by replaying an
|
||||
// earlier WAL, then we need to seal the memtable, add it to the
|
||||
// immutable memtable list and create a new active memtable.
|
||||
if (!cfd->mem()->IsEmpty() &&
|
||||
(curr_log_num == std::numeric_limits<uint64_t>::max() ||
|
||||
curr_log_num != log_number)) {
|
||||
MemTable* new_mem = cfd->ConstructNewMemtable(
|
||||
cfd->GetLatestMutableCFOptions(), seq_of_batch);
|
||||
cfd->mem()->SetNextLogNumber(log_number);
|
||||
cfd->mem()->ConstructFragmentedRangeTombstones();
|
||||
cfd->imm()->Add(cfd->mem(), &job_context->memtables_to_free);
|
||||
new_mem->Ref();
|
||||
cfd->SetMemtable(new_mem);
|
||||
// immutable memtable list and create a new active memtable, so that
|
||||
// the sealed memtable can be dropped once the primary has flushed
|
||||
// that earlier WAL.
|
||||
//
|
||||
// This is best effort: the sequence number check above can skip the
|
||||
// seal without skipping the insert that follows, leaving the active
|
||||
// memtable holding entries from more than one WAL.
|
||||
// MaybeSealFullyFlushedActiveMemtable() tolerates that because
|
||||
// `cf_id_to_current_log_[id]` is the newest of them.
|
||||
if (!cfd->mem()->IsEmpty() && prev_log_number != log_number) {
|
||||
SealActiveMemtable(cfd, log_number, seq_of_batch, job_context);
|
||||
}
|
||||
}
|
||||
bool has_valid_writes = false;
|
||||
selected_cf_ids.clear();
|
||||
status = WriteBatchInternal::InsertInto(
|
||||
&batch, column_family_memtables_.get(),
|
||||
nullptr /* flush_scheduler */, nullptr /* trim_history_scheduler*/,
|
||||
true, log_number, this, false /* concurrent_memtable_writes */,
|
||||
next_sequence, &has_valid_writes, seq_per_batch_, batch_per_txn_);
|
||||
next_sequence, &has_valid_writes, seq_per_batch_, batch_per_txn_,
|
||||
&selected_cf_ids);
|
||||
// A 2PC commit marker names no column family yet replays its prepared
|
||||
// batch into the memtables, so the recorded WAL would lag the committed
|
||||
// data and the gate would drop it. Mark them changed too, or a
|
||||
// commit-replay-only round skips the super version install and the
|
||||
// retention warning.
|
||||
//
|
||||
// The seal loop above runs over named ids, so a commit marker takes no
|
||||
// seal decision: its data joins the memtable's current WAL generation,
|
||||
// which then stays until the primary flushes the commit's WAL.
|
||||
for (const uint32_t id : selected_cf_ids) {
|
||||
RecordCurrentLog(id, log_number);
|
||||
ColumnFamilyData* cfd =
|
||||
versions_->GetColumnFamilySet()->GetColumnFamily(id);
|
||||
if (cfd != nullptr) {
|
||||
cfds_changed->insert(cfd);
|
||||
}
|
||||
}
|
||||
}
|
||||
// If column family was not found, it might mean that the WAL write
|
||||
// batch references to the column family that was dropped after the
|
||||
@@ -298,20 +327,6 @@ Status DBImplSecondary::RecoverLogFiles(
|
||||
// passing null flush_scheduler will disable memtable flushing which is
|
||||
// needed for secondary instances
|
||||
if (status.ok()) {
|
||||
for (const auto id : column_family_ids) {
|
||||
ColumnFamilyData* cfd =
|
||||
versions_->GetColumnFamilySet()->GetColumnFamily(id);
|
||||
if (cfd == nullptr) {
|
||||
continue;
|
||||
}
|
||||
std::unordered_map<ColumnFamilyData*, uint64_t>::iterator iter =
|
||||
cfd_to_current_log_.find(cfd);
|
||||
if (iter == cfd_to_current_log_.end()) {
|
||||
cfd_to_current_log_.insert({cfd, log_number});
|
||||
} else if (log_number > iter->second) {
|
||||
iter->second = log_number;
|
||||
}
|
||||
}
|
||||
auto last_sequence = *next_sequence - 1;
|
||||
if ((*next_sequence != kMaxSequenceNumber) &&
|
||||
(versions_->LastSequence() <= last_sequence)) {
|
||||
@@ -336,6 +351,176 @@ Status DBImplSecondary::RecoverLogFiles(
|
||||
return status;
|
||||
}
|
||||
|
||||
void DBImplSecondary::SealActiveMemtable(ColumnFamilyData* cfd,
|
||||
uint64_t next_log_number,
|
||||
SequenceNumber new_mem_earliest_seq,
|
||||
JobContext* job_context) {
|
||||
mutex_.AssertHeld();
|
||||
assert(cfd != nullptr);
|
||||
assert(job_context != nullptr);
|
||||
MemTable* new_mem = cfd->ConstructNewMemtable(
|
||||
cfd->GetLatestMutableCFOptions(), new_mem_earliest_seq);
|
||||
cfd->mem()->SetNextLogNumber(next_log_number);
|
||||
cfd->mem()->ConstructFragmentedRangeTombstones();
|
||||
cfd->imm()->Add(cfd->mem(), &job_context->memtables_to_free);
|
||||
new_mem->Ref();
|
||||
cfd->SetMemtable(new_mem);
|
||||
}
|
||||
|
||||
std::optional<uint64_t> DBImplSecondary::RecordCurrentLog(uint32_t cf_id,
|
||||
uint64_t log_number) {
|
||||
mutex_.AssertHeld();
|
||||
const auto [log_iter, inserted] =
|
||||
cf_id_to_current_log_.try_emplace(cf_id, log_number);
|
||||
if (inserted) {
|
||||
return std::nullopt;
|
||||
}
|
||||
const uint64_t prev_log_number = log_iter->second;
|
||||
if (log_number > prev_log_number) {
|
||||
log_iter->second = log_number;
|
||||
}
|
||||
return prev_log_number;
|
||||
}
|
||||
|
||||
bool DBImplSecondary::MaybeSealFullyFlushedActiveMemtable(
|
||||
ColumnFamilyData* cfd, uint64_t installed_log_number,
|
||||
JobContext* job_context) {
|
||||
mutex_.AssertHeld();
|
||||
assert(cfd != nullptr);
|
||||
assert(job_context != nullptr);
|
||||
assert(installed_log_number <= cfd->GetLogNumber());
|
||||
if (cfd->mem()->IsEmpty()) {
|
||||
return false;
|
||||
}
|
||||
const auto log_iter = cf_id_to_current_log_.find(cfd->GetID());
|
||||
if (log_iter == cf_id_to_current_log_.end()) {
|
||||
return false;
|
||||
}
|
||||
// Keep the memtable unless the installed Version covers everything it holds:
|
||||
// only the WALs older than `installed_log_number` are readable from the files
|
||||
// that Version references. The loop in TryCatchUpWithPrimary() explains why
|
||||
// the watermark has to come from the Version.
|
||||
if (log_iter->second >= installed_log_number) {
|
||||
return false;
|
||||
}
|
||||
// The file set is now the authority for these entries, so keeping them would
|
||||
// be incorrect rather than merely redundant: point lookups stop at the
|
||||
// memtable, and bottommost compaction rewriting the flushed entry's sequence
|
||||
// number to 0 makes the shadowing permanent.
|
||||
ROCKS_LOG_INFO(immutable_db_options_.info_log,
|
||||
"[%s] Sealing active memtable replayed from WAL %" PRIu64
|
||||
", flushed by the primary and readable as of log number "
|
||||
"%" PRIu64 "; its first sequence number is %" PRIu64,
|
||||
cfd->GetName().c_str(), log_iter->second, installed_log_number,
|
||||
static_cast<uint64_t>(cfd->mem()->GetFirstSequenceNumber()));
|
||||
// The memtable holds no entries from a WAL newer than `log_iter->second`, so
|
||||
// + 1 is the log number following its contents, and the check above puts that
|
||||
// at or below `installed_log_number`. The older immutable memtables that
|
||||
// RemoveOldMemTables() examines first were themselves sealed on a WAL switch
|
||||
// and stamped no higher, so nothing stops its scan short of this memtable and
|
||||
// it is collected in the same round.
|
||||
//
|
||||
// The replacement memtable's earliest sequence number is the VersionSet's
|
||||
// last sequence number, as in DBImplFollower::TryCatchUpWithLeader():
|
||||
// DBImpl::MultiCFSnapshot() compares it against the last sequence number to
|
||||
// detect a version change while collecting super version references without
|
||||
// the mutex. That can be above the sequence number replayed next, so it is
|
||||
// not yet the lower bound MemTable::SetEarliestSequenceNumber() documents;
|
||||
// MemTable::Add() corrects it downward on the first insert, and until then
|
||||
// the memtable holds no key a too-high bound could hide.
|
||||
SealActiveMemtable(cfd, log_iter->second + 1, versions_->LastSequence(),
|
||||
job_context);
|
||||
cf_id_to_current_log_.erase(log_iter);
|
||||
return true;
|
||||
}
|
||||
|
||||
void DBImplSecondary::MaybeWarnAboutRetainedMemtables(
|
||||
ColumnFamilyData* cfd, uint64_t installed_log_number) {
|
||||
mutex_.AssertHeld();
|
||||
assert(cfd != nullptr);
|
||||
// Tested before the count below because it is false whenever the secondary
|
||||
// is keeping up with the primary, which is the normal case.
|
||||
if (installed_log_number >= cfd->GetLogNumber()) {
|
||||
cf_id_to_retention_warning_.erase(cfd->GetID());
|
||||
return;
|
||||
}
|
||||
const int retained = cfd->imm()->NumNotFlushed();
|
||||
if (retained == 0) {
|
||||
cf_id_to_retention_warning_.erase(cfd->GetID());
|
||||
return;
|
||||
}
|
||||
// Warn only when either number changes. The condition lasts until the
|
||||
// primary's flushed files become readable, and TryCatchUpWithPrimary() is
|
||||
// called as often as the application chooses, so reporting it every round
|
||||
// would bury everything else in the log. Both values move only as the
|
||||
// condition worsens, making this one line per newly retained memtable.
|
||||
const std::pair<uint64_t, int> warning{installed_log_number, retained};
|
||||
const auto [warning_iter, inserted] =
|
||||
cf_id_to_retention_warning_.insert({cfd->GetID(), warning});
|
||||
if (!inserted) {
|
||||
if (warning_iter->second == warning) {
|
||||
return;
|
||||
}
|
||||
warning_iter->second = warning;
|
||||
}
|
||||
// ColumnFamilyData::RecalculateWriteStallConditions() also counts this as a
|
||||
// memtable limit stop, but attributes it to a flush that is not coming and to
|
||||
// a `max_write_buffer_number` that no writer of this instance's own can hit,
|
||||
// so name the actual cause.
|
||||
ROCKS_LOG_WARN(immutable_db_options_.info_log,
|
||||
"[%s] Retaining %d immutable memtable(s): the primary has "
|
||||
"flushed up to log number %" PRIu64
|
||||
" but no Version past log number %" PRIu64
|
||||
" could be installed, so they may hold the only readable copy "
|
||||
"of what was flushed",
|
||||
cfd->GetName().c_str(), retained, cfd->GetLogNumber(),
|
||||
installed_log_number);
|
||||
}
|
||||
|
||||
// The primary keeps min_log_number_to_keep at or below every unresolved prepare
|
||||
// and every prepare with unflushed committed data. A recovered transaction
|
||||
// entirely below it is therefore committed and flushed, or rolled back; see
|
||||
// PrecomputeMinLogNumberToKeep2PC(). FindNewLogNumbers() uses the same
|
||||
// threshold, so no marker below it can be replayed. If a resolution marker
|
||||
// appears in a later WAL, its handler tolerates missing recovery state.
|
||||
// Committed writes are skipped because the column family's log number is
|
||||
// already past their WAL.
|
||||
//
|
||||
// Drop before replay so a reused name starts a new generation instead of
|
||||
// merging with stale state. Open every reader first so a round that cannot
|
||||
// replay drops nothing.
|
||||
void DBImplSecondary::DeleteResolvedRecoveredTransactions() {
|
||||
mutex_.AssertHeld();
|
||||
if (recovered_transactions_.empty()) {
|
||||
return;
|
||||
}
|
||||
const uint64_t min_log_number_to_keep = versions_->min_log_number_to_keep();
|
||||
size_t deleted = 0;
|
||||
for (RecoveredTransactionMap::iterator it = recovered_transactions_.begin();
|
||||
it != recovered_transactions_.end();) {
|
||||
assert(!it->second->batches_.empty());
|
||||
bool all_batches_below_watermark = true;
|
||||
for (const auto& batch_info : it->second->batches_) {
|
||||
if (batch_info.second.log_number_ >= min_log_number_to_keep) {
|
||||
all_batches_below_watermark = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (all_batches_below_watermark) {
|
||||
it = DeleteRecoveredTransaction(it);
|
||||
++deleted;
|
||||
} else {
|
||||
++it;
|
||||
}
|
||||
}
|
||||
if (deleted > 0) {
|
||||
ROCKS_LOG_DEBUG(immutable_db_options_.info_log,
|
||||
"Dropped %" ROCKSDB_PRIszt
|
||||
" recovered transaction(s) prepared before WAL %" PRIu64,
|
||||
deleted, min_log_number_to_keep);
|
||||
}
|
||||
}
|
||||
|
||||
Iterator* DBImplSecondary::NewIterator(const ReadOptions& _read_options,
|
||||
ColumnFamilyHandle* column_family) {
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
@@ -496,10 +681,12 @@ Status DBImplSecondary::TryCatchUpWithPrimary() {
|
||||
{
|
||||
InstrumentedMutexLock lock_guard(&mutex_);
|
||||
assert(manifest_reader_.get() != nullptr);
|
||||
s = static_cast_with_check<ReactiveVersionSet>(versions_.get())
|
||||
->ReadAndApply(&mutex_, &manifest_reader_,
|
||||
manifest_reader_status_.get(), &cfds_changed,
|
||||
/*files_to_delete=*/nullptr);
|
||||
auto* reactive_versions =
|
||||
static_cast_with_check<ReactiveVersionSet>(versions_.get());
|
||||
s = reactive_versions->ReadAndApply(&mutex_, &manifest_reader_,
|
||||
manifest_reader_status_.get(),
|
||||
&cfds_changed,
|
||||
/*files_to_delete=*/nullptr);
|
||||
|
||||
ROCKS_LOG_INFO(immutable_db_options_.info_log, "Last sequence is %" PRIu64,
|
||||
static_cast<uint64_t>(versions_->LastSequence()));
|
||||
@@ -517,8 +704,10 @@ Status DBImplSecondary::TryCatchUpWithPrimary() {
|
||||
|
||||
// list wal_dir to discover new WALs and apply new changes to the secondary
|
||||
// instance
|
||||
bool replayed_every_wal_found = false;
|
||||
if (s.ok()) {
|
||||
s = FindAndRecoverLogFiles(&cfds_changed, &job_context);
|
||||
replayed_every_wal_found = s.ok();
|
||||
if (s.IsPathNotFound()) {
|
||||
ROCKS_LOG_INFO(
|
||||
immutable_db_options_.info_log,
|
||||
@@ -528,13 +717,65 @@ Status DBImplSecondary::TryCatchUpWithPrimary() {
|
||||
}
|
||||
}
|
||||
if (s.ok()) {
|
||||
for (auto cfd : cfds_changed) {
|
||||
cfd->imm()->RemoveOldMemTables(cfd->GetLogNumber(),
|
||||
// Iterate over every column family rather than only `cfds_changed`: a
|
||||
// stale memtable left behind because an earlier round failed after
|
||||
// ReadAndApply() had already advanced the log number must still be
|
||||
// dropped, and that column family may have no new MANIFEST record in this
|
||||
// round. Column families dropped in this round are skipped, unlike with
|
||||
// `cfds_changed`, which can still name a column family that a later
|
||||
// record in the same round made VersionEditHandler::DestroyCfAndCleanup()
|
||||
// drop. Column families that neither changed nor have anything to collect
|
||||
// are skipped below, so this installs no extra super versions.
|
||||
for (ColumnFamilyData* cfd : *versions_->GetColumnFamilySet()) {
|
||||
if (cfd->IsDropped()) {
|
||||
// Nothing more can ever be replayed into a dropped column family, so
|
||||
// drop its entry too: it had no removal path before. Ids are never
|
||||
// reused, so a leftover entry could only waste space, and one is
|
||||
// still left behind by a column family destroyed before this loop
|
||||
// next runs.
|
||||
cf_id_to_current_log_.erase(cfd->GetID());
|
||||
cf_id_to_retention_warning_.erase(cfd->GetID());
|
||||
continue;
|
||||
}
|
||||
if (!cfd->initialized()) {
|
||||
// There is no memtable to reconcile yet, and any entry the column
|
||||
// family already has still names the WAL a later round must gate on.
|
||||
continue;
|
||||
}
|
||||
// Drop memtables against the log number of the Version most recently
|
||||
// installed, never `cfd->GetLogNumber()`: that advances as soon as the
|
||||
// primary's flush record is read, even when the flushed files cannot be
|
||||
// opened or an incomplete atomic group parks the new Version. Until
|
||||
// such a Version is installed the memtable can hold the only readable
|
||||
// copy of what was flushed, so dropping it against that number would
|
||||
// turn a stale read into a vanished key.
|
||||
const uint64_t installed_log_number =
|
||||
reactive_versions->GetInstalledVersionLogNumber(cfd->GetID());
|
||||
const bool sealed = MaybeSealFullyFlushedActiveMemtable(
|
||||
cfd, installed_log_number, &job_context);
|
||||
// A round that failed after RecoverLogFiles() had sealed on a WAL
|
||||
// switch can also leave a collectible immutable memtable behind, so
|
||||
// check for one instead of relying on this round having sealed.
|
||||
const bool needs_super_version =
|
||||
sealed ||
|
||||
cfd->imm()->HasOldMemTablesToRemove(installed_log_number) ||
|
||||
cfds_changed.count(cfd) > 0;
|
||||
if (!needs_super_version) {
|
||||
continue;
|
||||
}
|
||||
cfd->imm()->RemoveOldMemTables(installed_log_number,
|
||||
&job_context.memtables_to_free);
|
||||
MaybeWarnAboutRetainedMemtables(cfd, installed_log_number);
|
||||
auto& sv_context = job_context.superversion_contexts.back();
|
||||
cfd->InstallSuperVersion(&sv_context, &mutex_);
|
||||
sv_context.NewSuperVersion();
|
||||
}
|
||||
if (replayed_every_wal_found) {
|
||||
// Secondary catch-up does not run the primary paths that prune the
|
||||
// completed tracker prefix. Replay and pre-replay cleanup add
|
||||
// completions; an outstanding prepare stops the scan.
|
||||
(void)logs_with_prep_tracker_.FindMinLogContainingOutstandingPrep();
|
||||
}
|
||||
}
|
||||
}
|
||||
job_context.Clean();
|
||||
@@ -581,7 +822,7 @@ Status DB::OpenAsSecondary(
|
||||
std::vector<ColumnFamilyHandle*>* handles, std::unique_ptr<DB>* dbptr) {
|
||||
return DBImplSecondary::OpenAsSecondaryImpl(
|
||||
db_options, dbname, secondary_path, column_families, handles, dbptr,
|
||||
/*recover_wal=*/true);
|
||||
/*recover_wal=*/true, /*trust_manifest_recovery=*/false);
|
||||
}
|
||||
|
||||
Status DBImplSecondary::OpenAsSecondaryImpl(
|
||||
@@ -589,7 +830,7 @@ Status DBImplSecondary::OpenAsSecondaryImpl(
|
||||
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) {
|
||||
*dbptr = nullptr;
|
||||
|
||||
DBOptions tmp_opts(db_options);
|
||||
@@ -630,6 +871,8 @@ Status DBImplSecondary::OpenAsSecondaryImpl(
|
||||
impl->file_options_, impl->table_cache_.get(),
|
||||
impl->write_buffer_manager_, &impl->write_controller_, impl->io_tracer_,
|
||||
impl->db_id_, impl->db_session_id_));
|
||||
static_cast_with_check<ReactiveVersionSet>(impl->versions_.get())
|
||||
->SetTrustManifestRecovery(trust_manifest_recovery);
|
||||
impl->column_family_memtables_.reset(
|
||||
new ColumnFamilyMemTablesImpl(impl->versions_->GetColumnFamilySet()));
|
||||
impl->wal_in_db_path_ = impl->immutable_db_options_.IsWalDirSameAsDBPath();
|
||||
@@ -1239,6 +1482,22 @@ Status DBImplSecondary::CompactWithoutInstallation(
|
||||
/*managed_snapshot=*/nullptr,
|
||||
kMaxSequenceNumber, std::move(snapshots));
|
||||
|
||||
// Ensure FileMetaData stats (num_entries, num_range_deletions) are
|
||||
// initialized for all input files. These stats are not persisted in the
|
||||
// MANIFEST and are loaded lazily from table properties. The Compaction
|
||||
// constructor's FilterInputsForCompactionIterator() relies on
|
||||
// FileIsStandAloneRangeTombstone() which needs these fields to be populated.
|
||||
// Without this, the remote worker may not filter the same files as the
|
||||
// primary host, leading to input record count verification failures.
|
||||
{
|
||||
const ReadOptions read_options(Env::IOActivity::kCompaction);
|
||||
for (const auto& level_files : input_files) {
|
||||
for (FileMetaData* file_meta : level_files.files) {
|
||||
version->MaybeInitializeFileMetaData(read_options, file_meta);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TODO - consider serializing the entire Compaction object and using it as
|
||||
// input instead of recreating it in the remote worker
|
||||
std::unique_ptr<Compaction> c;
|
||||
@@ -1404,12 +1663,21 @@ Status DB::OpenAndCompact(
|
||||
|
||||
// 5. Open db As Secondary (skip WAL recovery -- remote compaction only
|
||||
// needs LSM state from MANIFEST, not memtable data from WAL replay)
|
||||
//
|
||||
// A non-zero min_manifest_file_number means the primary provided a MANIFEST
|
||||
// floor (DBOptions::remote_compaction_manifest_floor was on when it scheduled
|
||||
// the job). Its presence couples on both (a) "trust the MANIFEST" recovery
|
||||
// and (b) the floor check below. Absence -> the worker uses the original
|
||||
// point-in-time recovery with no floor check (kill switch off, or an older
|
||||
// primary).
|
||||
const bool floor_provided = compaction_input.min_manifest_file_number != 0;
|
||||
std::unique_ptr<DB> db;
|
||||
std::vector<ColumnFamilyHandle*> handles;
|
||||
const uint64_t db_open_start_micros = db_options.env->NowMicros();
|
||||
s = DBImplSecondary::OpenAsSecondaryImpl(db_options, name, output_directory,
|
||||
column_families, &handles, &db,
|
||||
/*recover_wal=*/false);
|
||||
s = DBImplSecondary::OpenAsSecondaryImpl(
|
||||
db_options, name, output_directory, column_families, &handles, &db,
|
||||
/*recover_wal=*/false,
|
||||
/*trust_manifest_recovery=*/floor_provided);
|
||||
RecordTimeToHistogram(db_options.statistics.get(),
|
||||
OPEN_AND_COMPACT_DB_OPEN_MICROS,
|
||||
db_options.env->NowMicros() - db_open_start_micros);
|
||||
@@ -1421,6 +1689,57 @@ Status DB::OpenAndCompact(
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"DBImplSecondary::OpenAndCompact::AfterOpenAsSecondary:0", db.get());
|
||||
|
||||
// 5b. Manifest floor check. Refuse to reconstruct the compaction against an
|
||||
// older MANIFEST view than the primary scheduled from (e.g. an
|
||||
// eventually-consistent filesystem returning a stale CURRENT, or a truncated
|
||||
// MANIFEST): an older LSM shape could wrongly drop keys that should be kept.
|
||||
// Accept-equal-or-later is safe by monotonicity while the inputs stay locked;
|
||||
// only an older view is unsafe. On rejection, return Status::Incomplete so
|
||||
// the CompactionService implementation can fall back to a local compaction
|
||||
// (kUseLocal) rather than install a possibly-incorrect result.
|
||||
if (floor_provided) {
|
||||
VersionSet* recovered_versions =
|
||||
static_cast_with_check<DBImplSecondary>(db.get())->GetVersionSet();
|
||||
const uint64_t recovered_number =
|
||||
recovered_versions->manifest_file_number();
|
||||
// Compare the maximal valid prefix of the recovered MANIFEST (not the
|
||||
// reader's read-to-EOF high-water mark, which can include tolerated tail
|
||||
// garbage: a corrupt/torn trailing record or a partial atomic group at
|
||||
// EOF). This way a tolerated corrupt tail is fine as long as the
|
||||
// actually-installed valid prefix reaches the floor. Both sides are byte
|
||||
// sizes of the same append-only, uniquely-numbered MANIFEST, so equal
|
||||
// number + recovered_valid_size >= min means the worker installed a
|
||||
// same-or-newer view; anything short (older/truncated/torn/partial) is
|
||||
// below the floor and rejected.
|
||||
const uint64_t recovered_valid_size =
|
||||
recovered_versions->manifest_recovery_maximal_valid_size();
|
||||
bool below_floor =
|
||||
recovered_number < compaction_input.min_manifest_file_number ||
|
||||
(recovered_number == compaction_input.min_manifest_file_number &&
|
||||
recovered_valid_size < compaction_input.min_manifest_file_size);
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"DBImplSecondary::OpenAndCompact::ManifestFloorCheck", &below_floor);
|
||||
if (below_floor) {
|
||||
ROCKS_LOG_WARN(
|
||||
db_options.info_log,
|
||||
"Remote compaction recovered an older MANIFEST view (file number "
|
||||
"%" PRIu64 " valid size %" PRIu64
|
||||
") than the primary scheduled from "
|
||||
"(min file number %" PRIu64 " size %" PRIu64
|
||||
"); declining so the job can fall back to local compaction.",
|
||||
recovered_number, recovered_valid_size,
|
||||
compaction_input.min_manifest_file_number,
|
||||
compaction_input.min_manifest_file_size);
|
||||
for (auto& handle : handles) {
|
||||
delete handle;
|
||||
}
|
||||
db.reset();
|
||||
return Status::Incomplete(
|
||||
"Remote compaction worker recovered an older MANIFEST view than the "
|
||||
"primary scheduled from");
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Find the handle of the Column Family that this will compact
|
||||
ColumnFamilyHandle* cfh = nullptr;
|
||||
for (auto* handle : handles) {
|
||||
|
||||
@@ -5,7 +5,10 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "db/db_impl/db_impl.h"
|
||||
@@ -287,6 +290,23 @@ 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_;
|
||||
@@ -304,13 +324,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.
|
||||
@@ -404,19 +474,33 @@ 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_;
|
||||
|
||||
const std::string secondary_path_;
|
||||
|
||||
|
||||
+734
-9
@@ -13,11 +13,14 @@
|
||||
#include "db/db_with_timestamp_test_util.h"
|
||||
#include "db/wide/wide_column_test_util.h"
|
||||
#include "db/write_batch_internal.h"
|
||||
#include "env/composite_env_wrapper.h"
|
||||
#include "file/filename.h"
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/utilities/transaction_db.h"
|
||||
#include "test_util/sync_point.h"
|
||||
#include "test_util/testutil.h"
|
||||
#include "util/defer.h"
|
||||
#include "utilities/fault_injection_fs.h"
|
||||
#include "utilities/merge_operators/string_append/stringappend2.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
@@ -63,6 +66,149 @@ class DBSecondaryTestBase : public DBBasicTestWithTimestampBase {
|
||||
db_secondary_.reset();
|
||||
}
|
||||
|
||||
// Reopens the primary with `options`, opens a secondary on
|
||||
// `secondary_options` and catches it up after writing "foo"="v1" to the
|
||||
// primary's WAL without flushing, so that the value is readable only from the
|
||||
// secondary's WAL-replayed active memtable. An earlier flush makes the column
|
||||
// family's log number equal the WAL that the secondary then replays, which is
|
||||
// the boundary case for deciding whether that WAL has been flushed.
|
||||
void OpenSecondaryOnWalOnlyPut(const Options& options,
|
||||
const Options& secondary_options) {
|
||||
Reopen(options);
|
||||
ASSERT_OK(Put("seed", "s"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
OpenSecondary(secondary_options);
|
||||
ASSERT_OK(Put("foo", "v1"));
|
||||
ASSERT_OK(db_->FlushWAL(/*sync=*/true));
|
||||
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
|
||||
VerifySecondaryValue("foo", "v1");
|
||||
}
|
||||
|
||||
// Destroys the primary and recreates it as a TransactionDB, so that its WAL
|
||||
// carries 2PC markers. `db_` takes ownership; `*txn_db` is a borrowed view of
|
||||
// it for the transaction API. Call under ASSERT_NO_FATAL_FAILURE: on a failed
|
||||
// open `*txn_db` is left null.
|
||||
void RecreatePrimaryAsTransactionDB(const Options& options,
|
||||
TransactionDB** txn_db) {
|
||||
Close();
|
||||
Destroy(options, true);
|
||||
*txn_db = nullptr;
|
||||
TransactionDBOptions txn_db_opts;
|
||||
ASSERT_OK(TransactionDB::Open(options, txn_db_opts, dbname_, txn_db));
|
||||
ASSERT_NE(nullptr, *txn_db);
|
||||
db_.reset(*txn_db);
|
||||
}
|
||||
|
||||
// Opens a secondary that can replay the primary's 2PC markers.
|
||||
// OpenAsSecondary() does not force allow_2pc the way TransactionDB::Open()
|
||||
// does for the primary, and without it MarkBeginPrepare() rejects the
|
||||
// prepared section with NotSupported.
|
||||
void OpenSecondaryFor2PC(const Options& options) {
|
||||
Options secondary_options = options;
|
||||
secondary_options.max_open_files = -1;
|
||||
secondary_options.allow_2pc = true;
|
||||
OpenSecondary(secondary_options);
|
||||
}
|
||||
|
||||
// Prepares a transaction named `name` on the primary and catches the
|
||||
// secondary up to it, so that `*txn` awaits resolution and the secondary
|
||||
// holds its prepared batch as a recovered transaction. Call under
|
||||
// ASSERT_NO_FATAL_FAILURE.
|
||||
void PrepareTransactionAndCatchUp(TransactionDB* txn_db,
|
||||
const std::string& name,
|
||||
const std::string& key,
|
||||
const std::string& value,
|
||||
std::unique_ptr<Transaction>* txn) {
|
||||
txn->reset(txn_db->BeginTransaction(WriteOptions(), TransactionOptions()));
|
||||
ASSERT_NE(nullptr, txn->get());
|
||||
ASSERT_OK((*txn)->SetName(name));
|
||||
ASSERT_OK((*txn)->Put(key, value));
|
||||
ASSERT_OK((*txn)->Prepare());
|
||||
ASSERT_OK(db_->FlushWAL(/*sync=*/true));
|
||||
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
|
||||
ASSERT_NE(nullptr, db_secondary_full()->GetRecoveredTransaction(name));
|
||||
}
|
||||
|
||||
// Commits `*txn` on the primary and flushes, then purges the WAL holding its
|
||||
// commit marker, so that no later round can replay that marker. Reports the
|
||||
// purged WAL's log number in `*commit_log_number`. Call under
|
||||
// ASSERT_NO_FATAL_FAILURE.
|
||||
void CommitAndPurgeMarkerWal(TransactionDB* txn_db,
|
||||
std::unique_ptr<Transaction>* txn,
|
||||
uint64_t* commit_log_number) {
|
||||
DBImpl* primary = static_cast_with_check<DBImpl>(txn_db->GetRootDB());
|
||||
ASSERT_OK((*txn)->Commit());
|
||||
txn->reset();
|
||||
*commit_log_number = primary->TEST_GetCurrentLogNumber();
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "spacer", "s"));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
primary->TEST_DeleteObsoleteFiles();
|
||||
ASSERT_OK(primary->TEST_WaitForPurge());
|
||||
ASSERT_TRUE(env_->FileExists(LogFileName(dbname_, *commit_log_number))
|
||||
.IsNotFound());
|
||||
}
|
||||
|
||||
// Checks that the secondary reads `expected` for `key` through both Get() and
|
||||
// an iterator. The two paths order the active memtable against the file set
|
||||
// differently, so they can disagree on a stale memtable entry.
|
||||
void VerifySecondaryValue(const std::string& key,
|
||||
const std::string& expected) {
|
||||
VerifySecondaryValue(db_secondary_->DefaultColumnFamily(), key, expected);
|
||||
}
|
||||
|
||||
void VerifySecondaryValue(ColumnFamilyHandle* cfh, const std::string& key,
|
||||
const std::string& expected) {
|
||||
SCOPED_TRACE("key=" + key);
|
||||
ReadOptions ropts;
|
||||
std::string value;
|
||||
ASSERT_OK(db_secondary_->Get(ropts, cfh, key, &value));
|
||||
ASSERT_EQ(expected, value);
|
||||
std::unique_ptr<Iterator> iter(db_secondary_->NewIterator(ropts, cfh));
|
||||
iter->Seek(key);
|
||||
ASSERT_OK(iter->status());
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(key, iter->key().ToString());
|
||||
ASSERT_EQ(expected, iter->value().ToString());
|
||||
}
|
||||
|
||||
// Returns the path of the primary's newest table file for `cf_name`.
|
||||
std::string GetNewestTableFilePath(const std::string& cf_name) {
|
||||
std::vector<LiveFileMetaData> live_files;
|
||||
db_->GetLiveFilesMetaData(&live_files);
|
||||
std::string path;
|
||||
uint64_t newest_file_number = 0;
|
||||
for (const auto& md : live_files) {
|
||||
if (md.column_family_name == cf_name &&
|
||||
md.file_number > newest_file_number) {
|
||||
newest_file_number = md.file_number;
|
||||
path = md.directory + "/" + md.relative_filename;
|
||||
}
|
||||
}
|
||||
EXPECT_FALSE(path.empty());
|
||||
return path;
|
||||
}
|
||||
|
||||
// Moves `path` aside so that opening it fails, and returns where it went.
|
||||
// Every DB holding the file open must be closed first, because not every
|
||||
// platform allows renaming an open file. The caller must move it back before
|
||||
// the fixture is destroyed - DestroyDB() does not recognize the new name, so
|
||||
// it would otherwise be left behind for the next test sharing `dbname_` - and
|
||||
// must do so with a Defer, since a failing assertion leaves the test body
|
||||
// early.
|
||||
std::string MoveFileAside(const std::string& path) {
|
||||
const std::string aside_path = path + ".aside";
|
||||
EXPECT_OK(env_->RenameFile(path, aside_path));
|
||||
return aside_path;
|
||||
}
|
||||
|
||||
// Returns the secondary's ColumnFamilyData behind `cfh`, for the tests that
|
||||
// assert on memtable state directly rather than through a read.
|
||||
static ColumnFamilyData* GetSecondaryCfd(ColumnFamilyHandle* cfh) {
|
||||
return static_cast_with_check<ColumnFamilyHandleImpl>(cfh)->cfd();
|
||||
}
|
||||
|
||||
DBImplSecondary* db_secondary_full() {
|
||||
return static_cast<DBImplSecondary*>(db_secondary_.get());
|
||||
}
|
||||
@@ -997,6 +1143,593 @@ TEST_F(DBSecondaryTest, SecondaryTailingBug_ISSUE_8467) {
|
||||
}
|
||||
}
|
||||
|
||||
// Fixture for the catch-up rounds that fail partway through. The
|
||||
// fault-injecting Env is a member, and the destructor closes the secondary
|
||||
// before that member is destroyed, so the Env outlives the DB opened on it even
|
||||
// when an ASSERT returns from a test early.
|
||||
class DBSecondaryCatchUpFaultTest : public DBSecondaryTestBase {
|
||||
public:
|
||||
DBSecondaryCatchUpFaultTest()
|
||||
: DBSecondaryTestBase("db_secondary_catch_up_fault_test"),
|
||||
fault_fs_(
|
||||
std::make_shared<FaultInjectionTestFS>(env_->GetFileSystem())),
|
||||
fault_env_(new CompositeEnvWrapper(env_, fault_fs_)) {}
|
||||
|
||||
~DBSecondaryCatchUpFaultTest() override {
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
fault_fs_->SetFilesystemActive(true);
|
||||
CloseSecondary();
|
||||
}
|
||||
|
||||
protected:
|
||||
std::shared_ptr<FaultInjectionTestFS> fault_fs_;
|
||||
std::unique_ptr<Env> fault_env_;
|
||||
};
|
||||
|
||||
// A catch-up round can fail after ReadAndApply() has already advanced the
|
||||
// column family's log number, here because WAL discovery fails. Later rounds
|
||||
// see no new MANIFEST record for that column family, so they must still
|
||||
// reconcile its active memtable against the installed Version rather than
|
||||
// considering only the column families that changed in the round.
|
||||
TEST_F(DBSecondaryCatchUpFaultTest, ReconcilesMemtableAfterFailedRound) {
|
||||
Options options;
|
||||
options.env = env_;
|
||||
options.disable_auto_compactions = true;
|
||||
Options secondary_options;
|
||||
secondary_options.env = fault_env_.get();
|
||||
secondary_options.max_open_files = -1;
|
||||
OpenSecondaryOnWalOnlyPut(options, secondary_options);
|
||||
|
||||
ASSERT_OK(Put("foo", "v2"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// The round that would have reconciled the memtable fails right after the
|
||||
// MANIFEST tail has been applied, which is where the log number advances.
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImplSecondary::FindAndRecoverLogFiles:Begin", [this](void* /*arg*/) {
|
||||
fault_fs_->SetFilesystemActive(
|
||||
false, IOStatus::IOError("injected WAL discovery failure"));
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
ASSERT_NOK(db_secondary_->TryCatchUpWithPrimary());
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
fault_fs_->SetFilesystemActive(true);
|
||||
|
||||
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
|
||||
VerifySecondaryValue("foo", "v2");
|
||||
}
|
||||
|
||||
// A round whose WAL discovery reports a purged path replays nothing, yet
|
||||
// reports success. A marker still eligible for replay has not had its chance to
|
||||
// resolve its prepared section, so that round must drop no recovered
|
||||
// transaction, however far the watermark has moved past its prepare.
|
||||
TEST_F(DBSecondaryCatchUpFaultTest,
|
||||
KeepsRecoveredTransactionAfterUnreplayedRound) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
|
||||
TransactionDB* txn_db = nullptr;
|
||||
ASSERT_NO_FATAL_FAILURE(RecreatePrimaryAsTransactionDB(options, &txn_db));
|
||||
|
||||
Options secondary_options = options;
|
||||
secondary_options.env = fault_env_.get();
|
||||
OpenSecondaryFor2PC(secondary_options);
|
||||
|
||||
std::unique_ptr<Transaction> txn;
|
||||
ASSERT_NO_FATAL_FAILURE(
|
||||
PrepareTransactionAndCatchUp(txn_db, "t4", "k3", "v3", &txn));
|
||||
|
||||
// Leave the transaction resolved and its marker's WAL purged, so any round
|
||||
// that replayed every WAL it found would drop it.
|
||||
uint64_t commit_log_number = 0;
|
||||
ASSERT_NO_FATAL_FAILURE(
|
||||
CommitAndPurgeMarkerWal(txn_db, &txn, &commit_log_number));
|
||||
|
||||
// Discovery fails the way a WAL purged mid-round fails, which the round
|
||||
// reports as success after replaying nothing.
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImplSecondary::FindAndRecoverLogFiles:Begin", [this](void* /*arg*/) {
|
||||
fault_fs_->SetFilesystemActive(
|
||||
false, IOStatus::PathNotFound("injected purged WAL"));
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
fault_fs_->SetFilesystemActive(true);
|
||||
ASSERT_GT(db_secondary_full()->GetVersionSet()->min_log_number_to_keep(),
|
||||
commit_log_number);
|
||||
ASSERT_NE(nullptr, db_secondary_full()->GetRecoveredTransaction("t4"));
|
||||
|
||||
// The next round replays every WAL it finds, so it may drop the transaction.
|
||||
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
|
||||
ASSERT_EQ(nullptr, db_secondary_full()->GetRecoveredTransaction("t4"));
|
||||
}
|
||||
|
||||
// The column family's log number advances as soon as the flush record is read,
|
||||
// even when the flushed file cannot be opened and no Version reflecting it can
|
||||
// be installed. The active memtable then holds the only readable copy of what
|
||||
// was flushed, so dropping it would turn a stale read into data loss.
|
||||
TEST_F(DBSecondaryTest, KeepsMemtableWhenFlushedFileIsMissing) {
|
||||
Options options;
|
||||
options.env = env_;
|
||||
options.disable_auto_compactions = true;
|
||||
Options secondary_options;
|
||||
secondary_options.env = env_;
|
||||
secondary_options.max_open_files = -1;
|
||||
OpenSecondaryOnWalOnlyPut(options, secondary_options);
|
||||
|
||||
// The primary flushes "v2", then its file goes away before the secondary
|
||||
// reads the MANIFEST record that adds it.
|
||||
ASSERT_OK(Put("foo", "v2"));
|
||||
ASSERT_OK(Flush());
|
||||
const std::string table_file =
|
||||
GetNewestTableFilePath(kDefaultColumnFamilyName);
|
||||
Close();
|
||||
const std::string aside_file = MoveFileAside(table_file);
|
||||
Defer restore_file(
|
||||
[&] { EXPECT_OK(env_->RenameFile(aside_file, table_file)); });
|
||||
|
||||
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
|
||||
|
||||
// "v1" is stale but the newest value readable here: "v2"'s file is absent
|
||||
// from the installed Version and its WAL is obsolete. Reaching "v2" needs a
|
||||
// reopen. What must not happen is the key disappearing.
|
||||
VerifySecondaryValue("foo", "v1");
|
||||
}
|
||||
|
||||
// Same hazard reached without any missing file of the column family's own: an
|
||||
// atomic flush whose group is incomplete parks the new Version of every column
|
||||
// family in the group, including those whose files are all readable, while
|
||||
// their log numbers have already advanced.
|
||||
TEST_F(DBSecondaryTest, KeepsMemtableWhenAtomicFlushSiblingFileIsMissing) {
|
||||
Options options;
|
||||
options.env = env_;
|
||||
options.disable_auto_compactions = true;
|
||||
options.atomic_flush = true;
|
||||
CreateAndReopenWithCF({"cf1"}, options);
|
||||
|
||||
Options secondary_options;
|
||||
secondary_options.env = env_;
|
||||
secondary_options.max_open_files = -1;
|
||||
secondary_options.atomic_flush = true;
|
||||
OpenSecondaryWithColumnFamilies({"cf1"}, secondary_options);
|
||||
ASSERT_EQ(2, handles_secondary_.size());
|
||||
|
||||
ASSERT_OK(Put(0, "foo", "v1"));
|
||||
ASSERT_OK(Put(1, "bar", "w1"));
|
||||
ASSERT_OK(db_->FlushWAL(/*sync=*/true));
|
||||
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
|
||||
VerifySecondaryValue(handles_secondary_[0], "foo", "v1");
|
||||
VerifySecondaryValue(handles_secondary_[1], "bar", "w1");
|
||||
|
||||
// Both column families are flushed in one atomic group, but only the default
|
||||
// column family's file stays readable.
|
||||
ASSERT_OK(Flush({0, 1}));
|
||||
const std::string cf1_table_file = GetNewestTableFilePath("cf1");
|
||||
Close();
|
||||
const std::string aside_file = MoveFileAside(cf1_table_file);
|
||||
Defer restore_file(
|
||||
[&] { EXPECT_OK(env_->RenameFile(aside_file, cf1_table_file)); });
|
||||
|
||||
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
|
||||
|
||||
// Neither column family may lose its key. The default one has no missing
|
||||
// file of its own, but its Version is parked while the group is incomplete,
|
||||
// leaving its memtable the only source of "foo"; assert that directly too.
|
||||
ASSERT_FALSE(GetSecondaryCfd(handles_secondary_[0])->mem()->IsEmpty());
|
||||
VerifySecondaryValue(handles_secondary_[0], "foo", "v1");
|
||||
VerifySecondaryValue(handles_secondary_[1], "bar", "w1");
|
||||
}
|
||||
|
||||
// Same hazard for a flushed file that is present and the right size but
|
||||
// unreadable. With verify_sst_unique_id_in_manifest disabled nothing records it
|
||||
// missing, yet opening it fails while the Version is built and that failure is
|
||||
// swallowed so replay can continue, so again no Version covers the flush.
|
||||
TEST_F(DBSecondaryTest, KeepsMemtableWhenFlushedFileIsCorrupt) {
|
||||
Options options;
|
||||
options.env = env_;
|
||||
options.disable_auto_compactions = true;
|
||||
Options secondary_options;
|
||||
secondary_options.env = env_;
|
||||
secondary_options.max_open_files = -1;
|
||||
secondary_options.verify_sst_unique_id_in_manifest = false;
|
||||
OpenSecondaryOnWalOnlyPut(options, secondary_options);
|
||||
|
||||
ASSERT_OK(Put("foo", "v2"));
|
||||
ASSERT_OK(Flush());
|
||||
const std::string table_file =
|
||||
GetNewestTableFilePath(kDefaultColumnFamilyName);
|
||||
Close();
|
||||
// Corrupting the footer in place keeps the file size that the MANIFEST
|
||||
// records, which is the only thing verified without unique id verification.
|
||||
ASSERT_OK(test::CorruptFile(env_, table_file, /*offset=*/-8,
|
||||
/*bytes_to_corrupt=*/8,
|
||||
/*verify_checksum=*/false));
|
||||
|
||||
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
|
||||
VerifySecondaryValue("foo", "v1");
|
||||
}
|
||||
|
||||
// The same hazard for an *immutable* memtable. RecoverLogFiles() stamps a
|
||||
// memtable sealed on a WAL switch with the newer WAL, and the column family's
|
||||
// log number reaches that WAL as soon as the flush record is read, so the
|
||||
// watermark collecting it has to come from the installed Version too.
|
||||
TEST_F(DBSecondaryTest, KeepsImmutableMemtableWhenFlushedFileIsMissing) {
|
||||
Options options;
|
||||
options.env = env_;
|
||||
options.disable_auto_compactions = true;
|
||||
Options secondary_options;
|
||||
secondary_options.env = env_;
|
||||
secondary_options.max_open_files = -1;
|
||||
OpenSecondaryOnWalOnlyPut(options, secondary_options);
|
||||
|
||||
// Only "foo" is overwritten later, so "bar" ends up readable from nowhere
|
||||
// but this memtable once the flushed file is gone.
|
||||
ASSERT_OK(Put("bar", "w1"));
|
||||
ASSERT_OK(db_->FlushWAL(/*sync=*/true));
|
||||
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
|
||||
VerifySecondaryValue("foo", "v1");
|
||||
VerifySecondaryValue("bar", "w1");
|
||||
|
||||
// "v2" goes through the WAL, so closing the primary leaves it there for the
|
||||
// secondary to replay after the flush record. The flushed file disappears
|
||||
// before the secondary reads the MANIFEST record adding it.
|
||||
ASSERT_OK(Flush());
|
||||
const std::string table_file =
|
||||
GetNewestTableFilePath(kDefaultColumnFamilyName);
|
||||
ASSERT_OK(Put("foo", "v2"));
|
||||
ASSERT_OK(db_->FlushWAL(/*sync=*/true));
|
||||
Close();
|
||||
const std::string aside_file = MoveFileAside(table_file);
|
||||
Defer restore_file(
|
||||
[&] { EXPECT_OK(env_->RenameFile(aside_file, table_file)); });
|
||||
|
||||
// This round advances the log number and seals the memtable holding both
|
||||
// keys, stamped with the new WAL. No Version covers the flush, so it must
|
||||
// survive: "v2" supersedes "foo", but nothing supersedes "bar".
|
||||
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
|
||||
// "bar" survives in an immutable memtable, unlike the test above.
|
||||
ASSERT_EQ(1, GetSecondaryCfd(db_secondary_->DefaultColumnFamily())
|
||||
->imm()
|
||||
->NumNotFlushed());
|
||||
VerifySecondaryValue("foo", "v2");
|
||||
VerifySecondaryValue("bar", "w1");
|
||||
}
|
||||
|
||||
// WriteBatch::Iterate() stops at its first failure with earlier entries already
|
||||
// inserted, so a batch rejected partway leaves entries from the newer WAL in
|
||||
// the active memtable. The recorded WAL has to cover them, or the gate passes
|
||||
// as soon as the installed Version reaches the newer WAL and drops them.
|
||||
TEST_F(DBSecondaryTest, KeepsMemtableAfterPartialReplayFailure) {
|
||||
Options options;
|
||||
options.env = env_;
|
||||
options.disable_auto_compactions = true;
|
||||
CreateAndReopenWithCF({"cf1"}, options);
|
||||
|
||||
// Rejecting writes to "cf1" on the secondary only is what fails a batch
|
||||
// partway: the default column family's entry is inserted, then "cf1" stops
|
||||
// it. The primary allows both, so it can write such a batch.
|
||||
Options secondary_options;
|
||||
secondary_options.env = env_;
|
||||
secondary_options.max_open_files = -1;
|
||||
Options secondary_cf1_options = secondary_options;
|
||||
secondary_cf1_options.disallow_memtable_writes = true;
|
||||
std::vector<ColumnFamilyDescriptor> cf_descs;
|
||||
cf_descs.emplace_back(kDefaultColumnFamilyName, secondary_options);
|
||||
cf_descs.emplace_back("cf1", secondary_cf1_options);
|
||||
ASSERT_OK(DB::OpenAsSecondary(secondary_options, dbname_, secondary_path_,
|
||||
cf_descs, &handles_secondary_, &db_secondary_));
|
||||
ASSERT_EQ(2, handles_secondary_.size());
|
||||
|
||||
// The hazard needs an older WAL already recorded: with no entry at all the
|
||||
// gate keeps the memtable regardless.
|
||||
ASSERT_OK(Put(0, "keep", "v1"));
|
||||
ASSERT_OK(db_->FlushWAL(/*sync=*/true));
|
||||
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
|
||||
VerifySecondaryValue(handles_secondary_[0], "keep", "v1");
|
||||
|
||||
// "keep" becomes file-readable and the primary moves to the WAL the rejected
|
||||
// batch below lands in.
|
||||
ASSERT_OK(Flush(0));
|
||||
|
||||
{
|
||||
WriteBatch batch;
|
||||
ASSERT_OK(batch.Put(handles_[0], "lost", "w1"));
|
||||
ASSERT_OK(batch.Put(handles_[1], "x", "y"));
|
||||
ASSERT_OK(db_->Write(WriteOptions(), &batch));
|
||||
}
|
||||
ASSERT_OK(db_->FlushWAL(/*sync=*/true));
|
||||
ASSERT_NOK(db_secondary_->TryCatchUpWithPrimary());
|
||||
|
||||
// Both column families, so that WAL goes obsolete: until it does, the reader
|
||||
// the failed round poisoned fails every later round before the gate. The new
|
||||
// file then goes missing, so no Version covers "lost".
|
||||
ASSERT_OK(Flush({0, 1}));
|
||||
const std::string table_file =
|
||||
GetNewestTableFilePath(kDefaultColumnFamilyName);
|
||||
Close();
|
||||
const std::string aside_file = MoveFileAside(table_file);
|
||||
Defer restore_file(
|
||||
[&]() { EXPECT_OK(env_->RenameFile(aside_file, table_file)); });
|
||||
|
||||
// "lost" is readable only from the memtable the rejected batch wrote to, so
|
||||
// that memtable must survive.
|
||||
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
|
||||
VerifySecondaryValue(handles_secondary_[0], "keep", "v1");
|
||||
VerifySecondaryValue(handles_secondary_[0], "lost", "w1");
|
||||
}
|
||||
|
||||
// A 2PC commit record names no column family, so
|
||||
// CollectColumnFamilyIdsFromWriteBatch() reports nothing, yet MarkCommit()
|
||||
// replays the prepared batch into the memtables. The recorded WAL has to
|
||||
// advance anyway, or the gate drops committed data no file covers yet.
|
||||
TEST_F(DBSecondaryTest, KeepsMemtableAfterTwoPhaseCommitReplay) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
|
||||
// Recreate the primary as a TransactionDB so its WAL carries 2PC markers.
|
||||
TransactionDB* txn_db = nullptr;
|
||||
ASSERT_NO_FATAL_FAILURE(RecreatePrimaryAsTransactionDB(options, &txn_db));
|
||||
|
||||
// Flush so the WAL the prepared section lands in is not the one the column
|
||||
// family's log number already names.
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "seed", "s"));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
|
||||
OpenSecondaryFor2PC(options);
|
||||
|
||||
// The prepared batch names the default column family, so the secondary
|
||||
// records this WAL for it, but buffers the entries instead of inserting.
|
||||
std::unique_ptr<Transaction> txn;
|
||||
ASSERT_NO_FATAL_FAILURE(
|
||||
PrepareTransactionAndCatchUp(txn_db, "t1", "x", "2", &txn));
|
||||
ASSERT_TRUE(
|
||||
GetSecondaryCfd(db_secondary_->DefaultColumnFamily())->mem()->IsEmpty());
|
||||
|
||||
// No catch-up between this write and its flush: the secondary then applies
|
||||
// the flush record first and skips replaying "z" as already flushed, which
|
||||
// advances the installed Version while leaving its memtable empty.
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "z", "1"));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
|
||||
// Had "z" been replayed instead of skipped, this round would seal and the
|
||||
// commit below would record its own WAL, leaving nothing to get wrong.
|
||||
ColumnFamilyData* cfd_secondary =
|
||||
GetSecondaryCfd(db_secondary_->DefaultColumnFamily());
|
||||
ASSERT_TRUE(cfd_secondary->mem()->IsEmpty());
|
||||
ASSERT_EQ(0, cfd_secondary->imm()->NumNotFlushed());
|
||||
|
||||
// Replaying the marker inserts "x", which no file covers.
|
||||
ASSERT_OK(txn->Commit());
|
||||
txn.reset();
|
||||
ASSERT_OK(db_->FlushWAL(/*sync=*/true));
|
||||
|
||||
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
|
||||
VerifySecondaryValue("x", "2");
|
||||
}
|
||||
|
||||
// A secondary that observes a prepare but never its commit marker holds the
|
||||
// prepared batch until close: DeleteRecoveredTransaction() runs only from
|
||||
// MarkCommit() and MarkRollback(). Once the primary commits, flushes, and
|
||||
// purges the WAL holding the marker nothing can resolve the batch, so a
|
||||
// long-lived secondary accumulates them without bound while serving correct
|
||||
// reads from the SST.
|
||||
TEST_F(DBSecondaryTest, DropsRecoveredTransactionAfterCommitWalIsPurged) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
|
||||
TransactionDB* txn_db = nullptr;
|
||||
ASSERT_NO_FATAL_FAILURE(RecreatePrimaryAsTransactionDB(options, &txn_db));
|
||||
|
||||
OpenSecondaryFor2PC(options);
|
||||
|
||||
std::unique_ptr<Transaction> txn;
|
||||
ASSERT_NO_FATAL_FAILURE(
|
||||
PrepareTransactionAndCatchUp(txn_db, "t2", "k1", "v2", &txn));
|
||||
|
||||
// The primary commits and flushes, then the WAL holding the marker is purged
|
||||
// before the secondary catches up again, so the marker is never replayed.
|
||||
uint64_t commit_log_number = 0;
|
||||
ASSERT_NO_FATAL_FAILURE(
|
||||
CommitAndPurgeMarkerWal(txn_db, &txn, &commit_log_number));
|
||||
|
||||
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
|
||||
VerifySecondaryValue("k1", "v2");
|
||||
// The drop reads the secondary's own watermark, replicated by the MANIFEST.
|
||||
ASSERT_GT(db_secondary_full()->GetVersionSet()->min_log_number_to_keep(),
|
||||
commit_log_number);
|
||||
ASSERT_EQ(nullptr, db_secondary_full()->GetRecoveredTransaction("t2"));
|
||||
ASSERT_EQ(0, db_secondary_full()->TEST_LogsWithPrepSize());
|
||||
ASSERT_EQ(0, db_secondary_full()->TEST_PreparedSectionCompletedSize());
|
||||
}
|
||||
|
||||
// A primary can reuse a transaction name after commit unregisters it. If the
|
||||
// secondary missed the old marker, it must remove that generation before
|
||||
// replaying a new prepare with the same name.
|
||||
TEST_F(DBSecondaryTest, ReusesTransactionNameAfterResolvedBatchIsPurged) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
|
||||
TransactionDB* txn_db = nullptr;
|
||||
ASSERT_NO_FATAL_FAILURE(RecreatePrimaryAsTransactionDB(options, &txn_db));
|
||||
OpenSecondaryFor2PC(options);
|
||||
|
||||
std::unique_ptr<Transaction> old_txn;
|
||||
ASSERT_NO_FATAL_FAILURE(
|
||||
PrepareTransactionAndCatchUp(txn_db, "t6", "reuse", "v1", &old_txn));
|
||||
uint64_t old_commit_log_number = 0;
|
||||
ASSERT_NO_FATAL_FAILURE(
|
||||
CommitAndPurgeMarkerWal(txn_db, &old_txn, &old_commit_log_number));
|
||||
|
||||
std::unique_ptr<Transaction> new_txn;
|
||||
ASSERT_NO_FATAL_FAILURE(
|
||||
PrepareTransactionAndCatchUp(txn_db, "t6", "reuse", "v2", &new_txn));
|
||||
// The drop, not a replayed marker, is what removed the old generation.
|
||||
ASSERT_GT(db_secondary_full()->GetVersionSet()->min_log_number_to_keep(),
|
||||
old_commit_log_number);
|
||||
DBImpl::RecoveredTransaction* recovered =
|
||||
db_secondary_full()->GetRecoveredTransaction("t6");
|
||||
ASSERT_NE(nullptr, recovered);
|
||||
ASSERT_EQ(1, recovered->batches_.size());
|
||||
VerifySecondaryValue("reuse", "v1");
|
||||
|
||||
ASSERT_OK(new_txn->Commit());
|
||||
new_txn.reset();
|
||||
ASSERT_OK(db_->FlushWAL(/*sync=*/true));
|
||||
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
|
||||
ASSERT_EQ(nullptr, db_secondary_full()->GetRecoveredTransaction("t6"));
|
||||
VerifySecondaryValue("reuse", "v2");
|
||||
ASSERT_EQ(0, db_secondary_full()->TEST_LogsWithPrepSize());
|
||||
ASSERT_EQ(0, db_secondary_full()->TEST_PreparedSectionCompletedSize());
|
||||
}
|
||||
|
||||
// Commit replay deletes the recovered transaction and records its prepare WAL
|
||||
// as complete. A secondary never runs the primary flush and write paths that
|
||||
// normally prune that tracking state, so catch-up must do it.
|
||||
TEST_F(DBSecondaryTest, PrunesRecoveredTransactionTrackingAfterCommitReplay) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
|
||||
TransactionDB* txn_db = nullptr;
|
||||
ASSERT_NO_FATAL_FAILURE(RecreatePrimaryAsTransactionDB(options, &txn_db));
|
||||
OpenSecondaryFor2PC(options);
|
||||
|
||||
std::unique_ptr<Transaction> txn;
|
||||
ASSERT_NO_FATAL_FAILURE(
|
||||
PrepareTransactionAndCatchUp(txn_db, "t5", "k3", "v3", &txn));
|
||||
ASSERT_EQ(1, db_secondary_full()->TEST_LogsWithPrepSize());
|
||||
ASSERT_EQ(0, db_secondary_full()->TEST_PreparedSectionCompletedSize());
|
||||
|
||||
ASSERT_OK(txn->Commit());
|
||||
txn.reset();
|
||||
ASSERT_OK(db_->FlushWAL(/*sync=*/true));
|
||||
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
|
||||
ASSERT_EQ(nullptr, db_secondary_full()->GetRecoveredTransaction("t5"));
|
||||
ASSERT_EQ(0, db_secondary_full()->TEST_LogsWithPrepSize());
|
||||
ASSERT_EQ(0, db_secondary_full()->TEST_PreparedSectionCompletedSize());
|
||||
}
|
||||
|
||||
// A transaction whose prepare is still outstanding must be kept, no matter how
|
||||
// much unrelated data the primary flushes and purges.
|
||||
TEST_F(DBSecondaryTest, KeepsRecoveredTransactionWhilePrepareIsOutstanding) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
|
||||
TransactionDB* txn_db = nullptr;
|
||||
ASSERT_NO_FATAL_FAILURE(RecreatePrimaryAsTransactionDB(options, &txn_db));
|
||||
DBImpl* primary = static_cast_with_check<DBImpl>(db_->GetRootDB());
|
||||
|
||||
OpenSecondaryFor2PC(options);
|
||||
|
||||
std::unique_ptr<Transaction> txn;
|
||||
ASSERT_NO_FATAL_FAILURE(
|
||||
PrepareTransactionAndCatchUp(txn_db, "t3", "k2", "w2", &txn));
|
||||
const uint64_t prepare_log_number = primary->TEST_GetCurrentLogNumber();
|
||||
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "spacer" + std::to_string(i), "s"));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
}
|
||||
primary->TEST_DeleteObsoleteFiles();
|
||||
ASSERT_OK(primary->TEST_WaitForPurge());
|
||||
|
||||
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
|
||||
// The watermark the drop reads stops exactly at the prepare's WAL, which is
|
||||
// what makes comparing against it with `>=` load-bearing.
|
||||
ASSERT_EQ(prepare_log_number,
|
||||
db_secondary_full()->GetVersionSet()->min_log_number_to_keep());
|
||||
ASSERT_NE(nullptr, db_secondary_full()->GetRecoveredTransaction("t3"));
|
||||
|
||||
// MarkCommit() resolves it and applies the batch, so an early drop would have
|
||||
// lost "k2".
|
||||
ASSERT_OK(txn->Commit());
|
||||
txn.reset();
|
||||
ASSERT_OK(db_->FlushWAL(/*sync=*/true));
|
||||
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
|
||||
ASSERT_EQ(nullptr, db_secondary_full()->GetRecoveredTransaction("t3"));
|
||||
VerifySecondaryValue("k2", "w2");
|
||||
}
|
||||
|
||||
// Once the primary flushes and drops a WAL, its files are the authority for
|
||||
// that WAL's contents, but entries the secondary already replayed linger in the
|
||||
// active memtable and are consulted first, shadowing the newer file value.
|
||||
// Bottommost compaction rewrites the file entry's sequence number to 0, which
|
||||
// makes the shadowing permanent even for a seqno-aware read path.
|
||||
TEST_F(DBSecondaryTest, StaleActiveMemtableDoesNotShadowFlushedData) {
|
||||
Options options;
|
||||
options.env = env_;
|
||||
options.disable_auto_compactions = true;
|
||||
Options secondary_options;
|
||||
secondary_options.env = env_;
|
||||
secondary_options.max_open_files = -1;
|
||||
OpenSecondaryOnWalOnlyPut(options, secondary_options);
|
||||
|
||||
// "v2" is appended to the same WAL and flushed, and that WAL is deleted
|
||||
// before the secondary reads its tail, so the memtable is stuck at "v1".
|
||||
// Before the fix Get() returned "v1" here while an iterator returned "v2",
|
||||
// which is why both read paths are checked.
|
||||
ASSERT_OK(Put("foo", "v2"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
|
||||
VerifySecondaryValue("foo", "v2");
|
||||
|
||||
// A second flush gives the bottommost compaction below more than one input
|
||||
// file, so it has to rewrite rather than trivially move.
|
||||
ASSERT_OK(Put("foo", "v3"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
|
||||
VerifySecondaryValue("foo", "v3");
|
||||
|
||||
// Rewriting the surviving entry's sequence number to 0 puts it below any
|
||||
// residual memtable entry, which used to make the iterator go stale too.
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
|
||||
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
|
||||
std::vector<LiveFileMetaData> live_files;
|
||||
db_->GetLiveFilesMetaData(&live_files);
|
||||
ASSERT_EQ(1, live_files.size());
|
||||
ASSERT_EQ(0, live_files[0].largest_seqno);
|
||||
|
||||
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
|
||||
VerifySecondaryValue("foo", "v3");
|
||||
}
|
||||
|
||||
// Complements the test above: dropping the active memtable on catch-up is only
|
||||
// safe for the column families whose contents the primary has flushed. A column
|
||||
// family whose data still lives solely in a WAL must keep its memtable, even
|
||||
// when another column family in the same WAL gets flushed.
|
||||
TEST_F(DBSecondaryTest, CatchUpKeepsUnflushedWalData) {
|
||||
Options options;
|
||||
options.env = env_;
|
||||
options.disable_auto_compactions = true;
|
||||
CreateAndReopenWithCF({"cf1"}, options);
|
||||
|
||||
Options secondary_options;
|
||||
secondary_options.env = env_;
|
||||
secondary_options.max_open_files = -1;
|
||||
OpenSecondaryWithColumnFamilies({"cf1"}, secondary_options);
|
||||
ASSERT_EQ(2, handles_secondary_.size());
|
||||
|
||||
// Both column families write to the same WAL and neither is flushed, so the
|
||||
// secondary can only serve them from its WAL-replayed memtables.
|
||||
ASSERT_OK(Put(0, "foo", "v1"));
|
||||
ASSERT_OK(Put(1, "bar", "v1"));
|
||||
ASSERT_OK(db_->FlushWAL(/*sync=*/true));
|
||||
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
|
||||
VerifySecondaryValue(handles_secondary_[0], "foo", "v1");
|
||||
VerifySecondaryValue(handles_secondary_[1], "bar", "v1");
|
||||
|
||||
// Flushing only "cf1" advances that column family's log number while the WAL
|
||||
// is retained for the default column family, whose memtable must survive.
|
||||
ASSERT_OK(Flush(1));
|
||||
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
|
||||
VerifySecondaryValue(handles_secondary_[0], "foo", "v1");
|
||||
VerifySecondaryValue(handles_secondary_[1], "bar", "v1");
|
||||
}
|
||||
|
||||
TEST_F(DBSecondaryTest, RefreshIterator) {
|
||||
Options options;
|
||||
options.env = env_;
|
||||
@@ -1582,16 +2315,8 @@ TEST_F(DBSecondaryTest, OpenWithTransactionDB) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
|
||||
// Destroy the DB to recreate as a TransactionDB.
|
||||
Close();
|
||||
Destroy(options, true);
|
||||
|
||||
// Create a TransactionDB.
|
||||
TransactionDB* txn_db = nullptr;
|
||||
TransactionDBOptions txn_db_opts;
|
||||
ASSERT_OK(TransactionDB::Open(options, txn_db_opts, dbname_, &txn_db));
|
||||
ASSERT_NE(txn_db, nullptr);
|
||||
db_.reset(txn_db);
|
||||
ASSERT_NO_FATAL_FAILURE(RecreatePrimaryAsTransactionDB(options, &txn_db));
|
||||
|
||||
std::vector<std::string> cfs = {"new_CF"};
|
||||
CreateColumnFamilies(cfs, options);
|
||||
|
||||
@@ -4101,6 +4101,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) {
|
||||
|
||||
@@ -2306,6 +2306,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 +3043,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];
|
||||
|
||||
+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;
|
||||
|
||||
@@ -268,6 +268,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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
+2
-1
@@ -701,7 +701,8 @@ class Repairer {
|
||||
nullptr /* src_vstorage */, cfd->ioptions().force_consistency_checks,
|
||||
EpochNumberRequirement::kMightMissing, cfd->ioptions().clock,
|
||||
/*bottommost_file_compaction_delay=*/0,
|
||||
cfd->current()->version_set()->offpeak_time_option());
|
||||
cfd->current()->version_set()->offpeak_time_option(),
|
||||
PeriodicCompactionPhaseParams{});
|
||||
Status s;
|
||||
VersionEdit dummy_edit;
|
||||
for (const auto* table : cf_id_and_tables.second) {
|
||||
|
||||
+48
-24
@@ -45,7 +45,8 @@ class VersionBuilderTest : public testing::Test {
|
||||
vstorage_(&icmp_, ucmp_, options_.num_levels, kCompactionStyleLevel,
|
||||
nullptr, false, EpochNumberRequirement::kMustPresent,
|
||||
ioptions_.clock, options_.bottommost_file_compaction_delay,
|
||||
OffpeakTimeOption(options_.daily_offpeak_time_utc)),
|
||||
OffpeakTimeOption(options_.daily_offpeak_time_utc),
|
||||
PeriodicCompactionPhaseParams{}),
|
||||
file_num_(1) {
|
||||
mutable_cf_options_.RefreshDerivedOptions(ioptions_);
|
||||
size_being_compacted_.resize(options_.num_levels);
|
||||
@@ -328,7 +329,8 @@ TEST_F(VersionBuilderTest, ApplyAndSaveTo) {
|
||||
VersionStorageInfo new_vstorage(
|
||||
&icmp_, ucmp_, options_.num_levels, kCompactionStyleLevel, nullptr, false,
|
||||
EpochNumberRequirement::kMightMissing, nullptr, 0,
|
||||
OffpeakTimeOption(options_.daily_offpeak_time_utc));
|
||||
OffpeakTimeOption(options_.daily_offpeak_time_utc),
|
||||
PeriodicCompactionPhaseParams{});
|
||||
ASSERT_OK(version_builder.Apply(&version_edit));
|
||||
ASSERT_OK(version_builder.SaveTo(&new_vstorage));
|
||||
|
||||
@@ -380,7 +382,8 @@ TEST_F(VersionBuilderTest, ApplyAndSaveToDynamic) {
|
||||
VersionStorageInfo new_vstorage(
|
||||
&icmp_, ucmp_, options_.num_levels, kCompactionStyleLevel, nullptr, false,
|
||||
EpochNumberRequirement::kMightMissing, nullptr, 0,
|
||||
OffpeakTimeOption(options_.daily_offpeak_time_utc));
|
||||
OffpeakTimeOption(options_.daily_offpeak_time_utc),
|
||||
PeriodicCompactionPhaseParams{});
|
||||
ASSERT_OK(version_builder.Apply(&version_edit));
|
||||
ASSERT_OK(version_builder.SaveTo(&new_vstorage));
|
||||
|
||||
@@ -436,7 +439,8 @@ TEST_F(VersionBuilderTest, ApplyAndSaveToDynamic2) {
|
||||
VersionStorageInfo new_vstorage(
|
||||
&icmp_, ucmp_, options_.num_levels, kCompactionStyleLevel, nullptr, false,
|
||||
EpochNumberRequirement::kMightMissing, nullptr, 0,
|
||||
OffpeakTimeOption(options_.daily_offpeak_time_utc));
|
||||
OffpeakTimeOption(options_.daily_offpeak_time_utc),
|
||||
PeriodicCompactionPhaseParams{});
|
||||
ASSERT_OK(version_builder.Apply(&version_edit));
|
||||
ASSERT_OK(version_builder.SaveTo(&new_vstorage));
|
||||
|
||||
@@ -494,7 +498,8 @@ TEST_F(VersionBuilderTest, ApplyMultipleAndSaveTo) {
|
||||
VersionStorageInfo new_vstorage(
|
||||
&icmp_, ucmp_, options_.num_levels, kCompactionStyleLevel, nullptr, false,
|
||||
EpochNumberRequirement::kMightMissing, nullptr, 0,
|
||||
OffpeakTimeOption(options_.daily_offpeak_time_utc));
|
||||
OffpeakTimeOption(options_.daily_offpeak_time_utc),
|
||||
PeriodicCompactionPhaseParams{});
|
||||
ASSERT_OK(version_builder.Apply(&version_edit));
|
||||
ASSERT_OK(version_builder.SaveTo(&new_vstorage));
|
||||
|
||||
@@ -518,7 +523,8 @@ TEST_F(VersionBuilderTest, ApplyDeleteAndSaveTo) {
|
||||
VersionStorageInfo new_vstorage(
|
||||
&icmp_, ucmp_, options_.num_levels, kCompactionStyleLevel, nullptr, false,
|
||||
EpochNumberRequirement::kMightMissing, nullptr, 0,
|
||||
OffpeakTimeOption(options_.daily_offpeak_time_utc));
|
||||
OffpeakTimeOption(options_.daily_offpeak_time_utc),
|
||||
PeriodicCompactionPhaseParams{});
|
||||
|
||||
VersionEdit version_edit;
|
||||
version_edit.AddFile(
|
||||
@@ -687,7 +693,8 @@ TEST_F(VersionBuilderTest, ApplyFileDeletionAndAddition) {
|
||||
VersionStorageInfo new_vstorage(
|
||||
&icmp_, ucmp_, options_.num_levels, kCompactionStyleLevel, &vstorage_,
|
||||
force_consistency_checks, EpochNumberRequirement::kMightMissing, nullptr,
|
||||
0, OffpeakTimeOption(options_.daily_offpeak_time_utc));
|
||||
0, OffpeakTimeOption(options_.daily_offpeak_time_utc),
|
||||
PeriodicCompactionPhaseParams{});
|
||||
|
||||
ASSERT_OK(builder.SaveTo(&new_vstorage));
|
||||
|
||||
@@ -832,7 +839,8 @@ TEST_F(VersionBuilderTest, ApplyFileAdditionAndDeletion) {
|
||||
VersionStorageInfo new_vstorage(
|
||||
&icmp_, ucmp_, options_.num_levels, kCompactionStyleLevel, &vstorage_,
|
||||
force_consistency_checks, EpochNumberRequirement::kMightMissing, nullptr,
|
||||
0, OffpeakTimeOption(options_.daily_offpeak_time_utc));
|
||||
0, OffpeakTimeOption(options_.daily_offpeak_time_utc),
|
||||
PeriodicCompactionPhaseParams{});
|
||||
|
||||
ASSERT_OK(builder.SaveTo(&new_vstorage));
|
||||
|
||||
@@ -877,7 +885,8 @@ TEST_F(VersionBuilderTest, ApplyBlobFileAddition) {
|
||||
VersionStorageInfo new_vstorage(
|
||||
&icmp_, ucmp_, options_.num_levels, kCompactionStyleLevel, &vstorage_,
|
||||
force_consistency_checks, EpochNumberRequirement::kMightMissing, nullptr,
|
||||
0, OffpeakTimeOption(options_.daily_offpeak_time_utc));
|
||||
0, OffpeakTimeOption(options_.daily_offpeak_time_utc),
|
||||
PeriodicCompactionPhaseParams{});
|
||||
|
||||
ASSERT_OK(builder.SaveTo(&new_vstorage));
|
||||
|
||||
@@ -1017,7 +1026,8 @@ TEST_F(VersionBuilderTest, ApplyBlobFileGarbageFileInBase) {
|
||||
VersionStorageInfo new_vstorage(
|
||||
&icmp_, ucmp_, options_.num_levels, kCompactionStyleLevel, &vstorage_,
|
||||
force_consistency_checks, EpochNumberRequirement::kMightMissing, nullptr,
|
||||
0, OffpeakTimeOption(options_.daily_offpeak_time_utc));
|
||||
0, OffpeakTimeOption(options_.daily_offpeak_time_utc),
|
||||
PeriodicCompactionPhaseParams{});
|
||||
|
||||
ASSERT_OK(builder.SaveTo(&new_vstorage));
|
||||
|
||||
@@ -1091,7 +1101,8 @@ TEST_F(VersionBuilderTest, ApplyBlobFileGarbageFileAdditionApplied) {
|
||||
VersionStorageInfo new_vstorage(
|
||||
&icmp_, ucmp_, options_.num_levels, kCompactionStyleLevel, &vstorage_,
|
||||
force_consistency_checks, EpochNumberRequirement::kMightMissing, nullptr,
|
||||
0, OffpeakTimeOption(options_.daily_offpeak_time_utc));
|
||||
0, OffpeakTimeOption(options_.daily_offpeak_time_utc),
|
||||
PeriodicCompactionPhaseParams{});
|
||||
|
||||
ASSERT_OK(builder.SaveTo(&new_vstorage));
|
||||
|
||||
@@ -1272,7 +1283,8 @@ TEST_F(VersionBuilderTest, SaveBlobFilesTo) {
|
||||
VersionStorageInfo new_vstorage(
|
||||
&icmp_, ucmp_, options_.num_levels, kCompactionStyleLevel, &vstorage_,
|
||||
force_consistency_checks, EpochNumberRequirement::kMightMissing, nullptr,
|
||||
0, OffpeakTimeOption(options_.daily_offpeak_time_utc));
|
||||
0, OffpeakTimeOption(options_.daily_offpeak_time_utc),
|
||||
PeriodicCompactionPhaseParams{});
|
||||
|
||||
ASSERT_OK(builder.SaveTo(&new_vstorage));
|
||||
|
||||
@@ -1321,7 +1333,8 @@ TEST_F(VersionBuilderTest, SaveBlobFilesTo) {
|
||||
VersionStorageInfo new_vstorage_2(
|
||||
&icmp_, ucmp_, options_.num_levels, kCompactionStyleLevel, &new_vstorage,
|
||||
force_consistency_checks, EpochNumberRequirement::kMightMissing, nullptr,
|
||||
0, OffpeakTimeOption(options_.daily_offpeak_time_utc));
|
||||
0, OffpeakTimeOption(options_.daily_offpeak_time_utc),
|
||||
PeriodicCompactionPhaseParams{});
|
||||
|
||||
ASSERT_OK(second_builder.SaveTo(&new_vstorage_2));
|
||||
|
||||
@@ -1349,7 +1362,8 @@ TEST_F(VersionBuilderTest, SaveBlobFilesTo) {
|
||||
&icmp_, ucmp_, options_.num_levels, kCompactionStyleLevel,
|
||||
&new_vstorage_2, force_consistency_checks,
|
||||
EpochNumberRequirement::kMightMissing, nullptr, 0,
|
||||
OffpeakTimeOption(options_.daily_offpeak_time_utc));
|
||||
OffpeakTimeOption(options_.daily_offpeak_time_utc),
|
||||
PeriodicCompactionPhaseParams{});
|
||||
|
||||
ASSERT_OK(third_builder.SaveTo(&new_vstorage_3));
|
||||
|
||||
@@ -1431,7 +1445,8 @@ TEST_F(VersionBuilderTest, SaveBlobFilesToConcurrentJobs) {
|
||||
VersionStorageInfo new_vstorage(
|
||||
&icmp_, ucmp_, options_.num_levels, kCompactionStyleLevel, &vstorage_,
|
||||
force_consistency_checks, EpochNumberRequirement::kMightMissing, nullptr,
|
||||
0, OffpeakTimeOption(options_.daily_offpeak_time_utc));
|
||||
0, OffpeakTimeOption(options_.daily_offpeak_time_utc),
|
||||
PeriodicCompactionPhaseParams{});
|
||||
|
||||
ASSERT_OK(builder.SaveTo(&new_vstorage));
|
||||
|
||||
@@ -1535,7 +1550,8 @@ TEST_F(VersionBuilderTest, CheckConsistencyForBlobFiles) {
|
||||
VersionStorageInfo new_vstorage(
|
||||
&icmp_, ucmp_, options_.num_levels, kCompactionStyleLevel, &vstorage_,
|
||||
force_consistency_checks, EpochNumberRequirement::kMightMissing, nullptr,
|
||||
0, OffpeakTimeOption(options_.daily_offpeak_time_utc));
|
||||
0, OffpeakTimeOption(options_.daily_offpeak_time_utc),
|
||||
PeriodicCompactionPhaseParams{});
|
||||
|
||||
ASSERT_OK(builder.SaveTo(&new_vstorage));
|
||||
|
||||
@@ -1575,7 +1591,8 @@ TEST_F(VersionBuilderTest, CheckConsistencyForBlobFilesInconsistentLinks) {
|
||||
VersionStorageInfo new_vstorage(
|
||||
&icmp_, ucmp_, options_.num_levels, kCompactionStyleLevel, &vstorage_,
|
||||
force_consistency_checks, EpochNumberRequirement::kMightMissing, nullptr,
|
||||
0, OffpeakTimeOption(options_.daily_offpeak_time_utc));
|
||||
0, OffpeakTimeOption(options_.daily_offpeak_time_utc),
|
||||
PeriodicCompactionPhaseParams{});
|
||||
|
||||
const Status s = builder.SaveTo(&new_vstorage);
|
||||
ASSERT_TRUE(s.IsCorruption());
|
||||
@@ -1617,7 +1634,8 @@ TEST_F(VersionBuilderTest, CheckConsistencyForBlobFilesAllGarbage) {
|
||||
VersionStorageInfo new_vstorage(
|
||||
&icmp_, ucmp_, options_.num_levels, kCompactionStyleLevel, &vstorage_,
|
||||
force_consistency_checks, EpochNumberRequirement::kMightMissing, nullptr,
|
||||
0, OffpeakTimeOption(options_.daily_offpeak_time_utc));
|
||||
0, OffpeakTimeOption(options_.daily_offpeak_time_utc),
|
||||
PeriodicCompactionPhaseParams{});
|
||||
|
||||
const Status s = builder.SaveTo(&new_vstorage);
|
||||
ASSERT_TRUE(s.IsCorruption());
|
||||
@@ -1667,7 +1685,8 @@ TEST_F(VersionBuilderTest, CheckConsistencyForBlobFilesAllGarbageLinkedSsts) {
|
||||
VersionStorageInfo new_vstorage(
|
||||
&icmp_, ucmp_, options_.num_levels, kCompactionStyleLevel, &vstorage_,
|
||||
force_consistency_checks, EpochNumberRequirement::kMightMissing, nullptr,
|
||||
0, OffpeakTimeOption(options_.daily_offpeak_time_utc));
|
||||
0, OffpeakTimeOption(options_.daily_offpeak_time_utc),
|
||||
PeriodicCompactionPhaseParams{});
|
||||
|
||||
const Status s = builder.SaveTo(&new_vstorage);
|
||||
ASSERT_TRUE(s.IsCorruption());
|
||||
@@ -1831,7 +1850,8 @@ TEST_F(VersionBuilderTest, MaintainLinkedSstsForBlobFiles) {
|
||||
VersionStorageInfo new_vstorage(
|
||||
&icmp_, ucmp_, options_.num_levels, kCompactionStyleLevel, &vstorage_,
|
||||
force_consistency_checks, EpochNumberRequirement::kMightMissing, nullptr,
|
||||
0, OffpeakTimeOption(options_.daily_offpeak_time_utc));
|
||||
0, OffpeakTimeOption(options_.daily_offpeak_time_utc),
|
||||
PeriodicCompactionPhaseParams{});
|
||||
|
||||
ASSERT_OK(builder.SaveTo(&new_vstorage));
|
||||
|
||||
@@ -1884,7 +1904,8 @@ TEST_F(VersionBuilderTest, CheckConsistencyForFileDeletedTwice) {
|
||||
&icmp_, ucmp_, options_.num_levels, kCompactionStyleLevel, nullptr,
|
||||
true /* force_consistency_checks */,
|
||||
EpochNumberRequirement::kMightMissing, nullptr, 0,
|
||||
OffpeakTimeOption(options_.daily_offpeak_time_utc));
|
||||
OffpeakTimeOption(options_.daily_offpeak_time_utc),
|
||||
PeriodicCompactionPhaseParams{});
|
||||
ASSERT_OK(version_builder.Apply(&version_edit));
|
||||
ASSERT_OK(version_builder.SaveTo(&new_vstorage));
|
||||
|
||||
@@ -1896,7 +1917,8 @@ TEST_F(VersionBuilderTest, CheckConsistencyForFileDeletedTwice) {
|
||||
&icmp_, ucmp_, options_.num_levels, kCompactionStyleLevel, nullptr,
|
||||
true /* force_consistency_checks */,
|
||||
EpochNumberRequirement::kMightMissing, nullptr, 0,
|
||||
OffpeakTimeOption(options_.daily_offpeak_time_utc));
|
||||
OffpeakTimeOption(options_.daily_offpeak_time_utc),
|
||||
PeriodicCompactionPhaseParams{});
|
||||
ASSERT_NOK(version_builder2.Apply(&version_edit));
|
||||
|
||||
UnrefFilesInVersion(&new_vstorage);
|
||||
@@ -1936,7 +1958,8 @@ TEST_F(VersionBuilderTest, CheckConsistencyForL0FilesSortedByEpochNumber) {
|
||||
&icmp_, ucmp_, options_.num_levels, kCompactionStyleLevel,
|
||||
nullptr /* src_vstorage */, true /* force_consistency_checks */,
|
||||
EpochNumberRequirement::kMightMissing, nullptr, 0,
|
||||
OffpeakTimeOption(options_.daily_offpeak_time_utc));
|
||||
OffpeakTimeOption(options_.daily_offpeak_time_utc),
|
||||
PeriodicCompactionPhaseParams{});
|
||||
|
||||
ASSERT_OK(version_builder_1.Apply(&version_edit_1));
|
||||
s = version_builder_1.SaveTo(&new_vstorage_1);
|
||||
@@ -1975,7 +1998,8 @@ TEST_F(VersionBuilderTest, CheckConsistencyForL0FilesSortedByEpochNumber) {
|
||||
&icmp_, ucmp_, options_.num_levels, kCompactionStyleLevel,
|
||||
nullptr /* src_vstorage */, true /* force_consistency_checks */,
|
||||
EpochNumberRequirement::kMightMissing, nullptr, 0,
|
||||
OffpeakTimeOption(options_.daily_offpeak_time_utc));
|
||||
OffpeakTimeOption(options_.daily_offpeak_time_utc),
|
||||
PeriodicCompactionPhaseParams{});
|
||||
|
||||
ASSERT_OK(version_builder_2.Apply(&version_edit_2));
|
||||
s = version_builder_2.SaveTo(&new_vstorage_2);
|
||||
|
||||
+77
-40
@@ -462,7 +462,11 @@ void VersionEditHandler::CheckIterationResult(const log::Reader& reader,
|
||||
if (s->ok()) {
|
||||
version_set_->manifest_file_size_ = reader.GetReadOffset();
|
||||
assert(version_set_->manifest_file_size_ > 0);
|
||||
version_set_->manifest_recovery_last_valid_record_end_ =
|
||||
last_valid_record_end_;
|
||||
version_set_->manifest_last_valid_record_end_ = last_valid_record_end_;
|
||||
version_set_->manifest_last_valid_record_end_file_number_ =
|
||||
version_set_->manifest_file_number_;
|
||||
version_set_->next_file_number_.store(version_edit_params_.GetNextFile() +
|
||||
1);
|
||||
SequenceNumber last_seq = version_edit_params_.GetLastSequence();
|
||||
@@ -718,19 +722,21 @@ VersionEditHandlerPointInTime::VersionEditHandlerPointInTime(
|
||||
bool read_only, std::vector<ColumnFamilyDescriptor> column_families,
|
||||
VersionSet* version_set, const std::shared_ptr<IOTracer>& io_tracer,
|
||||
const ReadOptions& read_options, bool allow_incomplete_valid_version,
|
||||
bool trust_manifest_recovery,
|
||||
EpochNumberRequirement epoch_number_requirement)
|
||||
: VersionEditHandler(read_only, column_families, version_set,
|
||||
/*track_found_and_missing_files=*/true,
|
||||
/*no_error_if_files_missing=*/true, io_tracer,
|
||||
read_options, allow_incomplete_valid_version,
|
||||
epoch_number_requirement) {}
|
||||
epoch_number_requirement),
|
||||
trust_manifest_recovery_(trust_manifest_recovery) {}
|
||||
|
||||
VersionEditHandlerPointInTime::~VersionEditHandlerPointInTime() {
|
||||
for (const auto& cfid_and_version : atomic_update_versions_) {
|
||||
delete cfid_and_version.second;
|
||||
delete cfid_and_version.second.version;
|
||||
}
|
||||
for (const auto& elem : versions_) {
|
||||
delete elem.second;
|
||||
delete elem.second.version;
|
||||
}
|
||||
versions_.clear();
|
||||
}
|
||||
@@ -760,7 +766,7 @@ Status VersionEditHandlerPointInTime::OnAtomicGroupReplayBegin() {
|
||||
// complete it. They must not be used for completing the upcoming
|
||||
// AtomicGroup since they are too old.
|
||||
for (auto& cfid_and_version : atomic_update_versions_) {
|
||||
delete cfid_and_version.second;
|
||||
delete cfid_and_version.second.version;
|
||||
}
|
||||
|
||||
in_atomic_group_ = true;
|
||||
@@ -770,7 +776,7 @@ Status VersionEditHandlerPointInTime::OnAtomicGroupReplayBegin() {
|
||||
// best-effort recovery.
|
||||
atomic_update_versions_.clear();
|
||||
for (const auto& cfid_and_builder : builders_) {
|
||||
atomic_update_versions_[cfid_and_builder.first] = nullptr;
|
||||
atomic_update_versions_[cfid_and_builder.first] = PointInTimeVersion();
|
||||
}
|
||||
atomic_update_versions_missing_ = atomic_update_versions_.size();
|
||||
return Status::OK();
|
||||
@@ -814,10 +820,12 @@ void VersionEditHandlerPointInTime::CheckIterationResult(
|
||||
auto v_iter = versions_.find(cfd->GetID());
|
||||
auto builder_iter = builders_.find(cfd->GetID());
|
||||
if (v_iter != versions_.end()) {
|
||||
assert(v_iter->second != nullptr);
|
||||
assert(v_iter->second.version != nullptr);
|
||||
assert(builder_iter != builders_.end());
|
||||
|
||||
version_set_->AppendVersion(cfd, v_iter->second);
|
||||
version_set_->AppendVersion(cfd, v_iter->second.version);
|
||||
installed_version_log_numbers_[cfd->GetID()] =
|
||||
v_iter->second.log_number;
|
||||
versions_.erase(v_iter);
|
||||
// Let's clear found_files, since any files in that are part of the
|
||||
// installed Version. Any files that got obsoleted would have already
|
||||
@@ -827,7 +835,7 @@ void VersionEditHandlerPointInTime::CheckIterationResult(
|
||||
}
|
||||
} else {
|
||||
for (const auto& elem : versions_) {
|
||||
delete elem.second;
|
||||
delete elem.second.version;
|
||||
}
|
||||
versions_.clear();
|
||||
}
|
||||
@@ -845,9 +853,10 @@ ColumnFamilyData* VersionEditHandlerPointInTime::DestroyCfAndCleanup(
|
||||
}
|
||||
auto v_iter = versions_.find(cfid);
|
||||
if (v_iter != versions_.end()) {
|
||||
delete v_iter->second;
|
||||
delete v_iter->second.version;
|
||||
versions_.erase(v_iter);
|
||||
}
|
||||
installed_version_log_numbers_.erase(cfid);
|
||||
return cfd;
|
||||
}
|
||||
|
||||
@@ -898,25 +907,32 @@ Status VersionEditHandlerPointInTime::MaybeCreateVersionBeforeApplyEdit(
|
||||
if (negative_edge) {
|
||||
builder->RollbackLastApply();
|
||||
}
|
||||
// `cfd->GetLogNumber()` describes exactly the state the Version about to be
|
||||
// built reflects: VersionEditHandler::ExtractInfoFromVersionEdit() applies
|
||||
// `edit`'s own log number only after this function returns, so on a
|
||||
// negative edge it is still the pre-edit value.
|
||||
const uint64_t log_number = cfd->GetLogNumber();
|
||||
const auto& mopts = cfd->GetLatestMutableCFOptions();
|
||||
auto* version = new Version(
|
||||
cfd, version_set_, version_set_->file_options_, mopts, io_tracer_,
|
||||
version_set_->current_version_number_++, epoch_number_requirement_);
|
||||
s = builder->LoadTableHandlers(
|
||||
cfd->internal_stats(),
|
||||
version_set_->db_options_->max_file_opening_threads, false, true, mopts,
|
||||
MaxFileSizeForL0MetaPin(mopts), read_options_);
|
||||
if (!s.ok()) {
|
||||
delete version;
|
||||
if (negative_edge) {
|
||||
builder->RedoLastApply();
|
||||
if (!trust_manifest_recovery_) {
|
||||
s = builder->LoadTableHandlers(
|
||||
cfd->internal_stats(),
|
||||
version_set_->db_options_->max_file_opening_threads, false, true,
|
||||
mopts, MaxFileSizeForL0MetaPin(mopts), read_options_);
|
||||
if (!s.ok()) {
|
||||
delete version;
|
||||
if (negative_edge) {
|
||||
builder->RedoLastApply();
|
||||
}
|
||||
if (s.IsCorruption()) {
|
||||
// This point in time cannot be recovered; skip it and continue.
|
||||
s = Status::OK();
|
||||
}
|
||||
builder->CommitLastApply();
|
||||
return s;
|
||||
}
|
||||
if (s.IsCorruption()) {
|
||||
// This point in time cannot be recovered; skip it and continue.
|
||||
s = Status::OK();
|
||||
}
|
||||
builder->CommitLastApply();
|
||||
return s;
|
||||
}
|
||||
s = builder->SaveTo(version->storage_info());
|
||||
if (negative_edge) {
|
||||
@@ -924,7 +940,7 @@ Status VersionEditHandlerPointInTime::MaybeCreateVersionBeforeApplyEdit(
|
||||
}
|
||||
if (s.ok()) {
|
||||
if (AtomicUpdateVersionsContains(cfd->GetID())) {
|
||||
AtomicUpdateVersionsPut(version);
|
||||
AtomicUpdateVersionsPut(PointInTimeVersion{version, log_number});
|
||||
if (AtomicUpdateVersionsCompleted()) {
|
||||
AtomicUpdateVersionsApply();
|
||||
}
|
||||
@@ -934,10 +950,11 @@ Status VersionEditHandlerPointInTime::MaybeCreateVersionBeforeApplyEdit(
|
||||
!version_set_->db_options_->skip_stats_update_on_db_open);
|
||||
auto v_iter = versions_.find(cfd->GetID());
|
||||
if (v_iter != versions_.end()) {
|
||||
delete v_iter->second;
|
||||
v_iter->second = version;
|
||||
delete v_iter->second.version;
|
||||
v_iter->second = PointInTimeVersion{version, log_number};
|
||||
} else {
|
||||
versions_.emplace(cfd->GetID(), version);
|
||||
versions_.emplace(cfd->GetID(),
|
||||
PointInTimeVersion{version, log_number});
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -953,6 +970,11 @@ Status VersionEditHandlerPointInTime::VerifyFile(ColumnFamilyData* cfd,
|
||||
const std::string& fpath,
|
||||
int level,
|
||||
const FileMetaData& fmeta) {
|
||||
if (trust_manifest_recovery_) {
|
||||
// Trust the MANIFEST: do not stat/open the file to classify it. The
|
||||
// compaction opens (and unique-id verifies) only its input files on demand.
|
||||
return Status::OK();
|
||||
}
|
||||
return version_set_->VerifyFileMetadata(read_options_, cfd, fpath, level,
|
||||
fmeta);
|
||||
}
|
||||
@@ -960,6 +982,10 @@ Status VersionEditHandlerPointInTime::VerifyFile(ColumnFamilyData* cfd,
|
||||
Status VersionEditHandlerPointInTime::VerifyBlobFile(
|
||||
ColumnFamilyData* cfd, uint64_t blob_file_num,
|
||||
const BlobFileAddition& blob_addition) {
|
||||
if (trust_manifest_recovery_) {
|
||||
// Trust the MANIFEST: do not open the blob file to classify it.
|
||||
return Status::OK();
|
||||
}
|
||||
BlobSource* blob_source = cfd->blob_source();
|
||||
assert(blob_source);
|
||||
CacheHandleGuard<BlobFileReader> blob_file_reader;
|
||||
@@ -989,6 +1015,15 @@ bool VersionEditHandlerPointInTime::HasMissingFiles() const {
|
||||
return false;
|
||||
}
|
||||
|
||||
uint64_t VersionEditHandlerPointInTime::GetInstalledVersionLogNumber(
|
||||
uint32_t cf_id) const {
|
||||
auto iter = installed_version_log_numbers_.find(cf_id);
|
||||
if (iter == installed_version_log_numbers_.end()) {
|
||||
return 0;
|
||||
}
|
||||
return iter->second;
|
||||
}
|
||||
|
||||
bool VersionEditHandlerPointInTime::AtomicUpdateVersionsCompleted() {
|
||||
return atomic_update_versions_missing_ == 0;
|
||||
}
|
||||
@@ -1002,42 +1037,44 @@ void VersionEditHandlerPointInTime::AtomicUpdateVersionsDropCf(uint32_t cfid) {
|
||||
assert(!AtomicUpdateVersionsCompleted());
|
||||
auto atomic_update_versions_iter = atomic_update_versions_.find(cfid);
|
||||
assert(atomic_update_versions_iter != atomic_update_versions_.end());
|
||||
if (atomic_update_versions_iter->second == nullptr) {
|
||||
if (atomic_update_versions_iter->second.version == nullptr) {
|
||||
atomic_update_versions_missing_--;
|
||||
} else {
|
||||
delete atomic_update_versions_iter->second;
|
||||
delete atomic_update_versions_iter->second.version;
|
||||
}
|
||||
atomic_update_versions_.erase(atomic_update_versions_iter);
|
||||
}
|
||||
|
||||
void VersionEditHandlerPointInTime::AtomicUpdateVersionsPut(Version* version) {
|
||||
void VersionEditHandlerPointInTime::AtomicUpdateVersionsPut(
|
||||
PointInTimeVersion pit_version) {
|
||||
assert(!AtomicUpdateVersionsCompleted());
|
||||
assert(pit_version.version != nullptr);
|
||||
auto atomic_update_versions_iter =
|
||||
atomic_update_versions_.find(version->cfd()->GetID());
|
||||
atomic_update_versions_.find(pit_version.version->cfd()->GetID());
|
||||
assert(atomic_update_versions_iter != atomic_update_versions_.end());
|
||||
if (atomic_update_versions_iter->second == nullptr) {
|
||||
if (atomic_update_versions_iter->second.version == nullptr) {
|
||||
atomic_update_versions_missing_--;
|
||||
} else {
|
||||
delete atomic_update_versions_iter->second;
|
||||
delete atomic_update_versions_iter->second.version;
|
||||
}
|
||||
atomic_update_versions_iter->second = version;
|
||||
atomic_update_versions_iter->second = pit_version;
|
||||
}
|
||||
|
||||
void VersionEditHandlerPointInTime::AtomicUpdateVersionsApply() {
|
||||
assert(AtomicUpdateVersionsCompleted());
|
||||
for (const auto& cfid_and_version : atomic_update_versions_) {
|
||||
uint32_t cfid = cfid_and_version.first;
|
||||
Version* version = cfid_and_version.second;
|
||||
assert(version != nullptr);
|
||||
version->PrepareAppend(
|
||||
const PointInTimeVersion& pit_version = cfid_and_version.second;
|
||||
assert(pit_version.version != nullptr);
|
||||
pit_version.version->PrepareAppend(
|
||||
read_options_,
|
||||
!version_set_->db_options_->skip_stats_update_on_db_open);
|
||||
auto versions_iter = versions_.find(cfid);
|
||||
if (versions_iter != versions_.end()) {
|
||||
delete versions_iter->second;
|
||||
versions_iter->second = version;
|
||||
delete versions_iter->second.version;
|
||||
versions_iter->second = pit_version;
|
||||
} else {
|
||||
versions_.emplace(cfid, version);
|
||||
versions_.emplace(cfid, pit_version);
|
||||
}
|
||||
}
|
||||
atomic_update_versions_.clear();
|
||||
|
||||
+62
-12
@@ -48,7 +48,10 @@ class VersionEditHandlerBase {
|
||||
virtual void CheckIterationResult(const log::Reader& /*reader*/,
|
||||
Status* /*s*/) {}
|
||||
|
||||
void ClearReadBuffer() { read_buffer_.Clear(); }
|
||||
void ResetReadState() {
|
||||
read_buffer_.Clear();
|
||||
last_valid_record_end_ = 0;
|
||||
}
|
||||
|
||||
Status status_;
|
||||
|
||||
@@ -296,12 +299,24 @@ class VersionEditHandlerPointInTime : public VersionEditHandler {
|
||||
bool read_only, std::vector<ColumnFamilyDescriptor> column_families,
|
||||
VersionSet* version_set, const std::shared_ptr<IOTracer>& io_tracer,
|
||||
const ReadOptions& read_options, bool allow_incomplete_valid_version,
|
||||
bool trust_manifest_recovery,
|
||||
EpochNumberRequirement epoch_number_requirement =
|
||||
EpochNumberRequirement::kMustPresent);
|
||||
~VersionEditHandlerPointInTime() override;
|
||||
|
||||
bool HasMissingFiles() const;
|
||||
|
||||
// Returns the column family's log number as of the Version most recently
|
||||
// installed for it by this handler, i.e. the log number that the MANIFEST
|
||||
// records reflected by that Version had put in effect. Data in WALs older
|
||||
// than the returned number is readable from the files that Version
|
||||
// references, without those WALs.
|
||||
//
|
||||
// Returns 0 when no Version has been installed for `cf_id`.
|
||||
//
|
||||
// REQUIRES: db mutex
|
||||
uint64_t GetInstalledVersionLogNumber(uint32_t cf_id) const;
|
||||
|
||||
virtual Status VerifyFile(ColumnFamilyData* cfd, const std::string& fpath,
|
||||
int level, const FileMetaData& fmeta) override;
|
||||
virtual Status VerifyBlobFile(ColumnFamilyData* cfd, uint64_t blob_file_num,
|
||||
@@ -326,31 +341,64 @@ class VersionEditHandlerPointInTime : public VersionEditHandler {
|
||||
bool prefetch_index_and_filter_in_cache,
|
||||
bool is_initial_load) override;
|
||||
|
||||
std::unordered_map<uint32_t, Version*> versions_;
|
||||
// A Version built from the MANIFEST records read up to some point in time,
|
||||
// together with the column family's log number that those records had put in
|
||||
// effect. Keeping the two together is what lets an installed Version report
|
||||
// the log number it covers.
|
||||
struct PointInTimeVersion {
|
||||
Version* version = nullptr;
|
||||
uint64_t log_number = 0;
|
||||
};
|
||||
|
||||
std::unordered_map<uint32_t, PointInTimeVersion> versions_;
|
||||
|
||||
// `atomic_update_versions_` is for ensuring all-or-nothing AtomicGroup
|
||||
// recoveries. When `atomic_update_versions_` is nonempty, it serves as a
|
||||
// barrier to updating `versions_` until all its values are populated.
|
||||
std::unordered_map<uint32_t, Version*> atomic_update_versions_;
|
||||
// `atomic_update_versions_missing_` counts the nullptr values in
|
||||
std::unordered_map<uint32_t, PointInTimeVersion> atomic_update_versions_;
|
||||
// `atomic_update_versions_missing_` counts the null `version`s in
|
||||
// `atomic_update_versions_`.
|
||||
size_t atomic_update_versions_missing_;
|
||||
|
||||
bool in_atomic_group_ = false;
|
||||
|
||||
// When true (set only via the OpenAndCompact remote-compaction path),
|
||||
// recovery trusts the MANIFEST when reconstructing the LSM version: it does
|
||||
// not stat/open SST or blob files to classify them found/missing (VerifyFile
|
||||
// / VerifyBlobFile return OK) and does not open candidate-version table
|
||||
// handlers (LoadTableHandlers is skipped). This prevents both a fatal open of
|
||||
// a transient/obsolete file (added then later deleted in the MANIFEST,
|
||||
// already physically removed by a live primary) and a rollback to an earlier,
|
||||
// wrong LSM shape. Safe because the compaction's input files are
|
||||
// ref-protected from deletion by the primary, so any missing file is
|
||||
// necessarily a non-input file the compaction never reads; inputs are opened
|
||||
// (and unique-id verified) on demand by the compaction itself.
|
||||
//
|
||||
// NOTE: This does not yet suppress the bounded file-property sampling in
|
||||
// Version::PrepareAppend -> UpdateAccumulatedStats, which still reads some
|
||||
// non-input files' properties (tolerating any that are missing). That path is
|
||||
// needed today to populate input FileMetaData::num_entries for compaction
|
||||
// input-record-count verification. A strict "open only compaction inputs"
|
||||
// recovery is a planned follow-up that also skips that sampling and sources
|
||||
// input num_entries from the worker's own input-table-properties read.
|
||||
const bool trust_manifest_recovery_ = false;
|
||||
|
||||
private:
|
||||
bool AtomicUpdateVersionsCompleted();
|
||||
bool AtomicUpdateVersionsContains(uint32_t cfid);
|
||||
void AtomicUpdateVersionsDropCf(uint32_t cfid);
|
||||
|
||||
// This function is called for `Version*` updates for column families in an
|
||||
// incomplete atomic update. It buffers `Version*` updates in
|
||||
// `atomic_update_versions_`.
|
||||
void AtomicUpdateVersionsPut(Version* version);
|
||||
// This function is called for Version updates for column families in an
|
||||
// incomplete atomic update. It buffers them in `atomic_update_versions_`.
|
||||
void AtomicUpdateVersionsPut(PointInTimeVersion pit_version);
|
||||
|
||||
// This function is called upon completion of an atomic update. It applies
|
||||
// `Version*` updates in `atomic_update_versions_` to `versions_`.
|
||||
// This function is called upon completion of an atomic update. It applies the
|
||||
// updates buffered in `atomic_update_versions_` to `versions_`.
|
||||
void AtomicUpdateVersionsApply();
|
||||
|
||||
// The log number of the Version last installed for each column family. See
|
||||
// GetInstalledVersionLogNumber().
|
||||
std::unordered_map<uint32_t, uint64_t> installed_version_log_numbers_;
|
||||
};
|
||||
|
||||
// A class similar to `VersionEditHandlerPointInTime` that parse MANIFEST and
|
||||
@@ -365,12 +413,14 @@ class ManifestTailer : public VersionEditHandlerPointInTime {
|
||||
VersionSet* version_set,
|
||||
const std::shared_ptr<IOTracer>& io_tracer,
|
||||
const ReadOptions& read_options,
|
||||
bool trust_manifest_recovery,
|
||||
EpochNumberRequirement epoch_number_requirement =
|
||||
EpochNumberRequirement::kMustPresent)
|
||||
: VersionEditHandlerPointInTime(
|
||||
/*read_only=*/true, column_families, version_set, io_tracer,
|
||||
read_options,
|
||||
/*allow_incomplete_valid_version=*/false, epoch_number_requirement),
|
||||
/*allow_incomplete_valid_version=*/false, trust_manifest_recovery,
|
||||
epoch_number_requirement),
|
||||
mode_(Mode::kRecovery) {}
|
||||
|
||||
Status VerifyFile(ColumnFamilyData* cfd, const std::string& fpath, int level,
|
||||
@@ -378,7 +428,7 @@ class ManifestTailer : public VersionEditHandlerPointInTime {
|
||||
|
||||
void PrepareToReadNewManifest() {
|
||||
initialized_ = false;
|
||||
ClearReadBuffer();
|
||||
ResetReadState();
|
||||
}
|
||||
|
||||
std::unordered_set<ColumnFamilyData*>& GetUpdatedColumnFamilies() {
|
||||
|
||||
+190
-56
@@ -71,6 +71,8 @@
|
||||
#include "util/cast_util.h"
|
||||
#include "util/coding.h"
|
||||
#include "util/coro_utils.h"
|
||||
#include "util/fastrange.h"
|
||||
#include "util/hash.h"
|
||||
#include "util/stop_watch.h"
|
||||
#include "util/string_util.h"
|
||||
#include "util/user_comparator_wrapper.h"
|
||||
@@ -2660,7 +2662,8 @@ Status Version::OverlapWithLevelIterator(const ReadOptions& read_options,
|
||||
}
|
||||
|
||||
if (status.ok() && *overlap == false &&
|
||||
range_del_agg.IsRangeOverlapped(smallest_user_key, largest_user_key)) {
|
||||
range_del_agg.IsRangeOverlapped(smallest_user_key, largest_user_key,
|
||||
/*end_exclusive=*/false)) {
|
||||
*overlap = true;
|
||||
}
|
||||
return status;
|
||||
@@ -2673,7 +2676,8 @@ VersionStorageInfo::VersionStorageInfo(
|
||||
bool _force_consistency_checks,
|
||||
EpochNumberRequirement epoch_number_requirement, SystemClock* clock,
|
||||
uint32_t bottommost_file_compaction_delay,
|
||||
OffpeakTimeOption offpeak_time_option)
|
||||
OffpeakTimeOption offpeak_time_option,
|
||||
PeriodicCompactionPhaseParams periodic_compaction_phase_params)
|
||||
: internal_comparator_(internal_comparator),
|
||||
user_comparator_(user_comparator),
|
||||
// cfd is nullptr if Version is dummy
|
||||
@@ -2706,7 +2710,8 @@ VersionStorageInfo::VersionStorageInfo(
|
||||
finalized_(false),
|
||||
force_consistency_checks_(_force_consistency_checks),
|
||||
epoch_number_requirement_(epoch_number_requirement),
|
||||
offpeak_time_option_(std::move(offpeak_time_option)) {
|
||||
offpeak_time_option_(std::move(offpeak_time_option)),
|
||||
periodic_compaction_phase_params_(periodic_compaction_phase_params) {
|
||||
if (ref_vstorage != nullptr) {
|
||||
accumulated_file_size_ = ref_vstorage->accumulated_file_size_;
|
||||
accumulated_raw_key_size_ = ref_vstorage->accumulated_raw_key_size_;
|
||||
@@ -2718,6 +2723,7 @@ VersionStorageInfo::VersionStorageInfo(
|
||||
current_num_deletions_ = ref_vstorage->current_num_deletions_;
|
||||
current_num_samples_ = ref_vstorage->current_num_samples_;
|
||||
oldest_snapshot_seqnum_ = ref_vstorage->oldest_snapshot_seqnum_;
|
||||
preserve_time_min_seqno_ = ref_vstorage->preserve_time_min_seqno_;
|
||||
compact_cursor_ = ref_vstorage->compact_cursor_;
|
||||
compact_cursor_.resize(num_levels_);
|
||||
}
|
||||
@@ -2752,7 +2758,10 @@ Version::Version(ColumnFamilyData* column_family_data, VersionSet* vset,
|
||||
cfd_ == nullptr ? nullptr : cfd_->ioptions().clock,
|
||||
cfd_ == nullptr ? 0
|
||||
: mutable_cf_options.bottommost_file_compaction_delay,
|
||||
vset->offpeak_time_option()),
|
||||
vset->offpeak_time_option(),
|
||||
cfd_ == nullptr
|
||||
? PeriodicCompactionPhaseParams{}
|
||||
: vset->GetPeriodicCompactionPhaseParams(cfd_->GetID())),
|
||||
vset_(vset),
|
||||
next_(this),
|
||||
prev_(this),
|
||||
@@ -3822,18 +3831,22 @@ void VersionStorageInfo::ComputeFilesMarkedForPeriodicCompaction(
|
||||
return;
|
||||
}
|
||||
|
||||
const uint64_t allowed_time_limit =
|
||||
current_time - periodic_compaction_seconds;
|
||||
|
||||
// Find the adjust_allowed_time_limit such that it includes files that are
|
||||
// going to expire by the time next daily offpeak starts.
|
||||
// Existing offpeak behavior pulls a file's deadline earlier by the time until
|
||||
// the next daily offpeak window (so a whole TTL's worth can be marked at the
|
||||
// start of offpeak).
|
||||
const OffpeakTimeInfo offpeak_time_info =
|
||||
offpeak_time_option_.GetOffpeakTimeInfo(current_time);
|
||||
const uint64_t adjusted_allowed_time_limit =
|
||||
allowed_time_limit +
|
||||
(offpeak_time_info.is_now_offpeak
|
||||
? offpeak_time_info.seconds_till_next_offpeak_start
|
||||
: 0);
|
||||
const uint64_t offpeak_pull =
|
||||
offpeak_time_info.is_now_offpeak
|
||||
? offpeak_time_info.seconds_till_next_offpeak_start
|
||||
: 0;
|
||||
|
||||
// Preferred-phase scheduling (see
|
||||
// DBOptions::periodic_compaction_phase_recovery_percent). When
|
||||
// recovery_percent == 0 this is disabled and marking matches the classic
|
||||
// "file age >= periodic_compaction_seconds" behavior (adjusted for offpeak).
|
||||
const PeriodicCompactionPhaseParams& phase_params =
|
||||
periodic_compaction_phase_params_;
|
||||
|
||||
for (int level = 0; level <= last_level; level++) {
|
||||
for (auto f : files_[level]) {
|
||||
@@ -3860,8 +3873,13 @@ void VersionStorageInfo::ComputeFilesMarkedForPeriodicCompaction(
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (file_modification_time > 0 &&
|
||||
file_modification_time < adjusted_allowed_time_limit) {
|
||||
if (file_modification_time == 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint64_t trigger_time = PeriodicCompactionPhaser::TriggerTime(
|
||||
file_modification_time, periodic_compaction_seconds, phase_params);
|
||||
if (current_time + offpeak_pull > trigger_time) {
|
||||
files_marked_for_periodic_compaction_.emplace_back(level, f);
|
||||
}
|
||||
}
|
||||
@@ -4416,11 +4434,27 @@ void VersionStorageInfo::ComputeBottommostFilesMarkedForCompaction(
|
||||
current_time - static_cast<int64_t>(bottommost_file_compaction_delay_);
|
||||
}
|
||||
|
||||
// For UDT, we need to check if the file's max timestamp is below
|
||||
// full_history_ts_low. If not, the compaction won't be able to collapse the
|
||||
// timestamp to clean up the tombstone , so marking the file would be futile
|
||||
// and could cause an infinite compaction loop.
|
||||
const bool has_udt = ucmp && ucmp->timestamp_size() > 0;
|
||||
// A bottommost file should only be marked for compaction when that compaction
|
||||
// could actually zero out its largest sequence number; otherwise the rewrite
|
||||
// makes no progress and the file is re-marked forever (infinite compaction
|
||||
// loop). The zeroability conditions -- the seqno->time "preserve" window and
|
||||
// the UDT history cutoff -- are shared with CompactionIterator::PrepareOutput
|
||||
// through BottommostSeqnoCanBeZeroed() so the two decisions cannot drift.
|
||||
// Snapshot visibility is checked separately (largest_seqno <
|
||||
// oldest_snapshot_seqnum_) since that gate legitimately differs between the
|
||||
// two sites.
|
||||
const size_t ts_sz = ucmp ? ucmp->timestamp_size() : 0;
|
||||
const bool full_history_ts_low_set = !full_history_ts_low.empty();
|
||||
// Max seqno a bottommost compaction could zero given the preserve window.
|
||||
// CompactionJob computes preserve_seqno_after_ the same way but then further
|
||||
// caps it with min(., earliest_snapshot_). We intentionally omit that cap
|
||||
// here: the snapshot-visibility gate above (largest_seqno <
|
||||
// oldest_snapshot_seqnum_) already guarantees a marked file's largest seqno
|
||||
// is below the earliest snapshot, so the cap can never make compaction unable
|
||||
// to zero a file we marked. Omitting it can at most leave unmarked a file a
|
||||
// compaction could still zero -- a missed optimization, not a loop.
|
||||
const SequenceNumber preserve_seqno_after =
|
||||
std::max(preserve_time_min_seqno_, SequenceNumber{1}) - 1;
|
||||
|
||||
for (auto& level_and_file : bottommost_files_) {
|
||||
if (!level_and_file.second->being_compacted &&
|
||||
@@ -4428,25 +4462,27 @@ void VersionStorageInfo::ComputeBottommostFilesMarkedForCompaction(
|
||||
// largest_seqno might be nonzero due to containing the final key in an
|
||||
// earlier compaction, whose seqnum we didn't zero out.
|
||||
if (level_and_file.second->fd.largest_seqno < oldest_snapshot_seqnum_) {
|
||||
if (has_udt) {
|
||||
// Compute the file's timestamp collapsibility (only meaningful under
|
||||
// UDT with full_history_ts_low set). A file whose max timestamp is
|
||||
// below full_history_ts_low can have its timestamp history collapsed
|
||||
// and its seqno zeroed. An unknown (empty) max timestamp means the file
|
||||
// predates timestamp metadata (e.g. written by an older version); mark
|
||||
// it once so a bounded compaction backfills FileMetaData::max_timestamp
|
||||
// and reclaims obsolete versions/tombstones. When full_history_ts_low
|
||||
// is unset, the guard below (full_history_ts_low_set == false) keeps
|
||||
// such a file unmarked, since compaction could not collapse it and the
|
||||
// rewrite would loop.
|
||||
bool ts_below_full_history_ts_low = false;
|
||||
if (ts_sz > 0 && full_history_ts_low_set) {
|
||||
const std::string& max_ts = level_and_file.second->max_timestamp;
|
||||
// If max_timestamp is empty, the file could come from very old
|
||||
// version which does not have timestamp. In that case, we should pick
|
||||
// the file for compaction. After compaction, the file will have
|
||||
// max_timestamp set propertly.
|
||||
if (!max_ts.empty()) {
|
||||
// If full_history_ts_low is empty, it means it was never set, which
|
||||
// means its value is 0. Therefore, it would be always smaller than
|
||||
// max_timestamp
|
||||
if (full_history_ts_low.empty()) {
|
||||
continue;
|
||||
}
|
||||
// If max timestamp >= full_history_ts_low, skip this file
|
||||
if (ucmp->CompareTimestamp(Slice(max_ts), full_history_ts_low) >=
|
||||
0) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
ts_below_full_history_ts_low =
|
||||
max_ts.empty() ||
|
||||
ucmp->CompareTimestamp(Slice(max_ts), full_history_ts_low) < 0;
|
||||
}
|
||||
if (!BottommostSeqnoCanBeZeroed(
|
||||
level_and_file.second->fd.largest_seqno, preserve_seqno_after,
|
||||
ts_sz, full_history_ts_low_set, ts_below_full_history_ts_low)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!needs_delay) {
|
||||
@@ -5375,7 +5411,10 @@ VersionSet::VersionSet(
|
||||
prev_log_number_(0),
|
||||
current_version_number_(0),
|
||||
manifest_file_size_(0),
|
||||
manifest_recovery_last_valid_record_end_(0),
|
||||
manifest_last_valid_record_end_(0),
|
||||
manifest_last_valid_record_end_file_number_(0),
|
||||
force_new_manifest_on_open_(false),
|
||||
last_compacted_manifest_file_size_(0),
|
||||
file_options_(storage_options),
|
||||
block_cache_tracer_(block_cache_tracer),
|
||||
@@ -5585,7 +5624,10 @@ void VersionSet::Reset() {
|
||||
current_version_number_ = 0;
|
||||
manifest_writers_.clear();
|
||||
manifest_file_size_ = 0;
|
||||
manifest_recovery_last_valid_record_end_ = 0;
|
||||
manifest_last_valid_record_end_ = 0;
|
||||
manifest_last_valid_record_end_file_number_ = 0;
|
||||
force_new_manifest_on_open_ = false;
|
||||
last_compacted_manifest_file_size_ = 0;
|
||||
TuneMaxManifestFileSize();
|
||||
obsolete_files_.clear();
|
||||
@@ -5610,9 +5652,67 @@ void VersionSet::UpdatedMutableDbOptions(
|
||||
manifest_preallocation_size_ = updated_options.manifest_preallocation_size;
|
||||
verify_manifest_content_on_close_ =
|
||||
updated_options.verify_manifest_content_on_close;
|
||||
|
||||
if (periodic_compaction_phaser_.SetConfig(
|
||||
updated_options.periodic_compaction_phase_recovery_percent)) {
|
||||
// (Re)anchor phasing so that switching it on (at open or via SetDBOptions)
|
||||
// spreads the initial periodic-compaction catch-up burst over time rather
|
||||
// than triggering it all at once.
|
||||
int64_t now = 0;
|
||||
if (clock_ != nullptr && clock_->GetCurrentTime(&now).ok()) {
|
||||
periodic_compaction_phaser_.Reanchor(static_cast<uint64_t>(now));
|
||||
}
|
||||
if (mu != nullptr) {
|
||||
// Live SetDBOptions change (not construction): push the refreshed phase
|
||||
// params into each column family's current Version so the change takes
|
||||
// effect on the next periodic re-evaluation (which recomputes scores on
|
||||
// the current Version) rather than only when the next Version is built.
|
||||
// Safe because periodic_compaction_phase_params_ is read only under the
|
||||
// DB mutex, which is held here.
|
||||
for (auto* cfd : *column_family_set_) {
|
||||
if (cfd->IsDropped()) {
|
||||
continue;
|
||||
}
|
||||
Version* v = cfd->current();
|
||||
if (v != nullptr) {
|
||||
v->storage_info_.periodic_compaction_phase_params_ =
|
||||
GetPeriodicCompactionPhaseParams(cfd->GetID());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TuneMaxManifestFileSize();
|
||||
}
|
||||
|
||||
PeriodicCompactionPhaseParams VersionSet::GetPeriodicCompactionPhaseParams(
|
||||
uint32_t cf_id) const {
|
||||
return periodic_compaction_phaser_.ParamsForCf(cf_id);
|
||||
}
|
||||
|
||||
void VersionSet::ReanchorCompactionPhase() {
|
||||
// Caller holds the DB mutex. Move the phasing anchor to now and refresh each
|
||||
// CF's current Version's cached params, mirroring the refresh done for
|
||||
// seed/recovery changes in UpdatedMutableDbOptions(). Used when a CF's
|
||||
// periodic_compaction_seconds changes, so a turn-down's newly past-due cohort
|
||||
// is spread rather than fired at once. Re-anchoring is DB-level and benign to
|
||||
// CFs whose interval did not change (their not-past-due files keep phasing).
|
||||
int64_t now = 0;
|
||||
if (clock_ != nullptr && clock_->GetCurrentTime(&now).ok()) {
|
||||
periodic_compaction_phaser_.Reanchor(static_cast<uint64_t>(now));
|
||||
}
|
||||
for (auto* cfd : *column_family_set_) {
|
||||
if (cfd->IsDropped()) {
|
||||
continue;
|
||||
}
|
||||
Version* v = cfd->current();
|
||||
if (v != nullptr) {
|
||||
v->storage_info_.periodic_compaction_phase_params_ =
|
||||
GetPeriodicCompactionPhaseParams(cfd->GetID());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void VersionSet::TuneMaxManifestFileSize() {
|
||||
tuned_max_manifest_file_size_ =
|
||||
std::max(min_max_manifest_file_size_,
|
||||
@@ -6198,6 +6298,8 @@ Status VersionSet::ProcessManifestWrites(
|
||||
descriptor_last_sequence_ = max_last_sequence;
|
||||
manifest_file_number_ = pending_manifest_file_number_;
|
||||
manifest_file_size_ = new_manifest_file_size;
|
||||
manifest_last_valid_record_end_ = new_manifest_file_size;
|
||||
manifest_last_valid_record_end_file_number_ = manifest_file_number_;
|
||||
prev_log_number_ = first_writer.edit_list.front()->GetPrevLogNumber();
|
||||
}
|
||||
} else {
|
||||
@@ -6490,9 +6592,24 @@ std::unique_ptr<log::Writer> VersionSet::CreateManifestWriter(
|
||||
/*track_and_verify_wals=*/false, block_offset);
|
||||
}
|
||||
|
||||
Status VersionSet::GetManifestAppendBoundary(uint64_t* manifest_size) const {
|
||||
assert(manifest_size != nullptr);
|
||||
if (manifest_last_valid_record_end_file_number_ != manifest_file_number_) {
|
||||
return Status::TryAgain(
|
||||
"Current MANIFEST changed while determining its append boundary");
|
||||
}
|
||||
if (manifest_last_valid_record_end_ == 0 ||
|
||||
manifest_last_valid_record_end_ > manifest_file_size_) {
|
||||
return Status::Corruption("Invalid MANIFEST append boundary");
|
||||
}
|
||||
*manifest_size = manifest_last_valid_record_end_;
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status VersionSet::ReopenManifestForAppend(const std::string& manifest_path) {
|
||||
assert(db_options_->reuse_manifest_on_open);
|
||||
assert(manifest_last_valid_record_end_ > 0);
|
||||
assert(manifest_recovery_last_valid_record_end_ > 0);
|
||||
assert(manifest_last_valid_record_end_file_number_ == manifest_file_number_);
|
||||
|
||||
// Disabled under best_efforts_recovery: that mode rebuilds the
|
||||
// MANIFEST + CURRENT from scratch via the side-effect of
|
||||
@@ -6502,6 +6619,10 @@ Status VersionSet::ReopenManifestForAppend(const std::string& manifest_path) {
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
auto force_new_manifest_on_open = [&]() {
|
||||
force_new_manifest_on_open_ = true;
|
||||
};
|
||||
|
||||
FileOptions opt_file_opts = GetFileOptionsForManifestWrite();
|
||||
|
||||
// Bail if the on-disk size diverges from what Recover's Reader
|
||||
@@ -6510,12 +6631,14 @@ Status VersionSet::ReopenManifestForAppend(const std::string& manifest_path) {
|
||||
uint64_t physical_size = 0;
|
||||
IOStatus stat_s = fs_->GetFileSize(manifest_path, IOOptions(), &physical_size,
|
||||
/*dbg=*/nullptr);
|
||||
if (!stat_s.ok() || physical_size != manifest_last_valid_record_end_) {
|
||||
if (!stat_s.ok() ||
|
||||
physical_size != manifest_recovery_last_valid_record_end_) {
|
||||
ROCKS_LOG_WARN(db_options_->info_log,
|
||||
"reuse_manifest_on_open: physical size %" PRIu64
|
||||
" != last valid record end %" PRIu64
|
||||
" (tail corruption?); falling back to fresh MANIFEST",
|
||||
physical_size, manifest_last_valid_record_end_);
|
||||
" (tail corruption?); creating fresh MANIFEST during open",
|
||||
physical_size, manifest_recovery_last_valid_record_end_);
|
||||
force_new_manifest_on_open();
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
@@ -6525,7 +6648,8 @@ Status VersionSet::ReopenManifestForAppend(const std::string& manifest_path) {
|
||||
if (opt_file_opts.use_direct_writes) {
|
||||
ROCKS_LOG_WARN(db_options_->info_log,
|
||||
"reuse_manifest_on_open: direct writes enabled; "
|
||||
"falling back to fresh MANIFEST");
|
||||
"creating fresh MANIFEST during open");
|
||||
force_new_manifest_on_open();
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
@@ -6536,17 +6660,19 @@ Status VersionSet::ReopenManifestForAppend(const std::string& manifest_path) {
|
||||
ROCKS_LOG_WARN(db_options_->info_log,
|
||||
"Failed to reopen MANIFEST for append: %s",
|
||||
io_s.ToString().c_str());
|
||||
force_new_manifest_on_open();
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
const uint64_t reopened_size =
|
||||
manifest_file->GetFileSize(opt_file_opts.io_options, /*dbg=*/nullptr);
|
||||
if (reopened_size != manifest_last_valid_record_end_) {
|
||||
if (reopened_size != manifest_recovery_last_valid_record_end_) {
|
||||
ROCKS_LOG_WARN(db_options_->info_log,
|
||||
"reuse_manifest_on_open: reopened handle size %" PRIu64
|
||||
" != last valid record end %" PRIu64
|
||||
"; falling back to fresh MANIFEST",
|
||||
reopened_size, manifest_last_valid_record_end_);
|
||||
"; creating fresh MANIFEST during open",
|
||||
reopened_size, manifest_recovery_last_valid_record_end_);
|
||||
force_new_manifest_on_open();
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
@@ -6554,10 +6680,10 @@ Status VersionSet::ReopenManifestForAppend(const std::string& manifest_path) {
|
||||
std::move(manifest_file), manifest_path, opt_file_opts,
|
||||
manifest_preallocation_size_, reopened_size);
|
||||
|
||||
ROCKS_LOG_INFO(db_options_->info_log,
|
||||
"Reusing existing MANIFEST file: %s (valid data size: %" PRIu64
|
||||
")",
|
||||
manifest_path.c_str(), manifest_last_valid_record_end_);
|
||||
ROCKS_LOG_INFO(
|
||||
db_options_->info_log,
|
||||
"Reusing existing MANIFEST file: %s (valid data size: %" PRIu64 ")",
|
||||
manifest_path.c_str(), manifest_recovery_last_valid_record_end_);
|
||||
TEST_SYNC_POINT("VersionSet::ReopenManifestForAppend:Reopened");
|
||||
return Status::OK();
|
||||
}
|
||||
@@ -6727,6 +6853,7 @@ Status VersionSet::Recover(
|
||||
uint64_t current_manifest_file_size = 0;
|
||||
uint64_t log_number = 0;
|
||||
{
|
||||
force_new_manifest_on_open_ = false;
|
||||
VersionSet::LogReporter reporter;
|
||||
Status log_read_status;
|
||||
reporter.status = &log_read_status;
|
||||
@@ -6779,7 +6906,7 @@ Status VersionSet::Recover(
|
||||
}
|
||||
|
||||
if (s.ok() && !read_only && db_options_->reuse_manifest_on_open &&
|
||||
manifest_last_valid_record_end_ > 0) {
|
||||
manifest_recovery_last_valid_record_end_ > 0) {
|
||||
s = ReopenManifestForAppend(manifest_path);
|
||||
}
|
||||
|
||||
@@ -6919,7 +7046,7 @@ Status VersionSet::TryRecoverFromOneManifest(
|
||||
VersionEditHandlerPointInTime handler_pit(
|
||||
read_only, column_families, const_cast<VersionSet*>(this), io_tracer_,
|
||||
read_options, /*allow_incomplete_valid_version=*/true,
|
||||
EpochNumberRequirement::kMightMissing);
|
||||
/*trust_manifest_recovery=*/false, EpochNumberRequirement::kMightMissing);
|
||||
|
||||
handler_pit.Iterate(reader, &s);
|
||||
|
||||
@@ -8142,9 +8269,10 @@ Status ReactiveVersionSet::Recover(
|
||||
log::Reader* reader = manifest_reader->get();
|
||||
assert(reader);
|
||||
|
||||
manifest_tailer_.reset(new ManifestTailer(
|
||||
column_families, const_cast<ReactiveVersionSet*>(this), io_tracer_,
|
||||
read_options_, EpochNumberRequirement::kMightMissing));
|
||||
manifest_tailer_.reset(
|
||||
new ManifestTailer(column_families, const_cast<ReactiveVersionSet*>(this),
|
||||
io_tracer_, read_options_, trust_manifest_recovery_,
|
||||
EpochNumberRequirement::kMightMissing));
|
||||
|
||||
manifest_tailer_->Iterate(*reader, manifest_reader_status->get());
|
||||
|
||||
@@ -8184,6 +8312,12 @@ Status ReactiveVersionSet::ReadAndApply(
|
||||
return s;
|
||||
}
|
||||
|
||||
uint64_t ReactiveVersionSet::GetInstalledVersionLogNumber(
|
||||
uint32_t cf_id) const {
|
||||
assert(manifest_tailer_ != nullptr);
|
||||
return manifest_tailer_->GetInstalledVersionLogNumber(cf_id);
|
||||
}
|
||||
|
||||
Status ReactiveVersionSet::MaybeSwitchManifest(
|
||||
log::Reader::Reporter* reporter,
|
||||
std::unique_ptr<log::FragmentBufferedReader>* manifest_reader) {
|
||||
|
||||
+141
-15
@@ -41,6 +41,7 @@
|
||||
#include "db/error_handler.h"
|
||||
#include "db/file_indexer.h"
|
||||
#include "db/log_reader.h"
|
||||
#include "db/periodic_compaction_phaser.h"
|
||||
#include "db/range_del_aggregator.h"
|
||||
#include "db/read_callback.h"
|
||||
#include "db/table_cache.h"
|
||||
@@ -127,15 +128,15 @@ enum EpochNumberRequirement {
|
||||
// compaction, blob files, etc.
|
||||
class VersionStorageInfo {
|
||||
public:
|
||||
VersionStorageInfo(const InternalKeyComparator* internal_comparator,
|
||||
const Comparator* user_comparator, int num_levels,
|
||||
CompactionStyle compaction_style,
|
||||
VersionStorageInfo* src_vstorage,
|
||||
bool _force_consistency_checks,
|
||||
EpochNumberRequirement epoch_number_requirement,
|
||||
SystemClock* clock,
|
||||
uint32_t bottommost_file_compaction_delay,
|
||||
OffpeakTimeOption offpeak_time_option);
|
||||
VersionStorageInfo(
|
||||
const InternalKeyComparator* internal_comparator,
|
||||
const Comparator* user_comparator, int num_levels,
|
||||
CompactionStyle compaction_style, VersionStorageInfo* src_vstorage,
|
||||
bool _force_consistency_checks,
|
||||
EpochNumberRequirement epoch_number_requirement, SystemClock* clock,
|
||||
uint32_t bottommost_file_compaction_delay,
|
||||
OffpeakTimeOption offpeak_time_option,
|
||||
PeriodicCompactionPhaseParams periodic_compaction_phase_params);
|
||||
// No copying allowed
|
||||
VersionStorageInfo(const VersionStorageInfo&) = delete;
|
||||
void operator=(const VersionStorageInfo&) = delete;
|
||||
@@ -264,6 +265,28 @@ class VersionStorageInfo {
|
||||
bool allow_ingest_behind, const Comparator* ucmp,
|
||||
const std::string& full_history_ts_low);
|
||||
|
||||
// Sets the seqno->time preserve-window lower bound used when deciding whether
|
||||
// a bottommost file can be marked for compaction (see
|
||||
// preserve_time_min_seqno_ and BottommostSeqnoCanBeZeroed).
|
||||
// kMaxSequenceNumber disables the constraint. Fed from DBImpl, which owns the
|
||||
// seqno->time mapping. REQUIRES: DB mutex held
|
||||
void SetPreserveTimeMinSeqno(SequenceNumber preserve_time_min_seqno) {
|
||||
preserve_time_min_seqno_ = preserve_time_min_seqno;
|
||||
}
|
||||
|
||||
// The seqno->time preserve-window lower bound set by SetPreserveTimeMinSeqno.
|
||||
// For a bottommost-file (kBottommostFiles) compaction, CompactionJob folds
|
||||
// this into its own preserve_seqno_after_ computation so that the compaction
|
||||
// zeroes out at least the sequence numbers the marker
|
||||
// (ComputeBottommostFilesMarkedForCompaction) deemed zeroable, even when the
|
||||
// selected file's persisted seqno->time mapping is sparse or empty (e.g. a
|
||||
// file written before preserve/preclude was enabled). Otherwise the two would
|
||||
// disagree and a marked file could never make progress (infinite compaction
|
||||
// loop). kMaxSequenceNumber means the constraint is inactive.
|
||||
SequenceNumber GetPreserveTimeMinSeqno() const {
|
||||
return preserve_time_min_seqno_;
|
||||
}
|
||||
|
||||
int MaxInputLevel() const;
|
||||
int MaxOutputLevel(bool allow_ingest_behind) const;
|
||||
|
||||
@@ -782,6 +805,17 @@ class VersionStorageInfo {
|
||||
// created that references it.
|
||||
SequenceNumber oldest_snapshot_seqnum_ = 0;
|
||||
|
||||
// The smallest sequence number whose write time is still within the
|
||||
// seqno->time "preserve" window (preserve_internal_time_seconds /
|
||||
// preclude_last_level_data_seconds). Bottommost keys at or above this seqno
|
||||
// cannot have their sequence numbers zeroed out yet, so files whose largest
|
||||
// seqno is at or above it must not be marked for bottommost compaction (that
|
||||
// would be futile and loop). kMaxSequenceNumber means preserve is inactive.
|
||||
// Fed from DBImpl (which owns the seqno->time mapping) via
|
||||
// SetPreserveTimeMinSeqno; carried across versions like
|
||||
// oldest_snapshot_seqnum_.
|
||||
SequenceNumber preserve_time_min_seqno_ = kMaxSequenceNumber;
|
||||
|
||||
// Level that should be compacted next and its compaction score.
|
||||
// Score < 1 means compaction is not strictly needed. These fields
|
||||
// are initialized by ComputeCompactionScore.
|
||||
@@ -831,6 +865,8 @@ class VersionStorageInfo {
|
||||
|
||||
OffpeakTimeOption offpeak_time_option_;
|
||||
|
||||
PeriodicCompactionPhaseParams periodic_compaction_phase_params_;
|
||||
|
||||
friend class Version;
|
||||
friend class VersionSet;
|
||||
};
|
||||
@@ -1123,6 +1159,9 @@ class Version {
|
||||
friend class VersionSet;
|
||||
friend class VersionEditHandler;
|
||||
friend class VersionEditHandlerPointInTime;
|
||||
// Needs MaybeInitializeFileMetaData() to initialize input file stats before
|
||||
// constructing a Compaction on the remote worker.
|
||||
friend class DBImplSecondary;
|
||||
|
||||
const InternalKeyComparator* internal_comparator() const {
|
||||
return storage_info_.internal_comparator_;
|
||||
@@ -1607,6 +1646,32 @@ class VersionSet {
|
||||
// Return the size of the current manifest file
|
||||
uint64_t manifest_file_size() const { return manifest_file_size_; }
|
||||
|
||||
// Size of the maximal valid prefix recovered from the MANIFEST -- the largest
|
||||
// leading range that is entirely valid. It excludes a corrupt/torn tail
|
||||
// record or a partial atomic group at EOF, and is meant to include valid
|
||||
// trailing framing/padding once a MANIFEST format has it.
|
||||
//
|
||||
// Intended for a lower-bound test -- "did recovery reach at least some point
|
||||
// in the manifest?" (e.g. the DB::OpenAndCompact floor vs. a
|
||||
// manifest_file_size captured on the primary). It is maximal, so a
|
||||
// fully-recovered prefix never tests short; it excludes garbage, so a corrupt
|
||||
// tail never tests long. It is not a safe append/truncate offset; use
|
||||
// GetManifestAppendBoundary() for that.
|
||||
//
|
||||
// Immediately after recovery, this ==
|
||||
// manifest_recovery_last_valid_record_end_ == a clean manifest_file_size_;
|
||||
// a future padded/footered format may make manifest_last_valid_record_end_
|
||||
// <= manifest_file_size_ <= this (changing only this accessor's body, not
|
||||
// callers).
|
||||
uint64_t manifest_recovery_maximal_valid_size() const {
|
||||
return manifest_recovery_last_valid_record_end_;
|
||||
}
|
||||
|
||||
// Returns the valid prefix length at which records can be appended to a
|
||||
// copy of the current MANIFEST.
|
||||
// REQUIRES: DB mutex held, or the DB is not yet visible to other threads.
|
||||
Status GetManifestAppendBoundary(uint64_t* manifest_size) const;
|
||||
|
||||
Status GetMetadataForFile(uint64_t number, int* filelevel,
|
||||
FileMetaData** metadata, ColumnFamilyData** cfd);
|
||||
|
||||
@@ -1651,6 +1716,20 @@ class VersionSet {
|
||||
offpeak_time_option_.SetFromOffpeakTimeString(daily_offpeak_time_utc);
|
||||
}
|
||||
|
||||
// Thin forwarder to periodic_compaction_phaser_.ParamsForCf(cf_id): the
|
||||
// per-CF phasing params (DB base phase from the DB ID, golden-ratio CF
|
||||
// spread, anchor, and recovery percent). Returns disabled params when phasing
|
||||
// is off.
|
||||
PeriodicCompactionPhaseParams GetPeriodicCompactionPhaseParams(
|
||||
uint32_t cf_id) const;
|
||||
|
||||
// (Re)anchor periodic-compaction phasing to now and refresh the cached phase
|
||||
// params on every column family's current Version. Called when a CF's
|
||||
// periodic_compaction_seconds changes via SetOptions, so a turn-down's newly
|
||||
// past-due cohort is spread (over the phase grid within ~N/4 of now) instead
|
||||
// of firing all at once. Caller must hold the DB mutex.
|
||||
void ReanchorCompactionPhase();
|
||||
|
||||
const ImmutableDBOptions* db_options() const { return db_options_; }
|
||||
|
||||
static uint64_t GetNumLiveVersions(Version* dummy_versions);
|
||||
@@ -1801,6 +1880,9 @@ class VersionSet {
|
||||
SystemClock* const clock_;
|
||||
const std::string dbname_;
|
||||
std::string db_id_;
|
||||
// Periodic-compaction phasing. Declared right after db_id_ because it holds a
|
||||
// live reference to it (the DB base phase is hashed from the DB ID).
|
||||
PeriodicCompactionPhaser periodic_compaction_phaser_{db_id_};
|
||||
const ImmutableDBOptions* const db_options_;
|
||||
std::atomic<uint64_t> next_file_number_;
|
||||
// Any WAL number smaller than this should be ignored during recovery,
|
||||
@@ -1845,17 +1927,35 @@ class VersionSet {
|
||||
// Current size of manifest file
|
||||
uint64_t manifest_file_size_;
|
||||
|
||||
// File offset at the end of the last successfully completed logical
|
||||
// record during MANIFEST recovery. Unlike manifest_file_size_ (the
|
||||
// reader's I/O high-water mark, which includes any tolerated tail
|
||||
// garbage), this value points to the byte after the last valid record.
|
||||
// Used by ReopenManifestForAppend to detect intra-block tail
|
||||
// corruption that doesn't extend the physical file size.
|
||||
// File offset at the end of the last successfully completed logical unit
|
||||
// during MANIFEST recovery. For atomic groups, this advances only after the
|
||||
// whole group is buffered and applied. Unlike manifest_file_size_ (the
|
||||
// reader's I/O high-water mark, which includes any tolerated tail garbage),
|
||||
// this value points to the byte after the last valid record. Set only by
|
||||
// recovery -- NOT live-updated on the write path (hence the recovery_
|
||||
// prefix); ProcessManifestWrites keeps manifest_file_size_ current instead.
|
||||
// Used by ReopenManifestForAppend to detect intra-block tail corruption that
|
||||
// doesn't extend the physical file size, as well as partial atomic groups at
|
||||
// EOF.
|
||||
// manifest_file_size_ is kept separate because it is used for
|
||||
// rotation decisions (ProcessManifestWrites), close-time verification
|
||||
// (Close), and backup metadata.
|
||||
uint64_t manifest_recovery_last_valid_record_end_;
|
||||
|
||||
// Safe append boundary for a copy of the current MANIFEST. Initialized from
|
||||
// the recovery boundary and advanced after every successful MANIFEST write.
|
||||
// Unlike manifest_file_size_, it excludes any tolerated tail garbage.
|
||||
uint64_t manifest_last_valid_record_end_;
|
||||
|
||||
// MANIFEST file number associated with manifest_last_valid_record_end_.
|
||||
uint64_t manifest_last_valid_record_end_file_number_;
|
||||
|
||||
// True when reuse_manifest_on_open was requested but the recovered MANIFEST
|
||||
// could not be safely reopened for append. DB open must install a fresh
|
||||
// MANIFEST before returning so normal writable operation never appends to
|
||||
// the old tail.
|
||||
bool force_new_manifest_on_open_;
|
||||
|
||||
// Size of the populated manifest file last time it was re-written from
|
||||
// scratch.
|
||||
uint64_t last_compacted_manifest_file_size_;
|
||||
@@ -1949,6 +2049,27 @@ class ReactiveVersionSet : public VersionSet {
|
||||
std::unique_ptr<log::FragmentBufferedReader>* manifest_reader,
|
||||
std::unique_ptr<log::Reader::Reporter>* manifest_reporter,
|
||||
std::unique_ptr<Status>* manifest_reader_status);
|
||||
|
||||
// Must be called before Recover(). When set, Recover() trusts the MANIFEST
|
||||
// and reconstructs the version from FileMetaData without stat-ing/opening SST
|
||||
// or blob files. Only used by the DB::OpenAndCompact remote-compaction path.
|
||||
void SetTrustManifestRecovery(bool v) { trust_manifest_recovery_ = v; }
|
||||
|
||||
// Returns the column family's log number as of the Version currently
|
||||
// installed for it, i.e. the log number that the MANIFEST records that
|
||||
// Version was built from had put in effect. Data written to WALs older than
|
||||
// the returned number has been flushed by the primary into files the
|
||||
// installed Version references, so it is readable without those WALs.
|
||||
//
|
||||
// This is not the same as ColumnFamilyData::GetLogNumber(), which advances as
|
||||
// soon as a flush record is read from the MANIFEST even when no Version
|
||||
// reflecting that record could be installed.
|
||||
//
|
||||
// Returns 0 if no Version has been installed for `cf_id`.
|
||||
//
|
||||
// REQUIRES: db mutex
|
||||
uint64_t GetInstalledVersionLogNumber(uint32_t cf_id) const;
|
||||
|
||||
#ifndef NDEBUG
|
||||
uint64_t TEST_read_edits_in_atomic_group() const;
|
||||
#endif //! NDEBUG
|
||||
@@ -1967,6 +2088,11 @@ class ReactiveVersionSet : public VersionSet {
|
||||
|
||||
private:
|
||||
std::unique_ptr<ManifestTailer> manifest_tailer_;
|
||||
// When true, MANIFEST recovery trusts the manifest and does not stat/open SST
|
||||
// or blob files (see
|
||||
// VersionEditHandlerPointInTime::trust_manifest_recovery_). Set only for the
|
||||
// DB::OpenAndCompact remote-compaction path.
|
||||
bool trust_manifest_recovery_ = false;
|
||||
// TODO: plumb Env::IOActivity, Env::IOPriority
|
||||
const ReadOptions read_options_;
|
||||
using VersionSet::LogAndApply;
|
||||
|
||||
+65
-1
@@ -139,7 +139,7 @@ class VersionStorageInfoTestBase : public testing::Test {
|
||||
/*_force_consistency_checks=*/false,
|
||||
EpochNumberRequirement::kMustPresent, ioptions_.clock,
|
||||
mutable_cf_options_.bottommost_file_compaction_delay,
|
||||
OffpeakTimeOption()) {}
|
||||
OffpeakTimeOption(), PeriodicCompactionPhaseParams{}) {}
|
||||
|
||||
~VersionStorageInfoTestBase() override {
|
||||
for (int i = 0; i < vstorage_.num_levels(); ++i) {
|
||||
@@ -3004,6 +3004,70 @@ TEST_F(VersionSetAtomicGroupTest,
|
||||
EXPECT_EQ(kAtomicGroupSize, num_recovered_edits_);
|
||||
}
|
||||
|
||||
TEST_F(VersionSetAtomicGroupTest,
|
||||
ReactiveVersionSetResetsManifestAppendBoundaryOnSwitch) {
|
||||
CreateCurrentFile();
|
||||
std::unique_ptr<log::FragmentBufferedReader> manifest_reader;
|
||||
std::unique_ptr<log::Reader::Reporter> manifest_reporter;
|
||||
std::unique_ptr<Status> manifest_reader_status;
|
||||
ASSERT_OK(reactive_versions_->Recover(column_families_, &manifest_reader,
|
||||
&manifest_reporter,
|
||||
&manifest_reader_status));
|
||||
|
||||
uint64_t old_manifest_end = 0;
|
||||
ASSERT_OK(reactive_versions_->GetManifestAppendBoundary(&old_manifest_end));
|
||||
ASSERT_GT(old_manifest_end, 0);
|
||||
log_writer_.reset();
|
||||
|
||||
constexpr uint64_t kNewManifestFileNumber = 2;
|
||||
const std::string new_manifest_path =
|
||||
DescriptorFileName(dbname_, kNewManifestFileNumber);
|
||||
std::unique_ptr<WritableFileWriter> file_writer;
|
||||
ASSERT_OK(WritableFileWriter::Create(
|
||||
fs_, new_manifest_path, fs_->OptimizeForManifestWrite(env_options_),
|
||||
&file_writer, nullptr));
|
||||
log::Writer new_manifest_writer(std::move(file_writer), /*log_number=*/0,
|
||||
/*recycle_log_files=*/false);
|
||||
|
||||
edits_.emplace_back();
|
||||
VersionEdit& incomplete_edit = edits_.back();
|
||||
incomplete_edit.SetLogNumber(0);
|
||||
incomplete_edit.SetNextFile(kNewManifestFileNumber + 1);
|
||||
incomplete_edit.SetLastSequence(last_seqno_++);
|
||||
incomplete_edit.SetDBId(std::string(2 * log::kBlockSize, 'x'));
|
||||
incomplete_edit.MarkAtomicGroup(1);
|
||||
std::string record;
|
||||
ASSERT_TRUE(incomplete_edit.EncodeTo(&record, 0));
|
||||
ASSERT_OK(new_manifest_writer.AddRecord(WriteOptions(), record));
|
||||
ASSERT_OK(new_manifest_writer.Close(WriteOptions()));
|
||||
|
||||
uint64_t new_manifest_size = 0;
|
||||
ASSERT_OK(fs_->GetFileSize(new_manifest_path, IOOptions(), &new_manifest_size,
|
||||
nullptr));
|
||||
ASSERT_GE(new_manifest_size, old_manifest_end);
|
||||
ASSERT_OK(SetCurrentFile(WriteOptions(), fs_.get(), dbname_,
|
||||
kNewManifestFileNumber, Temperature::kUnknown,
|
||||
/*dir_contains_current_file=*/nullptr));
|
||||
|
||||
InstrumentedMutex mu;
|
||||
std::unordered_set<ColumnFamilyData*> cfds_changed;
|
||||
uint64_t new_manifest_end = 0;
|
||||
Status boundary_status;
|
||||
mu.Lock();
|
||||
Status read_status = reactive_versions_->ReadAndApply(
|
||||
&mu, &manifest_reader, manifest_reader_status.get(), &cfds_changed,
|
||||
/*files_to_delete=*/nullptr);
|
||||
if (read_status.ok()) {
|
||||
boundary_status =
|
||||
reactive_versions_->GetManifestAppendBoundary(&new_manifest_end);
|
||||
}
|
||||
mu.Unlock();
|
||||
|
||||
ASSERT_OK(read_status);
|
||||
ASSERT_EQ(1, reactive_versions_->TEST_read_edits_in_atomic_group());
|
||||
ASSERT_TRUE(boundary_status.IsCorruption()) << boundary_status.ToString();
|
||||
}
|
||||
|
||||
TEST_F(VersionSetAtomicGroupTest,
|
||||
HandleIncompleteTrailingAtomicGroupWithVersionSetRecover) {
|
||||
const int kAtomicGroupSize = 4;
|
||||
|
||||
@@ -1149,6 +1149,62 @@ TEST_F(DBLazyEntityTest, BlockCacheTierYieldsIncompleteOnMiss) {
|
||||
ASSERT_TRUE(s.IsIncomplete()) << s.ToString();
|
||||
}
|
||||
|
||||
// The embedded (same-file) read path probes the blob cache before honoring
|
||||
// read_tier == kBlockCacheTier, so a cached embedded record serves a
|
||||
// block-cache-only read -- whole column or sub-range -- instead of returning
|
||||
// Incomplete. Regression test for the former quirk where
|
||||
// ValidateEmbeddedBlobIndex rejected kBlockCacheTier up front, before the cache
|
||||
// probe (unlike the separate-file range path, which always probed first).
|
||||
TEST_F(DBLazyEntityTest, EmbeddedBlockCacheTierServedFromCachedValue) {
|
||||
Options options = GetLazyTestOptionsWithBlobCache();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string key = "entity";
|
||||
const std::string big(4000, 'a');
|
||||
const WideColumns columns{{kDefaultWideColumnName, "inline"}, {"data", big}};
|
||||
IngestEmbeddedEntity(options, key, columns);
|
||||
|
||||
// Warm the blob cache with a whole-column read of the embedded record.
|
||||
{
|
||||
LazyWideColumns warm;
|
||||
ASSERT_OK(db_->GetEntityLazy(ReadOptions(), db_->DefaultColumnFamily(), key,
|
||||
&warm));
|
||||
PinnableSlice whole;
|
||||
ASSERT_OK(warm.ResolveColumn(warm[1], &whole));
|
||||
ASSERT_EQ(whole, big);
|
||||
}
|
||||
|
||||
ASSERT_OK(options.statistics->Reset());
|
||||
|
||||
ReadOptions block_cache_only;
|
||||
block_cache_only.read_tier = kBlockCacheTier;
|
||||
|
||||
// Sub-range read on a fresh result (resolver cache empty) exercises the
|
||||
// embedded range path's blob-cache probe under kBlockCacheTier.
|
||||
{
|
||||
LazyWideColumns lazy;
|
||||
ASSERT_OK(db_->GetEntityLazy(block_cache_only, db_->DefaultColumnFamily(),
|
||||
key, &lazy));
|
||||
PinnableSlice range;
|
||||
ASSERT_OK(lazy.ResolveColumnRange(lazy[1], /*offset=*/1000, /*length=*/100,
|
||||
&range));
|
||||
ASSERT_EQ(range, big.substr(1000, 100));
|
||||
}
|
||||
|
||||
// Whole-column read on a fresh result, also served from the cache.
|
||||
{
|
||||
LazyWideColumns lazy;
|
||||
ASSERT_OK(db_->GetEntityLazy(block_cache_only, db_->DefaultColumnFamily(),
|
||||
key, &lazy));
|
||||
PinnableSlice whole;
|
||||
ASSERT_OK(lazy.ResolveColumn(lazy[1], &whole));
|
||||
ASSERT_EQ(whole, big);
|
||||
}
|
||||
|
||||
// Everything came from the blob cache: no disk blob bytes were read.
|
||||
ASSERT_EQ(BlobBytesRead(options), 0U);
|
||||
}
|
||||
|
||||
// A column that belongs to a different result is rejected with InvalidArgument
|
||||
// on the per-read status (columns identify their owning result, so there is no
|
||||
// untyped index to run out of range).
|
||||
|
||||
@@ -34,12 +34,46 @@ void ReadPathBlobResolver::Reset(
|
||||
|
||||
Status ReadPathBlobResolver::ResolveColumn(size_t column_index,
|
||||
Slice* resolved_value) {
|
||||
return ResolveColumnInternal(column_index, /*force_verify=*/false,
|
||||
resolved_value);
|
||||
// Whole-column resolution uses the captured ReadOptions' verify_checksums
|
||||
// (no force_verify).
|
||||
return ResolveColumnInternal(
|
||||
column_index,
|
||||
DeriveVerifyPolicy(blob_fetcher_.read_options().verify_checksums,
|
||||
/*force_verify=*/false),
|
||||
resolved_value);
|
||||
}
|
||||
|
||||
BlobVerifyPolicy ReadPathBlobResolver::DeriveVerifyPolicy(bool verify_checksums,
|
||||
bool force_verify) {
|
||||
if (force_verify) {
|
||||
return BlobVerifyPolicy::kVerifyIfPresent;
|
||||
}
|
||||
return verify_checksums ? BlobVerifyPolicy::kVerifyIfNoAmplification
|
||||
: BlobVerifyPolicy::kSkip;
|
||||
}
|
||||
|
||||
Status ReadPathBlobResolver::FetchBlobRef(const BlobIndex& blob_index,
|
||||
uint64_t range_offset,
|
||||
size_t range_length,
|
||||
BlobVerifyPolicy policy,
|
||||
PinnableSlice* out) {
|
||||
if (blob_index.IsSameFile()) {
|
||||
// Same-file ("embedded") references need the originating SST's reader.
|
||||
if (same_file_reader_ == nullptr) {
|
||||
return Status::Corruption(
|
||||
"Cannot resolve same-file blob reference: no same-file reader");
|
||||
}
|
||||
return same_file_reader_->GetSameFileBlob(blob_fetcher_.read_options(),
|
||||
blob_index, range_offset,
|
||||
range_length, policy, out);
|
||||
}
|
||||
return blob_fetcher_.FetchBlobRange(user_key_, blob_index, range_offset,
|
||||
range_length, policy, out,
|
||||
/*bytes_read=*/nullptr);
|
||||
}
|
||||
|
||||
Status ReadPathBlobResolver::ResolveColumnInternal(size_t column_index,
|
||||
bool force_verify,
|
||||
BlobVerifyPolicy policy,
|
||||
Slice* resolved_value) {
|
||||
assert(columns_);
|
||||
assert(resolved_value);
|
||||
@@ -72,53 +106,13 @@ Status ReadPathBlobResolver::ResolveColumnInternal(size_t column_index,
|
||||
std::make_unique<PinnableSlice>());
|
||||
auto& new_entry = resolved_cache_.back();
|
||||
|
||||
constexpr FilePrefetchBuffer* prefetch_buffer = nullptr;
|
||||
constexpr uint64_t* bytes_read = nullptr;
|
||||
|
||||
// TODO(lazy-blob-resolution-cleanup): the force_verify branching here
|
||||
// (and the whole-vs-range split in ResolveColumnRange) is really a
|
||||
// single 3-valued verify policy -- must-verify-if-present (today's
|
||||
// force_verify), verify-if-no-amplification (verify_checksums on: a
|
||||
// whole read verifies, a partial read skips), and ignore. Map
|
||||
// (verify_checksums, force_verify) to that internal enum once at this
|
||||
// boundary and thread it down, collapsing these branches. Deferred
|
||||
// for feature velocity; see the lazy blob resolution plan.
|
||||
if (force_verify && !blob_index.IsSameFile()) {
|
||||
// Honor force_verify for separate-file references: read and verify
|
||||
// the whole record even if ReadOptions::verify_checksums is off.
|
||||
status = blob_fetcher_.FetchBlobForceVerify(
|
||||
user_key_, blob_index, prefetch_buffer, new_entry.second.get(),
|
||||
bytes_read);
|
||||
} else if (force_verify && blob_index.IsSameFile()) {
|
||||
// Honor force_verify for same-file (embedded) references the same
|
||||
// way: force checksum verification on via a verify-enabled copy of
|
||||
// the read options. A same-file reference is only produced when the
|
||||
// entity was read from an SST that supplied its SameFileBlobReader,
|
||||
// so it is expected to be set; surface a clean error rather than
|
||||
// silently dropping force_verify (and taking the unverified path
|
||||
// below) if it somehow is not.
|
||||
assert(same_file_reader_ != nullptr);
|
||||
if (same_file_reader_ == nullptr) {
|
||||
status = Status::Corruption(
|
||||
"Cannot force-verify same-file blob: no same-file reader");
|
||||
} else {
|
||||
ReadOptions verify_read_options = blob_fetcher_.read_options();
|
||||
verify_read_options.verify_checksums = true;
|
||||
status = same_file_reader_->GetSameFileBlob(
|
||||
verify_read_options, blob_index, new_entry.second.get());
|
||||
}
|
||||
} else {
|
||||
// Route same-file ("embedded") references through the current SST's
|
||||
// SameFileBlobReader when one is set; separate-file references (and
|
||||
// the no-same-file-reader case) go straight to the Version-backed
|
||||
// fetcher. EffectiveFetcher() returns the plain base fetcher when
|
||||
// there is no same-file reader, adding no indirection.
|
||||
EmbeddedAwareBlobFetcher embedded_fetcher(&blob_fetcher_,
|
||||
same_file_reader_);
|
||||
status = embedded_fetcher.EffectiveFetcher()->FetchBlob(
|
||||
user_key_, blob_index, prefetch_buffer, new_entry.second.get(),
|
||||
bytes_read);
|
||||
}
|
||||
// Whole-column read of a blob reference, cached. FetchBlobRef routes
|
||||
// same-file vs separate-file references and applies the verify policy
|
||||
// (which folds in force_verify: kVerifyIfPresent verifies the whole
|
||||
// record even when ReadOptions::verify_checksums is off).
|
||||
status =
|
||||
FetchBlobRef(blob_index, /*range_offset=*/0, kWholeBlobLength,
|
||||
policy, new_entry.second.get());
|
||||
if (!status.ok()) {
|
||||
resolved_cache_.pop_back();
|
||||
} else {
|
||||
@@ -164,12 +158,17 @@ Status ReadPathBlobResolver::ResolveColumnRange(size_t column_index,
|
||||
return Status::InvalidArgument("Column index out of bounds");
|
||||
}
|
||||
|
||||
// Derive the verify policy once from (verify_checksums, force_verify) and let
|
||||
// it drive both the partial-vs-whole decision below and the downstream read.
|
||||
const BlobVerifyPolicy policy = DeriveVerifyPolicy(
|
||||
blob_fetcher_.read_options().verify_checksums, force_verify);
|
||||
|
||||
// No output buffer: the caller only wants to surface any I/O / integrity
|
||||
// error (and honor force_verify). Resolve the whole column and return; there
|
||||
// is nothing to slice into.
|
||||
if (result == nullptr) {
|
||||
Slice ignored;
|
||||
return ResolveColumnInternal(column_index, force_verify, &ignored);
|
||||
return ResolveColumnInternal(column_index, policy, &ignored);
|
||||
}
|
||||
|
||||
const BlobIndex* blob_index_ptr =
|
||||
@@ -179,10 +178,12 @@ Status ReadPathBlobResolver::ResolveColumnRange(size_t column_index,
|
||||
// reference that is: not already resolved (else we slice the cached whole
|
||||
// value), uncompressed (a strict sub-range of a compressed record can't be
|
||||
// decompressed in isolation), a strict sub-range (a whole-column read takes
|
||||
// the verifying + cache-filling path), and not force_verify. The read then
|
||||
// the verifying + cache-filling path), and a policy that permits skipping
|
||||
// verification (kVerifyIfPresent forces a whole verified read). The read then
|
||||
// goes to either the separate-file range fetcher (needs a Version) or, for a
|
||||
// same-file / embedded reference, the current SST's SameFileBlobReader.
|
||||
if (blob_index_ptr != nullptr && !force_verify &&
|
||||
if (blob_index_ptr != nullptr &&
|
||||
policy != BlobVerifyPolicy::kVerifyIfPresent &&
|
||||
blob_resolver_util::FindInCache(resolved_cache_, column_index) ==
|
||||
nullptr) {
|
||||
const BlobIndex& blob_index = *blob_index_ptr;
|
||||
@@ -204,24 +205,17 @@ Status ReadPathBlobResolver::ResolveColumnRange(size_t column_index,
|
||||
}
|
||||
const size_t avail = static_cast<size_t>(value_size - range_offset);
|
||||
const size_t actual_len = range_length > avail ? avail : range_length;
|
||||
if (blob_index.IsSameFile()) {
|
||||
return same_file_reader_->GetSameFileBlobRange(
|
||||
blob_fetcher_.read_options(), blob_index, range_offset,
|
||||
actual_len, result);
|
||||
}
|
||||
return blob_fetcher_.FetchBlobRange(user_key_, blob_index, range_offset,
|
||||
actual_len, result,
|
||||
/*bytes_read=*/nullptr);
|
||||
return FetchBlobRef(blob_index, range_offset, actual_len, policy,
|
||||
result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Full path: resolve the whole column (inline value directly, blob reference
|
||||
// via the resolver's cache, filling it on a miss and verifying under
|
||||
// ReadOptions::verify_checksums or force_verify), then slice out the
|
||||
// requested range.
|
||||
// via the resolver's cache, filling it on a miss and verifying per policy),
|
||||
// then slice out the requested range.
|
||||
Slice whole;
|
||||
Status s = ResolveColumnInternal(column_index, force_verify, &whole);
|
||||
Status s = ResolveColumnInternal(column_index, policy, &whole);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "db/blob/blob_constants.h"
|
||||
#include "db/blob/blob_fetcher.h"
|
||||
#include "db/blob/blob_index.h"
|
||||
#include "rocksdb/cleanable.h"
|
||||
@@ -124,11 +125,27 @@ class ReadPathBlobResolver {
|
||||
}
|
||||
|
||||
private:
|
||||
// Maps the public (ReadOptions::verify_checksums, force_verify) pair to the
|
||||
// internal 3-valued verify policy, once at this boundary (see
|
||||
// BlobVerifyPolicy). Every downstream blob read is driven by the resulting
|
||||
// policy rather than by re-deriving verification from two bools.
|
||||
static BlobVerifyPolicy DeriveVerifyPolicy(bool verify_checksums,
|
||||
bool force_verify);
|
||||
|
||||
// Reads a blob reference into *out, applying `policy`. range_length ==
|
||||
// kWholeBlobLength selects the whole value; any other length selects the
|
||||
// strict sub-range [range_offset, range_offset + range_length). Routes
|
||||
// same-file ("embedded") references to the current SST's SameFileBlobReader
|
||||
// and separate-file references to the Version-backed fetcher.
|
||||
Status FetchBlobRef(const BlobIndex& blob_index, uint64_t range_offset,
|
||||
size_t range_length, BlobVerifyPolicy policy,
|
||||
PinnableSlice* out);
|
||||
|
||||
// Shared implementation of ResolveColumn / the whole-column path of
|
||||
// ResolveColumnRange. When force_verify is set, a separate-file blob
|
||||
// reference is read with checksum verification forced on even if
|
||||
// ReadOptions::verify_checksums is off (see LazyColumnReadRequest).
|
||||
Status ResolveColumnInternal(size_t column_index, bool force_verify,
|
||||
// ResolveColumnRange: resolves and caches the whole column value under the
|
||||
// given verify policy (kVerifyIfPresent forces whole-record verification even
|
||||
// when ReadOptions::verify_checksums is off; see LazyColumnReadRequest).
|
||||
Status ResolveColumnInternal(size_t column_index, BlobVerifyPolicy policy,
|
||||
Slice* resolved_value);
|
||||
|
||||
// Owns its ReadOptions: a resolver's lifetime is independent of the caller
|
||||
|
||||
+15
-3
@@ -46,6 +46,7 @@
|
||||
#include <stdexcept>
|
||||
#include <type_traits>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "db/column_family.h"
|
||||
@@ -2037,6 +2038,8 @@ class MemTableInserter : public WriteBatch::Handler {
|
||||
size_t prot_info_idx_;
|
||||
|
||||
bool* has_valid_writes_;
|
||||
// Optional; see WriteBatchInternal::InsertInto() for the contract.
|
||||
std::unordered_set<uint32_t>* selected_cf_ids_;
|
||||
// On some (!) platforms just default creating
|
||||
// a map is too expensive in the Write() path as they
|
||||
// cause memory allocations though unused.
|
||||
@@ -2138,7 +2141,8 @@ class MemTableInserter : public WriteBatch::Handler {
|
||||
bool concurrent_memtable_writes,
|
||||
const WriteBatch::ProtectionInfo* prot_info,
|
||||
bool* has_valid_writes = nullptr, bool seq_per_batch = false,
|
||||
bool batch_per_txn = true, bool hint_per_batch = false)
|
||||
bool batch_per_txn = true, bool hint_per_batch = false,
|
||||
std::unordered_set<uint32_t>* selected_cf_ids = nullptr)
|
||||
: sequence_(_sequence),
|
||||
cf_mems_(cf_mems),
|
||||
flush_scheduler_(flush_scheduler),
|
||||
@@ -2152,6 +2156,7 @@ class MemTableInserter : public WriteBatch::Handler {
|
||||
prot_info_(prot_info),
|
||||
prot_info_idx_(0),
|
||||
has_valid_writes_(has_valid_writes),
|
||||
selected_cf_ids_(selected_cf_ids),
|
||||
rebuilding_trx_(nullptr),
|
||||
rebuilding_trx_seq_(0),
|
||||
seq_per_batch_(seq_per_batch),
|
||||
@@ -2264,6 +2269,11 @@ class MemTableInserter : public WriteBatch::Handler {
|
||||
*has_valid_writes_ = true;
|
||||
}
|
||||
|
||||
// Recovery only; null on the write path.
|
||||
if (UNLIKELY(selected_cf_ids_ != nullptr)) {
|
||||
selected_cf_ids_->insert(column_family_id);
|
||||
}
|
||||
|
||||
if (log_number_ref_ > 0) {
|
||||
cf_mems_->GetMemTable()->RefLogContainingPrepSection(log_number_ref_);
|
||||
}
|
||||
@@ -3330,12 +3340,14 @@ Status WriteBatchInternal::InsertInto(
|
||||
TrimHistoryScheduler* trim_history_scheduler,
|
||||
bool ignore_missing_column_families, uint64_t log_number, DB* db,
|
||||
bool concurrent_memtable_writes, SequenceNumber* next_seq,
|
||||
bool* has_valid_writes, bool seq_per_batch, bool batch_per_txn) {
|
||||
bool* has_valid_writes, bool seq_per_batch, bool batch_per_txn,
|
||||
std::unordered_set<uint32_t>* selected_cf_ids) {
|
||||
MemTableInserter inserter(Sequence(batch), memtables, flush_scheduler,
|
||||
trim_history_scheduler,
|
||||
ignore_missing_column_families, log_number, db,
|
||||
concurrent_memtable_writes, batch->prot_info_.get(),
|
||||
has_valid_writes, seq_per_batch, batch_per_txn);
|
||||
has_valid_writes, seq_per_batch, batch_per_txn,
|
||||
false /* hint_per_batch */, selected_cf_ids);
|
||||
Status s = batch->Iterate(&inserter);
|
||||
if (next_seq != nullptr) {
|
||||
*next_seq = inserter.sequence();
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
|
||||
#pragma once
|
||||
#include <array>
|
||||
#include <unordered_set>
|
||||
#include <vector>
|
||||
|
||||
#include "db/flush_scheduler.h"
|
||||
@@ -203,8 +204,17 @@ class WriteBatchInternal {
|
||||
// Convenience form of InsertInto when you have only one batch
|
||||
// next_seq returns the seq after last sequence number used in MemTable insert
|
||||
//
|
||||
// Under concurrent use, the caller is responsible for making sure that
|
||||
// the memtables object itself is thread-local.
|
||||
// `selected_cf_ids`, when not null, receives the id of every column family
|
||||
// this batch selected for writing, which matches neither the ids the batch
|
||||
// names nor what reached a memtable. Replaying a 2PC commit marker applies
|
||||
// the prepared batch's column families while naming none; a column family
|
||||
// already past `log_number` is skipped and never added; and an id is added
|
||||
// before the write, so a later per-write rejection can leave one recorded
|
||||
// for a column family nothing was written to. It accumulates as the batch
|
||||
// is applied, so it is meaningful when this returns an error partway.
|
||||
//
|
||||
// Under concurrent use, the caller is responsible for making sure that the
|
||||
// memtables object and `selected_cf_ids` are thread-local.
|
||||
static Status InsertInto(
|
||||
const WriteBatch* batch, ColumnFamilyMemTables* memtables,
|
||||
FlushScheduler* flush_scheduler,
|
||||
@@ -212,7 +222,8 @@ class WriteBatchInternal {
|
||||
bool ignore_missing_column_families = false, uint64_t log_number = 0,
|
||||
DB* db = nullptr, bool concurrent_memtable_writes = false,
|
||||
SequenceNumber* next_seq = nullptr, bool* has_valid_writes = nullptr,
|
||||
bool seq_per_batch = false, bool batch_per_txn = true);
|
||||
bool seq_per_batch = false, bool batch_per_txn = true,
|
||||
std::unordered_set<uint32_t>* selected_cf_ids = nullptr);
|
||||
|
||||
static Status InsertInto(WriteThread::Writer* writer, SequenceNumber sequence,
|
||||
ColumnFamilyMemTables* memtables,
|
||||
|
||||
@@ -185,6 +185,7 @@ DECLARE_uint32(sqfc_version);
|
||||
DECLARE_bool(use_sqfc_for_range_queries);
|
||||
DECLARE_int32(index_type);
|
||||
DECLARE_int32(data_block_index_type);
|
||||
DECLARE_int32(optimize_key_common_prefix);
|
||||
DECLARE_int32(index_block_search_type);
|
||||
DECLARE_double(uniform_cv_threshold);
|
||||
DECLARE_bool(use_trie_index);
|
||||
@@ -211,6 +212,7 @@ DECLARE_bool(sync);
|
||||
DECLARE_bool(use_fsync);
|
||||
DECLARE_uint64(stats_dump_period_sec);
|
||||
DECLARE_uint64(max_compaction_trigger_wakeup_seconds);
|
||||
DECLARE_int32(periodic_compaction_phase_recovery_percent);
|
||||
DECLARE_uint64(bytes_per_sync);
|
||||
DECLARE_uint64(wal_bytes_per_sync);
|
||||
DECLARE_int32(kill_random_test);
|
||||
@@ -237,6 +239,7 @@ DECLARE_int32(ingest_external_file_one_in);
|
||||
DECLARE_int32(ingest_external_file_width);
|
||||
DECLARE_int32(ingest_external_file_prepare_commit_one_in);
|
||||
DECLARE_int32(ingest_external_file_use_file_info_one_in);
|
||||
DECLARE_int32(ingest_external_file_atomic_replace_one_in);
|
||||
DECLARE_bool(ingest_external_file_with_embedded_blobs);
|
||||
DECLARE_int32(compact_files_one_in);
|
||||
DECLARE_int32(compact_range_one_in);
|
||||
|
||||
@@ -36,6 +36,12 @@ CompactionServiceJobStatus DbStressCompactionService::Wait(
|
||||
if (StressTest::IsErrorInjectedAndRetryable(s)) {
|
||||
return CompactionServiceJobStatus::kUseLocal;
|
||||
}
|
||||
// Note: a MANIFEST-floor rejection from OpenAndCompact surfaces here as
|
||||
// Status::Incomplete. We intentionally do NOT map it to kUseLocal (as
|
||||
// the unit-test CompactionService does): the stress test does not yet
|
||||
// inject stale/older MANIFEST view into the secondary open, so a floor
|
||||
// rejection means a real bug and should fail loudly rather than be
|
||||
// masked by a local fallback.
|
||||
if (result && result->empty()) {
|
||||
// If result is empty, set the compaction status in the result so
|
||||
// that it can be bubbled up to main thread
|
||||
|
||||
@@ -13,7 +13,6 @@ void DbStressCustomCompressionManager::Register() {
|
||||
// the current invocation, we can still read the SST files requiring it.
|
||||
static std::once_flag loaded;
|
||||
std::call_once(loaded, [&]() {
|
||||
TEST_AllowUnsupportedFormatVersion() = true;
|
||||
auto& library = *ObjectLibrary::Default();
|
||||
library.AddFactory<CompressionManager>(
|
||||
DbStressCustomCompressionManager().CompatibilityName(),
|
||||
|
||||
@@ -684,6 +684,13 @@ DEFINE_int32(
|
||||
ROCKSDB_NAMESPACE::BlockBasedTableOptions().data_block_index_type),
|
||||
"Index type for data blocks (see `enum DataBlockIndexType` in table.h)");
|
||||
|
||||
DEFINE_int32(optimize_key_common_prefix,
|
||||
static_cast<int32_t>(ROCKSDB_NAMESPACE::BlockBasedTableOptions()
|
||||
.optimize_key_common_prefix),
|
||||
"When/whether data blocks store the common user-key prefix once "
|
||||
"(see `enum OptimizeKeyCommonPrefix` in table.h): 0=kDisabled, "
|
||||
"1=kIfFastSeek, 2=kEnabled. Requires format_version >= 8.");
|
||||
|
||||
DEFINE_int32(index_block_search_type,
|
||||
static_cast<int32_t>(ROCKSDB_NAMESPACE::BlockBasedTableOptions()
|
||||
.index_block_search_type),
|
||||
@@ -918,6 +925,13 @@ DEFINE_int32(ingest_external_file_use_file_info_one_in, 0,
|
||||
"SstFileWriter::Finish) once every N ingestions on average, so "
|
||||
"ingestion skips re-opening and scanning the files.");
|
||||
|
||||
DEFINE_int32(
|
||||
ingest_external_file_atomic_replace_one_in, 0,
|
||||
"If non-zero, file ingestion atomically replaces a range strictly inside "
|
||||
"an existing SST once every N ingestion operations on average. Requires "
|
||||
"universal compaction, ingest_external_file_width >= 2, no user-defined "
|
||||
"timestamps, and no acquired snapshots.");
|
||||
|
||||
DEFINE_bool(
|
||||
ingest_external_file_with_embedded_blobs, false,
|
||||
"If true, external files for ingestion are written with embedded "
|
||||
@@ -1489,6 +1503,11 @@ DEFINE_uint64(
|
||||
ROCKSDB_NAMESPACE::Options().max_compaction_trigger_wakeup_seconds,
|
||||
"Sets DB option max_compaction_trigger_wakeup_seconds.");
|
||||
|
||||
DEFINE_int32(
|
||||
periodic_compaction_phase_recovery_percent,
|
||||
ROCKSDB_NAMESPACE::Options().periodic_compaction_phase_recovery_percent,
|
||||
"Sets DB option periodic_compaction_phase_recovery_percent.");
|
||||
|
||||
DEFINE_bool(verification_only, false,
|
||||
"If true, tests will only execute verification step");
|
||||
extern "C" bool RocksDbIOUringEnable() { return true; }
|
||||
|
||||
@@ -785,7 +785,10 @@ void StressTest::MaybeTestMultiGetEntityLazy(
|
||||
const size_t i = chosen[j].first;
|
||||
const size_t c = chosen[j].second;
|
||||
// Randomly fuzz a partial (byte-range) read vs a whole-column read, sized
|
||||
// against the reference when available, else the known logical size.
|
||||
// against the reference when available, else the known logical size; and
|
||||
// independently fuzz force_verify (which forces a whole, checksum-verified
|
||||
// read even for a sub-range) -- the resolved bytes are unchanged either
|
||||
// way.
|
||||
const WideColumns* reference = entity_ref[i];
|
||||
const std::optional<uint64_t> size_hint =
|
||||
reference ? std::optional<uint64_t>((*reference)[c].value().size())
|
||||
@@ -793,6 +796,7 @@ void StressTest::MaybeTestMultiGetEntityLazy(
|
||||
reads[j].column = &batch[i][c];
|
||||
PickLazyReadRange(&thread->rand, size_hint, &reads[j].offset,
|
||||
&reads[j].length);
|
||||
reads[j].force_verify = thread->rand.OneIn(4);
|
||||
reads[j].result = &results[j];
|
||||
reads[j].status = &statuses[j];
|
||||
}
|
||||
@@ -910,13 +914,17 @@ void StressTest::ResolveLazyEntity(ThreadState* thread, const std::string& key,
|
||||
for (size_t j = 0; j < chosen.size(); ++j) {
|
||||
const size_t c = chosen[j];
|
||||
// Randomly fuzz a partial (byte-range) read vs a whole-column read, sized
|
||||
// against the reference when available, else the known logical size.
|
||||
// against the reference when available, else the known logical size; and
|
||||
// independently fuzz force_verify (which forces a whole, checksum-verified
|
||||
// read even for a sub-range) -- the resolved bytes are unchanged either
|
||||
// way.
|
||||
const std::optional<uint64_t> size_hint =
|
||||
reference ? std::optional<uint64_t>((*reference)[c].value().size())
|
||||
: lazy[c].logical_size();
|
||||
reads[j].column = &lazy[c];
|
||||
PickLazyReadRange(&thread->rand, size_hint, &reads[j].offset,
|
||||
&reads[j].length);
|
||||
reads[j].force_verify = thread->rand.OneIn(4);
|
||||
reads[j].result = &results[j];
|
||||
reads[j].status = &statuses[j];
|
||||
}
|
||||
@@ -1489,8 +1497,19 @@ Status StressTest::AssertSame(DB* db, ColumnFamilyHandle* cf,
|
||||
ropt.auto_refresh_iterator_with_snapshot =
|
||||
FLAGS_auto_refresh_iterator_with_snapshot;
|
||||
Slice ts;
|
||||
std::string validation_ts_str;
|
||||
if (!snap_state.timestamp.empty()) {
|
||||
ts = snap_state.timestamp;
|
||||
if (!FLAGS_persist_user_defined_timestamps) {
|
||||
// Timestamp-enabled reads require a read timestamp. In memtable-only UDT
|
||||
// mode, the saved timestamp is not part of the long-running snapshot
|
||||
// contract because full_history_ts_low can advance while the snapshot is
|
||||
// held. Use a fresh timestamp to validate the pinned sequence-number
|
||||
// view.
|
||||
validation_ts_str = GetReadTimestamp();
|
||||
ts = validation_ts_str;
|
||||
} else {
|
||||
ts = snap_state.timestamp;
|
||||
}
|
||||
ropt.timestamp = &ts;
|
||||
}
|
||||
PinnableSlice exp_v(&snap_state.value);
|
||||
@@ -5704,15 +5723,18 @@ void StressTest::RecordManifestStateBeforeReopen() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (FLAGS_metadata_write_fault_one_in != 0 ||
|
||||
FLAGS_open_metadata_write_fault_one_in != 0) {
|
||||
manifest_verify_mode_ = MANIFEST_VERIFY_NONE;
|
||||
return;
|
||||
}
|
||||
|
||||
if (reuse_manifest && optimize_manifest) {
|
||||
// Check if ALL conditions for complete avoidance are met.
|
||||
// If so, use STRICT mode where failures are fatal.
|
||||
const bool no_fault_injection = FLAGS_metadata_write_fault_one_in == 0 &&
|
||||
FLAGS_open_metadata_write_fault_one_in == 0;
|
||||
const bool no_manifest_writes_expected =
|
||||
FLAGS_avoid_flush_during_recovery && // No flush during recovery
|
||||
!FLAGS_write_dbid_to_manifest && // No DB_ID write on open
|
||||
no_fault_injection;
|
||||
!FLAGS_write_dbid_to_manifest; // No DB_ID write on open
|
||||
// Note: avoid_flush_during_shutdown is NOT required for STRICT mode.
|
||||
// If avoid_flush_during_shutdown=true leaves data in WAL, but
|
||||
// avoid_flush_during_recovery=true prevents flushing it, so MANIFEST
|
||||
@@ -6195,6 +6217,9 @@ void InitializeOptionsFromFlags(
|
||||
block_based_options.data_block_index_type =
|
||||
static_cast<BlockBasedTableOptions::DataBlockIndexType>(
|
||||
FLAGS_data_block_index_type);
|
||||
block_based_options.optimize_key_common_prefix =
|
||||
static_cast<BlockBasedTableOptions::OptimizeKeyCommonPrefix>(
|
||||
FLAGS_optimize_key_common_prefix);
|
||||
block_based_options.index_block_search_type =
|
||||
static_cast<BlockBasedTableOptions::BlockSearchType>(
|
||||
FLAGS_index_block_search_type);
|
||||
@@ -6358,6 +6383,8 @@ void InitializeOptionsFromFlags(
|
||||
static_cast<unsigned int>(FLAGS_stats_dump_period_sec);
|
||||
options.max_compaction_trigger_wakeup_seconds =
|
||||
FLAGS_max_compaction_trigger_wakeup_seconds;
|
||||
options.periodic_compaction_phase_recovery_percent =
|
||||
FLAGS_periodic_compaction_phase_recovery_percent;
|
||||
options.ttl = FLAGS_compaction_ttl;
|
||||
options.enable_pipelined_write = FLAGS_enable_pipelined_write;
|
||||
options.enable_write_thread_adaptive_yield =
|
||||
|
||||
@@ -407,7 +407,9 @@ class StressTest {
|
||||
kLastOpSeekToLast
|
||||
};
|
||||
|
||||
// Enum used to track MANIFEST verification mode during DB reopen
|
||||
// Enum used to track MANIFEST verification mode during DB reopen.
|
||||
// Verification is skipped when metadata write fault injection is enabled
|
||||
// because recovery may have to abandon a partially written MANIFEST.
|
||||
enum ManifestVerifyMode {
|
||||
MANIFEST_VERIFY_NONE,
|
||||
// MANIFEST file should be reused (same file number), CURRENT should not
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
#include "db_stress_tool/db_stress_shared_state.h"
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/convenience.h"
|
||||
#include "table/format.h"
|
||||
#include "utilities/fault_injection_fs.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
@@ -124,6 +125,15 @@ int db_stress_tool(int argc, char** argv) {
|
||||
SanitizeDoubleParam(&FLAGS_memtable_prefix_bloom_size_ratio);
|
||||
SanitizeDoubleParam(&FLAGS_max_bytes_for_level_multiplier);
|
||||
|
||||
// db_stress may read and write in-development draft format_versions that are
|
||||
// not yet published to users. Enable the TEST-only opt-in once, centrally,
|
||||
// for the whole run. It stays on regardless of this run's --format_version:
|
||||
// the crash test re-randomizes format_version per invocation on the same DB,
|
||||
// so a run picking an older version may still need to READ draft-version SSTs
|
||||
// written by an earlier run. The opt-in is inert unless an unsupported
|
||||
// format_version is actually requested.
|
||||
TEST_AllowUnsupportedFormatVersion() = true;
|
||||
|
||||
#ifndef NDEBUG
|
||||
if (FLAGS_mock_direct_io) {
|
||||
SetupSyncPointsToMockDirectIO();
|
||||
@@ -350,6 +360,33 @@ int db_stress_tool(int argc, char** argv) {
|
||||
return ReturnValidationError(
|
||||
"nooverwritepercent must not be 100 when using file ingestion");
|
||||
}
|
||||
if (FLAGS_ingest_external_file_atomic_replace_one_in > 0) {
|
||||
if (FLAGS_ingest_external_file_one_in <= 0) {
|
||||
return ReturnValidationError(
|
||||
"ingest_external_file_one_in must be positive when testing atomic "
|
||||
"range replacement");
|
||||
}
|
||||
if (FLAGS_ingest_external_file_width < 2) {
|
||||
return ReturnValidationError(
|
||||
"ingest_external_file_width must be at least 2 when testing atomic "
|
||||
"range replacement");
|
||||
}
|
||||
if (FLAGS_compaction_style != kCompactionStyleUniversal) {
|
||||
return ReturnValidationError(
|
||||
"atomic range replacement ingestion requires universal "
|
||||
"compaction");
|
||||
}
|
||||
if (FLAGS_user_timestamp_size > 0) {
|
||||
return ReturnValidationError(
|
||||
"atomic range replacement ingestion does not support user-defined "
|
||||
"timestamps");
|
||||
}
|
||||
if (FLAGS_acquire_snapshot_one_in > 0) {
|
||||
return ReturnValidationError(
|
||||
"atomic range replacement ingestion is incompatible with acquired "
|
||||
"snapshots");
|
||||
}
|
||||
}
|
||||
if (FLAGS_clear_column_family_one_in > 0 && FLAGS_backup_one_in > 0) {
|
||||
return ReturnValidationError(
|
||||
"clear_column_family_one_in must be 0 when using backup");
|
||||
|
||||
@@ -796,8 +796,13 @@ Status FileExpectedStateManager::Restore(DB* db) {
|
||||
std::string trace_file_path = GetPathForFilename(trace_filename);
|
||||
|
||||
std::unique_ptr<TraceReader> trace_reader;
|
||||
Status s = NewFileTraceReader(Env::Default(), EnvOptions(), trace_file_path,
|
||||
&trace_reader);
|
||||
Status s = Status::OK();
|
||||
// If the DB recovered exactly to `saved_seqno_`, skip opening/replaying the
|
||||
// trace and reuse the normal state promotion and cleanup flow below.
|
||||
if (replay_write_ops > 0) {
|
||||
s = NewFileTraceReader(Env::Default(), EnvOptions(), trace_file_path,
|
||||
&trace_reader);
|
||||
}
|
||||
|
||||
std::string persisted_seqno_file_path = GetPathForFilename(
|
||||
kPersistedSeqnoBasename + kPersistedSeqnoFilenameSuffix);
|
||||
@@ -821,7 +826,7 @@ Status FileExpectedStateManager::Restore(DB* db) {
|
||||
num_column_families_));
|
||||
s = state->Open(false /* create */);
|
||||
}
|
||||
if (s.ok()) {
|
||||
if (s.ok() && replay_write_ops > 0) {
|
||||
handler.reset(
|
||||
new ExpectedStateTraceRecordHandler(replay_write_ops, state.get()));
|
||||
// TODO(ajkr): An API limitation requires we provide `handles` although
|
||||
@@ -831,10 +836,10 @@ Status FileExpectedStateManager::Restore(DB* db) {
|
||||
std::move(trace_reader), &replayer);
|
||||
}
|
||||
|
||||
if (s.ok()) {
|
||||
if (s.ok() && replay_write_ops > 0) {
|
||||
s = replayer->Prepare();
|
||||
}
|
||||
for (; s.ok();) {
|
||||
for (; s.ok() && replay_write_ops > 0;) {
|
||||
std::unique_ptr<TraceRecord> record;
|
||||
s = replayer->Next(&record);
|
||||
if (!s.ok()) {
|
||||
@@ -857,7 +862,7 @@ Status FileExpectedStateManager::Restore(DB* db) {
|
||||
std::unique_ptr<TraceRecordResult> res;
|
||||
s = record->Accept(handler.get(), &res);
|
||||
}
|
||||
if (s.ok() && !handler->IsDone()) {
|
||||
if (s.ok() && replay_write_ops > 0 && !handler->IsDone()) {
|
||||
s = Status::Corruption(
|
||||
"Trace ended before replaying all expected write ops",
|
||||
std::to_string(handler->NumWriteOps()) + " < " +
|
||||
|
||||
@@ -13,9 +13,13 @@
|
||||
#include "db_stress_tool/expected_state.h"
|
||||
#include "rocksdb/status.h"
|
||||
#ifdef GFLAGS
|
||||
#include <algorithm>
|
||||
#include <cinttypes>
|
||||
#include <deque>
|
||||
#include <optional>
|
||||
#include <unordered_map>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "db/wide/wide_columns_helper.h"
|
||||
#include "db_stress_tool/db_stress_common.h"
|
||||
@@ -23,6 +27,86 @@
|
||||
#include "utilities/fault_injection_fs.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
namespace {
|
||||
|
||||
// Finds a run of overwriteable keys strictly inside `file`'s key span, stopping
|
||||
// at `desired_width` keys. Runs shorter than two keys are not worth replacing
|
||||
// and are skipped. Returns the run as a half-open [begin, limit) key range, or
|
||||
// `nullopt` if the file has no such run.
|
||||
std::optional<std::pair<int64_t, int64_t>> FindOverwriteableRunInFile(
|
||||
const SstFileMetaData& file, SharedState* shared, uint64_t max_key,
|
||||
uint64_t desired_width) {
|
||||
uint64_t smallest_key = 0;
|
||||
uint64_t largest_key = 0;
|
||||
if (!GetIntVal(file.smallestkey, &smallest_key) ||
|
||||
!GetIntVal(file.largestkey, &largest_key) || smallest_key >= max_key) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const uint64_t search_limit = std::min(largest_key, max_key);
|
||||
uint64_t run_begin = 0;
|
||||
uint64_t run_size = 0;
|
||||
for (uint64_t key = smallest_key + 1; key < search_limit; ++key) {
|
||||
if (!shared->AllowsOverwrite(static_cast<int64_t>(key))) {
|
||||
if (run_size >= 2) {
|
||||
return std::make_pair(static_cast<int64_t>(run_begin),
|
||||
static_cast<int64_t>(key));
|
||||
}
|
||||
run_size = 0;
|
||||
continue;
|
||||
}
|
||||
if (run_size == 0) {
|
||||
run_begin = key;
|
||||
}
|
||||
++run_size;
|
||||
if (run_size == desired_width) {
|
||||
return std::make_pair(static_cast<int64_t>(run_begin),
|
||||
static_cast<int64_t>(key + 1));
|
||||
}
|
||||
}
|
||||
if (run_size >= 2) {
|
||||
return std::make_pair(static_cast<int64_t>(run_begin),
|
||||
static_cast<int64_t>(run_begin + run_size));
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<std::pair<int64_t, int64_t>> PickAtomicReplaceRange(
|
||||
ThreadState* thread, DB* db, ColumnFamilyHandle* column_family,
|
||||
SharedState* shared) {
|
||||
ColumnFamilyMetaData metadata;
|
||||
db->GetColumnFamilyMetaData(column_family, &metadata);
|
||||
|
||||
std::vector<std::pair<int64_t, int64_t>> candidates;
|
||||
const uint64_t max_key = static_cast<uint64_t>(shared->GetMaxKey());
|
||||
const uint64_t desired_width =
|
||||
static_cast<uint64_t>(FLAGS_ingest_external_file_width);
|
||||
for (const auto& level : metadata.levels) {
|
||||
for (const auto& file : level.files) {
|
||||
std::optional<std::pair<int64_t, int64_t>> candidate =
|
||||
FindOverwriteableRunInFile(file, shared, max_key, desired_width);
|
||||
if (candidate.has_value()) {
|
||||
candidates.push_back(*candidate);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (candidates.empty()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return candidates[thread->rand.Uniform(static_cast<int>(candidates.size()))];
|
||||
}
|
||||
|
||||
bool IsRetryableAtomicReplaceError(const Status& status) {
|
||||
return status.IsTryAgain() ||
|
||||
(status.IsInvalidArgument() &&
|
||||
status.ToString().find(
|
||||
"Atomic replace range overlaps with pending compaction") !=
|
||||
std::string::npos);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
class NonBatchedOpsStressTest : public StressTest {
|
||||
public:
|
||||
NonBatchedOpsStressTest(int db_index, const std::string& db_path,
|
||||
@@ -525,12 +609,9 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
read_opts.timestamp = &ts;
|
||||
}
|
||||
|
||||
std::unique_ptr<ManagedSnapshot> snapshot = nullptr;
|
||||
if (FLAGS_auto_refresh_iterator_with_snapshot) {
|
||||
snapshot = std::make_unique<ManagedSnapshot>(db_);
|
||||
read_opts.snapshot = snapshot->snapshot();
|
||||
read_opts.auto_refresh_iterator_with_snapshot = true;
|
||||
}
|
||||
// Secondary mode does not support snapshots. This verifier reads from the
|
||||
// secondary, so ignore auto_refresh_iterator_with_snapshot here; the
|
||||
// primary iterator verification path still exercises that option.
|
||||
|
||||
static Random64 rand64(shared->GetSeed());
|
||||
|
||||
@@ -557,10 +638,6 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
s.PermitUncheckedError();
|
||||
} else {
|
||||
// Use range scan
|
||||
if (read_opts.auto_refresh_iterator_with_snapshot) {
|
||||
snapshot = std::make_unique<ManagedSnapshot>(db_);
|
||||
read_opts.snapshot = snapshot->snapshot();
|
||||
}
|
||||
std::unique_ptr<Iterator> iter(
|
||||
secondary_db_->NewIterator(read_opts, handle));
|
||||
// Skip SeekToFirst, SeekToLast, SeekForPrev, and Prev when backward
|
||||
@@ -594,9 +671,6 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
for (int i = 0; i < 5 && iter->Valid(); ++i, iter->Prev()) {
|
||||
}
|
||||
}
|
||||
if (read_opts.auto_refresh_iterator_with_snapshot) {
|
||||
read_opts.snapshot = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2383,6 +2457,9 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
// deletion file's compaction input optimization.
|
||||
bool test_standalone_range_deletion = thread->rand.OneInOpt(
|
||||
FLAGS_test_ingest_standalone_range_deletion_one_in);
|
||||
bool test_atomic_replace_range =
|
||||
!test_standalone_range_deletion &&
|
||||
thread->rand.OneInOpt(FLAGS_ingest_external_file_atomic_replace_one_in);
|
||||
// When true, reuse the writer's metadata via IngestExternalFileArg's
|
||||
// file_infos so ingestion skips re-opening and scanning the file. Not
|
||||
// combined with the standalone range deletion mode (a range-del-only file).
|
||||
@@ -2393,8 +2470,21 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
Status s;
|
||||
std::ostringstream ingest_options_oss;
|
||||
|
||||
int64_t key_base = rand_keys[0];
|
||||
SharedState* shared = thread->shared;
|
||||
int column_family = rand_column_families[0];
|
||||
int64_t key_base = rand_keys[0];
|
||||
int64_t key_limit = std::min(shared->GetMaxKey(),
|
||||
key_base + FLAGS_ingest_external_file_width);
|
||||
if (test_atomic_replace_range) {
|
||||
auto range = PickAtomicReplaceRange(
|
||||
thread, db_, column_families_[column_family], shared);
|
||||
if (range.has_value()) {
|
||||
key_base = range->first;
|
||||
key_limit = range->second;
|
||||
} else {
|
||||
test_atomic_replace_range = false;
|
||||
}
|
||||
}
|
||||
std::vector<std::unique_ptr<MutexLock>> range_locks;
|
||||
range_locks.reserve(FLAGS_ingest_external_file_width);
|
||||
std::vector<int64_t> keys;
|
||||
@@ -2403,21 +2493,17 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
values.reserve(FLAGS_ingest_external_file_width);
|
||||
std::vector<PendingExpectedValue> pending_expected_values;
|
||||
pending_expected_values.reserve(FLAGS_ingest_external_file_width);
|
||||
SharedState* shared = thread->shared;
|
||||
|
||||
// Grab locks, add keys
|
||||
assert(FLAGS_nooverwritepercent < 100);
|
||||
for (int64_t key = key_base;
|
||||
key < shared->GetMaxKey() &&
|
||||
key < key_base + FLAGS_ingest_external_file_width;
|
||||
++key) {
|
||||
for (int64_t key = key_base; key < key_limit; ++key) {
|
||||
if (key == key_base ||
|
||||
(key & ((1 << FLAGS_log2_keys_per_lock) - 1)) == 0) {
|
||||
range_locks.emplace_back(
|
||||
new MutexLock(shared->GetMutexForKey(column_family, key)));
|
||||
}
|
||||
if (test_standalone_range_deletion) {
|
||||
// Testing standalone range deletion needs a continuous range of keys.
|
||||
if (test_standalone_range_deletion || test_atomic_replace_range) {
|
||||
// Both range modes need a continuous range of overwriteable keys.
|
||||
if (shared->AllowsOverwrite(key)) {
|
||||
if (keys.empty() || (!keys.empty() && keys.back() == key - 1)) {
|
||||
keys.push_back(key);
|
||||
@@ -2445,9 +2531,14 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
return;
|
||||
}
|
||||
size_t total_keys = keys.size();
|
||||
if (test_atomic_replace_range && total_keys < 2) {
|
||||
return;
|
||||
}
|
||||
|
||||
const size_t data_key_count =
|
||||
test_atomic_replace_range ? (total_keys + 1) / 2 : total_keys;
|
||||
const size_t data_file_count =
|
||||
std::min<size_t>(1 + thread->rand.Uniform(3), total_keys);
|
||||
std::min<size_t>(1 + thread->rand.Uniform(3), data_key_count);
|
||||
std::vector<std::string> external_files;
|
||||
std::vector<std::string> data_filenames;
|
||||
data_filenames.reserve(data_file_count);
|
||||
@@ -2524,10 +2615,20 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
return;
|
||||
}
|
||||
|
||||
// set pending state on expected values, create and ingest files.
|
||||
// Set pending state on expected values and create the files. Atomic range
|
||||
// replacement writes alternating keys and clears the holes between them.
|
||||
size_t data_key_index = 0;
|
||||
for (size_t i = 0; s.ok() && i < total_keys; i++) {
|
||||
auto& sst_file_writer = sst_file_writers[i % sst_file_writers.size()];
|
||||
int64_t key = keys.at(i);
|
||||
if (test_atomic_replace_range && i % 2 == 1) {
|
||||
pending_expected_values.push_back(
|
||||
shared->PrepareDelete(column_family, key));
|
||||
continue;
|
||||
}
|
||||
|
||||
auto& sst_file_writer =
|
||||
sst_file_writers[data_key_index % sst_file_writers.size()];
|
||||
++data_key_index;
|
||||
char value[100];
|
||||
auto key_str = Key(key);
|
||||
const Slice k(key_str);
|
||||
@@ -2575,6 +2676,7 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
}
|
||||
}
|
||||
bool dropped_without_commit = false;
|
||||
bool retryable_atomic_replace_error = false;
|
||||
if (s.ok()) {
|
||||
IngestExternalFileOptions ingest_options;
|
||||
ingest_options.move_files = thread->rand.OneInOpt(2);
|
||||
@@ -2585,39 +2687,51 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
ingest_options.file_opening_threads = 1 + thread->rand.Uniform(4);
|
||||
ingest_options.prefetch_lmax_index_and_filter_blocks =
|
||||
!thread->rand.OneInOpt(4);
|
||||
if (test_atomic_replace_range) {
|
||||
ingest_options.snapshot_consistency = false;
|
||||
ingest_options.allow_global_seqno = true;
|
||||
}
|
||||
const bool use_prepare_commit = thread->rand.OneInOpt(
|
||||
FLAGS_ingest_external_file_prepare_commit_one_in);
|
||||
const bool use_separate_prepare_calls = use_prepare_commit &&
|
||||
external_files.size() > 1 &&
|
||||
thread->rand.OneInOpt(2);
|
||||
ingest_options_oss << "move_files: " << ingest_options.move_files
|
||||
<< ", verify_checksums_before_ingest: "
|
||||
<< ingest_options.verify_checksums_before_ingest
|
||||
<< ", verify_checksums_readahead_size: "
|
||||
<< ingest_options.verify_checksums_readahead_size
|
||||
<< ", fill_cache: " << ingest_options.fill_cache
|
||||
<< ", file_opening_threads: "
|
||||
<< ingest_options.file_opening_threads
|
||||
<< ", prefetch_lmax_index_and_filter_blocks: "
|
||||
<< ingest_options.prefetch_lmax_index_and_filter_blocks
|
||||
<< ", ingest_external_file_data_file_count: "
|
||||
<< data_file_count
|
||||
<< ", num_external_files: " << external_files.size()
|
||||
<< ", test_standalone_range_deletion: "
|
||||
<< test_standalone_range_deletion
|
||||
<< ", use_prepare_commit: " << use_prepare_commit
|
||||
<< ", use_separate_prepare_calls: "
|
||||
<< use_separate_prepare_calls
|
||||
<< ", use_file_info: " << use_file_info;
|
||||
const bool use_separate_prepare_calls =
|
||||
use_prepare_commit && !test_atomic_replace_range &&
|
||||
external_files.size() > 1 && thread->rand.OneInOpt(2);
|
||||
ingest_options_oss
|
||||
<< "move_files: " << ingest_options.move_files
|
||||
<< ", verify_checksums_before_ingest: "
|
||||
<< ingest_options.verify_checksums_before_ingest
|
||||
<< ", verify_checksums_readahead_size: "
|
||||
<< ingest_options.verify_checksums_readahead_size
|
||||
<< ", fill_cache: " << ingest_options.fill_cache
|
||||
<< ", file_opening_threads: " << ingest_options.file_opening_threads
|
||||
<< ", prefetch_lmax_index_and_filter_blocks: "
|
||||
<< ingest_options.prefetch_lmax_index_and_filter_blocks
|
||||
<< ", ingest_external_file_data_file_count: " << data_file_count
|
||||
<< ", num_external_files: " << external_files.size()
|
||||
<< ", test_standalone_range_deletion: "
|
||||
<< test_standalone_range_deletion
|
||||
<< ", test_atomic_replace_range: " << test_atomic_replace_range
|
||||
<< ", use_prepare_commit: " << use_prepare_commit
|
||||
<< ", use_separate_prepare_calls: " << use_separate_prepare_calls
|
||||
<< ", use_file_info: " << use_file_info;
|
||||
IngestExternalFileArg arg;
|
||||
arg.column_family = column_families_[column_family];
|
||||
arg.external_files = external_files;
|
||||
arg.options = ingest_options;
|
||||
std::string atomic_replace_start;
|
||||
std::string atomic_replace_limit;
|
||||
if (test_atomic_replace_range) {
|
||||
atomic_replace_start = Key(keys.front());
|
||||
atomic_replace_limit = Key(keys.back() + 1);
|
||||
arg.atomic_replace_range = {
|
||||
{atomic_replace_start, atomic_replace_limit}};
|
||||
}
|
||||
if (use_file_info) {
|
||||
for (const auto& file_info : file_infos) {
|
||||
arg.file_infos.push_back(file_info.prepared_file_info.get());
|
||||
}
|
||||
}
|
||||
bool attempted_atomic_replace = false;
|
||||
if (use_prepare_commit) {
|
||||
std::vector<std::unique_ptr<FileIngestionHandle>> handles;
|
||||
handles.reserve(use_separate_prepare_calls ? external_files.size() : 1);
|
||||
@@ -2636,6 +2750,7 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
}
|
||||
} else {
|
||||
std::unique_ptr<FileIngestionHandle> handle;
|
||||
attempted_atomic_replace = test_atomic_replace_range;
|
||||
s = db_->PrepareFileIngestion({arg}, &handle);
|
||||
if (s.ok()) {
|
||||
handles.push_back(std::move(handle));
|
||||
@@ -2661,8 +2776,14 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
attempted_atomic_replace = test_atomic_replace_range;
|
||||
s = db_->IngestExternalFiles({arg});
|
||||
}
|
||||
if (attempted_atomic_replace && !s.ok() &&
|
||||
IsRetryableAtomicReplaceError(s)) {
|
||||
dropped_without_commit = true;
|
||||
retryable_atomic_replace_error = true;
|
||||
}
|
||||
}
|
||||
if (!s.ok() || dropped_without_commit) {
|
||||
for (PendingExpectedValue& pending_expected_value :
|
||||
@@ -2670,7 +2791,8 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
pending_expected_value.Rollback();
|
||||
}
|
||||
|
||||
if (!s.ok() && !IsErrorInjectedAndRetryable(s)) {
|
||||
if (!s.ok() && !retryable_atomic_replace_error &&
|
||||
!IsErrorInjectedAndRetryable(s)) {
|
||||
fprintf(stderr,
|
||||
"file ingestion error: %s under specified "
|
||||
"IngestExternalFileOptions: %s (Empty string or "
|
||||
|
||||
@@ -5232,6 +5232,13 @@ 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_lowest_used_cache_tier(
|
||||
rocksdb_options_t* opt, int v);
|
||||
|
||||
@@ -5252,6 +5259,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);
|
||||
@@ -6155,6 +6170,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);
|
||||
|
||||
@@ -2612,6 +2612,13 @@ class DB {
|
||||
// secondary instance does not delete the corresponding column family
|
||||
// handles, the data of the column family is still accessible to the
|
||||
// secondary.
|
||||
// If the primary has flushed data that this instance cannot read, because the
|
||||
// flushed file is missing or unreadable for example, the memtables holding
|
||||
// that data are kept rather than dropped, so that reads do not lose it. That
|
||||
// memory, which is charged to `write_buffer_manager` when one is configured,
|
||||
// is reclaimed only once those files become readable or this instance is
|
||||
// reopened; `rocksdb.num-immutable-mem-table` reports how many memtables are
|
||||
// being held.
|
||||
virtual Status TryCatchUpWithPrimary() {
|
||||
return Status::NotSupported("Supported only by secondary instance");
|
||||
}
|
||||
|
||||
@@ -270,6 +270,23 @@ class ExternalTableFactory : public Customizable {
|
||||
virtual ExternalTableBuilder* NewTableBuilder(
|
||||
const ExternalTableBuilderOptions& builder_options,
|
||||
const std::string& file_path, FSWritableFile* file) const = 0;
|
||||
|
||||
// Configures this factory using an implementation-defined string. RocksDB
|
||||
// calls this method while preparing the table factory, before it is used to
|
||||
// create table readers or builders. The configuration is immutable after
|
||||
// the DB is opened and is persisted in the RocksDB OPTIONS file.
|
||||
//
|
||||
// Implementations must copy any needed data from config and should make this
|
||||
// method idempotent because RocksDB may prepare an object more than once.
|
||||
// The configuration must not contain secrets because OPTIONS files are
|
||||
// stored as plaintext.
|
||||
virtual Status Configure(const std::string& config) {
|
||||
if (config.empty()) {
|
||||
return Status::OK();
|
||||
}
|
||||
return Status::NotSupported(
|
||||
"External table factory does not support configuration");
|
||||
}
|
||||
};
|
||||
|
||||
// Allocate a TableFactory that wraps around an ExternalTableFactory. Use this
|
||||
|
||||
+116
-11
@@ -455,7 +455,7 @@ struct DbPath {
|
||||
DbPath(const std::string& p, uint64_t t) : path(p), target_size(t) {}
|
||||
};
|
||||
|
||||
extern const char* kHostnameForDbHostId;
|
||||
extern const char* const kHostnameForDbHostId;
|
||||
|
||||
enum class CompactionServiceJobStatus : char {
|
||||
kSuccess,
|
||||
@@ -547,7 +547,18 @@ class CompactionService : public Customizable {
|
||||
return response;
|
||||
}
|
||||
|
||||
// Wait for the scheduled compaction to finish from the remote worker
|
||||
// Wait for the scheduled compaction to finish from the remote worker and
|
||||
// return its status; `result` is the serialized CompactionServiceResult from
|
||||
// the worker's DB::OpenAndCompact.
|
||||
//
|
||||
// DB::OpenAndCompact may return Status::Incomplete to *decline* a scheduled
|
||||
// job rather than fail it -- currently when the MANIFEST floor check finds
|
||||
// the worker recovered an older/stale view of the DB than the primary
|
||||
// scheduled from (see DBOptions::remote_compaction_manifest_floor). Whether
|
||||
// to map that to kUseLocal (fall back to a local compaction) or kFailure
|
||||
// (surface it) is up to the integrator; both are valid. (Cancellation also
|
||||
// surfaces as Status::Incomplete, distinguishable by SubCode
|
||||
// kManualCompactionPaused.)
|
||||
virtual CompactionServiceJobStatus Wait(
|
||||
const std::string& /*scheduled_job_id*/, std::string* /*result*/) {
|
||||
return CompactionServiceJobStatus::kUseLocal;
|
||||
@@ -1754,6 +1765,28 @@ struct DBOptions {
|
||||
// under development.
|
||||
std::shared_ptr<CompactionService> compaction_service = nullptr;
|
||||
|
||||
// Hardening for remote compaction (CompactionService / DB::OpenAndCompact).
|
||||
// When true (default), the primary includes in each remote compaction request
|
||||
// the MANIFEST position (file number and size) it scheduled the compaction
|
||||
// from. The remote worker refuses to reconstruct the compaction against an
|
||||
// older MANIFEST view than that -- e.g. an eventually-consistent filesystem
|
||||
// returning a stale CURRENT, or a truncated MANIFEST -- and falls back to a
|
||||
// local compaction rather than risk an incorrect result computed against the
|
||||
// wrong (older) LSM shape.
|
||||
//
|
||||
// The presence of this position on the request also enables the worker's
|
||||
// "trust the MANIFEST" recovery, which avoids failing a job over a
|
||||
// transiently-unavailable file that is not one of the compaction's inputs.
|
||||
//
|
||||
// Setting this to false is a kill switch for disabling both behaviors (for
|
||||
// subsequently scheduled compactions). Dynamically changeable through
|
||||
// SetDBOptions() API. DEPRECATED because this kill switch should not be
|
||||
// needed long term.
|
||||
//
|
||||
// Only affects a primary that schedules remote compactions; it has no effect
|
||||
// when compaction_service is not configured.
|
||||
bool remote_compaction_manifest_floor = true;
|
||||
|
||||
// It indicates, which lowest cache tier we want to
|
||||
// use for a certain DB. Currently we support volatile_tier and
|
||||
// non_volatile_tier. They are layered. By setting it to kVolatileTier, only
|
||||
@@ -1820,6 +1853,61 @@ struct DBOptions {
|
||||
// Dynamically changeable through SetDBOptions() API.
|
||||
uint64_t max_compaction_trigger_wakeup_seconds = 43200;
|
||||
|
||||
// EXPERIMENTAL
|
||||
// Enables and tunes preferred-phase scheduling of periodic (time-based)
|
||||
// compaction, which de-herds periodic_compaction_seconds compactions across a
|
||||
// fleet of similarly-configured DBs (a recurring "thundering herd" is
|
||||
// especially costly with remote compaction). Each (DB, column family) is
|
||||
// given a stable-but-well-spread preferred phase within
|
||||
// periodic_compaction_seconds and compactions are steered toward it. The
|
||||
// phase is derived from the DB ID -- stable across re-open and physical
|
||||
// cloning/migration (at least when write_dbid_to_manifest=true), but distinct
|
||||
// across DBs -- and each column family is spread quasi-uniformly around the
|
||||
// DB's base phase by a golden-ratio recurrence, so different CFs of one DB
|
||||
// get well-separated phases. No user tuning of the phase itself is needed.
|
||||
//
|
||||
// This value is the percent (0-100) of the gap between a file's unphased
|
||||
// trigger time and its preferred-phase time that is closed on each
|
||||
// periodic-compaction trigger, always by triggering *earlier* -- never later
|
||||
// than periodic_compaction_seconds, which remains a soft upper bound on data
|
||||
// age. Because each compaction re-stamps the file, the phase error then
|
||||
// decays geometrically toward the preferred phase over successive cycles.
|
||||
//
|
||||
// "Never later" holds in steady state. It can be exceeded only for files
|
||||
// already past due when phasing (re)anchors -- which it does on DB open, on
|
||||
// enabling phasing, and on any change to periodic_compaction_seconds. The
|
||||
// cases that matter are reopening a DB whose files aged out while it was
|
||||
// down (e.g. DC power-outage recovery) and turning the interval *down*
|
||||
// (which retroactively makes older files past due). Rather than compact that
|
||||
// whole cohort at once (a herd), phasing spreads it over the first quarter
|
||||
// of the interval after the anchor, on a stable per-(DB, CF) grid --
|
||||
// expedited but herd-avoiding -- so such a file may be rewritten up to a
|
||||
// quarter-interval past its (revised) deadline. To enforce the age limit
|
||||
// immediately at such a transition, trigger a manual compaction.
|
||||
//
|
||||
// 0 disables phasing entirely (exact legacy behavior; this is the kill
|
||||
// switch). 100 snaps to the preferred phase in a single cycle, which can
|
||||
// recompact some files well before their normal interval and is generally not
|
||||
// recommended. Values are clamped to [0, 100].
|
||||
//
|
||||
// Lower values converge more slowly but incur less extra compaction work,
|
||||
// especially under write-driven or bursty workloads that keep re-randomizing
|
||||
// a file's phase (where a high recovery rate wastefully "chases" the moving
|
||||
// phase). ~33 is a good balance of spreading speed vs. extra work: almost
|
||||
// always widely spread after a few periods while almost always below 5%
|
||||
// extra compaction work overall, trending toward 0% in the long term for
|
||||
// most workloads.
|
||||
//
|
||||
// This feature might become obsolete if a future feature is able to have
|
||||
// compaction pressure, whether local or remote, feed back into compaction
|
||||
// scheduling.
|
||||
//
|
||||
// Recommended setting: 33
|
||||
// Default: feature disabled
|
||||
//
|
||||
// Dynamically changeable through SetDBOptions() API.
|
||||
int periodic_compaction_phase_recovery_percent = 0;
|
||||
|
||||
// EXPERIMENTAL
|
||||
|
||||
// When a RocksDB database is opened in follower mode, this option
|
||||
@@ -2446,6 +2534,22 @@ struct ReadOptions {
|
||||
// Default: false
|
||||
bool auto_refresh_iterator_with_snapshot = false;
|
||||
|
||||
// EXPERIMENTAL
|
||||
//
|
||||
// Names which block-based-table index a read should use. This is a
|
||||
// preparatory API for replacing direct table_index_factory selection in a
|
||||
// follow-up change. Until that wiring lands, table_index_factory keeps its
|
||||
// existing behavior and read_index is not consulted by table reading.
|
||||
enum class ReadIndex : uint8_t {
|
||||
// Use the column family's default index routing.
|
||||
kDefault = 0,
|
||||
// Use the standard block-based-table index.
|
||||
kBuiltin = 1,
|
||||
// Prefer the configured custom index when present.
|
||||
kPreferCustom = 2,
|
||||
};
|
||||
ReadIndex read_index = ReadIndex::kDefault;
|
||||
|
||||
// EXPERIMENTAL
|
||||
//
|
||||
// Specify an alternate index to use in the SST files instead of the native
|
||||
@@ -3042,16 +3146,17 @@ struct IngestExternalFileArg {
|
||||
// to this range (not just the file ranges).
|
||||
// * Not compatible with ingest_behind=true.
|
||||
// * When options.snapshot_consistency = false, the range is cleared
|
||||
// similarly to DeleteFilesInRange, but fails if any files overlap the range
|
||||
// only partially.
|
||||
// * It is recommended to use fail_if_not_bottommost_level=true to ensure
|
||||
// data in the key range is ingested to a single compacted level (the
|
||||
// last level). (fail_if_not_bottommost_level=false allows overlap between
|
||||
// the ingested files.)
|
||||
// similarly to DeleteFilesInRange. With universal compaction and a table
|
||||
// factory that supports range deletion, a partial overlap is handled by
|
||||
// retaining the overlapping file and adding a range tombstone below the
|
||||
// ingested data. This requires allow_global_seqno=true,
|
||||
// allow_db_generated_files=false, and no user-defined timestamps. Other
|
||||
// configurations fail on partial overlap.
|
||||
// * fail_if_not_bottommost_level=true rejects partial overlap because the
|
||||
// retained file requires the tombstone and replacement files to be
|
||||
// placed on higher levels.
|
||||
// * options.snapshot_consistency = true is not yet supported.
|
||||
// BUG: the upper bound of the range may be interpreted as inclusive or
|
||||
// exclusive, so it is best not to depend on one or the other until it is
|
||||
// sorted out.
|
||||
// * The range is half-open: start is inclusive and limit is exclusive.
|
||||
std::optional<RangeOpt> atomic_replace_range;
|
||||
|
||||
// Optimizes for prepare performance, see `PreparedFileInfo` for details.
|
||||
|
||||
@@ -140,6 +140,7 @@ class OptSlice {
|
||||
/*implicit*/ OptSlice(const std::string& s) : slice_(s) {}
|
||||
/*implicit*/ OptSlice(const std::string_view& sv) : slice_(sv) {}
|
||||
/*implicit*/ OptSlice(const char* c_str) : slice_(c_str) {}
|
||||
OptSlice(const char* d, size_t n) : slice_(d, n) {}
|
||||
// For easier migrating from APIs uing Slice* as an optional type.
|
||||
// CAUTION: OptSlice{nullptr} is "no value" while Slice{nullptr} is "empty"
|
||||
/*implicit*/ OptSlice(std::nullptr_t) : OptSlice() {}
|
||||
|
||||
@@ -49,6 +49,28 @@ class SstPartitioner {
|
||||
// Return the name of this partitioner.
|
||||
virtual const char* Name() const = 0;
|
||||
|
||||
// Called for the start of an SST file or block. When return value is
|
||||
// present, it must be a prefix of user_key. Partitioning is requested
|
||||
// EXACTLY at the next future key that does not share that same prefix
|
||||
// with user_key. Implications for returned prefix p on recursive next
|
||||
// user key k:
|
||||
// * if k.size() < p.size(), partition is requested before k
|
||||
// * if k[0..p-1] != p, same
|
||||
// * if p.size() == 0, no further partitions are requested (odd 0-len prefix)
|
||||
// * if !p, ambiguous; must consult ShouldPartition on every key transition
|
||||
// Note that any prefix code (https://en.wikipedia.org/wiki/Prefix_code)
|
||||
// can be used with this optimization, not just fixed-size prefixes.
|
||||
//
|
||||
// Expected to be consistent with ShouldPartition for predictable
|
||||
// partitioning behavior.
|
||||
//
|
||||
// Implementing this function (built-in SstPartitionerFixedPrefix does) is
|
||||
// highly recommended for compaction CPU efficiency.
|
||||
virtual OptSlice ShouldPartitionByPrefix(const Slice& /*user_key*/) {
|
||||
// Default: must consult ShouldPartition()
|
||||
return {};
|
||||
}
|
||||
|
||||
// It is called for all keys in compaction. When partitioner want to create
|
||||
// new SST file it needs to return true. It means compaction job will finish
|
||||
// current SST file where last key is "prev_user_key" parameter and start new
|
||||
@@ -108,6 +130,8 @@ class SstPartitionerFixedPrefix : public SstPartitioner {
|
||||
|
||||
const char* Name() const override { return "SstPartitionerFixedPrefix"; }
|
||||
|
||||
OptSlice ShouldPartitionByPrefix(const Slice& user_key) override;
|
||||
|
||||
PartitionerResult ShouldPartition(const PartitionerRequest& request) override;
|
||||
|
||||
bool CanDoTrivialMove(const Slice& smallest_user_key,
|
||||
|
||||
@@ -599,6 +599,30 @@ struct BlockBasedTableOptions {
|
||||
// (CompressionOptions::parallel_threads sanitized to 1).
|
||||
std::shared_ptr<UserDefinedIndexFactory> user_defined_index_factory = nullptr;
|
||||
|
||||
// EXPERIMENTAL
|
||||
//
|
||||
// Names the intended relationship between the standard block-based-table
|
||||
// index and a custom index. This is a preparatory API for replacing the
|
||||
// legacy UDI booleans below in a follow-up change. Until that wiring lands,
|
||||
// use_udi_as_primary_index and fail_if_no_udi_on_open keep their existing
|
||||
// behavior and index_mode is not consulted by table building or reading.
|
||||
enum class IndexMode {
|
||||
// Build and read the standard index only.
|
||||
kStandardOnly = 0,
|
||||
// Build both indexes, read the standard index by default, and allow
|
||||
// per-read custom-index selection.
|
||||
kStandardDefault = 1,
|
||||
// Build both indexes and read the custom index by default.
|
||||
kCustomDefault = 2,
|
||||
// Build only the custom index. This requires a later format-safety change
|
||||
// before it can be used by table writing.
|
||||
kCustomOnly = 3,
|
||||
// Build both indexes, read the standard index by default, and require the
|
||||
// custom index block when a factory is configured.
|
||||
kStandardRequired = 4,
|
||||
};
|
||||
IndexMode index_mode = IndexMode::kStandardDefault;
|
||||
|
||||
// EXPERIMENTAL
|
||||
//
|
||||
// When true and user_defined_index_factory is set, the UDI becomes the
|
||||
@@ -729,6 +753,10 @@ struct BlockBasedTableOptions {
|
||||
// using a non-built-in CompatibilityName(). See `compression_manager` in
|
||||
// ColumnFamilyOptions. Also changes the format of TableProperties field
|
||||
// `compression_name`. Can be read by RocksDB versions >= 10.4.0.
|
||||
// 8 -- Support for optimize_key_common_prefix, cleaner support for some
|
||||
// features like super_block_alignment_size that impact index encoding, and
|
||||
// more future-proofing that might guard future opt-in features. Can be read
|
||||
// by RocksDB versions >= 11.10.0
|
||||
//
|
||||
// Using the default setting of format_version is strongly recommended, so
|
||||
// that available enhancements are adopted eventually and automatically. The
|
||||
@@ -763,6 +791,39 @@ struct BlockBasedTableOptions {
|
||||
// blocks in the future.
|
||||
double uniform_cv_threshold = -1;
|
||||
|
||||
// Controls the conditions under which the format_version >= 8 "common
|
||||
// user-key prefix" data-block optimization is enabled: the common prefix
|
||||
// shared by all keys in a data block is stored once at the start of the block
|
||||
// instead of at every restart point. This shrinks the block (and its
|
||||
// block-cache footprint) for prefix-heavy data and, for bytewise-ordered
|
||||
// keys, speeds up Seek by comparing prefix-stripped suffixes.
|
||||
//
|
||||
// Only takes effect at format_version >= 8, and requires use_delta_encoding
|
||||
// (it builds on delta encoding). When use_delta_encoding is false -- e.g. to
|
||||
// preserve zero-copy key pinning for ReadOptions::pin_data -- this is a
|
||||
// no-op.
|
||||
//
|
||||
// The Seek speedup ("fast seek") is only correct for the built-in bytewise
|
||||
// and reverse-bytewise comparators. Other comparators can still get the space
|
||||
// savings (the reader reconstructs full keys) at a small read-CPU cost.
|
||||
//
|
||||
// Expressed as a criteria so it need not be toggled per column family based
|
||||
// on the comparator (which can vary by CF).
|
||||
enum class OptimizeKeyCommonPrefix : char {
|
||||
// Never use the optimization.
|
||||
kDisabled = 0,
|
||||
// Use it only where it also enables the Seek speedup: format_version >= 8
|
||||
// with a (reverse-)bytewise comparator. Left off for other comparators.
|
||||
// This is the default.
|
||||
kIfFastSeek = 1,
|
||||
// Use it for all comparators at format_version >= 8. (Reverse-)bytewise get
|
||||
// space savings + fast seek; other comparators get the space savings only,
|
||||
// read via full-key reconstruction.
|
||||
kEnabled = 2,
|
||||
};
|
||||
OptimizeKeyCommonPrefix optimize_key_common_prefix =
|
||||
OptimizeKeyCommonPrefix::kIfFastSeek;
|
||||
|
||||
// Store index blocks on disk in compressed format. Changing this option to
|
||||
// false will avoid the overhead of decompression if index blocks are evicted
|
||||
// and read back
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// NOTE: in 'main' development branch, this should be the *next*
|
||||
// minor or major version number planned for release.
|
||||
#define ROCKSDB_MAJOR 11
|
||||
#define ROCKSDB_MINOR 9
|
||||
#define ROCKSDB_MINOR 10
|
||||
#define ROCKSDB_PATCH 0
|
||||
|
||||
// Make it easy to do conditional compilation based on version checks, i.e.
|
||||
|
||||
@@ -211,6 +211,7 @@ set(JAVA_MAIN_CLASSES
|
||||
src/main/java/org/rocksdb/OperationType.java
|
||||
src/main/java/org/rocksdb/OptimisticTransactionDB.java
|
||||
src/main/java/org/rocksdb/OptimisticTransactionOptions.java
|
||||
src/main/java/org/rocksdb/OptimizeKeyCommonPrefix.java
|
||||
src/main/java/org/rocksdb/OptionString.java
|
||||
src/main/java/org/rocksdb/Options.java
|
||||
src/main/java/org/rocksdb/OptionsUtil.java
|
||||
|
||||
+49
-1
@@ -7224,6 +7224,52 @@ class IndexSearchTypeJni {
|
||||
}
|
||||
};
|
||||
|
||||
// The portal class for org.rocksdb.OptimizeKeyCommonPrefix
|
||||
class OptimizeKeyCommonPrefixJni {
|
||||
public:
|
||||
// Returns the equivalent org.rocksdb.OptimizeKeyCommonPrefix for the provided
|
||||
// C++ ROCKSDB_NAMESPACE::BlockBasedTableOptions::OptimizeKeyCommonPrefix enum
|
||||
static jbyte toJavaOptimizeKeyCommonPrefix(
|
||||
const ROCKSDB_NAMESPACE::BlockBasedTableOptions::OptimizeKeyCommonPrefix&
|
||||
optimize_key_common_prefix) {
|
||||
switch (optimize_key_common_prefix) {
|
||||
case ROCKSDB_NAMESPACE::BlockBasedTableOptions::OptimizeKeyCommonPrefix::
|
||||
kDisabled:
|
||||
return 0x0;
|
||||
case ROCKSDB_NAMESPACE::BlockBasedTableOptions::OptimizeKeyCommonPrefix::
|
||||
kIfFastSeek:
|
||||
return 0x1;
|
||||
case ROCKSDB_NAMESPACE::BlockBasedTableOptions::OptimizeKeyCommonPrefix::
|
||||
kEnabled:
|
||||
return 0x2;
|
||||
default:
|
||||
return 0x7F; // undefined
|
||||
}
|
||||
}
|
||||
|
||||
// Returns the equivalent C++
|
||||
// ROCKSDB_NAMESPACE::BlockBasedTableOptions::OptimizeKeyCommonPrefix enum for
|
||||
// the provided Java org.rocksdb.OptimizeKeyCommonPrefix
|
||||
static ROCKSDB_NAMESPACE::BlockBasedTableOptions::OptimizeKeyCommonPrefix
|
||||
toCppOptimizeKeyCommonPrefix(jbyte joptimize_key_common_prefix) {
|
||||
switch (joptimize_key_common_prefix) {
|
||||
case 0x0:
|
||||
return ROCKSDB_NAMESPACE::BlockBasedTableOptions::
|
||||
OptimizeKeyCommonPrefix::kDisabled;
|
||||
case 0x1:
|
||||
return ROCKSDB_NAMESPACE::BlockBasedTableOptions::
|
||||
OptimizeKeyCommonPrefix::kIfFastSeek;
|
||||
case 0x2:
|
||||
return ROCKSDB_NAMESPACE::BlockBasedTableOptions::
|
||||
OptimizeKeyCommonPrefix::kEnabled;
|
||||
default:
|
||||
// undefined/default
|
||||
return ROCKSDB_NAMESPACE::BlockBasedTableOptions::
|
||||
OptimizeKeyCommonPrefix::kIfFastSeek;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// The portal class for org.rocksdb.ChecksumType
|
||||
class ChecksumTypeJni {
|
||||
public:
|
||||
@@ -9408,7 +9454,7 @@ class BlockBasedTableOptionsJni
|
||||
}
|
||||
|
||||
jmethodID method_id_init = env->GetMethodID(
|
||||
jclazz, "<init>", "(ZZZZBBDBZJIIIJZZZZZIIZDZZJJBBBJD)V");
|
||||
jclazz, "<init>", "(ZZZZBBDBZJIIIJZZZZZIIZDZZJJBBBBJD)V");
|
||||
if (method_id_init == nullptr) {
|
||||
// exception thrown: NoSuchMethodException or OutOfMemoryError
|
||||
return nullptr;
|
||||
@@ -9462,6 +9508,8 @@ class BlockBasedTableOptionsJni
|
||||
table_factory_options->index_shortening),
|
||||
IndexSearchTypeJni::toJavaIndexSearchType(
|
||||
table_factory_options->index_block_search_type),
|
||||
OptimizeKeyCommonPrefixJni::toJavaOptimizeKeyCommonPrefix(
|
||||
table_factory_options->optimize_key_common_prefix),
|
||||
FilterPolicyJni::toJavaIndexType(filter_policy_type),
|
||||
filter_policy_handle, filter_policy_config_value);
|
||||
if (env->ExceptionCheck()) {
|
||||
|
||||
@@ -45,7 +45,7 @@ jlong Java_org_rocksdb_PlainTableConfig_newTableFactoryHandle(
|
||||
/*
|
||||
* Class: org_rocksdb_BlockBasedTableConfig
|
||||
* Method: newTableFactoryHandle
|
||||
* Signature: (ZZZZBBDBZJJJIIIJZZZJZZIIZZJJBBJI)J
|
||||
* Signature: (ZZZZBBDBZJJJIIIJZZZJZZIIZDZZJJBBBJI)J
|
||||
*/
|
||||
jlong Java_org_rocksdb_BlockBasedTableConfig_newTableFactoryHandle(
|
||||
JNIEnv*, jclass, jboolean jcache_index_and_filter_blocks,
|
||||
@@ -66,8 +66,8 @@ jlong Java_org_rocksdb_BlockBasedTableConfig_newTableFactoryHandle(
|
||||
jboolean jenable_index_compression, jboolean jblock_align,
|
||||
jlong jsuper_block_alignment_size,
|
||||
jlong jsuper_block_alignment_space_overhead_ratio, jbyte jindex_shortening,
|
||||
jbyte jindex_search_type, jlong jblock_cache_size,
|
||||
jint jblock_cache_num_shard_bits) {
|
||||
jbyte jindex_search_type, jbyte joptimize_key_common_prefix,
|
||||
jlong jblock_cache_size, jint jblock_cache_num_shard_bits) {
|
||||
ROCKSDB_NAMESPACE::BlockBasedTableOptions options;
|
||||
options.cache_index_and_filter_blocks =
|
||||
static_cast<bool>(jcache_index_and_filter_blocks);
|
||||
@@ -152,6 +152,9 @@ jlong Java_org_rocksdb_BlockBasedTableConfig_newTableFactoryHandle(
|
||||
options.index_block_search_type =
|
||||
ROCKSDB_NAMESPACE::IndexSearchTypeJni::toCppIndexSearchType(
|
||||
jindex_search_type);
|
||||
options.optimize_key_common_prefix =
|
||||
ROCKSDB_NAMESPACE::OptimizeKeyCommonPrefixJni::
|
||||
toCppOptimizeKeyCommonPrefix(joptimize_key_common_prefix);
|
||||
|
||||
return GET_CPLUSPLUS_POINTER(
|
||||
ROCKSDB_NAMESPACE::NewBlockBasedTableFactory(options));
|
||||
|
||||
@@ -46,6 +46,7 @@ public class BlockBasedTableConfig extends TableFormatConfig {
|
||||
superBlockAlignmentSpaceOverheadRatio = 128;
|
||||
indexShortening = IndexShorteningMode.kShortenSeparators;
|
||||
indexSearchType = IndexSearchType.kBinary;
|
||||
optimizeKeyCommonPrefix = OptimizeKeyCommonPrefix.kIfFastSeek;
|
||||
|
||||
// NOTE: ONLY used if blockCache == null
|
||||
blockCacheSize = 8 * 1024 * 1024;
|
||||
@@ -68,8 +69,9 @@ public class BlockBasedTableConfig extends TableFormatConfig {
|
||||
final boolean separateKeyValueInDataBlock, final double uniformCvThreshold,
|
||||
final boolean enableIndexCompression, final boolean blockAlign,
|
||||
final long superBlockAlignmentSize, final long superBlockAlignmentSpaceOverheadRatio,
|
||||
final byte indexShortening, final byte indexSearchType, final byte filterPolicyType,
|
||||
final long filterPolicyHandle, final double filterPolicyConfigValue) {
|
||||
final byte indexShortening, final byte indexSearchType, final byte optimizeKeyCommonPrefix,
|
||||
final byte filterPolicyType, final long filterPolicyHandle,
|
||||
final double filterPolicyConfigValue) {
|
||||
this.cacheIndexAndFilterBlocks = cacheIndexAndFilterBlocks;
|
||||
this.cacheIndexAndFilterBlocksWithHighPriority = cacheIndexAndFilterBlocksWithHighPriority;
|
||||
this.pinL0FilterAndIndexBlocksInCache = pinL0FilterAndIndexBlocksInCache;
|
||||
@@ -99,6 +101,7 @@ public class BlockBasedTableConfig extends TableFormatConfig {
|
||||
this.superBlockAlignmentSpaceOverheadRatio = superBlockAlignmentSpaceOverheadRatio;
|
||||
this.indexShortening = IndexShorteningMode.values()[indexShortening];
|
||||
this.indexSearchType = IndexSearchType.values()[indexSearchType];
|
||||
this.optimizeKeyCommonPrefix = OptimizeKeyCommonPrefix.values()[optimizeKeyCommonPrefix];
|
||||
try (Filter filterPolicy = FilterPolicyType.values()[filterPolicyType].createFilter(
|
||||
filterPolicyHandle, filterPolicyConfigValue)) {
|
||||
if (filterPolicy != null) {
|
||||
@@ -956,6 +959,29 @@ public class BlockBasedTableConfig extends TableFormatConfig {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the criteria controlling the {@code format_version} 8+ common
|
||||
* user-key prefix block optimization.
|
||||
*
|
||||
* @return the currently set {@link OptimizeKeyCommonPrefix} criteria
|
||||
*/
|
||||
public OptimizeKeyCommonPrefix optimizeKeyCommonPrefix() {
|
||||
return optimizeKeyCommonPrefix;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the criteria controlling the {@code format_version} 8+ common
|
||||
* user-key prefix block optimization. See {@link OptimizeKeyCommonPrefix}.
|
||||
*
|
||||
* @param optimizeKeyCommonPrefix {@link org.rocksdb.OptimizeKeyCommonPrefix} value
|
||||
* @return the reference to the current option.
|
||||
*/
|
||||
public BlockBasedTableConfig setOptimizeKeyCommonPrefix(
|
||||
final OptimizeKeyCommonPrefix optimizeKeyCommonPrefix) {
|
||||
this.optimizeKeyCommonPrefix = optimizeKeyCommonPrefix;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the size of the cache in bytes that will be used by RocksDB.
|
||||
*
|
||||
@@ -1082,7 +1108,8 @@ public class BlockBasedTableConfig extends TableFormatConfig {
|
||||
readAmpBytesPerBit, formatVersion, separateKeyValueInDataBlock, uniformCvThreshold,
|
||||
enableIndexCompression, blockAlign, superBlockAlignmentSize,
|
||||
superBlockAlignmentSpaceOverheadRatio, indexShortening.getValue(),
|
||||
indexSearchType.getValue(), blockCacheSize, blockCacheNumShardBits);
|
||||
indexSearchType.getValue(), optimizeKeyCommonPrefix.getValue(), blockCacheSize,
|
||||
blockCacheNumShardBits);
|
||||
}
|
||||
|
||||
private static native long newTableFactoryHandle(final boolean cacheIndexAndFilterBlocks,
|
||||
@@ -1100,7 +1127,7 @@ public class BlockBasedTableConfig extends TableFormatConfig {
|
||||
final boolean separateKeyValueInDataBlock, final double uniformCvThreshold,
|
||||
final boolean enableIndexCompression, final boolean blockAlign,
|
||||
final long superBlockAlignmentSize, final long superBlockAlignmentSpaceOverheadRatio,
|
||||
final byte indexShortening, final byte indexSearchType,
|
||||
final byte indexShortening, final byte indexSearchType, final byte optimizeKeyCommonPrefix,
|
||||
|
||||
@Deprecated final long blockCacheSize, @Deprecated final int blockCacheNumShardBits);
|
||||
|
||||
@@ -1137,6 +1164,7 @@ public class BlockBasedTableConfig extends TableFormatConfig {
|
||||
private long superBlockAlignmentSpaceOverheadRatio;
|
||||
private IndexShorteningMode indexShortening;
|
||||
private IndexSearchType indexSearchType;
|
||||
private OptimizeKeyCommonPrefix optimizeKeyCommonPrefix;
|
||||
|
||||
// NOTE: ONLY used if blockCache == null
|
||||
@Deprecated private long blockCacheSize;
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// 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).
|
||||
|
||||
package org.rocksdb;
|
||||
|
||||
/**
|
||||
* Criteria controlling the {@code format_version} 8+ "common user-key prefix"
|
||||
* block optimization used with a {@link BlockBasedTableConfig}. When enabled,
|
||||
* the common user-key prefix shared by all keys in a data or index block is
|
||||
* stored once at the start of the block instead of at every restart point,
|
||||
* shrinking prefix-heavy blocks (and their block-cache footprint) and, for
|
||||
* bytewise-ordered keys, speeding up Seek by comparing prefix-stripped
|
||||
* suffixes.
|
||||
*
|
||||
* <p>Only takes effect at {@code format_version >= 8} and requires delta
|
||||
* encoding ({@link BlockBasedTableConfig#useDeltaEncoding()}); otherwise it is
|
||||
* a no-op.</p>
|
||||
*/
|
||||
public enum OptimizeKeyCommonPrefix {
|
||||
/**
|
||||
* Never use the optimization.
|
||||
*/
|
||||
kDisabled((byte) 0x0),
|
||||
|
||||
/**
|
||||
* Use it only where it also enables the Seek speedup: {@code format_version
|
||||
* >= 8} with a built-in (reverse-)bytewise comparator. This is the default.
|
||||
*/
|
||||
kIfFastSeek((byte) 0x1),
|
||||
|
||||
/**
|
||||
* Use it for all comparators at {@code format_version >= 8}. (Reverse-)bytewise
|
||||
* comparators get the space savings plus the Seek speedup; other comparators
|
||||
* get the space savings only (the reader reconstructs full keys).
|
||||
*/
|
||||
kEnabled((byte) 0x2);
|
||||
|
||||
private final byte value;
|
||||
|
||||
OptimizeKeyCommonPrefix(final byte value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the byte value of the enumerations value.
|
||||
*
|
||||
* @return byte representation
|
||||
*/
|
||||
byte getValue() {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
@@ -433,6 +433,21 @@ public class BlockBasedTableConfigTest {
|
||||
assertThat(blockBasedTableConfig.indexSearchType()).isEqualTo(IndexSearchType.kAuto);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void optimizeKeyCommonPrefix() {
|
||||
final BlockBasedTableConfig blockBasedTableConfig = new BlockBasedTableConfig();
|
||||
assertThat(OptimizeKeyCommonPrefix.values().length).isEqualTo(3);
|
||||
// Default is kIfFastSeek.
|
||||
assertThat(blockBasedTableConfig.optimizeKeyCommonPrefix())
|
||||
.isEqualTo(OptimizeKeyCommonPrefix.kIfFastSeek);
|
||||
blockBasedTableConfig.setOptimizeKeyCommonPrefix(OptimizeKeyCommonPrefix.kDisabled);
|
||||
assertThat(blockBasedTableConfig.optimizeKeyCommonPrefix())
|
||||
.isEqualTo(OptimizeKeyCommonPrefix.kDisabled);
|
||||
blockBasedTableConfig.setOptimizeKeyCommonPrefix(OptimizeKeyCommonPrefix.kEnabled);
|
||||
assertThat(blockBasedTableConfig.optimizeKeyCommonPrefix())
|
||||
.isEqualTo(OptimizeKeyCommonPrefix.kEnabled);
|
||||
}
|
||||
|
||||
@Deprecated
|
||||
@Test
|
||||
public void hashIndexAllowCollision() {
|
||||
|
||||
@@ -1055,8 +1055,12 @@ static void DataBlockSeek(benchmark::State& state) {
|
||||
Random rnd(301);
|
||||
Options options = Options();
|
||||
|
||||
BlockBuilder builder(16, true, false,
|
||||
BlockBasedTableOptions::kDataBlockBinarySearch);
|
||||
BlockBuilder builder(
|
||||
16, true, false, BlockBasedTableOptions::kDataBlockBinarySearch,
|
||||
0.75 /* data_block_hash_table_util_ratio */, 0 /* ts_sz */,
|
||||
true /* persist_user_defined_timestamps */, false /* is_user_key */,
|
||||
false /* use_separated_kv_storage */, nullptr /* statistics */,
|
||||
-1.0 /* uniform_cv_threshold */, false /* use_common_prefix */);
|
||||
|
||||
int num_records = 500;
|
||||
std::vector<std::string> keys;
|
||||
|
||||
+30
-3
@@ -134,6 +134,22 @@ static Status ParseCompressionOptions(const std::string& value,
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
static Status CloneTableFactoryForOptionsUpdate(
|
||||
TableFactory* table_factory,
|
||||
std::shared_ptr<TableFactory>* cloned_factory) {
|
||||
assert(table_factory != nullptr);
|
||||
assert(cloned_factory != nullptr);
|
||||
|
||||
std::unique_ptr<TableFactory> clone = table_factory->Clone();
|
||||
if (clone == nullptr) {
|
||||
return Status::InvalidArgument(
|
||||
"Table factory does not support option updates: " +
|
||||
std::string(table_factory->Name()));
|
||||
}
|
||||
*cloned_factory = std::move(clone);
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
static Status TableFactoryParseFn(const ConfigOptions& opts,
|
||||
const std::string& name,
|
||||
const std::string& value, void* addr) {
|
||||
@@ -170,7 +186,11 @@ static Status TableFactoryParseFn(const ConfigOptions& opts,
|
||||
if (table_factory->get() != nullptr) {
|
||||
std::string factory_name = table_factory->get()->Name();
|
||||
if (factory_name == TableFactory::kBlockBasedTableName()) {
|
||||
new_factory = table_factory->get()->Clone();
|
||||
s = CloneTableFactoryForOptionsUpdate(table_factory->get(),
|
||||
&new_factory);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
} else {
|
||||
s = Status::InvalidArgument("Cannot modify " + factory_name + " as " +
|
||||
name);
|
||||
@@ -185,7 +205,11 @@ static Status TableFactoryParseFn(const ConfigOptions& opts,
|
||||
if (table_factory->get() != nullptr) {
|
||||
std::string factory_name = table_factory->get()->Name();
|
||||
if (factory_name == TableFactory::kPlainTableName()) {
|
||||
new_factory = table_factory->get()->Clone();
|
||||
s = CloneTableFactoryForOptionsUpdate(table_factory->get(),
|
||||
&new_factory);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
} else {
|
||||
s = Status::InvalidArgument("Cannot modify " + factory_name + " as " +
|
||||
name);
|
||||
@@ -204,7 +228,10 @@ static Status TableFactoryParseFn(const ConfigOptions& opts,
|
||||
s = TableFactory::CreateFromString(opts, value, &new_factory);
|
||||
}
|
||||
} else if (table_factory->get() != nullptr) {
|
||||
new_factory = table_factory->get()->Clone();
|
||||
s = CloneTableFactoryForOptionsUpdate(table_factory->get(), &new_factory);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
// Presumably passing a value for a specific field of the table factory
|
||||
s = new_factory->ConfigureOption(opts, name, value);
|
||||
} else {
|
||||
|
||||
Loaded 100 of 173 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user