mirror of
https://github.com/facebook/rocksdb.git
synced 2026-11-08 03:52:33 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ec93377878 | ||
|
|
0e1c7f9e62 | ||
|
|
c12d364ad1 | ||
|
|
766ca8f985 | ||
|
|
3c8f8f417d | ||
|
|
507206e46b | ||
|
|
3c30d43baf | ||
|
|
96e7ef0269 | ||
|
|
5d6f321b70 | ||
|
|
256a699be3 | ||
|
|
7530287bde | ||
|
|
b03a058d61 | ||
|
|
8108b49e0c | ||
|
|
dd54c5abc9 | ||
|
|
7d721f398b | ||
|
|
bcf8711165 | ||
|
|
34d5074bfa | ||
|
|
79b58b0dd6 | ||
|
|
613a642b5c | ||
|
|
e231c1826c | ||
|
|
1419ae41b1 | ||
|
|
db114777b1 | ||
|
|
7a90dc5ba8 | ||
|
|
a4e298b8c4 | ||
|
|
576dc39899 | ||
|
|
4c2a977aa5 | ||
|
|
88a8fdf849 | ||
|
|
a844cbf5b7 | ||
|
|
3d8e4be2f5 | ||
|
|
7d3150ffce | ||
|
|
91b96f41cf | ||
|
|
dfde6a60b0 | ||
|
|
bcda8c4b1a | ||
|
|
37234200b5 | ||
|
|
ac0f4ae69f | ||
|
|
bec9dcec7c | ||
|
|
897162a6bc | ||
|
|
1ca38e1442 | ||
|
|
da3a0961fa | ||
|
|
96ed085402 | ||
|
|
2c1c7ea2f1 | ||
|
|
fe6406f7b2 | ||
|
|
0df7ed563b | ||
|
|
068aeda9d8 | ||
|
|
a0966fd94e | ||
|
|
fe5123681e | ||
|
|
f9c117e203 | ||
|
|
82dfd6a510 | ||
|
|
80a5a6e4b2 |
No files matched your search
@@ -206,6 +206,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"table/block_based/block_cache.cc",
|
||||
"table/block_based/block_prefetcher.cc",
|
||||
"table/block_based/block_prefix_index.cc",
|
||||
"table/block_based/builtin_index_factory.cc",
|
||||
"table/block_based/data_block_footer.cc",
|
||||
"table/block_based/data_block_hash_index.cc",
|
||||
"table/block_based/filter_block_reader_common.cc",
|
||||
@@ -4614,6 +4615,12 @@ cpp_unittest_wrapper(name="bloom_test",
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="builtin_index_factory_test",
|
||||
srcs=["table/block_based/builtin_index_factory_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="cache_reservation_manager_test",
|
||||
srcs=["cache/cache_reservation_manager_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
|
||||
@@ -59,6 +59,8 @@ Specifically:
|
||||
|
||||
**Backwards Compatibility:** RocksDB maintains strong backwards compatibility guarantees. Breaking changes are rare and require extensive justification. When deprecating features, follow the project's deprecation policy (typically spanning multiple releases).
|
||||
|
||||
**Behavioral Compatibility:** Behavior changes are human judgment calls that must be intentional, well-documented (including release notes), and well researched for potential downstream production impact. Agents and reviewers must call out potential gaps, ambiguity, or adverse impacts in intentionality, documentation, and production impact. For an existing accepted input—including an undocumented, discouraged, or historically ignored value—the analysis should compare old and new observable behavior, including performance and resource impact, identify known direct callers and stored configurations, and add or update regression coverage as appropriate.
|
||||
|
||||
**API Consistency:** New APIs should be consistent with existing patterns. Use similar naming conventions, parameter ordering, and return types. Reviewers will suggest changes to improve consistency with the broader codebase.
|
||||
|
||||
**Documentation:** Public APIs must be thoroughly documented, without unnecessary embelishment nor dwelling on implementation details nor project planning. When non-obvious, include usage examples, parameter descriptions, known bugs or limitations, notes on thread safety, performance characteristics, and compatibility considerations. Re-read comments for ambiguous terminology and phrasing, such as ambiguously re-purposed programming jargon.
|
||||
@@ -308,6 +310,7 @@ build (and where noted, run tests) across:
|
||||
| Axis | Must support |
|
||||
|------|--------------|
|
||||
| OS | Linux (x86_64 + ARM), macOS, Windows |
|
||||
| Architecture | Little-endian and big-endian targets, including Linux on s390x |
|
||||
| Compiler | GCC, Clang (libstdc++ **and** libc++), AppleClang, **MSVC (VS2022)**, MinGW (Linux cross-compile, build-only, no gflags) |
|
||||
| Build system | Make, CMake, and BUCK (internal) -- keep all in sync (see "Build system" above) |
|
||||
| Config | release (`-fno-rtti`), `ASSERT_STATUS_CHECKED`, ASAN/UBSAN/TSAN, folly, unity build, JNI/Java |
|
||||
@@ -322,6 +325,12 @@ abstractions; otherwise guard with `#ifdef OS_WIN` (POSIX `<unistd.h>` ->
|
||||
Windows `<process.h>`). Because libc++ is also tested, include what you use
|
||||
rather than relying on libstdc++ transitive includes.
|
||||
|
||||
Do not use `static_assert` to reject a supported architecture for an optional
|
||||
feature. Check an existing `port::` capability such as `port::kLittleEndian`
|
||||
and disable only the unsupported feature so the rest of RocksDB still builds.
|
||||
Use a portable representation when the feature itself must work across
|
||||
architectures.
|
||||
|
||||
### Unit Test
|
||||
* After all of the unit tests are added, review them and try to extract common
|
||||
reusable utility functions to reduce code duplication due to copy past between
|
||||
@@ -372,6 +381,9 @@ rather than relying on libstdc++ transitive includes.
|
||||
### Adding new public API
|
||||
Refer to claude_md/add_public_api.md
|
||||
|
||||
### Changing existing public API behavior
|
||||
Follow the Behavioral Compatibility requirements above.
|
||||
|
||||
### Adding new option
|
||||
Refer to claude_md/add_option.md
|
||||
|
||||
@@ -401,7 +413,7 @@ rather than relying on libstdc++ transitive includes.
|
||||
* Release note should be kept short at high level for external user consumption.
|
||||
Release notes identify what users might care about most in a release. They
|
||||
are not exhaustive and are not a guide. PLEASE learn from past agents who
|
||||
ried to build elaborate release notes with implementation details and
|
||||
tried to build elaborate release notes with implementation details and
|
||||
elsewhere-documented nuance. That wastes time. Fight the bias that
|
||||
"my change" is important so must be worthy of release note mention.
|
||||
* If more than single markdown line, consider how their formatting will be
|
||||
|
||||
@@ -938,6 +938,7 @@ set(SOURCES
|
||||
table/block_based/block_cache.cc
|
||||
table/block_based/block_prefetcher.cc
|
||||
table/block_based/block_prefix_index.cc
|
||||
table/block_based/builtin_index_factory.cc
|
||||
table/block_based/data_block_hash_index.cc
|
||||
table/block_based/data_block_footer.cc
|
||||
table/block_based/filter_block_reader_common.cc
|
||||
@@ -1595,6 +1596,7 @@ if(WITH_TESTS)
|
||||
options/options_test.cc
|
||||
table/block_based/block_based_table_reader_test.cc
|
||||
table/block_based/block_test.cc
|
||||
table/block_based/builtin_index_factory_test.cc
|
||||
table/block_based/data_block_hash_index_test.cc
|
||||
table/block_based/full_filter_block_test.cc
|
||||
table/block_based/partitioned_filter_block_test.cc
|
||||
|
||||
+59
@@ -1,6 +1,65 @@
|
||||
# Rocksdb Change Log
|
||||
> NOTE: Entries for next release do not go here. Follow instructions in `unreleased_history/README.txt`
|
||||
|
||||
## 11.11.1 (09/24/2026)
|
||||
### Behavior Changes
|
||||
* `Iterator::Refresh()` now returns `NotSupported` on iterators from a secondary DB's `DB::NewIterators()` only, because those iterators are pinned to one cross-column-family read view. Iterators from `DB::NewIterator()` are unaffected; to advance a batch, call `DB::NewIterators()` again after `TryCatchUpWithPrimary()`.
|
||||
|
||||
### Bug Fixes
|
||||
* Fixed secondary DB `NewIterators()` returning iterators from different database states or blocking for `TryCatchUpWithPrimary()` by serving the last completed catch-up view.
|
||||
|
||||
## 11.11.0 (09/21/2026)
|
||||
### New Features
|
||||
* External tables now support DB-assigned global sequence numbers when `IngestExternalFileOptions::allow_global_seqno` is enabled, allowing overlapping external tables to be ingested across multiple levels when compactions are disabled. This includes the deprecated `write_global_seqno` path when the reader supplies the properties block's file offset.
|
||||
* Added checksum handoff support for SST file writes when `BlockBasedTableOptions::checksum` is `kCRC32c`, allowing block and trailer CRC32C checksums to be passed from the table builder to the file writer for buffered-data validation and filesystem in-flight write protection.
|
||||
* Enhanced `sst_dump --command=recompress` to be more powerful for benchmarking, including through automation with a new `--json` option for machine-readable output.
|
||||
|
||||
### Public API Changes
|
||||
* Changed the experimental async read statistics API by removing `DB::AsyncCallback::EnableStats()` and changing `OnComplete(const PerfContext*, const IOStatsContext*)` to `OnComplete()`. Callers must configure TLS statistics before each async or coroutine read. `DB::GetAsync()` and `DB::MultiGetAsync()` callbacks must copy per-operation statistics from TLS during `OnComplete()`; coroutine users must copy them from inside a `CoroStackableDB` hook before suspending again. Each operation leaves TLS statistics collection disabled.
|
||||
* Added `Compressor::MaybeOverrideCompressionOptions()` (EXPERIMENTAL), an omnibus hook letting a custom compressor adjust the subset of `CompressionOptions` that the SST builder applies rather than the compressor itself: `parallel_threads`, `max_compressed_bytes_per_kb`, `auto_skip`, and `auto_skip_min_sample_every` (and potentially more in the future). This deprecates `Compressor::GetRecommendedParallelThreads()`, which it subsumes; the default implementation still honors an overridden `GetRecommendedParallelThreads()` for compatibility.
|
||||
* Added experimental `CoroDB::CoGet()` and `CoroDB::CoMultiGet()` overloads for point reads from an `SstFileReader`.
|
||||
* Added experimental `DB::GetWithMetadata()` and `DB::MultiGetWithMetadata()` APIs with opt-in output metadata for key timestamps and explicit-snapshot newer-version detection.
|
||||
* Added a `DB::NewIterators()` overload that accepts parallel vectors of `ReadOptions` and column family handles, allowing each returned iterator to use independent read settings while sharing a consistent database state. The vectors must have the same size, all snapshots must be null or identical, and all `tailing` settings must match.
|
||||
* Added `ReadOptions::custom_context`, an opaque, non-owning pointer passed through table reads so external table implementations can consume application-defined read options.
|
||||
|
||||
### Behavior Changes
|
||||
* `ExternalTableReader::MultiGet()` is now used by RocksDB's `DB::MultiGet()` path. Previously, the external table adapter used `TableReader`'s fallback, which called `ExternalTableReader::Get()` separately for each key.
|
||||
* `DB::OpenAndCompact()` now requires `output_directory` to be a simple name for a new subdirectory when the new `use_session_tmp_dir_for_remote_compaction` DB option is enabled. The option places that subdirectory under the canonical `<dbname>/session_tmp/` root, and `DB::Open()` deletes stale contents under the root after recovery. The disabled default preserves the existing `<dbname>/<output_directory>` layout without automatic cleanup. Previously, remote-compaction output directories were created directly under `<dbname>`, which made it challenging to safely identify and delete obsolete files.
|
||||
* Several block-based SST table properties now better support compression benchmarking and optimization: `uncompressed_data_size` is now persisted in the file (previously it was computed only at write time and read back as 0); a new `lsm_info_at_creation` property records the LSM level and "bottommost" status at the file's creation, distinguishing "not applicable" (e.g. SstFileWriter files) from "unknown" (older files); and the `compression_options` property now records the full compression manager configuration instead of only its name.
|
||||
|
||||
### Bug Fixes
|
||||
* Fixed a usually-minor performance bug where `Cache::ApplyToAllEntries()` used single-entry batches instead of the intended 256-entry default for `ApplyToAllEntriesOptions::average_entries_per_lock`. All of the Cache implementations (LRU and HCC) benefit from the fix, but the disfavored LRU cache was especially impacted by repeated mutex acquisition and release.
|
||||
* Fixed stats-disabled coroutine and asynchronous reads inheriting enabled TLS statistics state when interleaved with stats-enabled reads on the same executor. Each read now uses its captured per-call configuration, while enabled request counters remain isolated across suspensions.
|
||||
* Fixed a bug where a remote compaction (`CompactionService` / `DB::OpenAndCompact`) requested with `OpenAndCompactOptions::allow_resumption=true` but for which resumption was internally disabled (incompatible with `paranoid_file_checks` or output-iteration verification) could fail a retried attempt, because the output directory was not cleaned before starting fresh and output files left by a previously interrupted attempt collided with the reused file numbers.
|
||||
* Fixed a bug where a remote compaction (`CompactionService` / `DB::OpenAndCompact`) could fail when the primary rotated its MANIFEST while the worker was opening the primary's directory read-only. The open reads `CURRENT`, and the primary publishes a new MANIFEST by renaming a freshly written `CURRENT` over the old one; on file systems without POSIX read-after-unlink semantics (remote/object stores) the replaced object is then reported as gone (`PathNotFound`/`NotFound`, or `TryAgain` for the MANIFEST equivalent) instead of remaining readable. `DB::OpenAndCompact()` now retries the secondary open a bounded, configurable number of times for those transient statuses, so a concurrent rotation no longer fails the compaction (which, for a `CompactionService` that does not fall back to a local compaction, became a background error on the primary).
|
||||
* Fixed secondary DB `NewIterators()` returning iterators from different database states when `TryCatchUpWithPrimary()` runs concurrently.
|
||||
* Fixed a bug where a `SeekToFirst()` on an iterator previously used for a `Seek()` could end the scan early and silently omit keys with `ReadOptions::async_io`, or otherwise run with readahead effectively disabled. Affects scans using `ReadOptions::prefix_same_as_start` together with `ReadOptions::auto_readahead_size` (enabled by default); memory-mapped reads are unaffected.
|
||||
|
||||
### Performance Improvements
|
||||
* `DB::MultiGetEntityLazy()` now reads all keys of a batch under a single shared per-column-family SuperVersion pin, and a single `LazyWideColumnsBatch::MultiResolve()` coalesces the resulting blob reads across keys: one read per blob file (and one per SST for embedded blobs), for whole-column and byte-range reads alike, instead of reading each key's blobs separately.
|
||||
|
||||
## 11.10.0 (08/28/2026)
|
||||
### New Features
|
||||
* Added `BlockBasedTableOptions::optimize_key_common_prefix` as part of new `format_version=8`. When enabled, each data block and index block stores the common user-key prefix of its keys once instead of at every restart point, reducing block size and block-cache footprint for prefix-heavy data and speeding up Seek for bytewise-ordered keys.
|
||||
* A new experimental feature spreads out periodic (time-based) compaction trigger times across a fleet of similarly-configured DBs to avoid recurring "thundering herd" triggers, especially with remote compaction. See the new experimental DB option `periodic_compaction_phase_recovery_percent`.
|
||||
|
||||
### Public API Changes
|
||||
* `IngestExternalFileArg::atomic_replace_range` now supports a range that only partially overlaps existing files, instead of failing. Requires universal compaction and a table factory that supports range deletion; other configurations still fail on partial overlap. The range is also now specified as half-open (`start` inclusive, `limit` exclusive), resolving a previously documented ambiguity in the upper bound.
|
||||
* Added an immutable `external_table_config` option that is persisted in OPTIONS files and passed to external table factories during bootstrap.
|
||||
|
||||
### Bug Fixes
|
||||
* Fixed a liveness bug where RocksDB could remain indefinitely in recovery from a soft background error when concurrent writes continuously generated normal flush requests.
|
||||
* Fixed a race in online error recovery where a concurrent higher-severity I/O error could leave a stale retry-flush plan active, prevent the required WAL switch, and create duplicate internal WAL history detected on DB reopen.
|
||||
* Fix "bottommost files" infinite compaction loops (`kBottommostFiles`). This happens with the unsupported combination of leveled compaction and `preserve_internal_time_seconds` / `preclude_last_level_data_seconds`, and in an edge case with user-defined timestamps when `full_history_ts_low` is unset. The underlying logic is now more unified to reduce risk of a similar divergence in logic in the future.
|
||||
* Fixed `reuse_manifest_on_open` to create a fresh MANIFEST instead of appending after recovery sees an incomplete atomic group at the MANIFEST tail.
|
||||
* Starting remote compaction (`CompactionService` / `DB::OpenAndCompact`) is now more efficient and more reliable, by avoiding unnecessary file operations (latency) while using more accurate consistency / precondition checks, both avoiding some spurious failures and rejecting more legitimate inconsistencies. New DBOption `remote_compaction_manifest_floor` can be disabled in case of unexpected problems with the change.
|
||||
* Fixed unbounded retention of recovered transaction state by a long-lived secondary opened with `allow_2pc`.
|
||||
* Fixed stale reads on secondary instances, where entries replayed from a WAL that the primary had already flushed could shadow the newer values in the flushed files indefinitely. Also fixed a case where a secondary could stop returning such a key altogether while the primary's flushed file holding it was unreadable.
|
||||
* Fixed subset checkpoints to discard tolerated MANIFEST tail corruption before appending column-family drop records.
|
||||
|
||||
### Performance Improvements
|
||||
* Improved compaction CPU efficiency for column families using an `SstPartitioner` (such as the built-in `SstPartitionerFixedPrefix`): when the partitioner can express its next boundary as a key prefix via the new `SstPartitioner::ShouldPartitionByPrefix()` API, compaction now checks that cached prefix inline instead of copying every user key and making a virtual `ShouldPartition()` call for each key-value written to an SST.
|
||||
|
||||
## 11.9.0 (08/14/2026)
|
||||
### New Features
|
||||
* Added an integrated and improved compression auto-skip feature (new CompressionOptions, still experimental) that saves CPU by skipping compression when and where the overall ratio is inadequate. This improves on the previous auto-skip compression manager by (a) taking achieved compression ratios into account, (b) using a continuously evolving payoff estimate, and (c) combining natively with parallel compression.
|
||||
|
||||
@@ -2018,6 +2018,9 @@ block_fetcher_test: table/block_fetcher_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
block_test: $(OBJ_DIR)/table/block_based/block_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
builtin_index_factory_test: $(OBJ_DIR)/table/block_based/builtin_index_factory_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
data_block_hash_index_test: $(OBJ_DIR)/table/block_based/data_block_hash_index_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
|
||||
@@ -212,6 +212,11 @@ uint64_t rocksdb_table_properties_separate_key_value_in_data_block(
|
||||
return props->rep.separate_key_value_in_data_block;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_table_properties_lsm_info_at_creation(
|
||||
const rocksdb_table_properties_t* props) {
|
||||
return props->rep.lsm_info_at_creation;
|
||||
}
|
||||
|
||||
const char* rocksdb_table_properties_db_id(
|
||||
const rocksdb_table_properties_t* props, size_t* size) {
|
||||
*size = props->rep.db_id.size();
|
||||
|
||||
@@ -151,6 +151,10 @@ extern ROCKSDB_LIBRARY_API uint64_t
|
||||
rocksdb_table_properties_separate_key_value_in_data_block(
|
||||
const rocksdb_table_properties_t* props);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API uint64_t
|
||||
rocksdb_table_properties_lsm_info_at_creation(
|
||||
const rocksdb_table_properties_t* props);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API const char* rocksdb_table_properties_db_id(
|
||||
const rocksdb_table_properties_t* props, size_t* size);
|
||||
|
||||
|
||||
@@ -826,6 +826,16 @@ unsigned char rocksdb_options_get_remote_compaction_manifest_floor(
|
||||
return opt->rep.remote_compaction_manifest_floor;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_use_session_tmp_dir_for_remote_compaction(
|
||||
rocksdb_options_t* opt, unsigned char v) {
|
||||
opt->rep.use_session_tmp_dir_for_remote_compaction = v;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_options_get_use_session_tmp_dir_for_remote_compaction(
|
||||
rocksdb_options_t* opt) {
|
||||
return opt->rep.use_session_tmp_dir_for_remote_compaction;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_lowest_used_cache_tier(rocksdb_options_t* opt, int v) {
|
||||
opt->rep.lowest_used_cache_tier =
|
||||
static_cast<decltype(opt->rep.lowest_used_cache_tier)>(v);
|
||||
@@ -2137,6 +2147,16 @@ int rocksdb_ingestexternalfileoptions_get_file_opening_threads(
|
||||
|
||||
/* OpenAndCompactOptions */
|
||||
|
||||
void rocksdb_open_and_compact_options_set_max_secondary_open_retries(
|
||||
rocksdb_open_and_compact_options_t* opt, uint32_t v) {
|
||||
opt->rep.max_secondary_open_retries = v;
|
||||
}
|
||||
|
||||
uint32_t rocksdb_open_and_compact_options_get_max_secondary_open_retries(
|
||||
rocksdb_open_and_compact_options_t* opt) {
|
||||
return opt->rep.max_secondary_open_retries;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_open_and_compact_options_get_allow_resumption(
|
||||
rocksdb_open_and_compact_options_t* opt) {
|
||||
return opt->rep.allow_resumption;
|
||||
|
||||
@@ -254,6 +254,14 @@ rocksdb_options_set_remote_compaction_manifest_floor(rocksdb_options_t* opt,
|
||||
extern ROCKSDB_LIBRARY_API unsigned char
|
||||
rocksdb_options_get_remote_compaction_manifest_floor(rocksdb_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_options_set_use_session_tmp_dir_for_remote_compaction(
|
||||
rocksdb_options_t* opt, unsigned char v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API unsigned char
|
||||
rocksdb_options_get_use_session_tmp_dir_for_remote_compaction(
|
||||
rocksdb_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void rocksdb_options_set_lowest_used_cache_tier(
|
||||
rocksdb_options_t* opt, int v);
|
||||
|
||||
@@ -878,6 +886,14 @@ rocksdb_ingestexternalfileoptions_get_file_opening_threads(
|
||||
|
||||
/* OpenAndCompactOptions */
|
||||
|
||||
extern ROCKSDB_LIBRARY_API void
|
||||
rocksdb_open_and_compact_options_set_max_secondary_open_retries(
|
||||
rocksdb_open_and_compact_options_t* opt, uint32_t v);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API uint32_t
|
||||
rocksdb_open_and_compact_options_get_max_secondary_open_retries(
|
||||
rocksdb_open_and_compact_options_t* opt);
|
||||
|
||||
extern ROCKSDB_LIBRARY_API unsigned char
|
||||
rocksdb_open_and_compact_options_get_allow_resumption(
|
||||
rocksdb_open_and_compact_options_t* opt);
|
||||
|
||||
@@ -592,6 +592,20 @@ static void rocksdb_roundtrip_test_rocksdb_livefiles_storage_info_options(
|
||||
rocksdb_livefiles_storage_info_options_destroy(obj);
|
||||
}
|
||||
|
||||
static void rocksdb_roundtrip_test_rocksdb_open_and_compact_options(void) {
|
||||
rocksdb_open_and_compact_options_t* obj =
|
||||
rocksdb_open_and_compact_options_create();
|
||||
rocksdb_open_and_compact_options_set_max_secondary_open_retries(obj, 1);
|
||||
CheckCondition(
|
||||
rocksdb_open_and_compact_options_get_max_secondary_open_retries(obj) ==
|
||||
1);
|
||||
rocksdb_open_and_compact_options_set_max_secondary_open_retries(obj, 0);
|
||||
CheckCondition(
|
||||
rocksdb_open_and_compact_options_get_max_secondary_open_retries(obj) ==
|
||||
0);
|
||||
rocksdb_open_and_compact_options_destroy(obj);
|
||||
}
|
||||
|
||||
static void rocksdb_roundtrip_test_rocksdb_optimistictransactiondb_options(
|
||||
void) {
|
||||
rocksdb_optimistictransactiondb_options_t* obj =
|
||||
@@ -923,6 +937,12 @@ static void rocksdb_roundtrip_test_rocksdb_options(void) {
|
||||
CheckCondition(rocksdb_options_get_use_fsync(obj) == 1);
|
||||
rocksdb_options_set_use_fsync(obj, 0);
|
||||
CheckCondition(rocksdb_options_get_use_fsync(obj) == 0);
|
||||
rocksdb_options_set_use_session_tmp_dir_for_remote_compaction(obj, 1);
|
||||
CheckCondition(
|
||||
rocksdb_options_get_use_session_tmp_dir_for_remote_compaction(obj) == 1);
|
||||
rocksdb_options_set_use_session_tmp_dir_for_remote_compaction(obj, 0);
|
||||
CheckCondition(
|
||||
rocksdb_options_get_use_session_tmp_dir_for_remote_compaction(obj) == 0);
|
||||
rocksdb_options_set_verify_manifest_content_on_close(obj, 1);
|
||||
CheckCondition(rocksdb_options_get_verify_manifest_content_on_close(obj) ==
|
||||
1);
|
||||
@@ -1381,6 +1401,7 @@ static void rocksdb_run_generated_roundtrip_tests(void) {
|
||||
rocksdb_roundtrip_test_rocksdb_flushwaloptions();
|
||||
rocksdb_roundtrip_test_rocksdb_ingestexternalfileoptions();
|
||||
rocksdb_roundtrip_test_rocksdb_livefiles_storage_info_options();
|
||||
rocksdb_roundtrip_test_rocksdb_open_and_compact_options();
|
||||
rocksdb_roundtrip_test_rocksdb_optimistictransactiondb_options();
|
||||
rocksdb_roundtrip_test_rocksdb_options();
|
||||
rocksdb_roundtrip_test_rocksdb_readoptions();
|
||||
|
||||
Vendored
+1
-1
@@ -245,7 +245,7 @@ class ShardedCache : public ShardedCacheBase {
|
||||
std::unique_ptr<size_t[]> states(new size_t[num_shards]{});
|
||||
|
||||
size_t aepl = opts.average_entries_per_lock;
|
||||
aepl = std::min(aepl, size_t{1});
|
||||
aepl = std::max(aepl, size_t{1});
|
||||
|
||||
bool remaining_work;
|
||||
do {
|
||||
|
||||
@@ -4,6 +4,30 @@
|
||||
Thorough multi-agent code review for RocksDB commits following CLAUDE.md guidelines,
|
||||
with structured codebase exploration, inter-agent debate, and verification.
|
||||
|
||||
## Behavioral Compatibility Review Perspective
|
||||
|
||||
Behavior changes are human judgment calls. They must be intentional,
|
||||
well-documented (including release notes), and researched for potential
|
||||
downstream production impact. Agents and reviewers must call out concrete gaps,
|
||||
ambiguity, or adverse impacts in the change's intent, documentation, and
|
||||
production-impact analysis.
|
||||
|
||||
When a diff changes how an existing accepted public or serialized input is
|
||||
interpreted—including an undocumented, discouraged, or historically ignored
|
||||
value—reviewers must:
|
||||
- Compare the old and new observable behavior, including performance and
|
||||
resource impact.
|
||||
- Check that the rationale and user-facing documentation clearly describe the
|
||||
change.
|
||||
- Identify known direct callers and stored configurations that may depend on
|
||||
the previous behavior.
|
||||
- Check for appropriate regression coverage, or a documented reason why such
|
||||
coverage is not practical.
|
||||
|
||||
A behavior change is not automatically a defect. Report a finding when the
|
||||
intent, documentation, downstream-impact analysis, or mitigation has a concrete
|
||||
gap or ambiguity that could lead to an unintended compatibility regression.
|
||||
|
||||
IMPORTANT — Where the review goes: Your FINAL RESPONSE TEXT is the review.
|
||||
The PR comment is generated verbatim from your final response, so the
|
||||
complete, polished review MUST appear there in full. Do NOT end your turn with
|
||||
@@ -393,8 +417,8 @@ Write findings to `findings-caller-audit.md`.
|
||||
- Documentation completeness
|
||||
- Deprecation policy compliance
|
||||
- Forward compatibility (struct extensibility)
|
||||
- **Behavioral compatibility**: Do changed semantics (e.g., kOutOfBound vs
|
||||
kUnknown) break implicit contracts with callers?
|
||||
- Apply the Behavioral Compatibility Review Perspective above to changes in
|
||||
observable semantics and implicit caller contracts.
|
||||
|
||||
#### Agent: Serialization & Deserialization Reviewer
|
||||
- Format correctness and versioning
|
||||
|
||||
@@ -101,6 +101,12 @@ class VersionBlobFetcherBase : public BlobFetcher {
|
||||
return version_ != nullptr && !allow_write_path_fallback_;
|
||||
}
|
||||
|
||||
// The Version this fetcher reads separate-file blob references through (may
|
||||
// be null for a direct-write-fallback-only fetcher). Used by the cross-key
|
||||
// lazy coalescing path to group and dispatch reads via
|
||||
// Version::MultiGetBlobLazy.
|
||||
const Version* version() const { return version_; }
|
||||
|
||||
protected:
|
||||
VersionBlobFetcherBase(const Version* version, BlobFileCache* blob_file_cache,
|
||||
bool allow_write_path_fallback)
|
||||
|
||||
+202
-7
@@ -296,7 +296,11 @@ Status BlobFileReader::ReadFromFile(const RandomAccessFileReader* file_reader,
|
||||
}
|
||||
|
||||
if (slice->size() != read_size) {
|
||||
return Status::Corruption("Failed to read data from blob file");
|
||||
return Status::Corruption("Incomplete blob read from " +
|
||||
file_reader->file_name() + " at offset " +
|
||||
std::to_string(read_offset) + ": expected " +
|
||||
std::to_string(read_size) + " bytes, got " +
|
||||
std::to_string(slice->size()));
|
||||
}
|
||||
|
||||
return Status::OK();
|
||||
@@ -330,7 +334,10 @@ Status BlobFileReader::GetBlob(
|
||||
|
||||
if (!IsValidBlobOffset(offset, key_size, value_size, file_size_,
|
||||
has_footer_)) {
|
||||
return Status::Corruption("Invalid blob offset");
|
||||
return Status::Corruption(
|
||||
"Invalid blob offset " + std::to_string(offset) + " for value size " +
|
||||
std::to_string(value_size) + " in " + file_reader_->file_name() +
|
||||
" (file size " + std::to_string(file_size_) + ")");
|
||||
}
|
||||
|
||||
if (compression_type != compression_type_) {
|
||||
@@ -438,12 +445,18 @@ Status BlobFileReader::GetBlobRange(const ReadOptions& read_options,
|
||||
// performs); this guards the sub-range read below.
|
||||
if (!IsValidBlobOffset(offset, key_size, value_size, file_size_,
|
||||
has_footer_)) {
|
||||
return Status::Corruption("Invalid blob offset");
|
||||
return Status::Corruption(
|
||||
"Invalid blob offset " + std::to_string(offset) + " for value size " +
|
||||
std::to_string(value_size) + " in " + file_reader_->file_name() +
|
||||
" (file size " + std::to_string(file_size_) + ")");
|
||||
}
|
||||
|
||||
// The requested sub-range must lie within the value.
|
||||
if (range_offset > value_size || range_length > value_size - range_offset) {
|
||||
return Status::InvalidArgument("Blob range out of bounds");
|
||||
return Status::InvalidArgument(
|
||||
"Blob range [" + std::to_string(range_offset) + ", +" +
|
||||
std::to_string(range_length) + ") out of bounds for value size " +
|
||||
std::to_string(value_size) + " in " + file_reader_->file_name());
|
||||
}
|
||||
|
||||
// Read only the requested bytes, at the value's file position plus the range
|
||||
@@ -518,7 +531,10 @@ void BlobFileReader::MultiGetBlob(
|
||||
|
||||
if (!IsValidBlobOffset(offset, key_size, value_size, file_size_,
|
||||
has_footer_)) {
|
||||
*req->status = Status::Corruption("Invalid blob offset");
|
||||
*req->status = Status::Corruption(
|
||||
"Invalid blob offset " + std::to_string(offset) + " for value size " +
|
||||
std::to_string(value_size) + " in " + file_reader_->file_name() +
|
||||
" (file size " + std::to_string(file_size_) + ")");
|
||||
continue;
|
||||
}
|
||||
if (req->compression != compression_type_) {
|
||||
@@ -612,8 +628,11 @@ void BlobFileReader::MultiGetBlob(
|
||||
auto& read_req = read_reqs[j++];
|
||||
const auto& record_slice = read_req.result;
|
||||
if (read_req.status.ok() && record_slice.size() != read_req.len) {
|
||||
read_req.status =
|
||||
IOStatus::Corruption("Failed to read data from blob file");
|
||||
read_req.status = IOStatus::Corruption(
|
||||
"Incomplete blob read from " + file_reader_->file_name() +
|
||||
" at offset " + std::to_string(read_req.offset) + ": expected " +
|
||||
std::to_string(read_req.len) + " bytes, got " +
|
||||
std::to_string(record_slice.size()));
|
||||
}
|
||||
|
||||
*req->status = read_req.status;
|
||||
@@ -644,6 +663,182 @@ void BlobFileReader::MultiGetBlob(
|
||||
}
|
||||
}
|
||||
|
||||
void BlobFileReader::MultiGetBlobRange(
|
||||
const ReadOptions& read_options,
|
||||
autovector<std::pair<BlobRangeReadRequest*, std::unique_ptr<BlobContents>>>&
|
||||
blob_reqs,
|
||||
uint64_t* bytes_read) const {
|
||||
const size_t num_blobs = blob_reqs.size();
|
||||
assert(num_blobs > 0);
|
||||
assert(num_blobs <= MultiGetContext::MAX_BATCH_SIZE);
|
||||
|
||||
// Range reads are only issued for uncompressed blob references (a strict
|
||||
// sub-range of a compressed record cannot be decompressed in isolation).
|
||||
// Version::MultiGetBlobLazy rejects a compressed blob index before it reaches
|
||||
// here, so every request's blob index is uncompressed by contract; a
|
||||
// compressed blob file therefore means the blob index disagrees with the
|
||||
// file. Report that as the compression-type mismatch it is -- exactly like
|
||||
// the whole-value MultiGetBlob path -- rather than as a usage error. (Unlike
|
||||
// the single-read path, BlobSource has no per-request compression to check
|
||||
// for a range read, so this reader-level check is the range multi path's
|
||||
// compression backstop.)
|
||||
if (compression_type_ != kNoCompression) {
|
||||
for (auto& blob_req : blob_reqs) {
|
||||
assert(blob_req.first);
|
||||
assert(blob_req.first->status);
|
||||
*blob_req.first->status =
|
||||
Status::Corruption("Compression type mismatch when reading a blob");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#ifndef NDEBUG
|
||||
for (size_t i = 0; i < num_blobs - 1; ++i) {
|
||||
assert(blob_reqs[i].first->offset + blob_reqs[i].first->range_offset <=
|
||||
blob_reqs[i + 1].first->offset +
|
||||
blob_reqs[i + 1].first->range_offset);
|
||||
}
|
||||
#endif // !NDEBUG
|
||||
|
||||
std::vector<FSReadRequest> read_reqs;
|
||||
uint64_t total_len = 0;
|
||||
read_reqs.reserve(num_blobs);
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
BlobRangeReadRequest* const req = blob_reqs[i].first;
|
||||
assert(req);
|
||||
assert(req->status);
|
||||
|
||||
const size_t key_size = req->user_key.size();
|
||||
// Validate that the full value region is within the file (same check
|
||||
// GetBlob performs); this guards the sub-range read below.
|
||||
if (!IsValidBlobOffset(req->offset, key_size, req->value_size, file_size_,
|
||||
has_footer_)) {
|
||||
*req->status = Status::Corruption(
|
||||
"Invalid blob offset " + std::to_string(req->offset) +
|
||||
" for value size " + std::to_string(req->value_size) + " in " +
|
||||
file_reader_->file_name() + " (file size " +
|
||||
std::to_string(file_size_) + ")");
|
||||
continue;
|
||||
}
|
||||
// The requested sub-range must lie within the value.
|
||||
if (req->range_offset > req->value_size ||
|
||||
req->range_length > req->value_size - req->range_offset) {
|
||||
*req->status = Status::InvalidArgument(
|
||||
"Blob range [" + std::to_string(req->range_offset) + ", +" +
|
||||
std::to_string(req->range_length) +
|
||||
") out of bounds for value size " + std::to_string(req->value_size) +
|
||||
" in " + file_reader_->file_name());
|
||||
continue;
|
||||
}
|
||||
|
||||
// Read only the requested bytes, at the value's file position plus the
|
||||
// range offset -- no record-header adjustment, no whole-record checksum.
|
||||
FSReadRequest read_req;
|
||||
read_req.offset = req->offset + req->range_offset;
|
||||
read_req.len = req->range_length;
|
||||
total_len += read_req.len;
|
||||
read_reqs.emplace_back(std::move(read_req));
|
||||
}
|
||||
|
||||
RecordTick(statistics_, BLOB_DB_BLOB_FILE_BYTES_READ, total_len);
|
||||
|
||||
if (read_reqs.empty()) {
|
||||
if (bytes_read) {
|
||||
*bytes_read = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Buffer buf;
|
||||
AlignedBuffer direct_io_buffer;
|
||||
|
||||
Status s;
|
||||
bool direct_io = file_reader_->use_direct_io();
|
||||
if (direct_io) {
|
||||
for (size_t i = 0; i < read_reqs.size(); ++i) {
|
||||
read_reqs[i].scratch = nullptr;
|
||||
}
|
||||
} else {
|
||||
buf.reset(new char[total_len]);
|
||||
std::ptrdiff_t pos = 0;
|
||||
for (size_t i = 0; i < read_reqs.size(); ++i) {
|
||||
read_reqs[i].scratch = buf.get() + pos;
|
||||
pos += read_reqs[i].len;
|
||||
}
|
||||
}
|
||||
TEST_SYNC_POINT("BlobFileReader::MultiGetBlobRange:ReadFromFile");
|
||||
PERF_COUNTER_ADD(blob_read_count, num_blobs);
|
||||
PERF_COUNTER_ADD(blob_read_byte, total_len);
|
||||
IOOptions opts;
|
||||
IODebugContext dbg;
|
||||
s = file_reader_->PrepareIOOptions(read_options, opts, &dbg);
|
||||
if (s.ok()) {
|
||||
AlignedBufferAllocationContext direct_io_context{&direct_io_buffer};
|
||||
s = file_reader_->MultiRead(opts, read_reqs.data(), read_reqs.size(),
|
||||
&direct_io_context, &dbg);
|
||||
}
|
||||
if (!s.ok()) {
|
||||
// Batch read failed; s is written to every request's status below, so these
|
||||
// per-request FS statuses carry nothing extra.
|
||||
for (auto& req : read_reqs) {
|
||||
req.status.PermitUncheckedError();
|
||||
}
|
||||
for (auto& blob_req : blob_reqs) {
|
||||
BlobRangeReadRequest* const req = blob_req.first;
|
||||
assert(req);
|
||||
assert(req->status);
|
||||
if (!req->status->IsCorruption() && !req->status->IsInvalidArgument()) {
|
||||
// Avoid overwriting per-request validation errors.
|
||||
*req->status = s;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
assert(s.ok());
|
||||
|
||||
TEST_SYNC_POINT_CALLBACK("BlobFileReader::MultiGetBlobRange:TamperWithResult",
|
||||
&read_reqs);
|
||||
|
||||
uint64_t total_bytes = 0;
|
||||
for (size_t i = 0, j = 0; i < num_blobs; ++i) {
|
||||
BlobRangeReadRequest* const req = blob_reqs[i].first;
|
||||
assert(req);
|
||||
assert(req->status);
|
||||
|
||||
if (!req->status->ok()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
assert(j < read_reqs.size());
|
||||
auto& read_req = read_reqs[j++];
|
||||
const Slice& range_slice = read_req.result;
|
||||
if (read_req.status.ok() && range_slice.size() != read_req.len) {
|
||||
read_req.status = IOStatus::Corruption(
|
||||
"Incomplete blob range read from " + file_reader_->file_name() +
|
||||
" at offset " + std::to_string(read_req.offset) + ": expected " +
|
||||
std::to_string(read_req.len) + " bytes, got " +
|
||||
std::to_string(range_slice.size()));
|
||||
}
|
||||
|
||||
*req->status = read_req.status;
|
||||
if (!req->status->ok()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// The blob is uncompressed, so the bytes read are exactly the requested
|
||||
// value bytes; copy them into an owned BlobContents (no decompression).
|
||||
BlobContentsCreator::Create(&blob_reqs[i].second, /*out_charge=*/nullptr,
|
||||
range_slice, kNoCompression,
|
||||
/*allocator=*/nullptr);
|
||||
total_bytes += range_slice.size();
|
||||
}
|
||||
|
||||
if (bytes_read) {
|
||||
*bytes_read = total_bytes;
|
||||
}
|
||||
}
|
||||
|
||||
Status BlobFileReader::VerifyBlob(const Slice& record_slice,
|
||||
const Slice& user_key, uint64_t value_size) {
|
||||
PERF_TIMER_GUARD(blob_checksum_time);
|
||||
|
||||
@@ -88,6 +88,21 @@ class BlobFileReader {
|
||||
blob_reqs,
|
||||
uint64_t* bytes_read) const;
|
||||
|
||||
// Byte-range (partial) multi-read counterpart of MultiGetBlob: for each
|
||||
// request reads only its sub-range [offset + range_offset, +range_length) of
|
||||
// an *uncompressed* blob value, coalescing all requests into a single
|
||||
// MultiRead. Unlike MultiGetBlob it reads no record header/key, verifies no
|
||||
// whole-record checksum (a strict sub-range cannot cover it), and never
|
||||
// decompresses -- so it is only valid when this reader's blob file is
|
||||
// uncompressed (checked). Each result owns a copy of its requested bytes.
|
||||
// Requests must be sorted ascending by effective read offset (offset +
|
||||
// range_offset) by the caller.
|
||||
void MultiGetBlobRange(
|
||||
const ReadOptions& read_options,
|
||||
autovector<std::pair<BlobRangeReadRequest*,
|
||||
std::unique_ptr<BlobContents>>>& blob_reqs,
|
||||
uint64_t* bytes_read) const;
|
||||
|
||||
CompressionType GetCompressionType() const { return compression_type_; }
|
||||
|
||||
uint64_t GetFileSize() const { return file_size_; }
|
||||
|
||||
+291
-15
@@ -5,9 +5,11 @@
|
||||
|
||||
#include "db/blob/blob_gen2_format.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "file/random_access_file_reader.h"
|
||||
#include "file/writable_file_writer.h"
|
||||
@@ -16,9 +18,56 @@
|
||||
#include "table/format.h"
|
||||
#include "util/cast_util.h"
|
||||
#include "util/coding.h"
|
||||
#include "util/crc32c.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
namespace {
|
||||
|
||||
// Verifies a SimpleGen2Blob record's builtin checksum: the stored 4-byte
|
||||
// checksum (at record[payload_size + 1 ..], context-modified by the record's
|
||||
// file offset) against the checksum computed over the payload plus its 1-byte
|
||||
// compression marker. Returns OK, or a Corruption carrying debugging
|
||||
// information -- stored (with the per-offset context modifier removed),
|
||||
// computed, and checksum type, with CRC values unmasked so they can be compared
|
||||
// against a reference. Mirrors VerifyBlockChecksum() for the block-based
|
||||
// format, since a SimpleGen2Blob record's trailer uses the same
|
||||
// builtin-checksum scheme; shared by the scalar and batched readers so both
|
||||
// report the same diagnostics.
|
||||
Status VerifySimpleGen2BlobChecksum(ChecksumType checksum_type,
|
||||
uint32_t base_context_checksum,
|
||||
const char* record, size_t payload_size,
|
||||
uint64_t record_offset,
|
||||
const std::string& file_name) {
|
||||
uint32_t stored = DecodeFixed32(record + payload_size + 1);
|
||||
const uint32_t modifier =
|
||||
ChecksumModifierForContext(base_context_checksum, record_offset);
|
||||
stored -= modifier;
|
||||
const uint32_t computed = ComputeBuiltinChecksumWithLastByte(
|
||||
checksum_type, record, payload_size, record[payload_size]);
|
||||
if (stored == computed) {
|
||||
return Status::OK();
|
||||
}
|
||||
// Unmask CRC values (as VerifyBlockChecksum does) so a reader of the error
|
||||
// can compare against a reference checksum.
|
||||
uint32_t stored_for_msg = stored;
|
||||
uint32_t computed_for_msg = computed;
|
||||
if (checksum_type == kCRC32c) {
|
||||
stored_for_msg = crc32c::Unmask(stored);
|
||||
computed_for_msg = crc32c::Unmask(computed);
|
||||
}
|
||||
return Status::Corruption("SimpleGen2 blob record checksum mismatch: stored" +
|
||||
std::string(modifier ? "(context removed)" : "") +
|
||||
" = " + std::to_string(stored_for_msg) +
|
||||
", computed = " + std::to_string(computed_for_msg) +
|
||||
", type = " + std::to_string(checksum_type) +
|
||||
" in " + file_name + " offset " +
|
||||
std::to_string(record_offset) + " size " +
|
||||
std::to_string(payload_size));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Status ReadAndVerifySimpleGen2BlobRecord(
|
||||
const ReadOptions& read_options, RandomAccessFileReader* file,
|
||||
uint64_t record_offset, size_t payload_size, size_t record_size,
|
||||
@@ -40,7 +89,11 @@ Status ReadAndVerifySimpleGen2BlobRecord(
|
||||
return s;
|
||||
}
|
||||
if (result.size() != record_size) {
|
||||
return Status::Corruption("Could not read complete blob record");
|
||||
return Status::Corruption("Incomplete blob record read from " +
|
||||
file->file_name() + " at offset " +
|
||||
std::to_string(record_offset) + ": expected " +
|
||||
std::to_string(record_size) + " bytes, got " +
|
||||
std::to_string(result.size()));
|
||||
}
|
||||
// With mmap reads the data lands outside `buf`; copy it in so the caller can
|
||||
// rely on `buf` owning the bytes (this is the only copy on the mmap path).
|
||||
@@ -54,24 +107,32 @@ Status ReadAndVerifySimpleGen2BlobRecord(
|
||||
static_cast<CompressionType>(record[payload_size]);
|
||||
if (compression != expected_compression) {
|
||||
return Status::Corruption(
|
||||
"Blob record compression does not match blob index");
|
||||
"Blob record compression mismatch in " + file->file_name() +
|
||||
" at offset " + std::to_string(record_offset) + ": index expects " +
|
||||
std::to_string(static_cast<int>(expected_compression)) +
|
||||
", record has " + std::to_string(static_cast<int>(compression)));
|
||||
}
|
||||
|
||||
if (read_options.verify_checksums) {
|
||||
uint32_t stored = DecodeFixed32(record + payload_size + 1);
|
||||
stored -= ChecksumModifierForContext(base_context_checksum, record_offset);
|
||||
const uint32_t computed = ComputeBuiltinChecksumWithLastByte(
|
||||
checksum_type, record, payload_size, record[payload_size]);
|
||||
if (stored != computed) {
|
||||
return Status::Corruption("Blob record checksum mismatch in " +
|
||||
file->file_name() + " offset " +
|
||||
std::to_string(record_offset) + " size " +
|
||||
std::to_string(payload_size));
|
||||
const Status checksum_status = VerifySimpleGen2BlobChecksum(
|
||||
checksum_type, base_context_checksum, record, payload_size,
|
||||
record_offset, file->file_name());
|
||||
if (!checksum_status.ok()) {
|
||||
return checksum_status;
|
||||
}
|
||||
}
|
||||
|
||||
// The compression marker matched the blob index above, but SimpleGen2Blob
|
||||
// payloads are currently written uncompressed only (see
|
||||
// WriteSimpleGen2BlobRecord), so a record that actually carries a compression
|
||||
// type is not readable here. This guards a corrupt or forward-incompatible
|
||||
// record whose (compressed) marker happened to agree with a (compressed)
|
||||
// index.
|
||||
if (compression != kNoCompression) {
|
||||
return Status::Corruption("Blob record compression is not supported");
|
||||
return Status::Corruption("Blob record compression " +
|
||||
std::to_string(static_cast<int>(compression)) +
|
||||
" is not supported in " + file->file_name() +
|
||||
" at offset " + std::to_string(record_offset));
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
@@ -88,14 +149,21 @@ Status ReadSimpleGen2BlobRange(const ReadOptions& read_options,
|
||||
// A strict sub-range of a compressed payload cannot be decompressed in
|
||||
// isolation; callers resolve such payloads whole and slice instead.
|
||||
if (expected_compression != kNoCompression) {
|
||||
return Status::Corruption("Cannot range-read a compressed blob");
|
||||
return Status::Corruption(
|
||||
"Cannot range-read a compressed blob (compression " +
|
||||
std::to_string(static_cast<int>(expected_compression)) + ") in " +
|
||||
file->file_name() + " at offset " + std::to_string(record_offset));
|
||||
}
|
||||
|
||||
// The requested sub-range must lie within the payload (the trailer that
|
||||
// follows the payload is never part of a range read).
|
||||
if (range_offset > payload_size ||
|
||||
range_length > payload_size - range_offset) {
|
||||
return Status::InvalidArgument("Blob range out of bounds");
|
||||
return Status::InvalidArgument(
|
||||
"Blob range [" + std::to_string(range_offset) + ", +" +
|
||||
std::to_string(range_length) + ") out of bounds for payload size " +
|
||||
std::to_string(payload_size) + " in " + file->file_name() +
|
||||
" at offset " + std::to_string(record_offset));
|
||||
}
|
||||
|
||||
Slice result;
|
||||
@@ -110,7 +178,11 @@ Status ReadSimpleGen2BlobRange(const ReadOptions& read_options,
|
||||
return s;
|
||||
}
|
||||
if (result.size() != range_length) {
|
||||
return Status::Corruption("Could not read complete blob range");
|
||||
return Status::Corruption("Incomplete blob range read from " +
|
||||
file->file_name() + " at offset " +
|
||||
std::to_string(record_offset + range_offset) +
|
||||
": expected " + std::to_string(range_length) +
|
||||
" bytes, got " + std::to_string(result.size()));
|
||||
}
|
||||
// With mmap reads the data lands outside `buf`; copy it in so the caller can
|
||||
// rely on `buf` owning the bytes (the only copy on the mmap path).
|
||||
@@ -124,6 +196,210 @@ Status ReadSimpleGen2BlobRange(const ReadOptions& read_options,
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// One record's read description for the batched SimpleGen2Blob readers below:
|
||||
// where/how much to read, where to put it, and where to report the outcome.
|
||||
struct Gen2ReadSlot {
|
||||
uint64_t offset = 0;
|
||||
size_t len = 0;
|
||||
char* buf = nullptr;
|
||||
Status* status = nullptr;
|
||||
};
|
||||
|
||||
// Issues a single MultiRead for the records described by `slots` and copies
|
||||
// each result into its buf, setting *slots[i].status. Shared by the
|
||||
// whole-record and range batch readers; per-record format checks (compression
|
||||
// marker / checksum) are done by the whole-record caller afterwards from the
|
||||
// (now populated) buf.
|
||||
void MultiReadGen2(const ReadOptions& read_options,
|
||||
RandomAccessFileReader* file,
|
||||
std::vector<Gen2ReadSlot>& slots) {
|
||||
assert(file != nullptr);
|
||||
assert(!slots.empty());
|
||||
|
||||
// RandomAccessFileReader::MultiRead requires the requests sorted ascending by
|
||||
// file offset -- it asserts this, and in direct-I/O mode it aligns and merges
|
||||
// only consecutive requests, so unsorted input reads the wrong bytes. The
|
||||
// callers group records by SST but not by offset, so sort here. Each slot
|
||||
// carries its own output buf/status pointer, so sorting does not disturb
|
||||
// result routing.
|
||||
std::sort(slots.begin(), slots.end(),
|
||||
[](const Gen2ReadSlot& a, const Gen2ReadSlot& b) {
|
||||
return a.offset < b.offset;
|
||||
});
|
||||
|
||||
const size_t num = slots.size();
|
||||
std::vector<FSReadRequest> read_reqs;
|
||||
read_reqs.reserve(num);
|
||||
for (size_t i = 0; i < num; ++i) {
|
||||
FSReadRequest read_req;
|
||||
read_req.offset = slots[i].offset;
|
||||
read_req.len = slots[i].len;
|
||||
read_reqs.emplace_back(std::move(read_req));
|
||||
}
|
||||
|
||||
AlignedBuffer direct_io_buffer;
|
||||
const bool direct_io = file->use_direct_io();
|
||||
if (direct_io) {
|
||||
for (size_t i = 0; i < num; ++i) {
|
||||
read_reqs[i].scratch = nullptr;
|
||||
}
|
||||
} else {
|
||||
for (size_t i = 0; i < num; ++i) {
|
||||
read_reqs[i].scratch = slots[i].buf;
|
||||
}
|
||||
}
|
||||
|
||||
IOOptions opts;
|
||||
IODebugContext dbg;
|
||||
Status s = file->PrepareIOOptions(read_options, opts, &dbg);
|
||||
if (s.ok()) {
|
||||
AlignedBufferAllocationContext direct_io_context{&direct_io_buffer};
|
||||
s = file->MultiRead(opts, read_reqs.data(), read_reqs.size(),
|
||||
&direct_io_context, &dbg);
|
||||
}
|
||||
if (!s.ok()) {
|
||||
// Batch read failed; s is written to every slot's status below, so these
|
||||
// per-request FS statuses carry nothing extra.
|
||||
for (auto& read_req : read_reqs) {
|
||||
read_req.status.PermitUncheckedError();
|
||||
}
|
||||
for (size_t i = 0; i < num; ++i) {
|
||||
*slots[i].status = s;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < num; ++i) {
|
||||
FSReadRequest& read_req = read_reqs[i];
|
||||
if (read_req.status.ok() && read_req.result.size() != read_req.len) {
|
||||
read_req.status = IOStatus::Corruption(
|
||||
"Incomplete blob record read from " + file->file_name() +
|
||||
" at offset " + std::to_string(read_req.offset) + ": expected " +
|
||||
std::to_string(read_req.len) + " bytes, got " +
|
||||
std::to_string(read_req.result.size()));
|
||||
}
|
||||
*slots[i].status = read_req.status;
|
||||
if (!slots[i].status->ok()) {
|
||||
continue;
|
||||
}
|
||||
// With mmap/direct reads the data lands outside the caller's buf; copy it
|
||||
// in so the caller can rely on buf owning the bytes.
|
||||
if (read_req.result.data() != slots[i].buf) {
|
||||
memcpy(slots[i].buf, read_req.result.data(), read_req.len);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void ReadAndVerifySimpleGen2BlobRecords(const ReadOptions& read_options,
|
||||
RandomAccessFileReader* file,
|
||||
ChecksumType checksum_type,
|
||||
uint32_t base_context_checksum,
|
||||
size_t num_records,
|
||||
SimpleGen2RecordReadRequest* reqs) {
|
||||
assert(file != nullptr);
|
||||
assert(num_records > 0);
|
||||
|
||||
std::vector<Gen2ReadSlot> slots;
|
||||
slots.reserve(num_records);
|
||||
for (size_t i = 0; i < num_records; ++i) {
|
||||
assert(reqs[i].buf != nullptr);
|
||||
assert(reqs[i].status != nullptr);
|
||||
assert(reqs[i].record_size ==
|
||||
reqs[i].payload_size + kSimpleGen2BlobTrailerSize);
|
||||
slots.push_back({reqs[i].record_offset, reqs[i].record_size, reqs[i].buf,
|
||||
reqs[i].status});
|
||||
}
|
||||
|
||||
MultiReadGen2(read_options, file, slots);
|
||||
|
||||
for (size_t i = 0; i < num_records; ++i) {
|
||||
if (!reqs[i].status->ok()) {
|
||||
continue;
|
||||
}
|
||||
const char* record = reqs[i].buf;
|
||||
const size_t payload_size = reqs[i].payload_size;
|
||||
const CompressionType compression =
|
||||
static_cast<CompressionType>(record[payload_size]);
|
||||
if (compression != reqs[i].expected_compression) {
|
||||
*reqs[i].status = Status::Corruption(
|
||||
"Blob record compression mismatch in " + file->file_name() +
|
||||
" at offset " + std::to_string(reqs[i].record_offset) +
|
||||
": index expects " +
|
||||
std::to_string(static_cast<int>(reqs[i].expected_compression)) +
|
||||
", record has " + std::to_string(static_cast<int>(compression)));
|
||||
continue;
|
||||
}
|
||||
if (read_options.verify_checksums) {
|
||||
*reqs[i].status = VerifySimpleGen2BlobChecksum(
|
||||
checksum_type, base_context_checksum, record, payload_size,
|
||||
reqs[i].record_offset, file->file_name());
|
||||
if (!reqs[i].status->ok()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
// See ReadAndVerifySimpleGen2BlobRecord: the marker matched the index, but
|
||||
// only uncompressed SimpleGen2Blob payloads are currently supported.
|
||||
if (compression != kNoCompression) {
|
||||
*reqs[i].status = Status::Corruption(
|
||||
"Blob record compression " +
|
||||
std::to_string(static_cast<int>(compression)) +
|
||||
" is not supported in " + file->file_name() + " at offset " +
|
||||
std::to_string(reqs[i].record_offset));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ReadSimpleGen2BlobRanges(const ReadOptions& read_options,
|
||||
RandomAccessFileReader* file, size_t num_records,
|
||||
SimpleGen2RangeReadRequest* reqs) {
|
||||
assert(file != nullptr);
|
||||
assert(num_records > 0);
|
||||
|
||||
std::vector<Gen2ReadSlot> slots;
|
||||
slots.reserve(num_records);
|
||||
for (size_t i = 0; i < num_records; ++i) {
|
||||
assert(reqs[i].buf != nullptr);
|
||||
assert(reqs[i].status != nullptr);
|
||||
// A strict sub-range of a compressed payload cannot be decompressed in
|
||||
// isolation; callers resolve such payloads whole and slice instead.
|
||||
if (reqs[i].expected_compression != kNoCompression) {
|
||||
*reqs[i].status = Status::Corruption(
|
||||
"Cannot range-read a compressed blob (compression " +
|
||||
std::to_string(static_cast<int>(reqs[i].expected_compression)) +
|
||||
") in " + file->file_name() + " at offset " +
|
||||
std::to_string(reqs[i].record_offset));
|
||||
continue;
|
||||
}
|
||||
// The requested sub-range must lie within the payload (the trailer that
|
||||
// follows the payload is never part of a range read).
|
||||
if (reqs[i].range_offset > reqs[i].payload_size ||
|
||||
reqs[i].range_length > reqs[i].payload_size - reqs[i].range_offset) {
|
||||
*reqs[i].status = Status::InvalidArgument(
|
||||
"Blob range [" + std::to_string(reqs[i].range_offset) + ", +" +
|
||||
std::to_string(reqs[i].range_length) +
|
||||
") out of bounds for payload size " +
|
||||
std::to_string(reqs[i].payload_size) + " in " + file->file_name() +
|
||||
" at offset " + std::to_string(reqs[i].record_offset));
|
||||
continue;
|
||||
}
|
||||
slots.push_back({reqs[i].record_offset + reqs[i].range_offset,
|
||||
reqs[i].range_length, reqs[i].buf, reqs[i].status});
|
||||
}
|
||||
|
||||
if (slots.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// slots only covers requests that passed validation above; a failed
|
||||
// validation left *reqs[i].status non-OK and is skipped here.
|
||||
MultiReadGen2(read_options, file, slots);
|
||||
}
|
||||
|
||||
IOStatus WriteSimpleGen2BlobRecord(WritableFileWriter* file,
|
||||
const WriteOptions& write_options,
|
||||
ChecksumType checksum_type,
|
||||
|
||||
@@ -91,6 +91,57 @@ Status ReadSimpleGen2BlobRange(const ReadOptions& read_options,
|
||||
uint64_t range_offset, size_t range_length,
|
||||
CompressionType expected_compression, char* buf);
|
||||
|
||||
// One whole-record request in a batched SimpleGen2Blob read (see
|
||||
// ReadAndVerifySimpleGen2BlobRecords). Mirrors the scalar
|
||||
// ReadAndVerifySimpleGen2BlobRecord inputs, with the read bytes going into
|
||||
// caller-provided `buf` (capacity >= record_size) and the per-record outcome in
|
||||
// `*status`.
|
||||
struct SimpleGen2RecordReadRequest {
|
||||
uint64_t record_offset = 0;
|
||||
size_t payload_size = 0;
|
||||
size_t record_size = 0; // == payload_size + kSimpleGen2BlobTrailerSize
|
||||
CompressionType expected_compression = kNoCompression;
|
||||
char* buf = nullptr;
|
||||
Status* status = nullptr;
|
||||
};
|
||||
|
||||
// One sub-range request in a batched SimpleGen2Blob range read (see
|
||||
// ReadSimpleGen2BlobRanges). Mirrors the scalar ReadSimpleGen2BlobRange inputs.
|
||||
struct SimpleGen2RangeReadRequest {
|
||||
uint64_t record_offset = 0;
|
||||
size_t payload_size = 0;
|
||||
uint64_t range_offset = 0;
|
||||
size_t range_length = 0;
|
||||
CompressionType expected_compression = kNoCompression;
|
||||
char* buf = nullptr;
|
||||
Status* status = nullptr;
|
||||
};
|
||||
|
||||
// Batch counterpart of ReadAndVerifySimpleGen2BlobRecord: reads `num_records`
|
||||
// whole records from `file` in a single MultiRead, then per record verifies the
|
||||
// compression marker and (when read_options.verify_checksums) the
|
||||
// context-modified checksum, copying the record into reqs[i].buf. Per-record
|
||||
// outcome is written to *reqs[i].status; on success reqs[i].buf[0,
|
||||
// payload_size) holds the verified payload (the trailer sits unused at the
|
||||
// tail). Requests may be given in any order; they are sorted internally, as
|
||||
// RandomAccessFileReader::MultiRead requires ascending offsets.
|
||||
void ReadAndVerifySimpleGen2BlobRecords(const ReadOptions& read_options,
|
||||
RandomAccessFileReader* file,
|
||||
ChecksumType checksum_type,
|
||||
uint32_t base_context_checksum,
|
||||
size_t num_records,
|
||||
SimpleGen2RecordReadRequest* reqs);
|
||||
|
||||
// Batch counterpart of ReadSimpleGen2BlobRange: reads the requested sub-range
|
||||
// of `num_records` *uncompressed* payloads from `file` in a single MultiRead
|
||||
// (no trailer read, no checksum). Per-record outcome is in *reqs[i].status; on
|
||||
// success reqs[i].buf[0, range_length) holds the requested bytes. Requests may
|
||||
// be given in any order; they are sorted internally, as
|
||||
// RandomAccessFileReader::MultiRead requires ascending offsets.
|
||||
void ReadSimpleGen2BlobRanges(const ReadOptions& read_options,
|
||||
RandomAccessFileReader* file, size_t num_records,
|
||||
SimpleGen2RangeReadRequest* reqs);
|
||||
|
||||
// Writes a SimpleGen2Blob record for `payload` at the current end of `file`,
|
||||
// which the caller asserts is byte offset `record_offset`. Appends the payload
|
||||
// bytes followed by the 5-byte trailer (compression marker + context-modified
|
||||
|
||||
@@ -18,6 +18,7 @@ namespace ROCKSDB_NAMESPACE {
|
||||
// BlobFileReader::MultiGetBlob.
|
||||
struct BlobReadRequest {
|
||||
// User key to lookup the paired blob
|
||||
// TODO: consider simplifying to copied Slice?
|
||||
const Slice* user_key = nullptr;
|
||||
|
||||
// File offset in bytes
|
||||
@@ -55,4 +56,48 @@ using BlobFileReadRequests =
|
||||
std::tuple<uint64_t /* file_number */, uint64_t /* file_size */,
|
||||
autovector<BlobReadRequest>>;
|
||||
|
||||
// A byte-range (partial) blob read request for the lazy blob-read multi
|
||||
// primitives (BlobFileReader::MultiGetBlobRange /
|
||||
// BlobSource::MultiGetBlobRange). Unlike BlobReadRequest (whole value), it
|
||||
// reads only the sub-range [range_offset, range_offset + range_length) of an
|
||||
// *uncompressed* blob value, skipping whole-record checksum verification and
|
||||
// blob-cache population -- the multi-read counterpart of
|
||||
// BlobSource::GetBlobRange / BlobFileReader::GetBlobRange.
|
||||
struct BlobRangeReadRequest {
|
||||
// User key that pairs with the blob value (used for offset validation).
|
||||
Slice user_key;
|
||||
|
||||
// The blob value's file offset and full (logical == on-disk, uncompressed)
|
||||
// size, from the BlobIndex.
|
||||
uint64_t offset = 0;
|
||||
uint64_t value_size = 0;
|
||||
|
||||
// The requested sub-range within the value. `range_length` is the number of
|
||||
// bytes to read; the caller must ensure range_offset + range_length <=
|
||||
// value_size (it clamps before building the request).
|
||||
uint64_t range_offset = 0;
|
||||
size_t range_length = 0;
|
||||
|
||||
// Output parameter set to point to the requested bytes, and the read status.
|
||||
PinnableSlice* result = nullptr;
|
||||
Status* status = nullptr;
|
||||
|
||||
BlobRangeReadRequest(Slice _user_key, uint64_t _offset, uint64_t _value_size,
|
||||
uint64_t _range_offset, size_t _range_length,
|
||||
PinnableSlice* _result, Status* _status)
|
||||
: user_key(_user_key),
|
||||
offset(_offset),
|
||||
value_size(_value_size),
|
||||
range_offset(_range_offset),
|
||||
range_length(_range_length),
|
||||
result(_result),
|
||||
status(_status) {}
|
||||
|
||||
BlobRangeReadRequest() = default;
|
||||
};
|
||||
|
||||
using BlobFileRangeReadRequests =
|
||||
std::tuple<uint64_t /* file_number */, uint64_t /* file_size */,
|
||||
autovector<BlobRangeReadRequest>>;
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "cache/cache_reservation_manager.h"
|
||||
#include "cache/charged_cache.h"
|
||||
@@ -636,6 +637,188 @@ Status BlobSource::GetSimpleGen2BlobRange(
|
||||
return s;
|
||||
}
|
||||
|
||||
void BlobSource::MultiGetSimpleGen2Blob(
|
||||
const ReadOptions& read_options, const OffsetableCacheKey& base_cache_key,
|
||||
RandomAccessFileReader* file, ChecksumType checksum_type,
|
||||
uint32_t base_context_checksum, size_t num_records,
|
||||
SimpleGen2BlobReadRequest* reqs) {
|
||||
assert(file);
|
||||
if (num_records == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool no_io = read_options.read_tier == kBlockCacheTier;
|
||||
MemoryAllocator* const allocator = (blob_cache_ && read_options.fill_cache)
|
||||
? blob_cache_.get()->memory_allocator()
|
||||
: nullptr;
|
||||
|
||||
// Records that missed the cache and must be read from disk, in read order;
|
||||
// each owns its buffer until we build a BlobContents from it below.
|
||||
struct Pending {
|
||||
size_t idx;
|
||||
CacheAllocationPtr buf;
|
||||
uint64_t record_size;
|
||||
};
|
||||
std::vector<Pending> pending;
|
||||
std::vector<SimpleGen2RecordReadRequest> read_reqs;
|
||||
|
||||
for (size_t i = 0; i < num_records; ++i) {
|
||||
SimpleGen2BlobReadRequest& req = reqs[i];
|
||||
assert(req.result);
|
||||
assert(req.status);
|
||||
|
||||
const uint64_t record_size = req.payload_size + kSimpleGen2BlobTrailerSize;
|
||||
const CacheKey cache_key =
|
||||
GetSimpleGen2BlobCacheKey(base_cache_key, req.record_offset);
|
||||
|
||||
if (blob_cache_) {
|
||||
CacheHandleGuard<BlobContents> blob_handle;
|
||||
const Status cs = GetBlobFromCache(cache_key.AsSlice(), &blob_handle);
|
||||
if (cs.ok()) {
|
||||
*req.status = cs;
|
||||
PinCachedBlob(&blob_handle, req.result);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (no_io) {
|
||||
*req.status =
|
||||
Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
|
||||
continue;
|
||||
}
|
||||
|
||||
CacheAllocationPtr buf =
|
||||
AllocateBlock(static_cast<size_t>(record_size), allocator);
|
||||
read_reqs.push_back(SimpleGen2RecordReadRequest{
|
||||
req.record_offset, static_cast<size_t>(req.payload_size),
|
||||
static_cast<size_t>(record_size), req.expected_compression, buf.get(),
|
||||
req.status});
|
||||
pending.push_back(Pending{i, std::move(buf), record_size});
|
||||
}
|
||||
|
||||
if (read_reqs.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ReadAndVerifySimpleGen2BlobRecords(read_options, file, checksum_type,
|
||||
base_context_checksum, read_reqs.size(),
|
||||
read_reqs.data());
|
||||
|
||||
for (size_t k = 0; k < pending.size(); ++k) {
|
||||
SimpleGen2BlobReadRequest& req = reqs[pending[k].idx];
|
||||
if (!req.status->ok()) {
|
||||
continue;
|
||||
}
|
||||
const uint64_t record_size = pending[k].record_size;
|
||||
std::unique_ptr<BlobContents> blob_contents(new BlobContents(
|
||||
std::move(pending[k].buf), static_cast<size_t>(req.payload_size)));
|
||||
|
||||
RecordTick(statistics_, BLOB_DB_BLOB_FILE_BYTES_READ, record_size);
|
||||
PERF_COUNTER_ADD(blob_read_count, 1);
|
||||
PERF_COUNTER_ADD(blob_read_byte, record_size);
|
||||
if (read_options.io_activity == Env::IOActivity::kLazyResolve) {
|
||||
RecordLazyRead(statistics_, record_size);
|
||||
}
|
||||
|
||||
if (blob_cache_ && read_options.fill_cache) {
|
||||
const CacheKey cache_key =
|
||||
GetSimpleGen2BlobCacheKey(base_cache_key, req.record_offset);
|
||||
CacheHandleGuard<BlobContents> blob_handle;
|
||||
const Status ps =
|
||||
PutBlobIntoCache(cache_key.AsSlice(), &blob_contents, &blob_handle);
|
||||
if (!ps.ok()) {
|
||||
*req.status = ps;
|
||||
} else {
|
||||
PinCachedBlob(&blob_handle, req.result);
|
||||
}
|
||||
} else {
|
||||
PinOwnedBlob(&blob_contents, req.result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void BlobSource::MultiGetSimpleGen2BlobRange(
|
||||
const ReadOptions& read_options, const OffsetableCacheKey& base_cache_key,
|
||||
RandomAccessFileReader* file, size_t num_records,
|
||||
SimpleGen2BlobRangeReadRequest* reqs) {
|
||||
assert(file);
|
||||
// Range reads are only issued while resolving a lazy result (partial reads
|
||||
// are lazy-only), so the lazy read stats below are recorded unconditionally.
|
||||
assert(read_options.io_activity == Env::IOActivity::kLazyResolve);
|
||||
if (num_records == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const bool no_io = read_options.read_tier == kBlockCacheTier;
|
||||
|
||||
struct Pending {
|
||||
size_t idx;
|
||||
CacheAllocationPtr buf;
|
||||
};
|
||||
std::vector<Pending> pending;
|
||||
std::vector<SimpleGen2RangeReadRequest> read_reqs;
|
||||
|
||||
for (size_t i = 0; i < num_records; ++i) {
|
||||
SimpleGen2BlobRangeReadRequest& req = reqs[i];
|
||||
assert(req.result);
|
||||
assert(req.status);
|
||||
|
||||
const CacheKey cache_key =
|
||||
GetSimpleGen2BlobCacheKey(base_cache_key, req.record_offset);
|
||||
|
||||
// Probe the whole-payload cache; on a hit slice the requested sub-range.
|
||||
if (blob_cache_) {
|
||||
CacheHandleGuard<BlobContents> blob_handle;
|
||||
const Status cs = GetBlobFromCache(cache_key.AsSlice(), &blob_handle);
|
||||
if (cs.ok()) {
|
||||
*req.status = cs;
|
||||
PinCacheHitSubRange(&blob_handle, req.range_offset, req.range_length,
|
||||
req.result);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if (no_io) {
|
||||
*req.status =
|
||||
Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
|
||||
continue;
|
||||
}
|
||||
|
||||
// No cache-fill allocator: a partial payload is never inserted.
|
||||
CacheAllocationPtr buf =
|
||||
AllocateBlock(req.range_length, /*allocator=*/nullptr);
|
||||
read_reqs.push_back(SimpleGen2RangeReadRequest{
|
||||
req.record_offset, static_cast<size_t>(req.payload_size),
|
||||
req.range_offset, req.range_length, req.expected_compression, buf.get(),
|
||||
req.status});
|
||||
pending.push_back(Pending{i, std::move(buf)});
|
||||
}
|
||||
|
||||
if (read_reqs.empty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
ReadSimpleGen2BlobRanges(read_options, file, read_reqs.size(),
|
||||
read_reqs.data());
|
||||
|
||||
for (size_t k = 0; k < pending.size(); ++k) {
|
||||
SimpleGen2BlobRangeReadRequest& req = reqs[pending[k].idx];
|
||||
if (!req.status->ok()) {
|
||||
continue;
|
||||
}
|
||||
std::unique_ptr<BlobContents> blob_contents(
|
||||
new BlobContents(std::move(pending[k].buf), req.range_length));
|
||||
|
||||
RecordTick(statistics_, BLOB_DB_BLOB_FILE_BYTES_READ, req.range_length);
|
||||
PERF_COUNTER_ADD(blob_read_count, 1);
|
||||
PERF_COUNTER_ADD(blob_read_byte, req.range_length);
|
||||
RecordLazyRead(statistics_, req.range_length);
|
||||
RecordLazyPartialRead(statistics_, req.range_length, req.payload_size);
|
||||
|
||||
PinOwnedBlob(&blob_contents, req.result);
|
||||
}
|
||||
}
|
||||
|
||||
void BlobSource::MultiGetBlob(const ReadOptions& read_options,
|
||||
autovector<BlobFileReadRequests>& blob_reqs,
|
||||
uint64_t* bytes_read) {
|
||||
@@ -887,6 +1070,24 @@ void BlobSource::MultiGetBlobFromOneFile(const ReadOptions& read_options,
|
||||
}
|
||||
}
|
||||
|
||||
// Whole-column reads on the lazy resolve path count as lazy reads (mirrors
|
||||
// BlobSource::GetBlob, which records only on a disk read, not a cache hit).
|
||||
// Gated on the activity so regular MultiGet is unaffected; the misses read
|
||||
// above are exactly the disk reads for these blob columns.
|
||||
if (read_options.io_activity == Env::IOActivity::kLazyResolve) {
|
||||
for (auto& [req, blob_contents] : _blob_reqs) {
|
||||
assert(req);
|
||||
if (req->status->ok()) {
|
||||
const uint64_t adjustment =
|
||||
read_options.verify_checksums
|
||||
? BlobLogRecord::CalculateAdjustmentForRecordHeader(
|
||||
req->user_key->size())
|
||||
: 0;
|
||||
RecordLazyRead(statistics_, req->len + adjustment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
total_bytes += _bytes_read;
|
||||
if (bytes_read) {
|
||||
*bytes_read = total_bytes;
|
||||
@@ -894,6 +1095,138 @@ void BlobSource::MultiGetBlobFromOneFile(const ReadOptions& read_options,
|
||||
}
|
||||
}
|
||||
|
||||
void BlobSource::MultiGetBlobRange(
|
||||
const ReadOptions& read_options,
|
||||
autovector<BlobFileRangeReadRequests>& blob_reqs, uint64_t* bytes_read) {
|
||||
assert(blob_reqs.size() > 0);
|
||||
|
||||
uint64_t total_bytes_read = 0;
|
||||
uint64_t bytes_read_in_file = 0;
|
||||
|
||||
for (auto& [file_number, file_size, blob_reqs_in_file] : blob_reqs) {
|
||||
// Sort by effective read offset so the file system layer can coalesce
|
||||
// adjacent sub-range reads within one MultiRead.
|
||||
std::sort(blob_reqs_in_file.begin(), blob_reqs_in_file.end(),
|
||||
[](const BlobRangeReadRequest& lhs,
|
||||
const BlobRangeReadRequest& rhs) -> bool {
|
||||
return lhs.offset + lhs.range_offset <
|
||||
rhs.offset + rhs.range_offset;
|
||||
});
|
||||
|
||||
MultiGetBlobRangeFromOneFile(read_options, file_number, file_size,
|
||||
blob_reqs_in_file, &bytes_read_in_file);
|
||||
|
||||
total_bytes_read += bytes_read_in_file;
|
||||
bytes_read_in_file = 0; // reset: some early-return paths do not write it
|
||||
}
|
||||
|
||||
if (bytes_read) {
|
||||
*bytes_read = total_bytes_read;
|
||||
}
|
||||
}
|
||||
|
||||
void BlobSource::MultiGetBlobRangeFromOneFile(
|
||||
const ReadOptions& read_options, uint64_t file_number,
|
||||
uint64_t /*file_size*/, autovector<BlobRangeReadRequest>& blob_reqs,
|
||||
uint64_t* bytes_read) {
|
||||
const size_t num_blobs = blob_reqs.size();
|
||||
assert(num_blobs > 0);
|
||||
assert(num_blobs <= MultiGetContext::MAX_BATCH_SIZE);
|
||||
// Range reads are only issued while resolving a lazy result (partial reads
|
||||
// are lazy-only), so the lazy read stats below are recorded unconditionally.
|
||||
assert(read_options.io_activity == Env::IOActivity::kLazyResolve);
|
||||
|
||||
using Mask = uint64_t;
|
||||
Mask cache_hit_mask = 0;
|
||||
|
||||
uint64_t total_bytes = 0;
|
||||
const OffsetableCacheKey base_cache_key(db_id_, db_session_id_, file_number);
|
||||
|
||||
// Probe the whole-value cache per request; on a hit, slice the requested
|
||||
// sub-range (zero-copy, no disk read). A partial read is never cache-filled.
|
||||
if (blob_cache_) {
|
||||
size_t cached_blob_count = 0;
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
BlobRangeReadRequest& req = blob_reqs[i];
|
||||
assert(req.status);
|
||||
|
||||
CacheHandleGuard<BlobContents> blob_handle;
|
||||
const CacheKey cache_key = base_cache_key.WithOffset(req.offset);
|
||||
const Status s = GetBlobFromCache(cache_key.AsSlice(), &blob_handle);
|
||||
if (s.ok()) {
|
||||
*req.status = s;
|
||||
PinCacheHitSubRange(&blob_handle, req.range_offset, req.range_length,
|
||||
req.result);
|
||||
++cached_blob_count;
|
||||
cache_hit_mask |= (Mask{1} << i); // hit -> no disk read
|
||||
}
|
||||
}
|
||||
|
||||
if (cached_blob_count == num_blobs) {
|
||||
if (bytes_read) {
|
||||
*bytes_read = 0;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const bool no_io = read_options.read_tier == kBlockCacheTier;
|
||||
if (no_io) {
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
if (!(cache_hit_mask & (Mask{1} << i))) {
|
||||
assert(blob_reqs[i].status);
|
||||
*blob_reqs[i].status =
|
||||
Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
autovector<std::pair<BlobRangeReadRequest*, std::unique_ptr<BlobContents>>>
|
||||
_blob_reqs;
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
if (!(cache_hit_mask & (Mask{1} << i))) {
|
||||
_blob_reqs.emplace_back(&blob_reqs[i], std::unique_ptr<BlobContents>());
|
||||
}
|
||||
}
|
||||
|
||||
// Lazy range reads run against a pinned SuperVersion (max_open_files == -1),
|
||||
// and blob file numbers are never reused, so the stale cached-reader race
|
||||
// that MultiGetBlobFromOneFile guards against cannot occur here; a Corruption
|
||||
// is simply surfaced per request. Hence no reader-refresh retry and no
|
||||
// cache-fill (a partial value cannot represent the whole-record cache entry).
|
||||
CacheHandleGuard<BlobFileReader> blob_file_reader;
|
||||
Status s = blob_file_cache_->GetBlobFileReader(read_options, file_number,
|
||||
&blob_file_reader);
|
||||
if (!s.ok()) {
|
||||
for (auto& blob_req : _blob_reqs) {
|
||||
assert(blob_req.first);
|
||||
assert(blob_req.first->status);
|
||||
*blob_req.first->status = s;
|
||||
}
|
||||
return;
|
||||
}
|
||||
assert(blob_file_reader.GetValue());
|
||||
|
||||
uint64_t _bytes_read = 0;
|
||||
blob_file_reader.GetValue()->MultiGetBlobRange(read_options, _blob_reqs,
|
||||
&_bytes_read);
|
||||
|
||||
for (auto& [req, blob_contents] : _blob_reqs) {
|
||||
assert(req);
|
||||
if (req->status->ok()) {
|
||||
PinOwnedBlob(&blob_contents, req->result);
|
||||
RecordLazyRead(statistics_, req->range_length);
|
||||
RecordLazyPartialRead(statistics_, req->range_length, req->value_size);
|
||||
}
|
||||
}
|
||||
|
||||
total_bytes += _bytes_read;
|
||||
if (bytes_read) {
|
||||
*bytes_read = total_bytes;
|
||||
}
|
||||
}
|
||||
|
||||
bool BlobSource::TEST_BlobInCache(uint64_t file_number, uint64_t file_size,
|
||||
uint64_t offset, size_t* charge) const {
|
||||
const CacheKey cache_key = GetCacheKey(file_number, file_size, offset);
|
||||
|
||||
@@ -28,6 +28,30 @@ class Slice;
|
||||
class RandomAccessFileReader;
|
||||
enum ChecksumType : char;
|
||||
|
||||
// One whole-record request for BlobSource::MultiGetSimpleGen2Blob (the batched
|
||||
// counterpart of GetSimpleGen2Blob). The per-file context (base cache key,
|
||||
// file, checksum context) is passed once to the batch call; these are the
|
||||
// per-record inputs and outputs.
|
||||
struct SimpleGen2BlobReadRequest {
|
||||
uint64_t record_offset = 0;
|
||||
uint64_t payload_size = 0;
|
||||
CompressionType expected_compression = kNoCompression;
|
||||
PinnableSlice* result = nullptr;
|
||||
Status* status = nullptr;
|
||||
};
|
||||
|
||||
// One sub-range request for BlobSource::MultiGetSimpleGen2BlobRange (the
|
||||
// batched counterpart of GetSimpleGen2BlobRange).
|
||||
struct SimpleGen2BlobRangeReadRequest {
|
||||
uint64_t record_offset = 0;
|
||||
uint64_t payload_size = 0;
|
||||
uint64_t range_offset = 0;
|
||||
size_t range_length = 0;
|
||||
CompressionType expected_compression = kNoCompression;
|
||||
PinnableSlice* result = nullptr;
|
||||
Status* status = nullptr;
|
||||
};
|
||||
|
||||
// BlobSource is a class that provides universal access to blobs, regardless of
|
||||
// whether they are in the blob cache, secondary cache, or (remote) storage.
|
||||
// Depending on user settings, it always fetch blobs from multi-tier cache and
|
||||
@@ -146,6 +170,33 @@ class BlobSource {
|
||||
uint64_t range_offset, size_t range_length,
|
||||
PinnableSlice* value, uint64_t* bytes_read);
|
||||
|
||||
// Batched counterpart of GetSimpleGen2Blob: resolves `num_records` whole
|
||||
// embedded (SimpleGen2Blob) records from one file, coalescing all cache-miss
|
||||
// reads into a single MultiRead. The per-file context (base cache key, file,
|
||||
// checksum type, base context checksum) is shared by all records; per-record
|
||||
// inputs/outputs are in `reqs`. Each record independently probes the blob
|
||||
// cache and, on a miss, is read + verified from the file; whole records are
|
||||
// cache-filled (when configured and fill_cache) exactly as GetSimpleGen2Blob.
|
||||
// Per-record outcome is written to *reqs[i].status.
|
||||
void MultiGetSimpleGen2Blob(const ReadOptions& read_options,
|
||||
const OffsetableCacheKey& base_cache_key,
|
||||
RandomAccessFileReader* file,
|
||||
ChecksumType checksum_type,
|
||||
uint32_t base_context_checksum,
|
||||
size_t num_records,
|
||||
SimpleGen2BlobReadRequest* reqs);
|
||||
|
||||
// Batched counterpart of GetSimpleGen2BlobRange: resolves `num_records`
|
||||
// uncompressed embedded sub-ranges from one file, coalescing cache-miss reads
|
||||
// into a single MultiRead. Like GetSimpleGen2BlobRange, each record probes
|
||||
// the whole-payload cache (slicing on a hit) and a partial read is never
|
||||
// inserted into the cache. Per-record outcome is in *reqs[i].status.
|
||||
void MultiGetSimpleGen2BlobRange(const ReadOptions& read_options,
|
||||
const OffsetableCacheKey& base_cache_key,
|
||||
RandomAccessFileReader* file,
|
||||
size_t num_records,
|
||||
SimpleGen2BlobRangeReadRequest* reqs);
|
||||
|
||||
// Read multiple blobs from the underlying cache or blob file(s).
|
||||
//
|
||||
// If successful, returns ok and sets "result" in the elements of "blob_reqs"
|
||||
@@ -185,6 +236,24 @@ class BlobSource {
|
||||
autovector<BlobReadRequest>& blob_reqs,
|
||||
uint64_t* bytes_read);
|
||||
|
||||
// Byte-range (partial) multi-read counterpart of MultiGetBlob: for each
|
||||
// request reads only its sub-range of an *uncompressed* blob value, from the
|
||||
// blob cache (whole-value hit, sliced) or coalesced disk reads (one MultiRead
|
||||
// per file). Like GetBlobRange, a partial read never fills the blob cache.
|
||||
// Groups requests by file and dispatches each file via
|
||||
// MultiGetBlobRangeFromOneFile. Per-request outcome is in
|
||||
// *blob_reqs[..].status.
|
||||
void MultiGetBlobRange(const ReadOptions& read_options,
|
||||
autovector<BlobFileRangeReadRequests>& blob_reqs,
|
||||
uint64_t* bytes_read);
|
||||
|
||||
// Byte-range (partial) multi-read counterpart of MultiGetBlobFromOneFile for
|
||||
// a single blob file. See MultiGetBlobRange.
|
||||
void MultiGetBlobRangeFromOneFile(const ReadOptions& read_options,
|
||||
uint64_t file_number, uint64_t file_size,
|
||||
autovector<BlobRangeReadRequest>& blob_reqs,
|
||||
uint64_t* bytes_read);
|
||||
|
||||
inline Status GetBlobFileReader(
|
||||
const ReadOptions& read_options, uint64_t blob_file_number,
|
||||
CacheHandleGuard<BlobFileReader>* blob_file_reader) {
|
||||
|
||||
@@ -18,6 +18,18 @@ class BlobIndex;
|
||||
class PinnableSlice;
|
||||
struct ReadOptions;
|
||||
|
||||
// One request for SameFileBlobReader::MultiGetSameFileBlob (the batched
|
||||
// counterpart of GetSameFileBlob). See GetSameFileBlob for the per-field
|
||||
// semantics (range_length == kWholeBlobLength selects the whole value).
|
||||
struct SameFileBlobReadRequest {
|
||||
const BlobIndex* blob_index = nullptr;
|
||||
uint64_t range_offset = 0;
|
||||
size_t range_length = kWholeBlobLength;
|
||||
BlobVerifyPolicy verify_policy = BlobVerifyPolicy::kVerifyIfNoAmplification;
|
||||
PinnableSlice* result = nullptr;
|
||||
Status* status = nullptr;
|
||||
};
|
||||
|
||||
// Narrow interface for resolving a same-file ("embedded") blob reference
|
||||
// against the physical file that currently holds the table entry. Implemented
|
||||
// by BlockBasedTable, which owns the reader context (file, footer, blob cache)
|
||||
@@ -53,6 +65,26 @@ class SameFileBlobReader {
|
||||
uint64_t range_offset, size_t range_length,
|
||||
BlobVerifyPolicy verify_policy,
|
||||
PinnableSlice* value) const = 0;
|
||||
|
||||
// Batched counterpart of GetSameFileBlob: resolves `num_reads` same-file blob
|
||||
// references (whole or sub-range, mixed) against this reader, writing each
|
||||
// outcome to *reqs[i].status. Implementations may coalesce the underlying
|
||||
// disk reads into a single MultiRead. The default implementation simply loops
|
||||
// over GetSameFileBlob (correct, but not coalesced); BlockBasedTable
|
||||
// overrides it to coalesce reads of embedded records in the same SST.
|
||||
virtual void MultiGetSameFileBlob(const ReadOptions& read_options,
|
||||
size_t num_reads,
|
||||
SameFileBlobReadRequest* reqs) const {
|
||||
for (size_t i = 0; i < num_reads; ++i) {
|
||||
SameFileBlobReadRequest& req = reqs[i];
|
||||
const Status s =
|
||||
GetSameFileBlob(read_options, *req.blob_index, req.range_offset,
|
||||
req.range_length, req.verify_policy, req.result);
|
||||
if (req.status) {
|
||||
*req.status = s;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
+2
-1
@@ -207,7 +207,8 @@ Status BuildTable(
|
||||
std::move(file), fname, file_options, ioptions.clock, io_tracer,
|
||||
ioptions.stats, Histograms::SST_WRITE_MICROS, ioptions.listeners,
|
||||
ioptions.file_checksum_gen_factory.get(),
|
||||
tmp_set.Contains(FileType::kTableFile), false));
|
||||
tmp_set.Contains(FileType::kTableFile),
|
||||
tmp_set.Contains(FileType::kTableFile)));
|
||||
|
||||
builder = NewTableBuilder(tboptions, file_writer.get());
|
||||
}
|
||||
|
||||
@@ -2623,6 +2623,59 @@ char* rocksdb_get(rocksdb_t* db, const rocksdb_readoptions_t* options,
|
||||
return result;
|
||||
}
|
||||
|
||||
static char* rocksdb_get_with_metadata_impl(
|
||||
rocksdb_t* db, const rocksdb_readoptions_t* options,
|
||||
ColumnFamilyHandle* column_family, const char* key, size_t keylen,
|
||||
size_t* vallen, char** timestamp, size_t* timestamp_len,
|
||||
unsigned char* newer_version_present, char** errptr) {
|
||||
ROCKSDB_NAMESPACE::OutputMetadata output_metadata;
|
||||
if (timestamp != nullptr && timestamp_len != nullptr) {
|
||||
output_metadata.WantTimestamp();
|
||||
}
|
||||
if (newer_version_present != nullptr) {
|
||||
output_metadata.WantNewerVersionPresent();
|
||||
}
|
||||
|
||||
PinnableSlice pinnable_val;
|
||||
const Status s =
|
||||
db->rep->GetWithMetadata(options->rep, column_family, Slice(key, keylen),
|
||||
&pinnable_val, &output_metadata);
|
||||
if (newer_version_present != nullptr) {
|
||||
*newer_version_present = *output_metadata.newer_version_present ? 1 : 0;
|
||||
}
|
||||
|
||||
char* result = nullptr;
|
||||
if (s.ok()) {
|
||||
*vallen = pinnable_val.size();
|
||||
result = CopyString(pinnable_val);
|
||||
if (output_metadata.timestamp.has_value()) {
|
||||
*timestamp_len = output_metadata.timestamp->size();
|
||||
*timestamp = CopyString(*output_metadata.timestamp);
|
||||
}
|
||||
} else {
|
||||
*vallen = 0;
|
||||
if (output_metadata.timestamp.has_value()) {
|
||||
*timestamp = nullptr;
|
||||
*timestamp_len = 0;
|
||||
}
|
||||
if (!s.IsNotFound()) {
|
||||
SaveError(errptr, s);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
char* rocksdb_get_with_metadata(rocksdb_t* db,
|
||||
const rocksdb_readoptions_t* options,
|
||||
const char* key, size_t keylen, size_t* vallen,
|
||||
char** timestamp, size_t* timestamp_len,
|
||||
unsigned char* newer_version_present,
|
||||
char** errptr) {
|
||||
return rocksdb_get_with_metadata_impl(
|
||||
db, options, db->rep->DefaultColumnFamily(), key, keylen, vallen,
|
||||
timestamp, timestamp_len, newer_version_present, errptr);
|
||||
}
|
||||
|
||||
char* rocksdb_get_cf(rocksdb_t* db, const rocksdb_readoptions_t* options,
|
||||
rocksdb_column_family_handle_t* column_family,
|
||||
const char* key, size_t keylen, size_t* vallen,
|
||||
@@ -2645,6 +2698,16 @@ char* rocksdb_get_cf(rocksdb_t* db, const rocksdb_readoptions_t* options,
|
||||
return result;
|
||||
}
|
||||
|
||||
char* rocksdb_get_cf_with_metadata(
|
||||
rocksdb_t* db, const rocksdb_readoptions_t* options,
|
||||
rocksdb_column_family_handle_t* column_family, const char* key,
|
||||
size_t keylen, size_t* vallen, char** timestamp, size_t* timestamp_len,
|
||||
unsigned char* newer_version_present, char** errptr) {
|
||||
return rocksdb_get_with_metadata_impl(
|
||||
db, options, column_family->rep, key, keylen, vallen, timestamp,
|
||||
timestamp_len, newer_version_present, errptr);
|
||||
}
|
||||
|
||||
char* rocksdb_get_with_ts(rocksdb_t* db, const rocksdb_readoptions_t* options,
|
||||
const char* key, size_t keylen, size_t* vallen,
|
||||
char** ts, size_t* tslen, char** errptr) {
|
||||
@@ -2736,6 +2799,72 @@ void rocksdb_multi_get(rocksdb_t* db, const rocksdb_readoptions_t* options,
|
||||
}
|
||||
}
|
||||
|
||||
static void rocksdb_multi_get_with_metadata_impl(
|
||||
rocksdb_t* db, const rocksdb_readoptions_t* options,
|
||||
ColumnFamilyHandle* const* column_families, size_t num_keys,
|
||||
const char* const* keys_list, const size_t* keys_list_sizes,
|
||||
char** values_list, size_t* values_list_sizes, char** timestamp_list,
|
||||
size_t* timestamp_list_sizes, unsigned char* newer_version_present,
|
||||
char** errs) {
|
||||
std::unique_ptr<Slice[]> keys(new Slice[num_keys]);
|
||||
std::vector<PinnableSlice> values(num_keys);
|
||||
std::vector<Status> statuses(num_keys);
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
keys[i] = Slice(keys_list[i], keys_list_sizes[i]);
|
||||
}
|
||||
|
||||
ROCKSDB_NAMESPACE::MultiGetOutputMetadata output_metadata;
|
||||
if (timestamp_list != nullptr && timestamp_list_sizes != nullptr) {
|
||||
output_metadata.WantTimestamps();
|
||||
}
|
||||
if (newer_version_present != nullptr) {
|
||||
output_metadata.WantNewerVersionPresent();
|
||||
}
|
||||
db->rep->MultiGetWithMetadata(options->rep, num_keys, column_families,
|
||||
keys.get(), values.data(), statuses.data(),
|
||||
&output_metadata);
|
||||
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
if (newer_version_present != nullptr) {
|
||||
newer_version_present[i] =
|
||||
(*output_metadata.newer_version_present)[i] ? 1 : 0;
|
||||
}
|
||||
if (statuses[i].ok()) {
|
||||
values_list[i] = CopyString(values[i]);
|
||||
values_list_sizes[i] = values[i].size();
|
||||
if (output_metadata.timestamps.has_value()) {
|
||||
timestamp_list[i] = CopyString((*output_metadata.timestamps)[i]);
|
||||
timestamp_list_sizes[i] = (*output_metadata.timestamps)[i].size();
|
||||
}
|
||||
errs[i] = nullptr;
|
||||
} else {
|
||||
values_list[i] = nullptr;
|
||||
values_list_sizes[i] = 0;
|
||||
if (output_metadata.timestamps.has_value()) {
|
||||
timestamp_list[i] = nullptr;
|
||||
timestamp_list_sizes[i] = 0;
|
||||
}
|
||||
errs[i] = statuses[i].IsNotFound()
|
||||
? nullptr
|
||||
: strdup(statuses[i].ToString().c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void rocksdb_multi_get_with_metadata(
|
||||
rocksdb_t* db, const rocksdb_readoptions_t* options, size_t num_keys,
|
||||
const char* const* keys_list, const size_t* keys_list_sizes,
|
||||
char** values_list, size_t* values_list_sizes, char** timestamp_list,
|
||||
size_t* timestamp_list_sizes, unsigned char* newer_version_present,
|
||||
char** errs) {
|
||||
std::vector<ColumnFamilyHandle*> column_families(
|
||||
num_keys, db->rep->DefaultColumnFamily());
|
||||
rocksdb_multi_get_with_metadata_impl(
|
||||
db, options, column_families.data(), num_keys, keys_list, keys_list_sizes,
|
||||
values_list, values_list_sizes, timestamp_list, timestamp_list_sizes,
|
||||
newer_version_present, errs);
|
||||
}
|
||||
|
||||
void rocksdb_multi_get_with_ts(rocksdb_t* db,
|
||||
const rocksdb_readoptions_t* options,
|
||||
size_t num_keys, const char* const* keys_list,
|
||||
@@ -2811,6 +2940,24 @@ void rocksdb_multi_get_cf(
|
||||
}
|
||||
}
|
||||
|
||||
void rocksdb_multi_get_cf_with_metadata(
|
||||
rocksdb_t* db, const rocksdb_readoptions_t* options,
|
||||
const rocksdb_column_family_handle_t* const* column_families,
|
||||
size_t num_keys, const char* const* keys_list,
|
||||
const size_t* keys_list_sizes, char** values_list,
|
||||
size_t* values_list_sizes, char** timestamp_list,
|
||||
size_t* timestamp_list_sizes, unsigned char* newer_version_present,
|
||||
char** errs) {
|
||||
std::vector<ColumnFamilyHandle*> cfs(num_keys);
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
cfs[i] = column_families[i]->rep;
|
||||
}
|
||||
rocksdb_multi_get_with_metadata_impl(
|
||||
db, options, cfs.data(), num_keys, keys_list, keys_list_sizes,
|
||||
values_list, values_list_sizes, timestamp_list, timestamp_list_sizes,
|
||||
newer_version_present, errs);
|
||||
}
|
||||
|
||||
void rocksdb_multi_get_cf_with_ts(
|
||||
rocksdb_t* db, const rocksdb_readoptions_t* options,
|
||||
const rocksdb_column_family_handle_t* const* column_families,
|
||||
@@ -5120,6 +5267,11 @@ uint64_t rocksdb_table_properties_separate_key_value_in_data_block(
|
||||
return props->rep.separate_key_value_in_data_block;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_table_properties_lsm_info_at_creation(
|
||||
const rocksdb_table_properties_t* props) {
|
||||
return props->rep.lsm_info_at_creation;
|
||||
}
|
||||
|
||||
const char* rocksdb_table_properties_db_id(
|
||||
const rocksdb_table_properties_t* props, size_t* size) {
|
||||
*size = props->rep.db_id.size();
|
||||
@@ -9893,6 +10045,16 @@ unsigned char rocksdb_options_get_remote_compaction_manifest_floor(
|
||||
return opt->rep.remote_compaction_manifest_floor;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_use_session_tmp_dir_for_remote_compaction(
|
||||
rocksdb_options_t* opt, unsigned char v) {
|
||||
opt->rep.use_session_tmp_dir_for_remote_compaction = v;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_options_get_use_session_tmp_dir_for_remote_compaction(
|
||||
rocksdb_options_t* opt) {
|
||||
return opt->rep.use_session_tmp_dir_for_remote_compaction;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_lowest_used_cache_tier(rocksdb_options_t* opt, int v) {
|
||||
opt->rep.lowest_used_cache_tier =
|
||||
static_cast<decltype(opt->rep.lowest_used_cache_tier)>(v);
|
||||
@@ -11204,6 +11366,16 @@ int rocksdb_ingestexternalfileoptions_get_file_opening_threads(
|
||||
|
||||
/* OpenAndCompactOptions */
|
||||
|
||||
void rocksdb_open_and_compact_options_set_max_secondary_open_retries(
|
||||
rocksdb_open_and_compact_options_t* opt, uint32_t v) {
|
||||
opt->rep.max_secondary_open_retries = v;
|
||||
}
|
||||
|
||||
uint32_t rocksdb_open_and_compact_options_get_max_secondary_open_retries(
|
||||
rocksdb_open_and_compact_options_t* opt) {
|
||||
return opt->rep.max_secondary_open_retries;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_open_and_compact_options_get_allow_resumption(
|
||||
rocksdb_open_and_compact_options_t* opt) {
|
||||
return opt->rep.allow_resumption;
|
||||
|
||||
+29
@@ -2474,6 +2474,35 @@ int main(int argc, char** argv) {
|
||||
CheckNoError(err);
|
||||
rocksdb_readoptions_set_snapshot(roptions, snap);
|
||||
CheckGet(db, roptions, "foo", "hello");
|
||||
|
||||
size_t metadata_value_len = 0;
|
||||
unsigned char newer_version_present = 0;
|
||||
char* metadata_value =
|
||||
rocksdb_get_with_metadata(db, roptions, "foo", 3, &metadata_value_len,
|
||||
NULL, NULL, &newer_version_present, &err);
|
||||
// NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling)
|
||||
CheckNoError(err);
|
||||
CheckEqual("hello", metadata_value, metadata_value_len);
|
||||
// NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling)
|
||||
CheckCondition(newer_version_present == 1);
|
||||
Free(&metadata_value);
|
||||
|
||||
const char* metadata_keys[] = {"foo"};
|
||||
const size_t metadata_key_sizes[] = {3};
|
||||
char* metadata_values[1];
|
||||
size_t metadata_value_sizes[1];
|
||||
unsigned char newer_versions[1];
|
||||
char* metadata_errors[1];
|
||||
rocksdb_multi_get_with_metadata(
|
||||
db, roptions, 1, metadata_keys, metadata_key_sizes, metadata_values,
|
||||
metadata_value_sizes, NULL, NULL, newer_versions, metadata_errors);
|
||||
// NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling)
|
||||
CheckCondition(metadata_errors[0] == NULL);
|
||||
CheckEqual("hello", metadata_values[0], metadata_value_sizes[0]);
|
||||
// NOLINTNEXTLINE(clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling)
|
||||
CheckCondition(newer_versions[0] == 1);
|
||||
Free(&metadata_values[0]);
|
||||
|
||||
rocksdb_readoptions_set_snapshot(roptions, NULL);
|
||||
CheckGet(db, roptions, "foo", NULL);
|
||||
rocksdb_release_snapshot(db, snap);
|
||||
|
||||
@@ -2634,7 +2634,8 @@ Status CompactionJob::OpenCompactionOutputFile(SubcompactionState* sub_compact,
|
||||
std::move(writable_file), fname, fo_copy, db_options_.clock, io_tracer_,
|
||||
db_options_.stats, Histograms::SST_WRITE_MICROS, listeners,
|
||||
db_options_.file_checksum_gen_factory.get(),
|
||||
tmp_set.Contains(FileType::kTableFile), false));
|
||||
tmp_set.Contains(FileType::kTableFile),
|
||||
tmp_set.Contains(FileType::kTableFile)));
|
||||
|
||||
// TODO(hx235): pass in the correct `oldest_key_time` instead of `0`
|
||||
const ReadOptions read_options(Env::IOActivity::kCompaction);
|
||||
|
||||
@@ -591,6 +591,7 @@ class CompactionJob {
|
||||
struct CompactionServiceInput {
|
||||
std::string cf_name;
|
||||
|
||||
// Snapshot sequence numbers in strictly increasing order.
|
||||
std::vector<SequenceNumber> snapshots;
|
||||
|
||||
// SST files for compaction, it should already be expended to include all the
|
||||
|
||||
@@ -1572,6 +1572,10 @@ TEST_F(CompactionJobTest, InputSerialization) {
|
||||
while (!rnd.OneIn(10)) {
|
||||
input.snapshots.emplace_back(rnd64.Uniform(UINT64_MAX));
|
||||
}
|
||||
std::sort(input.snapshots.begin(), input.snapshots.end());
|
||||
input.snapshots.erase(
|
||||
std::unique(input.snapshots.begin(), input.snapshots.end()),
|
||||
input.snapshots.end());
|
||||
while (!rnd.OneIn(10)) {
|
||||
input.input_files.emplace_back(rnd.RandomString(
|
||||
rnd.Uniform(kStrMaxLen - 1) +
|
||||
@@ -1641,6 +1645,25 @@ TEST_F(CompactionJobTest, InputSerialization) {
|
||||
ASSERT_TRUE(s.IsNotSupported());
|
||||
}
|
||||
|
||||
TEST_F(CompactionJobTest, OpenAndCompactRejectsUnorderedSnapshots) {
|
||||
for (const std::vector<SequenceNumber>& snapshots :
|
||||
{std::vector<SequenceNumber>{2, 1}, std::vector<SequenceNumber>{1, 1}}) {
|
||||
CompactionServiceInput input;
|
||||
input.snapshots = snapshots;
|
||||
|
||||
std::string serialized_input;
|
||||
ASSERT_OK(input.Write(&serialized_input));
|
||||
|
||||
std::string serialized_result;
|
||||
Status s =
|
||||
DB::OpenAndCompact("", "output", serialized_input, &serialized_result,
|
||||
CompactionServiceOptionsOverride());
|
||||
ASSERT_TRUE(s.IsInvalidArgument());
|
||||
ASSERT_NE(s.ToString().find("snapshots must be strictly increasing"),
|
||||
std::string::npos);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(CompactionJobTest, ResultSerialization) {
|
||||
// Setup a random CompactionServiceResult
|
||||
CompactionServiceResult result;
|
||||
|
||||
@@ -84,7 +84,6 @@ CompactionJob::ProcessKeyValueCompactionWithCompactionService(
|
||||
compaction->is_full_compaction(), compaction->is_manual_compaction(),
|
||||
compaction->bottommost_level(), compaction->start_level(),
|
||||
compaction->output_level());
|
||||
|
||||
CompactionServiceScheduleResponse response =
|
||||
db_options_.compaction_service->Schedule(info, compaction_input_binary);
|
||||
switch (response.status) {
|
||||
@@ -1085,9 +1084,19 @@ Status CompactionServiceInput::Read(const std::string& data_str,
|
||||
ConfigOptions cf;
|
||||
cf.invoke_prepare_options = false;
|
||||
cf.ignore_unknown_options = true;
|
||||
return OptionTypeInfo::ParseType(
|
||||
Status s = OptionTypeInfo::ParseType(
|
||||
cf, data_str.substr(sizeof(BinaryFormatVersion)), cs_input_type_info,
|
||||
obj);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
for (size_t i = 1; i < obj->snapshots.size(); ++i) {
|
||||
if (obj->snapshots[i - 1] >= obj->snapshots[i]) {
|
||||
return Status::InvalidArgument(
|
||||
"CompactionServiceInput snapshots must be strictly increasing");
|
||||
}
|
||||
}
|
||||
return Status::OK();
|
||||
} else {
|
||||
return Status::NotSupported(
|
||||
"Compaction Service Input data version not supported: " +
|
||||
|
||||
@@ -5,9 +5,12 @@
|
||||
|
||||
#include <atomic>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "db/db_test_util.h"
|
||||
#include "file/file_util.h"
|
||||
#include "file/filename.h"
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/advanced_compression.h"
|
||||
#include "rocksdb/filter_policy.h"
|
||||
@@ -81,6 +84,82 @@ size_t CountFirstCountsFieldElements(const std::string& s) {
|
||||
return count;
|
||||
}
|
||||
|
||||
// Simulates a filesystem without POSIX read-after-unlink semantics (a remote
|
||||
// or object store): once the primary renames a freshly written CURRENT over
|
||||
// the old one, a reader that already opened the previous CURRENT object can no
|
||||
// longer read it and the filesystem reports the path as gone rather than
|
||||
// serving the unlinked object. Fails the first `fail_times` reads of a CURRENT
|
||||
// file that way; later reads succeed, as they do once the reader picks up the
|
||||
// replacement.
|
||||
class VanishingCurrentFileFS : public FileSystemWrapper {
|
||||
public:
|
||||
explicit VanishingCurrentFileFS(const std::shared_ptr<FileSystem>& base)
|
||||
: FileSystemWrapper(base) {}
|
||||
|
||||
static const char* kClassName() { return "VanishingCurrentFileFS"; }
|
||||
const char* Name() const override { return kClassName(); }
|
||||
|
||||
IOStatus NewSequentialFile(const std::string& fname, const FileOptions& opts,
|
||||
std::unique_ptr<FSSequentialFile>* result,
|
||||
IODebugContext* dbg) override {
|
||||
IOStatus io_s = target()->NewSequentialFile(fname, opts, result, dbg);
|
||||
if (io_s.ok() && IsCurrentFileName(fname)) {
|
||||
*result = std::make_unique<MaybeVanishedFile>(std::move(*result), this);
|
||||
}
|
||||
return io_s;
|
||||
}
|
||||
|
||||
void SetFailuresToInject(int failures) {
|
||||
failures_to_inject_.store(failures);
|
||||
}
|
||||
|
||||
int injected_failures() const { return injected_failures_.load(); }
|
||||
|
||||
private:
|
||||
static bool IsCurrentFileName(const std::string& fname) {
|
||||
const std::string suffix = "/" + kCurrentFileName;
|
||||
return fname.size() > suffix.size() &&
|
||||
fname.compare(fname.size() - suffix.size(), suffix.size(), suffix) ==
|
||||
0;
|
||||
}
|
||||
|
||||
// Returns true (consuming one injection) while failures remain.
|
||||
bool ConsumeFailure() {
|
||||
int remaining = failures_to_inject_.load();
|
||||
while (remaining > 0) {
|
||||
if (failures_to_inject_.compare_exchange_weak(remaining, remaining - 1)) {
|
||||
injected_failures_.fetch_add(1);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
class MaybeVanishedFile : public FSSequentialFileOwnerWrapper {
|
||||
public:
|
||||
MaybeVanishedFile(std::unique_ptr<FSSequentialFile>&& target,
|
||||
VanishingCurrentFileFS* fs)
|
||||
: FSSequentialFileOwnerWrapper(std::move(target)), fs_(fs) {}
|
||||
|
||||
IOStatus Read(size_t n, const IOOptions& options, Slice* result,
|
||||
char* scratch, IODebugContext* dbg) override {
|
||||
if (fs_->ConsumeFailure()) {
|
||||
return IOStatus::PathNotFound(
|
||||
"Failed to read CURRENT: the object was replaced and is no longer "
|
||||
"readable");
|
||||
}
|
||||
return FSSequentialFileOwnerWrapper::Read(n, options, result, scratch,
|
||||
dbg);
|
||||
}
|
||||
|
||||
private:
|
||||
VanishingCurrentFileFS* fs_;
|
||||
};
|
||||
|
||||
std::atomic<int> failures_to_inject_{0};
|
||||
std::atomic<int> injected_failures_{0};
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
class MyTestCompactionService : public CompactionService {
|
||||
@@ -113,6 +192,7 @@ class MyTestCompactionService : public CompactionService {
|
||||
start_info_ = info;
|
||||
assert(info.db_name == db_path_);
|
||||
std::string unique_id = Env::Default()->GenerateUniqueId();
|
||||
last_output_directory_name_ = unique_id;
|
||||
jobs_.emplace(unique_id, compaction_service_input);
|
||||
infos_.emplace(unique_id, info);
|
||||
CompactionServiceScheduleResponse response(
|
||||
@@ -149,10 +229,10 @@ class MyTestCompactionService : public CompactionService {
|
||||
|
||||
OpenAndCompactOptions options;
|
||||
options.canceled = &canceled_;
|
||||
options.max_secondary_open_retries = max_secondary_open_retries_;
|
||||
|
||||
Status s =
|
||||
DB::OpenAndCompact(options, db_path_, GetOutputPath(scheduled_job_id),
|
||||
compaction_input, result, options_override);
|
||||
Status s = DB::OpenAndCompact(options, db_path_, scheduled_job_id,
|
||||
compaction_input, result, options_override);
|
||||
{
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
if (is_override_wait_result_) {
|
||||
@@ -212,6 +292,11 @@ class MyTestCompactionService : public CompactionService {
|
||||
|
||||
int GetCompactionNum() { return compaction_num_.load(); }
|
||||
|
||||
std::string GetLastOutputDirectoryName() {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
return last_output_directory_name_;
|
||||
}
|
||||
|
||||
CompactionServiceJobInfo GetCompactionInfoForStart() { return start_info_; }
|
||||
CompactionServiceJobInfo GetCompactionInfoForWait() { return wait_info_; }
|
||||
|
||||
@@ -239,6 +324,10 @@ class MyTestCompactionService : public CompactionService {
|
||||
void SetCanceled(bool canceled) { canceled_ = canceled; }
|
||||
bool GetCanceled() { return canceled_; }
|
||||
|
||||
void SetMaxSecondaryOpenRetries(uint32_t retries) {
|
||||
max_secondary_open_retries_ = retries;
|
||||
}
|
||||
|
||||
void GetResult(CompactionServiceResult* deserialized) {
|
||||
CompactionServiceResult::Read(result_, deserialized).PermitUncheckedError();
|
||||
}
|
||||
@@ -256,9 +345,12 @@ class MyTestCompactionService : public CompactionService {
|
||||
std::map<std::string, std::string> jobs_;
|
||||
std::map<std::string, CompactionServiceJobInfo> infos_;
|
||||
std::string result_;
|
||||
std::string last_output_directory_name_;
|
||||
|
||||
std::string GetOutputPath(const std::string& scheduled_job_id) {
|
||||
return db_path_ + "/" + scheduled_job_id;
|
||||
return RemoteCompactionJobDir(
|
||||
db_path_, options_.use_session_tmp_dir_for_remote_compaction,
|
||||
scheduled_job_id);
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -280,6 +372,8 @@ class MyTestCompactionService : public CompactionService {
|
||||
std::atomic_int installation_callback_count_{0};
|
||||
std::atomic<CompactionServiceJobStatus> final_updated_status_{
|
||||
CompactionServiceJobStatus::kUseLocal};
|
||||
uint32_t max_secondary_open_retries_ =
|
||||
OpenAndCompactOptions().max_secondary_open_retries;
|
||||
|
||||
protected:
|
||||
std::atomic_bool canceled_{false};
|
||||
@@ -450,6 +544,27 @@ TEST_F(CompactionServiceTest, BasicCompactions) {
|
||||
}
|
||||
ASSERT_TRUE(s.IsAborted());
|
||||
|
||||
CompactionServiceResult result;
|
||||
my_cs->GetResult(&result);
|
||||
ASSERT_NOK(result.status);
|
||||
|
||||
// The aborted job's output files were never installed, so they are still in
|
||||
// the job's staging directory. Size them before reopening: the reopen starts
|
||||
// a new DB session, and DB::Open() deletes staging directories belonging to
|
||||
// any other session.
|
||||
uint64_t total_size = 0;
|
||||
Env* const env = options.env;
|
||||
ASSERT_TRUE(options.env != nullptr);
|
||||
for (const auto& output_file : result.output_files) {
|
||||
std::string file_name = result.output_path + "/" + output_file.file_name;
|
||||
|
||||
uint64_t file_size = 0;
|
||||
ASSERT_OK(env->GetFileSize(file_name, &file_size));
|
||||
ASSERT_GT(file_size, 0);
|
||||
total_size += file_size;
|
||||
}
|
||||
ASSERT_EQ(total_size, result.internal_stats.TotalBytesWritten());
|
||||
|
||||
// Test re-open and successful unique id verification
|
||||
std::atomic_int verify_passed{0};
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
@@ -464,13 +579,6 @@ TEST_F(CompactionServiceTest, BasicCompactions) {
|
||||
ReopenWithColumnFamilies({kDefaultColumnFamilyName, "cf_1", "cf_2", "cf_3"},
|
||||
options);
|
||||
ASSERT_GT(verify_passed, 0);
|
||||
CompactionServiceResult result;
|
||||
my_cs->GetResult(&result);
|
||||
if (s.IsAborted()) {
|
||||
ASSERT_NOK(result.status);
|
||||
} else {
|
||||
ASSERT_OK(result.status);
|
||||
}
|
||||
ASSERT_GE(result.internal_stats.output_level_stats.micros, 1);
|
||||
ASSERT_GE(result.internal_stats.output_level_stats.cpu_micros, 1);
|
||||
|
||||
@@ -478,17 +586,6 @@ TEST_F(CompactionServiceTest, BasicCompactions) {
|
||||
ASSERT_EQ(result.output_files.size(),
|
||||
result.internal_stats.output_level_stats.num_output_files);
|
||||
|
||||
uint64_t total_size = 0;
|
||||
for (auto output_file : result.output_files) {
|
||||
std::string file_name = result.output_path + "/" + output_file.file_name;
|
||||
|
||||
uint64_t file_size = 0;
|
||||
ASSERT_OK(options.env->GetFileSize(file_name, &file_size));
|
||||
ASSERT_GT(file_size, 0);
|
||||
total_size += file_size;
|
||||
}
|
||||
ASSERT_EQ(total_size, result.internal_stats.TotalBytesWritten());
|
||||
|
||||
ASSERT_TRUE(result.stats.is_remote_compaction);
|
||||
ASSERT_TRUE(result.stats.is_manual_compaction);
|
||||
ASSERT_FALSE(result.stats.is_full_compaction);
|
||||
@@ -2461,6 +2558,209 @@ TEST_F(CompactionServiceTest, RemoteCompactionManifestFloorMutable) {
|
||||
ASSERT_EQ(0, floor_check_count);
|
||||
}
|
||||
|
||||
namespace {
|
||||
// A remote compaction service whose output directory already contains an SST
|
||||
// left behind by a previously interrupted (canceled) fresh attempt, then runs
|
||||
// OpenAndCompact with allow_resumption=true. Used to verify that when
|
||||
// resumption is internally disabled, the output directory is cleaned before the
|
||||
// fresh compaction starts.
|
||||
class LeftoverOutputCompactionService : public MyTestCompactionService {
|
||||
public:
|
||||
using MyTestCompactionService::MyTestCompactionService;
|
||||
|
||||
CompactionServiceJobStatus Wait(const std::string& scheduled_job_id,
|
||||
std::string* result) override {
|
||||
std::string compaction_input;
|
||||
{
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
auto it = jobs_.find(scheduled_job_id);
|
||||
if (it == jobs_.end()) {
|
||||
return CompactionServiceJobStatus::kFailure;
|
||||
}
|
||||
compaction_input = it->second;
|
||||
jobs_.erase(it);
|
||||
}
|
||||
|
||||
CompactionServiceOptionsOverride options_override = GetOptionsOverride();
|
||||
Env* env = options_override.env;
|
||||
const std::string output_dir = GetOutputPath(scheduled_job_id);
|
||||
EXPECT_OK(env->CreateDirIfMissing(output_dir));
|
||||
|
||||
// An output SST left behind by a previously interrupted fresh attempt. Uses
|
||||
// a high file number the fresh compaction will not reuse, so whether it is
|
||||
// cleaned up is directly observable.
|
||||
leftover_file_ = MakeTableFileName(output_dir, 999999);
|
||||
{
|
||||
std::unique_ptr<WritableFile> f;
|
||||
EXPECT_OK(env->NewWritableFile(leftover_file_, &f, EnvOptions()));
|
||||
EXPECT_OK(f->Append("leftover"));
|
||||
EXPECT_OK(f->Close());
|
||||
}
|
||||
|
||||
OpenAndCompactOptions open_options;
|
||||
open_options.allow_resumption = true;
|
||||
Status s = DB::OpenAndCompact(open_options, db_path_, scheduled_job_id,
|
||||
compaction_input, result, options_override);
|
||||
{
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
result_ = *result;
|
||||
}
|
||||
if (!s.ok()) {
|
||||
return CompactionServiceJobStatus::kFailure;
|
||||
}
|
||||
leftover_cleaned_ = env->FileExists(leftover_file_).IsNotFound();
|
||||
return CompactionServiceJobStatus::kSuccess;
|
||||
}
|
||||
|
||||
bool leftover_cleaned() const { return leftover_cleaned_; }
|
||||
|
||||
private:
|
||||
std::string leftover_file_;
|
||||
bool leftover_cleaned_ = false;
|
||||
};
|
||||
} // namespace
|
||||
|
||||
TEST_F(CompactionServiceTest, CleansOutputDirWhenResumptionDisabled) {
|
||||
// Regression test: allow_resumption=true is requested, but resumption is
|
||||
// internally disabled because paranoid_file_checks=true is incompatible with
|
||||
// the resumption path's output-hash verification. OpenAndCompact then starts
|
||||
// a fresh compaction, which must clean the output directory first (honoring
|
||||
// the documented OpenAndCompactOptions::allow_resumption fallback contract),
|
||||
// so an output file left by a previously interrupted attempt does not collide
|
||||
// with the file numbers the fresh compaction reuses. Before the fix the
|
||||
// leftover file was kept, which under a no-reopen filesystem (as in the crash
|
||||
// test) fails the fresh compaction's NewWritableFile with a
|
||||
// "no-reopen-for-write contract" violation.
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
options.paranoid_file_checks = true; // disables resumption on the worker
|
||||
options.use_session_tmp_dir_for_remote_compaction = true;
|
||||
options.env = env_;
|
||||
auto statistics = CreateDBStatistics();
|
||||
|
||||
auto cs = std::make_shared<LeftoverOutputCompactionService>(
|
||||
dbname_, options, statistics,
|
||||
std::vector<std::shared_ptr<EventListener>>{},
|
||||
std::vector<std::shared_ptr<TablePropertiesCollectorFactory>>{});
|
||||
options.compaction_service = cs;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Two overlapping flushed files so the manual compaction has real work.
|
||||
ASSERT_OK(Put(Key(1), "v1"));
|
||||
ASSERT_OK(Put(Key(3), "v3"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put(Key(2), "v2"));
|
||||
ASSERT_OK(Put(Key(3), "v3new"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
|
||||
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
|
||||
|
||||
// The fresh compaction cleaned the leftover output before starting.
|
||||
ASSERT_TRUE(cs->leftover_cleaned());
|
||||
ASSERT_EQ(Get(Key(1)), "v1");
|
||||
ASSERT_EQ(Get(Key(2)), "v2");
|
||||
ASSERT_EQ(Get(Key(3)), "v3new");
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, RetriesSecondaryOpenWhenCurrentFileVanishes) {
|
||||
// The remote compaction worker opens the live primary's DB directory
|
||||
// read-only inside OpenAndCompact. The primary replaces CURRENT on every
|
||||
// MANIFEST rotation, and on a filesystem without POSIX read-after-unlink
|
||||
// semantics the CURRENT object the worker already opened becomes unreadable.
|
||||
// That is a transient race, so the secondary open must be retried; otherwise
|
||||
// a CompactionService that does not fall back to a local compaction
|
||||
// (db_stress with remote_compaction_failure_fall_back_to_local=false) turns
|
||||
// it into a primary background error that fails every later flush.
|
||||
auto fs = std::make_shared<VanishingCurrentFileFS>(env_->GetFileSystem());
|
||||
std::unique_ptr<Env> vanishing_env(NewCompositeEnv(fs));
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
options.env = vanishing_env.get();
|
||||
auto statistics = CreateDBStatistics();
|
||||
|
||||
auto cs = std::make_shared<MyTestCompactionService>(
|
||||
dbname_, options, statistics,
|
||||
std::vector<std::shared_ptr<EventListener>>{},
|
||||
std::vector<std::shared_ptr<TablePropertiesCollectorFactory>>{});
|
||||
cs->SetMaxSecondaryOpenRetries(3);
|
||||
options.compaction_service = cs;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Two overlapping flushed files so the manual compaction has real work.
|
||||
ASSERT_OK(Put(Key(1), "v1"));
|
||||
ASSERT_OK(Put(Key(3), "v3"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put(Key(2), "v2"));
|
||||
ASSERT_OK(Put(Key(3), "v3new"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Arm the injection only now: the primary is done opening, so the failed
|
||||
// CURRENT reads below happen in the worker's secondary open. Three failures
|
||||
// exercise a non-default retry limit and require the fourth attempt.
|
||||
fs->SetFailuresToInject(3);
|
||||
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
|
||||
const Status compact_s = db_->CompactRange(cro, nullptr, nullptr);
|
||||
|
||||
// Collect everything before closing: the DB must be closed while the
|
||||
// injecting Env is still alive, so no assertion may return early above it.
|
||||
const int injected_failures = fs->injected_failures();
|
||||
const int compactions = cs->GetCompactionNum();
|
||||
const std::string v1 = Get(Key(1));
|
||||
const std::string v2 = Get(Key(2));
|
||||
const std::string v3 = Get(Key(3));
|
||||
Close();
|
||||
|
||||
ASSERT_OK(compact_s);
|
||||
ASSERT_EQ(3, injected_failures);
|
||||
ASSERT_GE(compactions, 1);
|
||||
ASSERT_EQ(v1, "v1");
|
||||
ASSERT_EQ(v2, "v2");
|
||||
ASSERT_EQ(v3, "v3new");
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, CanDisableSecondaryOpenRetry) {
|
||||
auto fs = std::make_shared<VanishingCurrentFileFS>(env_->GetFileSystem());
|
||||
std::unique_ptr<Env> vanishing_env(NewCompositeEnv(fs));
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
options.env = vanishing_env.get();
|
||||
auto statistics = CreateDBStatistics();
|
||||
|
||||
auto cs = std::make_shared<MyTestCompactionService>(
|
||||
dbname_, options, statistics,
|
||||
std::vector<std::shared_ptr<EventListener>>{},
|
||||
std::vector<std::shared_ptr<TablePropertiesCollectorFactory>>{});
|
||||
cs->SetMaxSecondaryOpenRetries(0);
|
||||
options.compaction_service = cs;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(Put(Key(1), "v1"));
|
||||
ASSERT_OK(Put(Key(3), "v3"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put(Key(2), "v2"));
|
||||
ASSERT_OK(Put(Key(3), "v3new"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
fs->SetFailuresToInject(1);
|
||||
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
|
||||
const Status compact_s = db_->CompactRange(cro, nullptr, nullptr);
|
||||
const int injected_failures = fs->injected_failures();
|
||||
const int compactions = cs->GetCompactionNum();
|
||||
Close();
|
||||
|
||||
ASSERT_TRUE(compact_s.IsIncomplete());
|
||||
ASSERT_EQ(1, injected_failures);
|
||||
ASSERT_EQ(1, compactions);
|
||||
}
|
||||
|
||||
TEST_F(CompactionServiceTest, CompactionInfo) {
|
||||
Options options = CurrentOptions();
|
||||
ReopenWithCompactionService(&options);
|
||||
@@ -3146,7 +3446,7 @@ class ResumableCompactionService : public MyTestCompactionService {
|
||||
EXPECT_OK(statistics_->Reset());
|
||||
|
||||
Status s =
|
||||
DB::OpenAndCompact(options, db_path_, GetOutputPath(scheduled_job_id),
|
||||
DB::OpenAndCompact(options, db_path_, scheduled_job_id,
|
||||
compaction_input, &temp_result, override_options);
|
||||
|
||||
EXPECT_TRUE(s.IsManualCompactionPaused());
|
||||
@@ -3163,9 +3463,8 @@ class ResumableCompactionService : public MyTestCompactionService {
|
||||
std::string* result) {
|
||||
EXPECT_OK(statistics_->Reset());
|
||||
|
||||
Status s =
|
||||
DB::OpenAndCompact(options, db_path_, GetOutputPath(scheduled_job_id),
|
||||
compaction_input, result, override_options);
|
||||
Status s = DB::OpenAndCompact(options, db_path_, scheduled_job_id,
|
||||
compaction_input, result, override_options);
|
||||
|
||||
EXPECT_TRUE(s.ok());
|
||||
|
||||
@@ -3670,6 +3969,182 @@ TEST_F(ResumableCompactionKeyTypeTest, CancelAndResumeWithTimedPut) {
|
||||
|
||||
VerifyResumeBytes();
|
||||
}
|
||||
|
||||
// Covers the DB session temporary directory lifecycle when remote compaction
|
||||
// opts in to using it.
|
||||
class SessionTmpDirTest : public DBTestBase {
|
||||
public:
|
||||
SessionTmpDirTest()
|
||||
: DBTestBase("session_tmp_dir_test",
|
||||
/*env_do_fsync=*/true) {}
|
||||
|
||||
protected:
|
||||
// Opens with a CompactionService opted in to the session temporary directory.
|
||||
Options OptionsWithCompactionService() {
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
options.use_session_tmp_dir_for_remote_compaction = true;
|
||||
worker_statistics_ = CreateDBStatistics();
|
||||
compaction_service_ = std::make_shared<MyTestCompactionService>(
|
||||
dbname_, options, worker_statistics_,
|
||||
std::vector<std::shared_ptr<EventListener>>{},
|
||||
std::vector<std::shared_ptr<TablePropertiesCollectorFactory>>{});
|
||||
options.compaction_service = compaction_service_;
|
||||
return options;
|
||||
}
|
||||
|
||||
std::string SessionTmpPath() { return SessionTmpDir(dbname_); }
|
||||
|
||||
// Creates and closes a DB session before planting temporary output for the
|
||||
// next session to delete.
|
||||
void CreateDbAndQuiesce(const Options& options) {
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_TRUE(Exists(SessionTmpPath()));
|
||||
Close();
|
||||
}
|
||||
|
||||
// Creates `parent/name/` holding one file, mimicking a leaked job directory.
|
||||
void PlantDir(const std::string& parent, const std::string& name) {
|
||||
SpecialEnv* const env = env_;
|
||||
if (env == nullptr) {
|
||||
ADD_FAILURE() << "Test environment is not initialized";
|
||||
return;
|
||||
}
|
||||
ASSERT_OK(env->CreateDirIfMissing(parent));
|
||||
const std::string dir = parent + "/" + name;
|
||||
ASSERT_OK(env->CreateDirIfMissing(dir));
|
||||
ASSERT_OK(WriteStringToFile(env, "leftover", dir + "/000123.sst"));
|
||||
}
|
||||
|
||||
bool Exists(const std::string& path) {
|
||||
return env_ != nullptr && env_->FileExists(path).ok();
|
||||
}
|
||||
|
||||
// Reopens after planting stale output. Cleanup completes before DB::Open()
|
||||
// schedules compactions or returns.
|
||||
void ReopenAndDeleteStaleOutput(const Options& options) { Reopen(options); }
|
||||
|
||||
std::shared_ptr<Statistics> worker_statistics_;
|
||||
std::shared_ptr<CompactionService> compaction_service_;
|
||||
};
|
||||
|
||||
TEST_F(SessionTmpDirTest, ResolvesJobDirectory) {
|
||||
const std::string job_dir_name = "SESSION01_4294967296";
|
||||
const std::string legacy_output_path = dbname_ + "/" + job_dir_name;
|
||||
|
||||
ASSERT_EQ(RemoteCompactionJobDir(dbname_, false, job_dir_name),
|
||||
legacy_output_path);
|
||||
ASSERT_EQ(RemoteCompactionJobDir(dbname_, false, legacy_output_path),
|
||||
legacy_output_path);
|
||||
ASSERT_EQ(RemoteCompactionJobDir(dbname_, true, job_dir_name),
|
||||
SessionTmpPath() + "/" + job_dir_name);
|
||||
}
|
||||
|
||||
TEST_F(SessionTmpDirTest, DeletesDirsFromPreviousSessions) {
|
||||
Options options = OptionsWithCompactionService();
|
||||
CreateDbAndQuiesce(options);
|
||||
|
||||
// Two leaked job directories, one of them with a nested subdirectory to prove
|
||||
// the removal is recursive.
|
||||
PlantDir(SessionTmpPath(), "SESSIONFROMDEADHOST01_4294967296");
|
||||
PlantDir(SessionTmpPath() + "/SESSIONFROMDEADHOST01_4294967296", "nested");
|
||||
PlantDir(SessionTmpPath(), "ANOTHERDEADSESSION02_8589934592");
|
||||
|
||||
ReopenAndDeleteStaleOutput(options);
|
||||
|
||||
ASSERT_TRUE(Exists(SessionTmpPath()));
|
||||
ASSERT_FALSE(Exists(SessionTmpPath() + "/SESSIONFROMDEADHOST01_4294967296"));
|
||||
ASSERT_FALSE(Exists(SessionTmpPath() + "/ANOTHERDEADSESSION02_8589934592"));
|
||||
}
|
||||
|
||||
TEST_F(SessionTmpDirTest, IgnoresDirsOutsideSessionTmpDir) {
|
||||
Options options = OptionsWithCompactionService();
|
||||
CreateDbAndQuiesce(options);
|
||||
|
||||
// A CompactionService that stages output outside the configured root owns
|
||||
// cleaning it up; the DB must not touch it.
|
||||
const std::string elsewhere = "service_owned_staging_dir";
|
||||
PlantDir(dbname_, elsewhere);
|
||||
PlantDir(SessionTmpPath(), "SESSIONFROMDEADHOST01_4294967296");
|
||||
|
||||
ReopenAndDeleteStaleOutput(options);
|
||||
|
||||
ASSERT_TRUE(Exists(dbname_ + "/" + elsewhere));
|
||||
ASSERT_FALSE(Exists(SessionTmpPath() + "/SESSIONFROMDEADHOST01_4294967296"));
|
||||
|
||||
// Nothing in RocksDB owns this directory, so nothing else will remove it --
|
||||
// including the DestroyDB that the next test's setup runs against the shared
|
||||
// per-thread DB path.
|
||||
ASSERT_OK(DestroyDir(env_, dbname_ + "/" + elsewhere));
|
||||
}
|
||||
|
||||
TEST_F(SessionTmpDirTest, DisabledOptionSkipsCleanup) {
|
||||
if (Exists(SessionTmpPath())) {
|
||||
ASSERT_OK(DestroyDir(env_, SessionTmpPath()));
|
||||
}
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_FALSE(Exists(SessionTmpPath()));
|
||||
|
||||
PlantDir(SessionTmpPath(), "SESSIONFROMDEADHOST01_4294967296");
|
||||
|
||||
Reopen(options);
|
||||
Close();
|
||||
|
||||
ASSERT_TRUE(Exists(SessionTmpPath() + "/SESSIONFROMDEADHOST01_4294967296"));
|
||||
ASSERT_OK(DestroyDir(env_, SessionTmpPath()));
|
||||
}
|
||||
|
||||
TEST_F(SessionTmpDirTest, EnabledSessionTmpDirIsCreated) {
|
||||
Options options = CurrentOptions();
|
||||
options.use_session_tmp_dir_for_remote_compaction = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_TRUE(Exists(SessionTmpPath()));
|
||||
}
|
||||
|
||||
TEST_F(SessionTmpDirTest, DestroyDBRemovesSessionTmpDir) {
|
||||
Options options = OptionsWithCompactionService();
|
||||
CreateDbAndQuiesce(options);
|
||||
PlantDir(SessionTmpPath(), "SESSIONFROMDEADHOST01_4294967296");
|
||||
|
||||
// DestroyDB's final DeleteDir(dbname) is non-recursive, so it can only remove
|
||||
// the DB directory if the session temporary directory is gone first.
|
||||
ASSERT_OK(DestroyDB(dbname_, options));
|
||||
ASSERT_FALSE(Exists(dbname_));
|
||||
}
|
||||
|
||||
TEST_F(SessionTmpDirTest, CompactionUsesSessionTmpDir) {
|
||||
Options options = OptionsWithCompactionService();
|
||||
options.disable_auto_compactions = true;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
for (int i = 0; i < 20; i++) {
|
||||
ASSERT_OK(Put(Key(i), "value" + std::to_string(i)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
for (int i = 0; i < 20; i += 2) {
|
||||
ASSERT_OK(Put(Key(i), "value_new" + std::to_string(i)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
|
||||
auto* my_cs = static_cast_with_check<MyTestCompactionService>(
|
||||
compaction_service_.get());
|
||||
ASSERT_GE(my_cs->GetCompactionNum(), 1);
|
||||
CompactionServiceResult result;
|
||||
my_cs->GetResult(&result);
|
||||
ASSERT_OK(result.status);
|
||||
ASSERT_EQ(result.output_path,
|
||||
RemoteCompactionJobDir(dbname_, true,
|
||||
my_cs->GetLastOutputDirectoryName()));
|
||||
|
||||
for (int i = 0; i < 20; i++) {
|
||||
ASSERT_EQ(Get(Key(i)), (i % 2 ? "value" : "value_new") + std::to_string(i));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
+212
-6
@@ -10,6 +10,7 @@
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "folly/Executor.h"
|
||||
@@ -18,7 +19,10 @@
|
||||
#include "folly/io/async/EventBase.h"
|
||||
#include "rocksdb/coro_db.h"
|
||||
#include "rocksdb/file_system.h"
|
||||
#include "rocksdb/sst_file_reader.h"
|
||||
#include "table/multiget_context.h"
|
||||
#include "table/sst_file_reader_impl.h"
|
||||
#include "util/coro_stats_util.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
@@ -33,14 +37,28 @@ folly::coro::Task<Status> CoroDB::CoGet(DB* db, const ReadOptions& options,
|
||||
file_system == nullptr ? nullptr : file_system->GetReadExecutor();
|
||||
auto* read_event_base =
|
||||
read_executor == nullptr ? nullptr : read_executor->getEventBase();
|
||||
CoroutineStatsConfig stats_config = CaptureAndDisableCoroutineStatsConfig();
|
||||
|
||||
Status status;
|
||||
if (coro_db == nullptr || read_event_base == nullptr) {
|
||||
CoroutineStatsContextScope stats_scope(std::move(stats_config),
|
||||
db->GetEnv());
|
||||
status = db->Get(options, column_family, key, value, timestamp);
|
||||
} else {
|
||||
auto task = [](CoroutineStatsConfig task_stats_config, Env* task_env,
|
||||
CoroDB* task_db, const ReadOptions& task_options,
|
||||
ColumnFamilyHandle* task_column_family, Slice task_key,
|
||||
PinnableSlice* task_value,
|
||||
std::string* task_timestamp) -> folly::coro::Task<Status> {
|
||||
CoroutineStatsContextScope stats_scope(std::move(task_stats_config),
|
||||
task_env);
|
||||
co_return co_await folly::coro::co_nothrow(
|
||||
task_db->GetCoroutine(task_options, task_column_family, task_key,
|
||||
task_value, task_timestamp));
|
||||
}(std::move(stats_config), db->GetEnv(), coro_db, options, column_family,
|
||||
key, value, timestamp);
|
||||
status = co_await folly::coro::co_nothrow(folly::coro::co_withExecutor(
|
||||
folly::Executor::getKeepAliveToken(read_event_base),
|
||||
coro_db->GetCoroutine(options, column_family, key, value, timestamp)));
|
||||
folly::Executor::getKeepAliveToken(read_event_base), std::move(task)));
|
||||
}
|
||||
co_return status;
|
||||
}
|
||||
@@ -73,6 +91,131 @@ folly::coro::Task<Status> CoroDB::CoGet(DB* db, const ReadOptions& options,
|
||||
return CoGet(db, options, db->DefaultColumnFamily(), key, value, timestamp);
|
||||
}
|
||||
|
||||
folly::coro::Task<Status> CoroDB::CoGet(SstFileReader* reader,
|
||||
const ReadOptions& options,
|
||||
const Slice& key,
|
||||
PinnableSlice* value) {
|
||||
assert(reader != nullptr);
|
||||
SstFileReader::Rep* rep = reader->rep_.get();
|
||||
auto* read_executor = rep->read_executor;
|
||||
auto* read_event_base =
|
||||
read_executor == nullptr ? nullptr : read_executor->getEventBase();
|
||||
CoroutineStatsConfig stats_config = CaptureAndDisableCoroutineStatsConfig();
|
||||
|
||||
Status status;
|
||||
if (read_event_base == nullptr) {
|
||||
CoroutineStatsContextScope stats_scope(std::move(stats_config),
|
||||
rep->options.env);
|
||||
status = reader->Get(options, key, value);
|
||||
} else {
|
||||
auto task = [](CoroutineStatsConfig task_stats_config, Env* task_env,
|
||||
SstFileReader* task_reader, const ReadOptions& task_options,
|
||||
Slice task_key,
|
||||
PinnableSlice* task_value) -> folly::coro::Task<Status> {
|
||||
CoroutineStatsContextScope stats_scope(std::move(task_stats_config),
|
||||
task_env);
|
||||
co_return co_await folly::coro::co_nothrow(
|
||||
task_reader->rep_->GetCoroutine(task_options, task_key, task_value));
|
||||
}(std::move(stats_config), rep->options.env, reader, options, key, value);
|
||||
status = co_await folly::coro::co_nothrow(folly::coro::co_withExecutor(
|
||||
folly::Executor::getKeepAliveToken(read_event_base), std::move(task)));
|
||||
}
|
||||
co_return status;
|
||||
}
|
||||
|
||||
folly::coro::Task<Status> CoroDB::CoGet(SstFileReader* reader,
|
||||
const ReadOptions& options,
|
||||
const Slice& key, std::string* value) {
|
||||
assert(value != nullptr);
|
||||
PinnableSlice pinnable_value(value);
|
||||
assert(!pinnable_value.IsPinned());
|
||||
Status status = co_await folly::coro::co_nothrow(
|
||||
CoGet(reader, options, key, &pinnable_value));
|
||||
if (status.ok() && pinnable_value.IsPinned()) {
|
||||
value->assign(pinnable_value.data(), pinnable_value.size());
|
||||
}
|
||||
co_return status;
|
||||
}
|
||||
|
||||
folly::coro::Task<Status> CoroDB::CoGetEntity(DB* db,
|
||||
const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
const Slice& key,
|
||||
PinnableWideColumns* columns) {
|
||||
assert(db != nullptr);
|
||||
CoroDB* coro_db = db->GetCoroDB();
|
||||
auto* file_system = db->GetFileSystem();
|
||||
auto* read_executor =
|
||||
file_system == nullptr ? nullptr : file_system->GetReadExecutor();
|
||||
auto* read_event_base =
|
||||
read_executor == nullptr ? nullptr : read_executor->getEventBase();
|
||||
CoroutineStatsConfig stats_config = CaptureAndDisableCoroutineStatsConfig();
|
||||
|
||||
Status status;
|
||||
if (coro_db == nullptr || read_event_base == nullptr) {
|
||||
CoroutineStatsContextScope stats_scope(std::move(stats_config),
|
||||
db->GetEnv());
|
||||
status = db->GetEntity(options, column_family, key, columns);
|
||||
} else {
|
||||
auto task =
|
||||
[](CoroutineStatsConfig task_stats_config, Env* task_env,
|
||||
CoroDB* task_db, const ReadOptions& task_options,
|
||||
ColumnFamilyHandle* task_column_family, Slice task_key,
|
||||
PinnableWideColumns* task_columns) -> folly::coro::Task<Status> {
|
||||
CoroutineStatsContextScope stats_scope(std::move(task_stats_config),
|
||||
task_env);
|
||||
co_return co_await folly::coro::co_nothrow(task_db->GetEntityCoroutine(
|
||||
task_options, task_column_family, task_key, task_columns));
|
||||
}(std::move(stats_config), db->GetEnv(), coro_db, options, column_family,
|
||||
key, columns);
|
||||
status = co_await folly::coro::co_nothrow(folly::coro::co_withExecutor(
|
||||
folly::Executor::getKeepAliveToken(read_event_base), std::move(task)));
|
||||
}
|
||||
co_return status;
|
||||
}
|
||||
|
||||
folly::coro::Task<Status> CoroDB::CoGetEntity(DB* db,
|
||||
const ReadOptions& options,
|
||||
const Slice& key,
|
||||
PinnableWideColumns* columns) {
|
||||
assert(db != nullptr);
|
||||
return CoGetEntity(db, options, db->DefaultColumnFamily(), key, columns);
|
||||
}
|
||||
|
||||
folly::coro::Task<Status> CoroDB::CoGetEntity(DB* db,
|
||||
const ReadOptions& options,
|
||||
const Slice& key,
|
||||
PinnableAttributeGroups* result) {
|
||||
assert(db != nullptr);
|
||||
CoroDB* coro_db = db->GetCoroDB();
|
||||
auto* file_system = db->GetFileSystem();
|
||||
auto* read_executor =
|
||||
file_system == nullptr ? nullptr : file_system->GetReadExecutor();
|
||||
auto* read_event_base =
|
||||
read_executor == nullptr ? nullptr : read_executor->getEventBase();
|
||||
CoroutineStatsConfig stats_config = CaptureAndDisableCoroutineStatsConfig();
|
||||
|
||||
Status status;
|
||||
if (coro_db == nullptr || read_event_base == nullptr) {
|
||||
CoroutineStatsContextScope stats_scope(std::move(stats_config),
|
||||
db->GetEnv());
|
||||
status = db->GetEntity(options, key, result);
|
||||
} else {
|
||||
auto task =
|
||||
[](CoroutineStatsConfig task_stats_config, Env* task_env,
|
||||
CoroDB* task_db, const ReadOptions& task_options, Slice task_key,
|
||||
PinnableAttributeGroups* task_result) -> folly::coro::Task<Status> {
|
||||
CoroutineStatsContextScope stats_scope(std::move(task_stats_config),
|
||||
task_env);
|
||||
co_return co_await folly::coro::co_nothrow(
|
||||
task_db->GetEntityCoroutine(task_options, task_key, task_result));
|
||||
}(std::move(stats_config), db->GetEnv(), coro_db, options, key, result);
|
||||
status = co_await folly::coro::co_nothrow(folly::coro::co_withExecutor(
|
||||
folly::Executor::getKeepAliveToken(read_event_base), std::move(task)));
|
||||
}
|
||||
co_return status;
|
||||
}
|
||||
|
||||
folly::coro::Task<std::vector<Status>> CoroDB::CoMultiGet(
|
||||
DB* db, const ReadOptions& options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
@@ -110,6 +253,54 @@ folly::coro::Task<std::vector<Status>> CoroDB::CoMultiGet(
|
||||
CoMultiGet(db, options, column_families, keys, values, timestamps));
|
||||
}
|
||||
|
||||
folly::coro::Task<std::vector<Status>> CoroDB::CoMultiGet(
|
||||
SstFileReader* reader, const ReadOptions& options,
|
||||
const std::vector<Slice>& keys, std::vector<PinnableSlice>* values) {
|
||||
assert(reader != nullptr);
|
||||
SstFileReader::Rep* rep = reader->rep_.get();
|
||||
auto* read_executor = rep->read_executor;
|
||||
auto* read_event_base =
|
||||
read_executor == nullptr ? nullptr : read_executor->getEventBase();
|
||||
CoroutineStatsConfig stats_config = CaptureAndDisableCoroutineStatsConfig();
|
||||
|
||||
std::vector<Status> statuses;
|
||||
if (read_event_base == nullptr) {
|
||||
CoroutineStatsContextScope stats_scope(std::move(stats_config),
|
||||
rep->options.env);
|
||||
statuses = reader->MultiGet(options, keys, values);
|
||||
} else {
|
||||
auto task = [](CoroutineStatsConfig task_stats_config, Env* task_env,
|
||||
SstFileReader* task_reader, const ReadOptions& task_options,
|
||||
const std::vector<Slice>& task_keys,
|
||||
std::vector<PinnableSlice>* task_values)
|
||||
-> folly::coro::Task<std::vector<Status>> {
|
||||
CoroutineStatsContextScope stats_scope(std::move(task_stats_config),
|
||||
task_env);
|
||||
co_return co_await folly::coro::co_nothrow(
|
||||
task_reader->rep_->MultiGetCoroutine(task_options, task_keys,
|
||||
task_values));
|
||||
}(std::move(stats_config), rep->options.env, reader, options, keys, values);
|
||||
statuses = co_await folly::coro::co_nothrow(folly::coro::co_withExecutor(
|
||||
folly::Executor::getKeepAliveToken(read_event_base), std::move(task)));
|
||||
}
|
||||
co_return statuses;
|
||||
}
|
||||
|
||||
folly::coro::Task<std::vector<Status>> CoroDB::CoMultiGet(
|
||||
SstFileReader* reader, const ReadOptions& options,
|
||||
const std::vector<Slice>& keys, std::vector<std::string>* values) {
|
||||
std::vector<PinnableSlice> pinnable_values;
|
||||
std::vector<Status> statuses = co_await folly::coro::co_nothrow(
|
||||
CoMultiGet(reader, options, keys, &pinnable_values));
|
||||
values->resize(keys.size());
|
||||
for (size_t i = 0; i < keys.size(); ++i) {
|
||||
if (statuses[i].ok()) {
|
||||
(*values)[i].assign(pinnable_values[i].data(), pinnable_values[i].size());
|
||||
}
|
||||
}
|
||||
co_return statuses;
|
||||
}
|
||||
|
||||
folly::coro::Task<void> CoroDB::CoMultiGet(
|
||||
DB* db, const ReadOptions& options, size_t num_keys,
|
||||
ColumnFamilyHandle** column_families, const Slice* keys,
|
||||
@@ -122,16 +313,31 @@ folly::coro::Task<void> CoroDB::CoMultiGet(
|
||||
file_system == nullptr ? nullptr : file_system->GetReadExecutor();
|
||||
auto* read_event_base =
|
||||
read_executor == nullptr ? nullptr : read_executor->getEventBase();
|
||||
CoroutineStatsConfig stats_config = CaptureAndDisableCoroutineStatsConfig();
|
||||
|
||||
if (coro_db == nullptr || read_event_base == nullptr) {
|
||||
CoroutineStatsContextScope stats_scope(std::move(stats_config),
|
||||
db->GetEnv());
|
||||
db->MultiGet(options, num_keys, column_families, keys, values, timestamps,
|
||||
statuses, sorted_input);
|
||||
} else {
|
||||
auto task = [](CoroutineStatsConfig task_stats_config, Env* task_env,
|
||||
CoroDB* task_db, const ReadOptions& task_options,
|
||||
size_t task_num_keys,
|
||||
ColumnFamilyHandle** task_column_families,
|
||||
const Slice* task_keys, PinnableSlice* task_values,
|
||||
std::string* task_timestamps, Status* task_statuses,
|
||||
bool task_sorted_input) -> folly::coro::Task<void> {
|
||||
CoroutineStatsContextScope stats_scope(std::move(task_stats_config),
|
||||
task_env);
|
||||
co_await folly::coro::co_nothrow(task_db->MultiGetCoroutine(
|
||||
task_options, task_num_keys, task_column_families, task_keys,
|
||||
task_values, task_timestamps, task_statuses, task_sorted_input));
|
||||
}(std::move(stats_config), db->GetEnv(), coro_db, options, num_keys,
|
||||
column_families, keys, values,
|
||||
timestamps, statuses, sorted_input);
|
||||
co_await folly::coro::co_nothrow(folly::coro::co_withExecutor(
|
||||
folly::Executor::getKeepAliveToken(read_event_base),
|
||||
coro_db->MultiGetCoroutine(options, num_keys, column_families, keys,
|
||||
values, timestamps, statuses,
|
||||
sorted_input)));
|
||||
folly::Executor::getKeepAliveToken(read_event_base), std::move(task)));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+684
-8
@@ -21,6 +21,7 @@
|
||||
#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"
|
||||
@@ -37,6 +38,19 @@
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
namespace {
|
||||
|
||||
OutputMetadata NewerVersionOutputMetadata() {
|
||||
OutputMetadata output_metadata;
|
||||
output_metadata.WantNewerVersionPresent();
|
||||
return output_metadata;
|
||||
}
|
||||
|
||||
MultiGetOutputMetadata NewerVersionMultiGetOutputMetadata() {
|
||||
MultiGetOutputMetadata output_metadata;
|
||||
output_metadata.WantNewerVersionPresent();
|
||||
return output_metadata;
|
||||
}
|
||||
|
||||
class MyFlushBlockPolicy : public FlushBlockPolicy {
|
||||
public:
|
||||
explicit MyFlushBlockPolicy(const int num_keys_in_block,
|
||||
@@ -356,7 +370,53 @@ TEST_F(DBBasicTest,
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest,
|
||||
OptimizeManifestForRecoveryIgnoresOptionsFilesForNextFileNumber) {
|
||||
OptimizeManifestForRecoveryKeepsCurrentOptionsFileWithStaleOptions) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
options.avoid_unnecessary_blocking_io = false;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("k", "v"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
const uint64_t next_before_close =
|
||||
dbfull()->GetVersionSet()->current_next_file_number();
|
||||
Close();
|
||||
|
||||
const uint64_t stale_options_number = next_before_close + 100;
|
||||
ASSERT_OK(WriteStringToFile(
|
||||
env_, "" /*data*/, dbname_ + "/" + OptionsFileName(stale_options_number),
|
||||
/*should_sync=*/true));
|
||||
ASSERT_OK(WriteStringToFile(
|
||||
env_, "" /*data*/,
|
||||
dbname_ + "/" + OptionsFileName(stale_options_number + 1),
|
||||
/*should_sync=*/true));
|
||||
|
||||
RecoveryOptimizationCounters counters;
|
||||
counters.Install();
|
||||
Reopen(options);
|
||||
counters.Uninstall();
|
||||
|
||||
const uint64_t current_options_number =
|
||||
dbfull()->GetVersionSet()->options_file_number();
|
||||
ASSERT_EQ(1, counters.next_file_number.load());
|
||||
ASSERT_GT(dbfull()->GetVersionSet()->current_next_file_number(),
|
||||
stale_options_number + 1);
|
||||
ASSERT_GT(current_options_number, 0u);
|
||||
ASSERT_OK(env_->FileExists(OptionsFileName(dbname_, current_options_number)));
|
||||
|
||||
const std::string checkpoint_dir = dbname_ + "_checkpoint";
|
||||
ASSERT_OK(DestroyDir(env_, checkpoint_dir));
|
||||
Checkpoint* checkpoint = nullptr;
|
||||
ASSERT_OK(Checkpoint::Create(db_.get(), &checkpoint));
|
||||
std::unique_ptr<Checkpoint> checkpoint_guard(checkpoint);
|
||||
ASSERT_OK(checkpoint_guard->CreateCheckpoint(checkpoint_dir));
|
||||
ASSERT_OK(DestroyDir(env_, checkpoint_dir));
|
||||
ASSERT_EQ("v", Get("k"));
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest,
|
||||
OptimizeManifestForRecoveryReservesTempOptionsFileNumbersInMemory) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.optimize_manifest_for_recovery = true;
|
||||
@@ -368,13 +428,11 @@ TEST_F(DBBasicTest,
|
||||
dbfull()->GetVersionSet()->current_next_file_number();
|
||||
Close();
|
||||
|
||||
const uint64_t options_number = next_before_close + 100;
|
||||
ASSERT_OK(WriteStringToFile(env_, "" /*data*/,
|
||||
dbname_ + "/" + OptionsFileName(options_number),
|
||||
/*should_sync=*/true));
|
||||
ASSERT_OK(WriteStringToFile(env_, "" /*data*/,
|
||||
TempOptionsFileName(dbname_, options_number + 1),
|
||||
/*should_sync=*/true));
|
||||
const uint64_t stale_temp_options_number = next_before_close + 100;
|
||||
ASSERT_OK(
|
||||
WriteStringToFile(env_, "" /*data*/,
|
||||
TempOptionsFileName(dbname_, stale_temp_options_number),
|
||||
/*should_sync=*/true));
|
||||
|
||||
RecoveryOptimizationCounters counters;
|
||||
counters.Install();
|
||||
@@ -382,6 +440,8 @@ TEST_F(DBBasicTest,
|
||||
counters.Uninstall();
|
||||
|
||||
ASSERT_EQ(1, counters.next_file_number.load());
|
||||
ASSERT_GT(dbfull()->GetVersionSet()->current_next_file_number(),
|
||||
stale_temp_options_number);
|
||||
ASSERT_EQ("v", Get("k"));
|
||||
}
|
||||
|
||||
@@ -1274,6 +1334,24 @@ TEST_F(DBBasicTest, ReadOnlyDB) {
|
||||
ASSERT_OK(EnforcedReadOnlyReopen(options));
|
||||
ASSERT_EQ("v3", Get("foo"));
|
||||
ASSERT_EQ("v2", Get("bar"));
|
||||
std::string metadata_value;
|
||||
ReadOptions metadata_read_options;
|
||||
OutputMetadata output_metadata = NewerVersionOutputMetadata();
|
||||
ASSERT_OK(db_->GetWithMetadata(metadata_read_options, "foo", &metadata_value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ("v3", metadata_value);
|
||||
ASSERT_FALSE(*output_metadata.newer_version_present);
|
||||
std::vector<Slice> metadata_keys{Slice("foo")};
|
||||
std::vector<std::string> metadata_values;
|
||||
MultiGetOutputMetadata multiget_output_metadata =
|
||||
NewerVersionMultiGetOutputMetadata();
|
||||
std::vector<Status> metadata_statuses =
|
||||
db_->MultiGetWithMetadata(metadata_read_options, metadata_keys,
|
||||
&metadata_values, &multiget_output_metadata);
|
||||
ASSERT_EQ(1, metadata_statuses.size());
|
||||
ASSERT_OK(metadata_statuses[0]);
|
||||
ASSERT_EQ("v3", metadata_values[0]);
|
||||
ASSERT_FALSE((*multiget_output_metadata.newer_version_present)[0]);
|
||||
verify_all_iters();
|
||||
ASSERT_EQ(db_->SyncWAL().code(), Status::Code::kNotSupported);
|
||||
|
||||
@@ -1455,6 +1533,35 @@ TEST_F(DBBasicTest, CompactedDB) {
|
||||
ASSERT_EQ(DummyString(kFileSize / 2, 'j'), Get("jjj"));
|
||||
ASSERT_EQ("NOT_FOUND", Get("kkk"));
|
||||
|
||||
std::string metadata_value;
|
||||
ReadOptions metadata_read_options;
|
||||
const Snapshot* metadata_snapshot = db_->GetSnapshot();
|
||||
metadata_read_options.snapshot = metadata_snapshot;
|
||||
OutputMetadata output_metadata = NewerVersionOutputMetadata();
|
||||
ASSERT_OK(db_->GetWithMetadata(metadata_read_options, "aaa", &metadata_value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ(DummyString(kFileSize / 2, 'a'), metadata_value);
|
||||
ASSERT_FALSE(*output_metadata.newer_version_present);
|
||||
std::vector<Slice> metadata_keys{Slice("aaa"), Slice("ccc")};
|
||||
std::vector<std::string> metadata_values;
|
||||
MultiGetOutputMetadata multiget_output_metadata =
|
||||
NewerVersionMultiGetOutputMetadata();
|
||||
std::vector<Status> metadata_statuses =
|
||||
db_->MultiGetWithMetadata(metadata_read_options, metadata_keys,
|
||||
&metadata_values, &multiget_output_metadata);
|
||||
ASSERT_EQ(metadata_keys.size(), metadata_statuses.size());
|
||||
ASSERT_OK(metadata_statuses[0]);
|
||||
ASSERT_EQ(DummyString(kFileSize / 2, 'a'), metadata_values[0]);
|
||||
ASSERT_FALSE((*multiget_output_metadata.newer_version_present)[0]);
|
||||
ASSERT_TRUE(metadata_statuses[1].IsNotFound());
|
||||
ASSERT_FALSE((*multiget_output_metadata.newer_version_present)[1]);
|
||||
db_->ReleaseSnapshot(metadata_snapshot);
|
||||
|
||||
ASSERT_TRUE(db_->GetWithMetadata(ReadOptions(), db_->DefaultColumnFamily(),
|
||||
"aaa", static_cast<PinnableSlice*>(nullptr),
|
||||
&output_metadata)
|
||||
.IsInvalidArgument());
|
||||
|
||||
// TODO: validate that other write ops return NotImplemented
|
||||
// (CompactedDB is missing some overrides)
|
||||
|
||||
@@ -1642,6 +1749,430 @@ TEST_P(DBBasicGetWithParam, GetSnapshot) {
|
||||
} while (ChangeOptions());
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, GetNewerVersionPresent) {
|
||||
// Snapshot reads report later point and range-delete writes without changing
|
||||
// the snapshot-visible value.
|
||||
Options options = CurrentOptions();
|
||||
options.merge_operator = MergeOperators::CreateStringAppendOperator();
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
ASSERT_OK(Put(1, "key", "v1"));
|
||||
ASSERT_OK(Put(1, "merge_key", "base"));
|
||||
ASSERT_OK(Put(1, "stable", "s1"));
|
||||
ASSERT_OK(Put(1, "range_key", "r1"));
|
||||
const Snapshot* snapshot = db_->GetSnapshot();
|
||||
|
||||
ReadOptions snapshot_read;
|
||||
snapshot_read.snapshot = snapshot;
|
||||
|
||||
std::string value;
|
||||
OutputMetadata output_metadata = NewerVersionOutputMetadata();
|
||||
ASSERT_OK(db_->GetWithMetadata(snapshot_read, handles_[1], "key", &value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ("v1", value);
|
||||
ASSERT_FALSE(*output_metadata.newer_version_present);
|
||||
|
||||
ASSERT_OK(Put(1, "other", "v2"));
|
||||
ASSERT_OK(Put(1, "created_after_put", "new"));
|
||||
ASSERT_OK(Merge(1, "created_after_merge", "new"));
|
||||
ASSERT_OK(
|
||||
db_->DeleteRange(WriteOptions(), handles_[1], "range_key", "range_key~"));
|
||||
ASSERT_OK(Merge(1, "merge_key", "new"));
|
||||
value.clear();
|
||||
*output_metadata.newer_version_present = true;
|
||||
ASSERT_OK(db_->GetWithMetadata(snapshot_read, handles_[1], "stable", &value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ("s1", value);
|
||||
ASSERT_FALSE(*output_metadata.newer_version_present);
|
||||
|
||||
value.clear();
|
||||
*output_metadata.newer_version_present = true;
|
||||
ASSERT_TRUE(db_->GetWithMetadata(snapshot_read, handles_[1],
|
||||
"created_after_put", &value,
|
||||
&output_metadata)
|
||||
.IsNotFound());
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
value.clear();
|
||||
*output_metadata.newer_version_present = false;
|
||||
ASSERT_TRUE(db_->GetWithMetadata(snapshot_read, handles_[1],
|
||||
"created_after_merge", &value,
|
||||
&output_metadata)
|
||||
.IsNotFound());
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
value.clear();
|
||||
*output_metadata.newer_version_present = false;
|
||||
ASSERT_OK(db_->GetWithMetadata(snapshot_read, handles_[1], "range_key",
|
||||
&value, &output_metadata));
|
||||
ASSERT_EQ("r1", value);
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
value.clear();
|
||||
*output_metadata.newer_version_present = false;
|
||||
ASSERT_OK(db_->GetWithMetadata(snapshot_read, handles_[1], "merge_key",
|
||||
&value, &output_metadata));
|
||||
ASSERT_EQ("base", value);
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
ASSERT_OK(Put(1, "key", "v2"));
|
||||
value.clear();
|
||||
ASSERT_OK(db_->GetWithMetadata(snapshot_read, handles_[1], "key", &value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ("v1", value);
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
ASSERT_OK(Flush(1));
|
||||
value.clear();
|
||||
*output_metadata.newer_version_present = false;
|
||||
ASSERT_OK(db_->GetWithMetadata(snapshot_read, handles_[1], "key", &value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ("v1", value);
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
value.clear();
|
||||
*output_metadata.newer_version_present = false;
|
||||
ASSERT_OK(db_->GetWithMetadata(snapshot_read, handles_[1], "range_key",
|
||||
&value, &output_metadata));
|
||||
ASSERT_EQ("r1", value);
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
ReadOptions latest_read;
|
||||
value.clear();
|
||||
*output_metadata.newer_version_present = true;
|
||||
ASSERT_OK(db_->GetWithMetadata(latest_read, handles_[1], "key", &value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ("v2", value);
|
||||
ASSERT_FALSE(*output_metadata.newer_version_present);
|
||||
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, NewerVersionPresentRequestedByOutputMetadata) {
|
||||
// Newer-version tracking is opt-in through OutputMetadata and
|
||||
// MultiGetOutputMetadata.
|
||||
Options options = CurrentOptions();
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
ASSERT_OK(Put(1, "key", "old"));
|
||||
const Snapshot* snapshot = db_->GetSnapshot();
|
||||
ASSERT_OK(Put(1, "key", "new"));
|
||||
|
||||
ReadOptions read_options;
|
||||
read_options.snapshot = snapshot;
|
||||
|
||||
std::string value;
|
||||
OutputMetadata output_metadata = NewerVersionOutputMetadata();
|
||||
ASSERT_OK(db_->GetWithMetadata(read_options, handles_[1], "key", &value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ("old", value);
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
std::vector<Slice> keys{"key"};
|
||||
std::vector<ColumnFamilyHandle*> cfs(keys.size(), handles_[1]);
|
||||
std::vector<std::string> values;
|
||||
MultiGetOutputMetadata multiget_output_metadata =
|
||||
NewerVersionMultiGetOutputMetadata();
|
||||
std::vector<Status> statuses = db_->MultiGetWithMetadata(
|
||||
read_options, cfs, keys, &values, &multiget_output_metadata);
|
||||
ASSERT_EQ(1, statuses.size());
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ("old", values[0]);
|
||||
ASSERT_EQ(1, multiget_output_metadata.newer_version_present->size());
|
||||
ASSERT_TRUE((*multiget_output_metadata.newer_version_present)[0]);
|
||||
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, NewerVersionPresentIgnoreRangeDeletions) {
|
||||
// ignore_range_deletions affects the returned value, not newer-version
|
||||
// metadata about covering range tombstones.
|
||||
Options options = CurrentOptions();
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
ASSERT_OK(Put(1, "range_key", "old"));
|
||||
const Snapshot* snapshot = db_->GetSnapshot();
|
||||
ASSERT_OK(
|
||||
db_->DeleteRange(WriteOptions(), handles_[1], "range_key", "range_key~"));
|
||||
|
||||
ReadOptions read_options;
|
||||
read_options.snapshot = snapshot;
|
||||
read_options.ignore_range_deletions = true;
|
||||
|
||||
std::string value;
|
||||
OutputMetadata output_metadata = NewerVersionOutputMetadata();
|
||||
ASSERT_OK(db_->GetWithMetadata(read_options, handles_[1], "range_key", &value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ("old", value);
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
std::vector<Slice> keys{"range_key"};
|
||||
std::vector<ColumnFamilyHandle*> cfs(keys.size(), handles_[1]);
|
||||
std::vector<std::string> values;
|
||||
MultiGetOutputMetadata multiget_output_metadata =
|
||||
NewerVersionMultiGetOutputMetadata();
|
||||
std::vector<Status> statuses = db_->MultiGetWithMetadata(
|
||||
read_options, cfs, keys, &values, &multiget_output_metadata);
|
||||
ASSERT_EQ(1, statuses.size());
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ("old", values[0]);
|
||||
ASSERT_TRUE((*multiget_output_metadata.newer_version_present)[0]);
|
||||
|
||||
ASSERT_OK(Flush(1));
|
||||
value.clear();
|
||||
*output_metadata.newer_version_present = false;
|
||||
ASSERT_OK(db_->GetWithMetadata(read_options, handles_[1], "range_key", &value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ("old", value);
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
values.clear();
|
||||
multiget_output_metadata.newer_version_present->clear();
|
||||
statuses = db_->MultiGetWithMetadata(read_options, cfs, keys, &values,
|
||||
&multiget_output_metadata);
|
||||
ASSERT_EQ(1, statuses.size());
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ("old", values[0]);
|
||||
ASSERT_TRUE((*multiget_output_metadata.newer_version_present)[0]);
|
||||
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, NewerVersionPresentFromImmutableMemTable) {
|
||||
Options options = CurrentOptions();
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
ASSERT_OK(Put(1, "point_key", "old_point"));
|
||||
ASSERT_OK(Put(1, "range_key", "old_range"));
|
||||
ManagedSnapshot snapshot(db_.get());
|
||||
|
||||
ASSERT_OK(Put(1, "point_key", "new_point"));
|
||||
ASSERT_OK(
|
||||
db_->DeleteRange(WriteOptions(), handles_[1], "range_key", "range_key~"));
|
||||
|
||||
auto* cfd =
|
||||
static_cast_with_check<ColumnFamilyHandleImpl>(handles_[1])->cfd();
|
||||
Status resume_status;
|
||||
{
|
||||
ASSERT_OK(db_->PauseBackgroundWork());
|
||||
Defer resume_background(
|
||||
[&] { resume_status = db_->ContinueBackgroundWork(); });
|
||||
ASSERT_OK(dbfull()->TEST_SwitchMemtable(cfd));
|
||||
|
||||
ReadOptions read_options;
|
||||
read_options.snapshot = snapshot.snapshot();
|
||||
|
||||
std::string value;
|
||||
OutputMetadata output_metadata = NewerVersionOutputMetadata();
|
||||
ASSERT_OK(db_->GetWithMetadata(read_options, handles_[1], "point_key",
|
||||
&value, &output_metadata));
|
||||
ASSERT_EQ("old_point", value);
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
value.clear();
|
||||
*output_metadata.newer_version_present = false;
|
||||
ASSERT_OK(db_->GetWithMetadata(read_options, handles_[1], "range_key",
|
||||
&value, &output_metadata));
|
||||
ASSERT_EQ("old_range", value);
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
std::vector<Slice> keys{"point_key", "range_key"};
|
||||
std::vector<ColumnFamilyHandle*> cfs(keys.size(), handles_[1]);
|
||||
std::vector<std::string> values;
|
||||
MultiGetOutputMetadata multiget_output_metadata =
|
||||
NewerVersionMultiGetOutputMetadata();
|
||||
const std::vector<Status> statuses = db_->MultiGetWithMetadata(
|
||||
read_options, cfs, keys, &values, &multiget_output_metadata);
|
||||
ASSERT_EQ(2, statuses.size());
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_OK(statuses[1]);
|
||||
ASSERT_EQ(std::vector<std::string>({"old_point", "old_range"}), values);
|
||||
ASSERT_TRUE((*multiget_output_metadata.newer_version_present)[0]);
|
||||
ASSERT_TRUE((*multiget_output_metadata.newer_version_present)[1]);
|
||||
}
|
||||
ASSERT_OK(resume_status);
|
||||
ASSERT_OK(Flush(1));
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, NewerVersionPresentBypassesRowCacheWhileTracking) {
|
||||
// Prime row-cache entries for the snapshot values, then verify metadata
|
||||
// reads still inspect SST sequence numbers instead of replaying those
|
||||
// entries.
|
||||
option_config_ = kRowCache;
|
||||
Options options = CurrentOptions();
|
||||
options.statistics = ROCKSDB_NAMESPACE::CreateDBStatistics();
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
ASSERT_OK(Put(1, "get_key", "old_get"));
|
||||
ASSERT_OK(Put(1, "multiget_key", "old_multiget"));
|
||||
ASSERT_OK(Flush(1));
|
||||
ManagedSnapshot snapshot(db_.get());
|
||||
ASSERT_OK(Put(1, "get_key", "new_get"));
|
||||
ASSERT_OK(Put(1, "multiget_key", "new_multiget"));
|
||||
ASSERT_OK(Flush(1));
|
||||
|
||||
ReadOptions read_options;
|
||||
read_options.snapshot = snapshot.snapshot();
|
||||
|
||||
std::string primed_value;
|
||||
ASSERT_OK(db_->Get(read_options, handles_[1], "get_key", &primed_value));
|
||||
ASSERT_EQ("old_get", primed_value);
|
||||
std::vector<Slice> primed_keys{"multiget_key"};
|
||||
std::vector<ColumnFamilyHandle*> primed_cfs(primed_keys.size(), handles_[1]);
|
||||
std::vector<std::string> primed_values;
|
||||
const std::vector<Status> primed_statuses =
|
||||
db_->MultiGet(read_options, primed_cfs, primed_keys, &primed_values);
|
||||
ASSERT_OK(primed_statuses[0]);
|
||||
ASSERT_EQ("old_multiget", primed_values[0]);
|
||||
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
SCOPED_TRACE(i);
|
||||
std::string value;
|
||||
OutputMetadata output_metadata = NewerVersionOutputMetadata();
|
||||
ASSERT_OK(db_->GetWithMetadata(read_options, handles_[1], "get_key", &value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ("old_get", value);
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
}
|
||||
|
||||
std::vector<Slice> keys{"multiget_key"};
|
||||
std::vector<ColumnFamilyHandle*> cfs(keys.size(), handles_[1]);
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
SCOPED_TRACE(i);
|
||||
std::vector<std::string> values;
|
||||
MultiGetOutputMetadata multiget_output_metadata =
|
||||
NewerVersionMultiGetOutputMetadata();
|
||||
std::vector<Status> statuses = db_->MultiGetWithMetadata(
|
||||
read_options, cfs, keys, &values, &multiget_output_metadata);
|
||||
ASSERT_EQ(1, statuses.size());
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ("old_multiget", values[0]);
|
||||
ASSERT_TRUE((*multiget_output_metadata.newer_version_present)[0]);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, NewerVersionPresentUsesRowCacheAfterObservation) {
|
||||
option_config_ = kRowCache;
|
||||
Options options = CurrentOptions();
|
||||
options.statistics = ROCKSDB_NAMESPACE::CreateDBStatistics();
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
ASSERT_OK(Put(1, "key", "old"));
|
||||
ASSERT_OK(Flush(1));
|
||||
ManagedSnapshot snapshot(db_.get());
|
||||
|
||||
ReadOptions read_options;
|
||||
read_options.snapshot = snapshot.snapshot();
|
||||
std::vector<Slice> keys{"key"};
|
||||
std::vector<ColumnFamilyHandle*> cfs(keys.size(), handles_[1]);
|
||||
std::vector<std::string> values;
|
||||
std::vector<Status> statuses =
|
||||
db_->MultiGet(read_options, cfs, keys, &values);
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ("old", values[0]);
|
||||
|
||||
ASSERT_OK(Put(1, "key", "new"));
|
||||
const uint64_t row_cache_hits = TestGetTickerCount(options, ROW_CACHE_HIT);
|
||||
|
||||
values.clear();
|
||||
MultiGetOutputMetadata output_metadata = NewerVersionMultiGetOutputMetadata();
|
||||
statuses = db_->MultiGetWithMetadata(read_options, cfs, keys, &values,
|
||||
&output_metadata);
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ("old", values[0]);
|
||||
ASSERT_TRUE((*output_metadata.newer_version_present)[0]);
|
||||
ASSERT_EQ(row_cache_hits + 1, TestGetTickerCount(options, ROW_CACHE_HIT));
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, NewerVersionPresentPersistedTierNotSupported) {
|
||||
// kPersistedTier is incompatible with snapshot newer-version tracking but not
|
||||
// with untracked latest-version reads.
|
||||
Options options = CurrentOptions();
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
ASSERT_OK(Put(1, "key", "old"));
|
||||
const Snapshot* snapshot = db_->GetSnapshot();
|
||||
ASSERT_OK(Put(1, "key", "new"));
|
||||
|
||||
ReadOptions read_options;
|
||||
read_options.snapshot = snapshot;
|
||||
read_options.read_tier = kPersistedTier;
|
||||
|
||||
std::string value;
|
||||
OutputMetadata output_metadata = NewerVersionOutputMetadata();
|
||||
ASSERT_TRUE(db_->GetWithMetadata(read_options, handles_[1], "key", &value,
|
||||
&output_metadata)
|
||||
.IsNotSupported());
|
||||
|
||||
std::vector<Slice> keys{"key"};
|
||||
std::vector<ColumnFamilyHandle*> cfs(keys.size(), handles_[1]);
|
||||
std::vector<std::string> values;
|
||||
MultiGetOutputMetadata multiget_output_metadata =
|
||||
NewerVersionMultiGetOutputMetadata();
|
||||
std::vector<Status> statuses = db_->MultiGetWithMetadata(
|
||||
read_options, cfs, keys, &values, &multiget_output_metadata);
|
||||
ASSERT_EQ(1, statuses.size());
|
||||
ASSERT_TRUE(statuses[0].IsNotSupported());
|
||||
ASSERT_FALSE((*multiget_output_metadata.newer_version_present)[0]);
|
||||
|
||||
read_options.snapshot = nullptr;
|
||||
value.clear();
|
||||
*output_metadata.newer_version_present = true;
|
||||
ASSERT_OK(db_->GetWithMetadata(read_options, handles_[1], "key", &value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ("new", value);
|
||||
ASSERT_FALSE(*output_metadata.newer_version_present);
|
||||
|
||||
values.clear();
|
||||
multiget_output_metadata.newer_version_present->clear();
|
||||
statuses = db_->MultiGetWithMetadata(read_options, cfs, keys, &values,
|
||||
&multiget_output_metadata);
|
||||
ASSERT_EQ(1, statuses.size());
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ("new", values[0]);
|
||||
ASSERT_FALSE((*multiget_output_metadata.newer_version_present)[0]);
|
||||
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, ReadOnlyNewerVersionPresentReturnsFalse) {
|
||||
// Read-only DBs do not widen snapshot reads, so requested metadata remains
|
||||
// false.
|
||||
Options options = CurrentOptions();
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
ASSERT_OK(Put(1, "key", "value"));
|
||||
ASSERT_OK(Flush(1));
|
||||
ASSERT_OK(
|
||||
TryReopenReadOnlyWithColumnFamilies({"default", "pikachu"}, options));
|
||||
|
||||
const Snapshot* snapshot = db_->GetSnapshot();
|
||||
ReadOptions read_options;
|
||||
read_options.snapshot = snapshot;
|
||||
|
||||
std::string value;
|
||||
OutputMetadata output_metadata = NewerVersionOutputMetadata();
|
||||
ASSERT_OK(db_->GetWithMetadata(read_options, handles_[1], "key", &value,
|
||||
&output_metadata));
|
||||
ASSERT_EQ("value", value);
|
||||
ASSERT_FALSE(*output_metadata.newer_version_present);
|
||||
|
||||
std::vector<Slice> keys{"key"};
|
||||
std::vector<ColumnFamilyHandle*> cfs(keys.size(), handles_[1]);
|
||||
std::vector<std::string> values;
|
||||
MultiGetOutputMetadata multiget_output_metadata =
|
||||
NewerVersionMultiGetOutputMetadata();
|
||||
std::vector<Status> statuses = db_->MultiGetWithMetadata(
|
||||
read_options, cfs, keys, &values, &multiget_output_metadata);
|
||||
ASSERT_EQ(1, statuses.size());
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ("value", values[0]);
|
||||
ASSERT_EQ(1, multiget_output_metadata.newer_version_present->size());
|
||||
ASSERT_FALSE((*multiget_output_metadata.newer_version_present)[0]);
|
||||
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, CheckLock) {
|
||||
do {
|
||||
std::unique_ptr<DB> localdb;
|
||||
@@ -2234,6 +2765,151 @@ TEST_F(DBBasicTest, MultiGetSimple) {
|
||||
} while (ChangeCompactOptions());
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, MultiGetNewerVersionPresent) {
|
||||
// A mixed MultiGet batch reports newer writes independently for each key in
|
||||
// input order.
|
||||
Options options = CurrentOptions();
|
||||
options.merge_operator = MergeOperators::CreateStringAppendOperator();
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
ASSERT_OK(Put(1, "updated", "old"));
|
||||
ASSERT_OK(Put(1, "unchanged", "same"));
|
||||
ASSERT_OK(Put(1, "deleted", "old"));
|
||||
ASSERT_OK(Put(1, "merged", "old"));
|
||||
ASSERT_OK(Put(1, "range_deleted", "old"));
|
||||
const Snapshot* snapshot = db_->GetSnapshot();
|
||||
|
||||
ASSERT_OK(Put(1, "updated", "new"));
|
||||
ASSERT_OK(Put(1, "created_after_put", "new"));
|
||||
ASSERT_OK(Merge(1, "created_after_merge", "new"));
|
||||
ASSERT_OK(Delete(1, "deleted"));
|
||||
ASSERT_OK(Merge(1, "merged", "new"));
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), handles_[1], "range_deleted",
|
||||
"range_deleted~"));
|
||||
|
||||
ReadOptions read_options;
|
||||
read_options.snapshot = snapshot;
|
||||
std::vector<Slice> keys({"updated", "unchanged", "deleted", "merged",
|
||||
"range_deleted", "created_after_put",
|
||||
"created_after_merge", "missing"});
|
||||
std::vector<ColumnFamilyHandle*> cfs(keys.size(), handles_[1]);
|
||||
std::vector<std::string> values;
|
||||
MultiGetOutputMetadata output_metadata = NewerVersionMultiGetOutputMetadata();
|
||||
|
||||
std::vector<Status> statuses = db_->MultiGetWithMetadata(
|
||||
read_options, cfs, keys, &values, &output_metadata);
|
||||
|
||||
ASSERT_EQ(keys.size(), statuses.size());
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ("old", values[0]);
|
||||
ASSERT_TRUE((*output_metadata.newer_version_present)[0]);
|
||||
|
||||
ASSERT_OK(statuses[1]);
|
||||
ASSERT_EQ("same", values[1]);
|
||||
ASSERT_FALSE((*output_metadata.newer_version_present)[1]);
|
||||
|
||||
ASSERT_OK(statuses[2]);
|
||||
ASSERT_EQ("old", values[2]);
|
||||
ASSERT_TRUE((*output_metadata.newer_version_present)[2]);
|
||||
|
||||
ASSERT_OK(statuses[3]);
|
||||
ASSERT_EQ("old", values[3]);
|
||||
ASSERT_TRUE((*output_metadata.newer_version_present)[3]);
|
||||
|
||||
ASSERT_OK(statuses[4]);
|
||||
ASSERT_EQ("old", values[4]);
|
||||
ASSERT_TRUE((*output_metadata.newer_version_present)[4]);
|
||||
|
||||
ASSERT_TRUE(statuses[5].IsNotFound());
|
||||
ASSERT_TRUE((*output_metadata.newer_version_present)[5]);
|
||||
|
||||
ASSERT_TRUE(statuses[6].IsNotFound());
|
||||
ASSERT_TRUE((*output_metadata.newer_version_present)[6]);
|
||||
|
||||
ASSERT_TRUE(statuses[7].IsNotFound());
|
||||
ASSERT_FALSE((*output_metadata.newer_version_present)[7]);
|
||||
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, MultiGetNewerVersionPresentAcrossColumnFamilies) {
|
||||
// Unsorted mixed-CF reads keep metadata aligned with input order after each
|
||||
// CF is processed independently.
|
||||
Options options = CurrentOptions();
|
||||
CreateAndReopenWithCF({"one", "two"}, options);
|
||||
|
||||
ASSERT_OK(Put(1, "updated_one", "old_one"));
|
||||
ASSERT_OK(Put(2, "updated_two", "old_two"));
|
||||
ASSERT_OK(Put(2, "stable_two", "stable"));
|
||||
const Snapshot* snapshot = db_->GetSnapshot();
|
||||
ASSERT_OK(Put(1, "updated_one", "new_one"));
|
||||
ASSERT_OK(Put(2, "updated_two", "new_two"));
|
||||
ASSERT_OK(Flush(1));
|
||||
ASSERT_OK(Flush(2));
|
||||
|
||||
ReadOptions read_options;
|
||||
read_options.snapshot = snapshot;
|
||||
std::vector<Slice> keys{"updated_two", "updated_one", "stable_two"};
|
||||
std::vector<ColumnFamilyHandle*> column_families{handles_[2], handles_[1],
|
||||
handles_[2]};
|
||||
std::vector<std::string> values;
|
||||
MultiGetOutputMetadata output_metadata = NewerVersionMultiGetOutputMetadata();
|
||||
const std::vector<Status> statuses = db_->MultiGetWithMetadata(
|
||||
read_options, column_families, keys, &values, &output_metadata);
|
||||
|
||||
for (const Status& status : statuses) {
|
||||
ASSERT_OK(status);
|
||||
}
|
||||
ASSERT_EQ(std::vector<std::string>({"old_two", "old_one", "stable"}), values);
|
||||
ASSERT_TRUE((*output_metadata.newer_version_present)[0]);
|
||||
ASSERT_TRUE((*output_metadata.newer_version_present)[1]);
|
||||
ASSERT_FALSE((*output_metadata.newer_version_present)[2]);
|
||||
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, MultiGetWithMetadataColumnFamiliesKeysSizeMismatch) {
|
||||
CreateAndReopenWithCF({"pikachu"}, CurrentOptions());
|
||||
|
||||
std::vector<Slice> keys({"k1", "k2"});
|
||||
std::vector<ColumnFamilyHandle*> cfs({handles_[1]});
|
||||
std::vector<std::string> values({"stale"});
|
||||
MultiGetOutputMetadata output_metadata;
|
||||
output_metadata.timestamps.emplace();
|
||||
output_metadata.newer_version_present = std::vector<uint8_t>({1});
|
||||
|
||||
std::vector<Status> statuses = db_->MultiGetWithMetadata(
|
||||
ReadOptions(), cfs, keys, &values, &output_metadata);
|
||||
|
||||
ASSERT_EQ(keys.size(), statuses.size());
|
||||
ASSERT_EQ(keys.size(), values.size());
|
||||
for (const Status& status : statuses) {
|
||||
ASSERT_TRUE(status.IsInvalidArgument());
|
||||
}
|
||||
ASSERT_EQ(keys.size(), output_metadata.timestamps->size());
|
||||
ASSERT_EQ(keys.size(), output_metadata.newer_version_present->size());
|
||||
ASSERT_FALSE((*output_metadata.newer_version_present)[0]);
|
||||
ASSERT_FALSE((*output_metadata.newer_version_present)[1]);
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, GetWithMetadataNullValueInvalidArgument) {
|
||||
CreateAndReopenWithCF({"pikachu"}, CurrentOptions());
|
||||
|
||||
OutputMetadata output_metadata = NewerVersionOutputMetadata();
|
||||
ASSERT_TRUE(db_->GetWithMetadata(ReadOptions(), handles_[1], "key",
|
||||
static_cast<PinnableSlice*>(nullptr),
|
||||
&output_metadata)
|
||||
.IsInvalidArgument());
|
||||
ASSERT_FALSE(*output_metadata.newer_version_present);
|
||||
|
||||
*output_metadata.newer_version_present = true;
|
||||
ASSERT_TRUE(db_->GetWithMetadata(ReadOptions(), "key",
|
||||
static_cast<std::string*>(nullptr),
|
||||
&output_metadata)
|
||||
.IsInvalidArgument());
|
||||
ASSERT_FALSE(*output_metadata.newer_version_present);
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, MultiGetEmpty) {
|
||||
do {
|
||||
CreateAndReopenWithCF({"pikachu"}, CurrentOptions());
|
||||
|
||||
@@ -2542,9 +2542,13 @@ TEST_P(DBBloomFilterTestVaryPrefixAndFormatVer, PartitionedMultiGet) {
|
||||
values[i] = PinnableSlice();
|
||||
}
|
||||
|
||||
get_perf_context()->Reset();
|
||||
const PerfLevel previous_perf_level = GetPerfLevel();
|
||||
SetPerfLevel(PerfLevel::kEnableTime);
|
||||
db_->MultiGet(ropts, Q, column_families.data(), key_slices.data(),
|
||||
values.data(),
|
||||
/*timestamps=*/nullptr, statuses.data(), true);
|
||||
SetPerfLevel(previous_perf_level);
|
||||
|
||||
// Confirm correct status results
|
||||
uint32_t number_not_found = 0;
|
||||
@@ -2574,6 +2578,8 @@ TEST_P(DBBloomFilterTestVaryPrefixAndFormatVer, PartitionedMultiGet) {
|
||||
// Confirm no duplicate loading same filter partition
|
||||
uint64_t filter_accesses = PopTicker(options, BLOCK_CACHE_FILTER_HIT) +
|
||||
PopTicker(options, BLOCK_CACHE_FILTER_MISS);
|
||||
// Partition reads must contribute to the filter-block timing metric.
|
||||
EXPECT_GT(get_perf_context()->read_filter_block_nanos, 0);
|
||||
if (stride == 1) {
|
||||
EXPECT_EQ(filter_accesses, 1);
|
||||
} else {
|
||||
|
||||
@@ -204,6 +204,19 @@ class DBFollowerTest : public DBTestBase {
|
||||
return result;
|
||||
}
|
||||
|
||||
void VerifyFollowerBatchValue(const std::string& key,
|
||||
const std::string& expected) {
|
||||
std::vector<Iterator*> iterators;
|
||||
ASSERT_OK(follower()->NewIterators(
|
||||
ReadOptions(), {follower()->DefaultColumnFamily()}, &iterators));
|
||||
ASSERT_EQ(1U, iterators.size());
|
||||
std::unique_ptr<Iterator> iterator(iterators[0]);
|
||||
iterator->Seek(key);
|
||||
ASSERT_OK(iterator->status());
|
||||
ASSERT_TRUE(iterator->Valid());
|
||||
ASSERT_EQ(expected, iterator->value());
|
||||
}
|
||||
|
||||
DB* follower() { return follower_.get(); }
|
||||
DBFollowerTestFS* follower_fs() {
|
||||
return static_cast<DBFollowerTestFS*>(follower_env_->GetFileSystem().get());
|
||||
@@ -253,6 +266,32 @@ TEST_F(DBFollowerTest, Basic) {
|
||||
CheckDirs();
|
||||
}
|
||||
|
||||
TEST_F(DBFollowerTest, NewIteratorsRefreshesAfterCatchUp) {
|
||||
ASSERT_OK(Put("key", "v1"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
SyncPoint::GetInstance()->LoadDependency({
|
||||
{"DBImplFollower::TryCatchupWithLeader:Begin1", "Leader::Start"},
|
||||
{"Leader::Done", "DBImplFollower::TryCatchupWithLeader:Begin2"},
|
||||
{"DBImplFollower::TryCatchupWithLeader:End", "Follower::WaitForCatchup"},
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
ASSERT_OK(OpenAsFollower());
|
||||
VerifyFollowerBatchValue("key", "v1");
|
||||
|
||||
TEST_SYNC_POINT("Leader::Start");
|
||||
ASSERT_OK(Put("key", "v2"));
|
||||
ASSERT_OK(Flush());
|
||||
TEST_SYNC_POINT("Leader::Done");
|
||||
TEST_SYNC_POINT("Follower::WaitForCatchup");
|
||||
|
||||
VerifyFollowerBatchValue("key", "v2");
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearTrace();
|
||||
}
|
||||
|
||||
TEST_F(DBFollowerTest, Flush) {
|
||||
SyncPoint::GetInstance()->LoadDependency({
|
||||
{"DBImplFollower::TryCatchupWithLeader:Begin1", "Leader::Start"},
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include "db/blob/blob_fetcher.h"
|
||||
#include "db/db_impl/db_impl.h"
|
||||
#include "db/db_impl/db_impl_metadata.h"
|
||||
#include "db/version_set.h"
|
||||
#include "logging/logging.h"
|
||||
#include "table/get_context.h"
|
||||
@@ -34,6 +35,46 @@ size_t CompactedDBImpl::FindFile(const Slice& key) {
|
||||
files_.files);
|
||||
}
|
||||
|
||||
Status CompactedDBImpl::GetWithMetadata(const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableSlice* value,
|
||||
OutputMetadata* output_metadata) {
|
||||
std::string* timestamp = GetOutputTimestamp(output_metadata);
|
||||
bool* newer_version_present = GetOutputNewerVersionPresent(output_metadata);
|
||||
if (newer_version_present != nullptr) {
|
||||
*newer_version_present = false;
|
||||
}
|
||||
if (value == nullptr) {
|
||||
return Status::InvalidArgument(
|
||||
"Cannot call GetWithMetadata with a null value");
|
||||
}
|
||||
return Get(options, column_family, key, value, timestamp);
|
||||
}
|
||||
|
||||
void CompactedDBImpl::MultiGetWithMetadata(
|
||||
const ReadOptions& options, size_t num_keys,
|
||||
ColumnFamilyHandle* const* column_families, const Slice* keys,
|
||||
PinnableSlice* values, Status* statuses,
|
||||
MultiGetOutputMetadata* output_metadata, const bool sorted_input) {
|
||||
std::vector<std::string>* timestamps = GetOutputTimestamps(output_metadata);
|
||||
if (timestamps != nullptr) {
|
||||
timestamps->resize(num_keys);
|
||||
}
|
||||
std::vector<uint8_t>* newer_version_present =
|
||||
GetOutputNewerVersionPresent(output_metadata);
|
||||
if (newer_version_present != nullptr) {
|
||||
newer_version_present->assign(num_keys, false);
|
||||
}
|
||||
autovector<ColumnFamilyHandle*, MultiGetContext::MAX_BATCH_SIZE>
|
||||
stack_column_families;
|
||||
std::vector<ColumnFamilyHandle*> heap_column_families;
|
||||
ColumnFamilyHandle** mutable_column_families = MakeMutableCfHandles(
|
||||
column_families, num_keys, &stack_column_families, &heap_column_families);
|
||||
MultiGet(options, num_keys, mutable_column_families, keys, values,
|
||||
timestamps != nullptr ? timestamps->data() : nullptr, statuses,
|
||||
sorted_input);
|
||||
}
|
||||
|
||||
Status CompactedDBImpl::Init(const Options& options) {
|
||||
SuperVersionContext sv_context(/* create_superversion */ true);
|
||||
mutex_.Lock();
|
||||
|
||||
@@ -31,6 +31,12 @@ class CompactedDBImpl : public DBImpl {
|
||||
const Slice& key, PinnableSlice* value,
|
||||
std::string* timestamp);
|
||||
|
||||
using DB::GetWithMetadata;
|
||||
Status GetWithMetadata(const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
PinnableSlice* value,
|
||||
OutputMetadata* output_metadata) override;
|
||||
|
||||
using DB::MultiGet;
|
||||
// Note that CompactedDBImpl::MultiGet is not the optimized version of
|
||||
// MultiGet to use.
|
||||
@@ -42,6 +48,14 @@ class CompactedDBImpl : public DBImpl {
|
||||
std::string* timestamps, Status* statuses,
|
||||
const bool sorted_input);
|
||||
|
||||
using DB::MultiGetWithMetadata;
|
||||
void MultiGetWithMetadata(const ReadOptions& options, size_t num_keys,
|
||||
ColumnFamilyHandle* const* column_families,
|
||||
const Slice* keys, PinnableSlice* values,
|
||||
Status* statuses,
|
||||
MultiGetOutputMetadata* output_metadata,
|
||||
const bool sorted_input) override;
|
||||
|
||||
using DBImpl::Put;
|
||||
Status Put(const WriteOptions& /*options*/,
|
||||
ColumnFamilyHandle* /*column_family*/, const Slice& /*key*/,
|
||||
|
||||
@@ -6,10 +6,6 @@
|
||||
|
||||
#include "util/coro_utils.h"
|
||||
|
||||
#if defined(USE_COROUTINES) && defined(WITH_COROUTINES)
|
||||
#include "util/coro_stats_util.h"
|
||||
#endif // USE_COROUTINES && WITH_COROUTINES
|
||||
|
||||
#if defined(WITHOUT_COROUTINES) || \
|
||||
(defined(USE_COROUTINES) && defined(WITH_COROUTINES))
|
||||
|
||||
@@ -18,10 +14,6 @@ namespace ROCKSDB_NAMESPACE {
|
||||
DEFINE_SYNC_AND_ASYNC(Status, CompactedDBImpl::Get)
|
||||
(const ReadOptions& _read_options, ColumnFamilyHandle*, const Slice& key,
|
||||
PinnableSlice* value, std::string* timestamp) {
|
||||
#ifdef WITH_COROUTINES
|
||||
INSTALL_COROUTINE_STATS_CONTEXT_SCOPE(
|
||||
immutable_db_options_.fs->GetReadExecutor(), immutable_db_options_.env);
|
||||
#endif
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
_read_options.io_activity != Env::IOActivity::kGet) {
|
||||
CO_RETURN Status::InvalidArgument(
|
||||
@@ -110,10 +102,6 @@ DEFINE_SYNC_AND_ASYNC(void, CompactedDBImpl::MultiGet)
|
||||
ColumnFamilyHandle** /*column_families*/, const Slice* keys,
|
||||
PinnableSlice* values, std::string* timestamps, Status* statuses,
|
||||
const bool /*sorted_input*/) {
|
||||
#ifdef WITH_COROUTINES
|
||||
INSTALL_COROUTINE_STATS_CONTEXT_SCOPE(
|
||||
immutable_db_options_.fs->GetReadExecutor(), immutable_db_options_.env);
|
||||
#endif
|
||||
assert(user_comparator_);
|
||||
Status s;
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
|
||||
+558
-270
File diff suppressed because it is too large.
Load diff
+101
-6
@@ -290,12 +290,22 @@ class DBImpl : public DB
|
||||
const Slice& key, PinnableSlice* value,
|
||||
std::string* timestamp);
|
||||
|
||||
using DB::GetWithMetadata;
|
||||
Status GetWithMetadata(const ReadOptions& _read_options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
PinnableSlice* value,
|
||||
OutputMetadata* output_metadata) override;
|
||||
|
||||
using DB::GetEntity;
|
||||
Status GetEntity(const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
PinnableWideColumns* columns) override;
|
||||
Status GetEntity(const ReadOptions& options, const Slice& key,
|
||||
PinnableAttributeGroups* result) override;
|
||||
DECLARE_SYNC_AND_ASYNC_OVERRIDE(Status, GetEntity,
|
||||
const ReadOptions& _read_options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
const Slice& key,
|
||||
PinnableWideColumns* columns);
|
||||
DECLARE_SYNC_AND_ASYNC_OVERRIDE(Status, GetEntity,
|
||||
const ReadOptions& _read_options,
|
||||
const Slice& key,
|
||||
PinnableAttributeGroups* result);
|
||||
|
||||
Status GetEntityLazy(const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
@@ -332,6 +342,15 @@ class DBImpl : public DB
|
||||
std::string* timestamps, Status* statuses,
|
||||
const bool sorted_input = false);
|
||||
|
||||
using DB::MultiGetWithMetadata;
|
||||
void MultiGetWithMetadata(const ReadOptions& _read_options,
|
||||
const size_t num_keys,
|
||||
ColumnFamilyHandle* const* column_families,
|
||||
const Slice* keys, PinnableSlice* values,
|
||||
Status* statuses,
|
||||
MultiGetOutputMetadata* output_metadata,
|
||||
const bool sorted_input = false) override;
|
||||
|
||||
void MultiGetWithCallback(
|
||||
const ReadOptions& _read_options, ColumnFamilyHandle* column_family,
|
||||
ReadCallback* callback,
|
||||
@@ -419,6 +438,9 @@ class DBImpl : public DB
|
||||
Status NewIterators(const ReadOptions& _read_options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
std::vector<Iterator*>* iterators) override;
|
||||
Status NewIterators(const std::vector<ReadOptions>& read_options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
std::vector<Iterator*>* iterators) override;
|
||||
|
||||
using DB::NewMultiScan;
|
||||
std::unique_ptr<MultiScan> NewMultiScan(
|
||||
@@ -765,6 +787,7 @@ class DBImpl : public DB
|
||||
PinnableSlice* value = nullptr;
|
||||
PinnableWideColumns* columns = nullptr;
|
||||
std::string* timestamp = nullptr;
|
||||
bool* newer_version_present = nullptr;
|
||||
bool* value_found = nullptr;
|
||||
ReadCallback* callback = nullptr;
|
||||
bool* is_blob_index = nullptr;
|
||||
@@ -795,6 +818,18 @@ class DBImpl : public DB
|
||||
Cleanable* lazy_columns_pin = nullptr;
|
||||
const Version** lazy_columns_version = nullptr;
|
||||
const SameFileBlobReader** lazy_columns_same_file_reader = nullptr;
|
||||
|
||||
// Batched lazy read (MultiGetEntityLazy). When `lazy_columns_shared_sv` is
|
||||
// non-null, GetImpl reads against this externally-owned SuperVersion at
|
||||
// `lazy_columns_snapshot_seq` (the consistent per-batch sequence number)
|
||||
// instead of acquiring its own SuperVersion and deriving its own sequence
|
||||
// number, and it does NOT transfer a per-key pin -- the batch holds one
|
||||
// shared SuperVersion pin per column family. `lazy_columns_pin` must then
|
||||
// be null; `lazy_columns_version` / `lazy_columns_same_file_reader` are
|
||||
// still filled. (Single-key GetEntityLazy leaves this null and uses
|
||||
// `lazy_columns_pin`.)
|
||||
SuperVersion* lazy_columns_shared_sv = nullptr;
|
||||
SequenceNumber lazy_columns_snapshot_seq = kMaxSequenceNumber;
|
||||
};
|
||||
|
||||
DECLARE_SYNC_AND_ASYNC(Status, GetImpl, const ReadOptions& read_options,
|
||||
@@ -805,6 +840,11 @@ class DBImpl : public DB
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
PinnableSlice* value, std::string* timestamp);
|
||||
|
||||
Status GetImpl(const ReadOptions& read_options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
PinnableSlice* value, std::string* timestamp,
|
||||
bool* newer_version_present);
|
||||
|
||||
// Function that Get and KeyMayExist call with no_io true or false
|
||||
// Note: 'value_found' from KeyMayExist propagates here
|
||||
// This function is also called by GetMergeOperands
|
||||
@@ -827,6 +867,18 @@ class DBImpl : public DB
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
LazyWideColumns* result);
|
||||
|
||||
// Batched-read counterpart of GetEntityLazyImpl for one key of
|
||||
// MultiGetEntityLazy: performs the lazy point lookup against the batch's
|
||||
// shared `super_version` at the batch's consistent `snapshot_seq` (rather
|
||||
// than acquiring its own), and binds the entity's resolver to that Version
|
||||
// without taking a per-key pin (the batch owns one shared pin per column
|
||||
// family).
|
||||
Status GetEntityLazyForBatch(const ReadOptions& read_options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
SuperVersion* super_version,
|
||||
SequenceNumber snapshot_seq, const Slice& key,
|
||||
LazyWideColumns* result);
|
||||
|
||||
// If `snapshot` == kMaxSequenceNumber, set a recent one inside the file.
|
||||
ArenaWrappedDBIter* NewIteratorImpl(const ReadOptions& options,
|
||||
ColumnFamilyHandleImpl* cfh,
|
||||
@@ -2865,6 +2917,9 @@ class DBImpl : public DB
|
||||
Env::Priority thread_pri);
|
||||
void BackgroundCallFlush(Env::Priority thread_pri);
|
||||
void BackgroundCallPurge();
|
||||
// Recursively removes all children of the DB session temporary directory.
|
||||
// Best effort: errors are logged, not returned. REQUIRES: mutex_ not held.
|
||||
void CleanupSessionTmpDir();
|
||||
Status BackgroundCompaction(bool* madeProgress, JobContext* job_context,
|
||||
LogBuffer* log_buffer,
|
||||
PrepickedCompaction* prepicked_compaction,
|
||||
@@ -3130,7 +3185,9 @@ class DBImpl : public DB
|
||||
ColumnFamilyHandle** column_families,
|
||||
const Slice* keys, PinnableSlice* values,
|
||||
PinnableWideColumns* columns, std::string* timestamps,
|
||||
Status* statuses, bool sorted_input);
|
||||
Status* statuses,
|
||||
std::vector<uint8_t>* newer_version_present,
|
||||
bool sorted_input);
|
||||
|
||||
// A structure to hold the information required to process MultiGet of keys
|
||||
// belonging to one column family. For a multi column family MultiGet, there
|
||||
@@ -3814,9 +3871,47 @@ class GetWithTimestampReadCallback : public ReadCallback {
|
||||
public:
|
||||
explicit GetWithTimestampReadCallback(SequenceNumber seq)
|
||||
: ReadCallback(seq) {}
|
||||
// A null result is used by MultiGet, whose per-key results live in
|
||||
// KeyContext/GetContext.
|
||||
void EnableNewerVersionTracking(SequenceNumber read_snapshot_seq,
|
||||
SequenceNumber upper_bound_seq,
|
||||
bool* single_key_result) {
|
||||
metadata_read_bounds_.emplace(
|
||||
MetadataReadBounds{read_snapshot_seq, upper_bound_seq});
|
||||
newer_version_present_ = single_key_result;
|
||||
}
|
||||
const MetadataReadBounds* GetMetadataReadBounds() const override {
|
||||
return metadata_read_bounds_.has_value() ? &*metadata_read_bounds_
|
||||
: nullptr;
|
||||
}
|
||||
bool NeedToTrackNewerVersions(
|
||||
const bool* per_key_result = nullptr) const override {
|
||||
const bool* result =
|
||||
per_key_result != nullptr ? per_key_result : newer_version_present_;
|
||||
return metadata_read_bounds_.has_value() && result != nullptr && !*result;
|
||||
}
|
||||
bool IsVisibleFullCheck(SequenceNumber seq) override {
|
||||
return seq <= max_visible_seq_;
|
||||
}
|
||||
bool IsNewerVisibleForMetadataRead(SequenceNumber seq) override {
|
||||
const MetadataReadBounds* bounds = GetMetadataReadBounds();
|
||||
return bounds != nullptr && bounds->read_snapshot_seq < seq &&
|
||||
seq <= bounds->newer_version_upper_bound_seq;
|
||||
}
|
||||
void MaybeRecordNewerVersion(SequenceNumber seq, ValueType type,
|
||||
bool* per_key_result = nullptr) override {
|
||||
bool* result =
|
||||
per_key_result != nullptr ? per_key_result : newer_version_present_;
|
||||
if (result != nullptr && !*result &&
|
||||
(IsValueType(type) || type == kTypeRangeDeletion) &&
|
||||
IsNewerVisibleForMetadataRead(seq)) {
|
||||
*result = true;
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::optional<MetadataReadBounds> metadata_read_bounds_;
|
||||
bool* newer_version_present_ = nullptr;
|
||||
};
|
||||
|
||||
Options SanitizeOptions(const std::string& db, const Options& src,
|
||||
|
||||
@@ -4760,7 +4760,8 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
|
||||
immutable_db_options_.stats, Histograms::SST_WRITE_MICROS,
|
||||
c->immutable_options().listeners,
|
||||
immutable_db_options_.file_checksum_gen_factory.get(),
|
||||
tmp_set.Contains(FileType::kTableFile), false));
|
||||
tmp_set.Contains(FileType::kTableFile),
|
||||
tmp_set.Contains(FileType::kTableFile)));
|
||||
}
|
||||
|
||||
ROCKS_LOG_BUFFER(
|
||||
|
||||
+21
-13
@@ -489,31 +489,33 @@ void DBImpl::DeleteObsoleteFileImpl(int job_id, const std::string& fname,
|
||||
&file_deletion_status);
|
||||
if (file_deletion_status.ok()) {
|
||||
ROCKS_LOG_DEBUG(immutable_db_options_.info_log,
|
||||
"[JOB %d] Delete %s type=%d #%" PRIu64 " -- %s\n", job_id,
|
||||
fname.c_str(), type, number,
|
||||
"[JOB %d] [db_session_id=%s] Delete %s type=%d #%" PRIu64
|
||||
" -- %s\n",
|
||||
job_id, db_session_id_.c_str(), fname.c_str(), type, number,
|
||||
file_deletion_status.ToString().c_str());
|
||||
} else if (env_->FileExists(fname).IsNotFound()) {
|
||||
ROCKS_LOG_INFO(
|
||||
immutable_db_options_.info_log,
|
||||
"[JOB %d] Tried to delete a non-existing file %s type=%d #%" PRIu64
|
||||
" -- %s\n",
|
||||
job_id, fname.c_str(), type, number,
|
||||
"[JOB %d] [db_session_id=%s] Tried to delete a non-existing file %s"
|
||||
" type=%d #%" PRIu64 " -- %s\n",
|
||||
job_id, db_session_id_.c_str(), fname.c_str(), type, number,
|
||||
file_deletion_status.ToString().c_str());
|
||||
} else {
|
||||
ROCKS_LOG_ERROR(immutable_db_options_.info_log,
|
||||
"[JOB %d] Failed to delete %s type=%d #%" PRIu64 " -- %s\n",
|
||||
job_id, fname.c_str(), type, number,
|
||||
"[JOB %d] [db_session_id=%s] Failed to delete %s type=%d"
|
||||
" #%" PRIu64 " -- %s\n",
|
||||
job_id, db_session_id_.c_str(), fname.c_str(), type, number,
|
||||
file_deletion_status.ToString().c_str());
|
||||
}
|
||||
if (type == kTableFile) {
|
||||
EventHelpers::LogAndNotifyTableFileDeletion(
|
||||
&event_logger_, job_id, number, fname, file_deletion_status, GetName(),
|
||||
immutable_db_options_.listeners);
|
||||
db_session_id_, immutable_db_options_.listeners);
|
||||
}
|
||||
if (type == kBlobFile) {
|
||||
EventHelpers::LogAndNotifyBlobFileDeletion(
|
||||
&event_logger_, immutable_db_options_.listeners, job_id, number, fname,
|
||||
file_deletion_status, GetName());
|
||||
file_deletion_status, GetName(), db_session_id_);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1192,6 +1194,7 @@ Status DBImpl::MaybeUpdateNextFileNumber(RecoveryContext* recovery_ctx) {
|
||||
const uint64_t prev_next_file_number = versions_->current_next_file_number();
|
||||
uint64_t largest_file_number = prev_next_file_number;
|
||||
bool on_disk_file_advanced_counter = false;
|
||||
bool manifest_relevant_file_advanced_counter = false;
|
||||
Status s;
|
||||
for (const auto& path : CollectAllDBPaths()) {
|
||||
std::vector<std::string> files;
|
||||
@@ -1211,10 +1214,11 @@ Status DBImpl::MaybeUpdateNextFileNumber(RecoveryContext* recovery_ctx) {
|
||||
// Use >= so a crashed-mid-allocation file at exactly
|
||||
// prev_next_file_number triggers the advance -- otherwise the next
|
||||
// NewFileNumber() would collide with it.
|
||||
const bool must_reserve_file_number =
|
||||
type != kOptionsFile && type != kTempFile;
|
||||
if (must_reserve_file_number && number >= prev_next_file_number) {
|
||||
if (number >= prev_next_file_number) {
|
||||
on_disk_file_advanced_counter = true;
|
||||
if (type != kOptionsFile && type != kTempFile) {
|
||||
manifest_relevant_file_advanced_counter = true;
|
||||
}
|
||||
}
|
||||
largest_file_number = std::max(largest_file_number, number);
|
||||
if ((type == kTableFile || type == kBlobFile)) {
|
||||
@@ -1236,7 +1240,8 @@ Status DBImpl::MaybeUpdateNextFileNumber(RecoveryContext* recovery_ctx) {
|
||||
mutable_db_options_.optimize_manifest_for_recovery &&
|
||||
!immutable_db_options_.best_efforts_recovery;
|
||||
|
||||
if (!optimize_manifest_for_recovery || on_disk_file_advanced_counter) {
|
||||
if (!optimize_manifest_for_recovery ||
|
||||
manifest_relevant_file_advanced_counter) {
|
||||
if (largest_file_number >= prev_next_file_number) {
|
||||
versions_->next_file_number_.store(largest_file_number + 1);
|
||||
}
|
||||
@@ -1249,6 +1254,9 @@ Status DBImpl::MaybeUpdateNextFileNumber(RecoveryContext* recovery_ctx) {
|
||||
assert(default_cfd);
|
||||
recovery_ctx->UpdateVersionEdits(default_cfd, edit);
|
||||
} else {
|
||||
if (on_disk_file_advanced_counter) {
|
||||
versions_->next_file_number_.store(largest_file_number + 1);
|
||||
}
|
||||
TEST_SYNC_POINT("DBImpl::Recovery:SkippedNoopEdit:NextFileNumber");
|
||||
}
|
||||
return s;
|
||||
|
||||
@@ -153,6 +153,7 @@ Status DBImplFollower::TryCatchUpWithLeader() {
|
||||
cfd->InstallSuperVersion(&sv_context, &mutex_);
|
||||
sv_context.NewSuperVersion();
|
||||
}
|
||||
PublishSecondaryReadView();
|
||||
}
|
||||
|
||||
for (auto& file : files_to_delete) {
|
||||
@@ -164,6 +165,7 @@ Status DBImplFollower::TryCatchUpWithLeader() {
|
||||
}
|
||||
}
|
||||
}
|
||||
CleanupRetiredSecondaryReadViews();
|
||||
job_context.Clean();
|
||||
|
||||
// Cleanup unused, obsolete files.
|
||||
@@ -322,9 +324,11 @@ Status DB::OpenAsFollower(
|
||||
sv_context.NewSuperVersion();
|
||||
cfd->InstallSuperVersion(&sv_context, &impl->mutex_);
|
||||
}
|
||||
impl->PublishSecondaryReadView();
|
||||
impl->MarkAsyncFileOpenNotNeeded();
|
||||
}
|
||||
impl->mutex_.Unlock();
|
||||
impl->CleanupRetiredSecondaryReadViews();
|
||||
sv_context.Clean();
|
||||
if (s.ok()) {
|
||||
dbptr->reset(impl);
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "rocksdb/db.h"
|
||||
#include "table/multiget_context.h"
|
||||
#include "util/autovector.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
inline std::string* GetOutputTimestamp(OutputMetadata* output_metadata) {
|
||||
return output_metadata != nullptr && output_metadata->timestamp.has_value()
|
||||
? &*output_metadata->timestamp
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
inline std::vector<std::string>* GetOutputTimestamps(
|
||||
MultiGetOutputMetadata* output_metadata) {
|
||||
return output_metadata != nullptr && output_metadata->timestamps.has_value()
|
||||
? &*output_metadata->timestamps
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
inline bool* GetOutputNewerVersionPresent(OutputMetadata* output_metadata) {
|
||||
return output_metadata != nullptr &&
|
||||
output_metadata->newer_version_present.has_value()
|
||||
? &*output_metadata->newer_version_present
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
inline std::vector<uint8_t>* GetOutputNewerVersionPresent(
|
||||
MultiGetOutputMetadata* output_metadata) {
|
||||
return output_metadata != nullptr &&
|
||||
output_metadata->newer_version_present.has_value()
|
||||
? &*output_metadata->newer_version_present
|
||||
: nullptr;
|
||||
}
|
||||
|
||||
inline ColumnFamilyHandle** MakeMutableCfHandles(
|
||||
ColumnFamilyHandle* const* column_families, size_t num_keys,
|
||||
autovector<ColumnFamilyHandle*, MultiGetContext::MAX_BATCH_SIZE>*
|
||||
stack_column_families,
|
||||
std::vector<ColumnFamilyHandle*>* heap_column_families) {
|
||||
ColumnFamilyHandle** mutable_column_families;
|
||||
if (num_keys <= MultiGetContext::MAX_BATCH_SIZE) {
|
||||
stack_column_families->resize(num_keys);
|
||||
mutable_column_families =
|
||||
num_keys == 0 ? nullptr : &(*stack_column_families)[0];
|
||||
} else {
|
||||
heap_column_families->resize(num_keys);
|
||||
mutable_column_families = heap_column_families->data();
|
||||
}
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
mutable_column_families[i] = column_families[i];
|
||||
}
|
||||
return mutable_column_families;
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -2711,6 +2711,12 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname,
|
||||
if (s.ok()) {
|
||||
s = impl->CreateArchivalDirectory();
|
||||
}
|
||||
if (s.ok() &&
|
||||
impl->immutable_db_options_.use_session_tmp_dir_for_remote_compaction) {
|
||||
// Create the session-scoped temporary directory before a CompactionService
|
||||
// can create any per-job directory under it.
|
||||
s = impl->env_->CreateDirIfMissing(SessionTmpDir(dbname));
|
||||
}
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -2925,6 +2931,13 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname,
|
||||
impl->immutable_db_options_.db_paths[0].path);
|
||||
}
|
||||
|
||||
if (s.ok() &&
|
||||
impl->immutable_db_options_.use_session_tmp_dir_for_remote_compaction) {
|
||||
// Finish deleting data from the previous DB session before this open can
|
||||
// schedule a new compaction in the directory.
|
||||
impl->CleanupSessionTmpDir();
|
||||
}
|
||||
|
||||
if (s.ok()) {
|
||||
// When the DB is stopped, it's possible that there are some .trash files
|
||||
// that were not deleted yet, when we open the DB we will find these .trash
|
||||
|
||||
+114
-43
@@ -11,6 +11,7 @@
|
||||
#include "db/blob/blob_fetcher.h"
|
||||
#include "db/db_impl/compacted_db_impl.h"
|
||||
#include "db/db_impl/db_impl.h"
|
||||
#include "db/db_impl/db_impl_metadata.h"
|
||||
#include "db/manifest_ops.h"
|
||||
#include "db/merge_context.h"
|
||||
#include "logging/logging.h"
|
||||
@@ -30,6 +31,30 @@ DBImplReadOnly::DBImplReadOnly(const DBOptions& db_options,
|
||||
|
||||
DBImplReadOnly::~DBImplReadOnly() = default;
|
||||
|
||||
void DBImplReadOnly::MultiGetWithMetadata(
|
||||
const ReadOptions& options, const size_t num_keys,
|
||||
ColumnFamilyHandle* const* column_families, const Slice* keys,
|
||||
PinnableSlice* values, Status* statuses,
|
||||
MultiGetOutputMetadata* output_metadata, const bool sorted_input) {
|
||||
std::vector<std::string>* timestamps = GetOutputTimestamps(output_metadata);
|
||||
if (timestamps != nullptr) {
|
||||
timestamps->resize(num_keys);
|
||||
}
|
||||
std::vector<uint8_t>* newer_version_present =
|
||||
GetOutputNewerVersionPresent(output_metadata);
|
||||
if (newer_version_present != nullptr) {
|
||||
newer_version_present->assign(num_keys, false);
|
||||
}
|
||||
autovector<ColumnFamilyHandle*, MultiGetContext::MAX_BATCH_SIZE>
|
||||
stack_column_families;
|
||||
std::vector<ColumnFamilyHandle*> heap_column_families;
|
||||
ColumnFamilyHandle** mutable_column_families = MakeMutableCfHandles(
|
||||
column_families, num_keys, &stack_column_families, &heap_column_families);
|
||||
DBImpl::MultiGet(options, num_keys, mutable_column_families, keys, values,
|
||||
timestamps != nullptr ? timestamps->data() : nullptr,
|
||||
statuses, sorted_input);
|
||||
}
|
||||
|
||||
Iterator* DBImplReadOnly::NewIterator(const ReadOptions& _read_options,
|
||||
ColumnFamilyHandle* column_family) {
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
@@ -82,65 +107,111 @@ Status DBImplReadOnly::NewIterators(
|
||||
const ReadOptions& read_options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
std::vector<Iterator*>* iterators) {
|
||||
if (read_options.timestamp) {
|
||||
for (auto* cf : column_families) {
|
||||
assert(cf);
|
||||
const Status s = FailIfTsMismatchCf(cf, *(read_options.timestamp));
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (auto* cf : column_families) {
|
||||
assert(cf);
|
||||
const Status s = FailIfCfHasTs(cf);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
return NewIterators(
|
||||
std::vector<ReadOptions>(column_families.size(), read_options),
|
||||
column_families, iterators);
|
||||
}
|
||||
|
||||
ReadCallback* read_callback = nullptr; // No read callback provided.
|
||||
Status DBImplReadOnly::NewIterators(
|
||||
const std::vector<ReadOptions>& read_options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
std::vector<Iterator*>* iterators) {
|
||||
if (read_options.size() != column_families.size()) {
|
||||
return Status::InvalidArgument(
|
||||
"read_options and column_families must have the same size");
|
||||
}
|
||||
if (iterators == nullptr) {
|
||||
return Status::InvalidArgument("iterators not allowed to be nullptr");
|
||||
}
|
||||
|
||||
if (column_families.empty()) {
|
||||
iterators->clear();
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
const Snapshot* const snapshot = read_options.front().snapshot;
|
||||
const bool tailing = read_options.front().tailing;
|
||||
std::vector<ReadOptions> normalized_read_options;
|
||||
normalized_read_options.reserve(read_options.size());
|
||||
for (size_t i = 0; i < read_options.size(); ++i) {
|
||||
const ReadOptions& options = read_options[i];
|
||||
if (options.io_activity != Env::IOActivity::kUnknown &&
|
||||
options.io_activity != Env::IOActivity::kDBIterator) {
|
||||
return Status::InvalidArgument(
|
||||
"Can only call NewIterators with `ReadOptions::io_activity` is "
|
||||
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kDBIterator`");
|
||||
}
|
||||
if (options.read_tier == kPersistedTier) {
|
||||
return Status::NotSupported(
|
||||
"ReadTier::kPersistedData is not yet supported in iterators.");
|
||||
}
|
||||
if (options.snapshot != snapshot) {
|
||||
return Status::InvalidArgument(
|
||||
"All ReadOptions must use the same snapshot");
|
||||
}
|
||||
if (options.tailing != tailing) {
|
||||
return Status::InvalidArgument(
|
||||
"All ReadOptions must use the same tailing setting");
|
||||
}
|
||||
|
||||
auto* cf = column_families[i];
|
||||
assert(cf);
|
||||
Status s;
|
||||
if (options.timestamp) {
|
||||
s = FailIfTsMismatchCf(cf, *(options.timestamp));
|
||||
} else {
|
||||
s = FailIfCfHasTs(cf);
|
||||
}
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
normalized_read_options.emplace_back(options);
|
||||
if (normalized_read_options.back().io_activity ==
|
||||
Env::IOActivity::kUnknown) {
|
||||
normalized_read_options.back().io_activity = Env::IOActivity::kDBIterator;
|
||||
}
|
||||
}
|
||||
|
||||
iterators->clear();
|
||||
iterators->reserve(column_families.size());
|
||||
SequenceNumber latest_snapshot = versions_->LastSequence();
|
||||
|
||||
SequenceNumber read_seq =
|
||||
read_options.snapshot != nullptr
|
||||
? static_cast<const SnapshotImpl*>(read_options.snapshot)->number_
|
||||
: latest_snapshot;
|
||||
|
||||
snapshot != nullptr ? static_cast<const SnapshotImpl*>(snapshot)->number_
|
||||
: versions_->LastSequence();
|
||||
autovector<std::tuple<ColumnFamilyHandleImpl*, SuperVersion*>> cfh_to_sv;
|
||||
for (auto* cf : column_families) {
|
||||
auto* cfh = static_cast_with_check<ColumnFamilyHandleImpl>(cf);
|
||||
cfh_to_sv.emplace_back(cfh, cfh->cfd()->GetSuperVersion()->Ref());
|
||||
}
|
||||
|
||||
const bool check_read_ts =
|
||||
read_options.timestamp && read_options.timestamp->size() > 0;
|
||||
for (auto cfh : column_families) {
|
||||
auto* cfd = static_cast_with_check<ColumnFamilyHandleImpl>(cfh)->cfd();
|
||||
auto* sv = cfd->GetSuperVersion()->Ref();
|
||||
cfh_to_sv.emplace_back(static_cast_with_check<ColumnFamilyHandleImpl>(cfh),
|
||||
sv);
|
||||
if (check_read_ts) {
|
||||
const auto unref_super_versions = [&]() {
|
||||
for (const auto& entry : cfh_to_sv) {
|
||||
std::get<1>(entry)->Unref();
|
||||
}
|
||||
};
|
||||
for (size_t i = 0; i < cfh_to_sv.size(); ++i) {
|
||||
const ReadOptions& options = normalized_read_options[i];
|
||||
if (options.timestamp && !options.timestamp->empty()) {
|
||||
auto* cfd = std::get<0>(cfh_to_sv[i])->cfd();
|
||||
auto* sv = std::get<1>(cfh_to_sv[i]);
|
||||
const Status s =
|
||||
FailIfReadCollapsedHistory(cfd, sv, *(read_options.timestamp));
|
||||
FailIfReadCollapsedHistory(cfd, sv, *(options.timestamp));
|
||||
if (!s.ok()) {
|
||||
for (auto prev_entry : cfh_to_sv) {
|
||||
std::get<1>(prev_entry)->Unref();
|
||||
}
|
||||
unref_super_versions();
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert(cfh_to_sv.size() == column_families.size());
|
||||
for (auto [cfh, sv] : cfh_to_sv) {
|
||||
auto* db_iter = NewArenaWrappedDbIterator(
|
||||
env_, read_options, cfh, sv, read_seq, read_callback, this,
|
||||
/*expose_blob_index=*/false, /*allow_refresh=*/false,
|
||||
/*allow_mark_memtable_for_flush=*/false);
|
||||
iterators->push_back(db_iter);
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < cfh_to_sv.size(); ++i) {
|
||||
auto* cfh = std::get<0>(cfh_to_sv[i]);
|
||||
auto* sv = std::get<1>(cfh_to_sv[i]);
|
||||
iterators->push_back(NewArenaWrappedDbIterator(
|
||||
env_, normalized_read_options[i], cfh, sv, read_seq,
|
||||
nullptr /* read_callback */, this, /* expose_blob_index */ false,
|
||||
/* allow_refresh */ false, /* allow_mark_memtable_for_flush */ false));
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,13 @@ class DBImplReadOnly : public DBImpl {
|
||||
const Slice& key,
|
||||
GetImplOptions& get_impl_options);
|
||||
|
||||
// TODO: Implement ReadOnly MultiGet?
|
||||
using DBImpl::MultiGetWithMetadata;
|
||||
void MultiGetWithMetadata(const ReadOptions& options, const size_t num_keys,
|
||||
ColumnFamilyHandle* const* column_families,
|
||||
const Slice* keys, PinnableSlice* values,
|
||||
Status* statuses,
|
||||
MultiGetOutputMetadata* output_metadata,
|
||||
const bool sorted_input = false) override;
|
||||
|
||||
using DBImpl::NewIterator;
|
||||
Iterator* NewIterator(const ReadOptions& _read_options,
|
||||
@@ -37,6 +43,9 @@ class DBImplReadOnly : public DBImpl {
|
||||
Status NewIterators(const ReadOptions& options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
std::vector<Iterator*>* iterators) override;
|
||||
Status NewIterators(const std::vector<ReadOptions>& read_options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
std::vector<Iterator*>* iterators) override;
|
||||
|
||||
using DBImpl::Put;
|
||||
Status Put(const WriteOptions& /*options*/,
|
||||
|
||||
@@ -20,6 +20,10 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImplReadOnly::GetImpl)
|
||||
get_impl_options.merge_operands != nullptr);
|
||||
assert(get_impl_options.column_family);
|
||||
|
||||
// This instance cannot observe writes after one of its snapshots, so the
|
||||
// false result initialized by GetWithMetadata is definitive.
|
||||
get_impl_options.newer_version_present = nullptr;
|
||||
|
||||
Status s;
|
||||
|
||||
if (read_options.timestamp) {
|
||||
@@ -49,7 +53,13 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImplReadOnly::GetImpl)
|
||||
|
||||
const Comparator* ucmp = get_impl_options.column_family->GetComparator();
|
||||
assert(ucmp);
|
||||
SequenceNumber snapshot = versions_->LastSequence();
|
||||
// For a batched lazy read (MultiGetEntityLazy), use the SuperVersion and
|
||||
// consistent sequence number the batch already fixed (see
|
||||
// GetImplOptions::lazy_columns_shared_sv); otherwise this read-only
|
||||
// instance's stable current SuperVersion and last sequence.
|
||||
SequenceNumber snapshot = get_impl_options.lazy_columns_shared_sv != nullptr
|
||||
? get_impl_options.lazy_columns_snapshot_seq
|
||||
: versions_->LastSequence();
|
||||
GetWithTimestampReadCallback read_cb(snapshot);
|
||||
auto cfh = static_cast_with_check<ColumnFamilyHandleImpl>(
|
||||
get_impl_options.column_family);
|
||||
@@ -77,7 +87,10 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImplReadOnly::GetImpl)
|
||||
|
||||
// In read-only mode Get(), no super version operation is needed (i.e.
|
||||
// GetAndRefSuperVersion and ReturnAndCleanupSuperVersion)
|
||||
SuperVersion* super_version = cfd->GetSuperVersion();
|
||||
SuperVersion* super_version =
|
||||
get_impl_options.lazy_columns_shared_sv != nullptr
|
||||
? get_impl_options.lazy_columns_shared_sv
|
||||
: cfd->GetSuperVersion();
|
||||
if (read_options.timestamp && read_options.timestamp->size() > 0) {
|
||||
s = FailIfReadCollapsedHistory(cfd, super_version,
|
||||
*(read_options.timestamp));
|
||||
@@ -157,15 +170,19 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImplReadOnly::GetImpl)
|
||||
PERF_COUNTER_ADD(get_read_bytes, size);
|
||||
}
|
||||
}
|
||||
if (get_impl_options.lazy_columns_version != nullptr && s.ok()) {
|
||||
// Lazy result (GetEntityLazy / MultiGetEntityLazy): hand back the Version
|
||||
// the entity's blob references must be resolved against (both the
|
||||
// single-key and batched paths).
|
||||
*get_impl_options.lazy_columns_version = super_version->current;
|
||||
}
|
||||
if (get_impl_options.lazy_columns_pin != nullptr && s.ok()) {
|
||||
// Lazy result (GetEntityLazy): hand back the Version the entity's blob
|
||||
// references must be resolved against, and transfer a SuperVersion pin into
|
||||
// the result so it -- and deferred blob reads -- stay valid after this call
|
||||
// (as an iterator's pin does). Read-only borrows the current SuperVersion
|
||||
// without a reference, so the pin takes its own.
|
||||
if (get_impl_options.lazy_columns_version != nullptr) {
|
||||
*get_impl_options.lazy_columns_version = super_version->current;
|
||||
}
|
||||
// Single-key GetEntityLazy: transfer a SuperVersion pin into the result so
|
||||
// it -- and deferred blob reads -- stay valid after this call (as an
|
||||
// iterator's pin does). Read-only borrows the current SuperVersion without
|
||||
// a reference, so the pin takes its own. The batched path passes no pin (it
|
||||
// uses lazy_columns_shared_sv); the batch holds one shared pin per column
|
||||
// family instead.
|
||||
TransferSuperVersionPin(super_version, get_impl_options.lazy_columns_pin);
|
||||
}
|
||||
CO_RETURN s;
|
||||
|
||||
+477
-80
@@ -11,6 +11,7 @@
|
||||
|
||||
#include "db/arena_wrapped_db_iter.h"
|
||||
#include "db/blob/blob_fetcher.h"
|
||||
#include "db/db_impl/db_impl_metadata.h"
|
||||
#include "db/log_reader.h"
|
||||
#include "db/log_writer.h"
|
||||
#include "db/merge_context.h"
|
||||
@@ -22,6 +23,7 @@
|
||||
#include "monitoring/perf_context_imp.h"
|
||||
#include "rocksdb/convenience.h"
|
||||
#include "rocksdb/utilities/options_util.h"
|
||||
#include "util/atomic.h"
|
||||
#include "util/cast_util.h"
|
||||
#include "util/write_batch_util.h"
|
||||
|
||||
@@ -32,12 +34,222 @@ DBImplSecondary::DBImplSecondary(const DBOptions& db_options,
|
||||
std::string secondary_path)
|
||||
: DBImpl(db_options, dbname, false, true, true),
|
||||
secondary_path_(std::move(secondary_path)) {
|
||||
secondary_read_view_reclaimer_.reset(new port::Thread(
|
||||
&DBImplSecondary::ReclaimRetiredSecondaryReadViewsLoop, this));
|
||||
ROCKS_LOG_INFO(immutable_db_options_.info_log,
|
||||
"Opening the db in secondary mode");
|
||||
LogFlush(immutable_db_options_.info_log);
|
||||
}
|
||||
|
||||
DBImplSecondary::~DBImplSecondary() = default;
|
||||
DBImplSecondary::~DBImplSecondary() {
|
||||
ResetSecondaryReadView();
|
||||
StopSecondaryReadViewReclaimer();
|
||||
}
|
||||
|
||||
// REQUIRES: mutex_ not held. CleanupSuperVersion() locks mutex_ to release a
|
||||
// SuperVersion's last reference, so dropping the last reference to a view while
|
||||
// mutex_ is held self-deadlocks on the non-reentrant mutex.
|
||||
DBImplSecondary::SecondaryReadView::~SecondaryReadView() {
|
||||
for (const SuperVersions::value_type& entry : super_versions) {
|
||||
db->CleanupSuperVersion(entry.second);
|
||||
}
|
||||
}
|
||||
|
||||
void DBImplSecondary::PublishSecondaryReadView() {
|
||||
mutex_.AssertHeld();
|
||||
|
||||
if (secondary_read_view_closed_) {
|
||||
// A catch-up that raced ResetSecondaryReadView() must not republish, or the
|
||||
// new view would outlive the drain that close waits on.
|
||||
return;
|
||||
}
|
||||
|
||||
std::shared_ptr<SecondaryReadView> new_view =
|
||||
std::make_shared<SecondaryReadView>(this, versions_->LastSequence());
|
||||
for (ColumnFamilyData* cfd : *versions_->GetColumnFamilySet()) {
|
||||
if (cfd->IsDropped() || !cfd->initialized()) {
|
||||
continue;
|
||||
}
|
||||
SuperVersion* super_version = cfd->GetSuperVersion();
|
||||
if (super_version == nullptr) {
|
||||
continue;
|
||||
}
|
||||
new_view->super_versions.emplace(cfd->GetID(), super_version->Ref());
|
||||
}
|
||||
|
||||
// Load the outgoing view before the store, and keep that reference until it
|
||||
// reaches the retirement queue. The store drops the published reference, so
|
||||
// without this local it could be the last one and ~SecondaryReadView() would
|
||||
// run here, under mutex_. Retiring before the store would leave the view
|
||||
// reachable by readers after it was queued for reclamation.
|
||||
std::shared_ptr<const SecondaryReadView> previous_view =
|
||||
AtomicSharedPtrLoad(&secondary_read_view_, std::memory_order_acquire);
|
||||
AtomicSharedPtrStore(
|
||||
&secondary_read_view_,
|
||||
std::shared_ptr<const SecondaryReadView>(std::move(new_view)),
|
||||
std::memory_order_release);
|
||||
RetireSecondaryReadView(std::move(previous_view));
|
||||
}
|
||||
|
||||
void DBImplSecondary::RetireSecondaryReadView(
|
||||
std::shared_ptr<const SecondaryReadView> read_view) {
|
||||
if (read_view == nullptr) {
|
||||
return;
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(retired_secondary_read_views_mutex_);
|
||||
retired_secondary_read_views_.emplace_back(std::move(read_view));
|
||||
}
|
||||
retired_secondary_read_views_cv_.notify_all();
|
||||
}
|
||||
|
||||
bool DBImplSecondary::HasReclaimableSecondaryReadView() const {
|
||||
for (const auto& read_view : retired_secondary_read_views_) {
|
||||
if (read_view.use_count() == 1) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool DBImplSecondary::AllSecondaryReadViewsReclaimed() const {
|
||||
return retired_secondary_read_views_.empty() &&
|
||||
secondary_read_view_reclaims_in_flight_ == 0;
|
||||
}
|
||||
|
||||
bool DBImplSecondary::ReclaimRetiredSecondaryReadViews() {
|
||||
// Keep moved views alive until after the retirement lock is released. Their
|
||||
// destructors call CleanupSuperVersion(), which can lock mutex_.
|
||||
std::vector<std::shared_ptr<const SecondaryReadView>> reclaimable_views;
|
||||
size_t num_reclaiming;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(retired_secondary_read_views_mutex_);
|
||||
for (auto iter = retired_secondary_read_views_.begin();
|
||||
iter != retired_secondary_read_views_.end();) {
|
||||
// The view is no longer published, so its reference count can only
|
||||
// decrease. A count of one means the retirement queue is the sole owner.
|
||||
if (iter->use_count() == 1) {
|
||||
reclaimable_views.emplace_back(std::move(*iter));
|
||||
iter = retired_secondary_read_views_.erase(iter);
|
||||
} else {
|
||||
++iter;
|
||||
}
|
||||
}
|
||||
num_reclaiming = reclaimable_views.size();
|
||||
secondary_read_view_reclaims_in_flight_ += num_reclaiming;
|
||||
if (num_reclaiming == 0) {
|
||||
return AllSecondaryReadViewsReclaimed();
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SYNC_POINT("DBImplSecondary::ReclaimRetiredReadViews:BeforeReclaim");
|
||||
reclaimable_views.clear();
|
||||
bool all_reclaimed;
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(retired_secondary_read_views_mutex_);
|
||||
assert(secondary_read_view_reclaims_in_flight_ >= num_reclaiming);
|
||||
secondary_read_view_reclaims_in_flight_ -= num_reclaiming;
|
||||
all_reclaimed = AllSecondaryReadViewsReclaimed();
|
||||
}
|
||||
retired_secondary_read_views_cv_.notify_all();
|
||||
TEST_SYNC_POINT("DBImplSecondary::ReclaimRetiredReadViews:AfterReclaim");
|
||||
return all_reclaimed;
|
||||
}
|
||||
|
||||
void DBImplSecondary::ReclaimRetiredSecondaryReadViewsLoop() {
|
||||
while (true) {
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(retired_secondary_read_views_mutex_);
|
||||
retired_secondary_read_views_cv_.wait(lock, [this]() {
|
||||
if (secondary_read_view_reclaimer_stopping_ &&
|
||||
AllSecondaryReadViewsReclaimed()) {
|
||||
return true;
|
||||
}
|
||||
return HasReclaimableSecondaryReadView();
|
||||
});
|
||||
if (secondary_read_view_reclaimer_stopping_ &&
|
||||
AllSecondaryReadViewsReclaimed()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
TEST_SYNC_POINT(
|
||||
"DBImplSecondary::ReclaimRetiredReadViewsLoop:BeforeReclaim");
|
||||
ReclaimRetiredSecondaryReadViews();
|
||||
}
|
||||
}
|
||||
|
||||
void DBImplSecondary::CleanupRetiredSecondaryReadViews() {
|
||||
ReclaimRetiredSecondaryReadViews();
|
||||
}
|
||||
|
||||
void DBImplSecondary::DrainRetiredSecondaryReadViews() {
|
||||
while (!ReclaimRetiredSecondaryReadViews()) {
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(retired_secondary_read_views_mutex_);
|
||||
TEST_SYNC_POINT("DBImplSecondary::DrainRetiredReadViews:Wait");
|
||||
retired_secondary_read_views_cv_.wait(lock, [this]() {
|
||||
if (AllSecondaryReadViewsReclaimed()) {
|
||||
return true;
|
||||
}
|
||||
return HasReclaimableSecondaryReadView();
|
||||
});
|
||||
}
|
||||
TEST_SYNC_POINT("DBImplSecondary::DrainRetiredReadViews:AfterWait");
|
||||
}
|
||||
}
|
||||
|
||||
void DBImplSecondary::ReleaseSecondaryReadView(
|
||||
std::shared_ptr<const SecondaryReadView>* read_view) {
|
||||
read_view->reset();
|
||||
{
|
||||
// Synchronize the reference-count change with the wait predicate so its
|
||||
// notification cannot be lost.
|
||||
std::lock_guard<std::mutex> lock(retired_secondary_read_views_mutex_);
|
||||
retired_secondary_read_views_cv_.notify_all();
|
||||
}
|
||||
}
|
||||
|
||||
void DBImplSecondary::StopSecondaryReadViewReclaimer() {
|
||||
if (secondary_read_view_reclaimer_ == nullptr) {
|
||||
return;
|
||||
}
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(retired_secondary_read_views_mutex_);
|
||||
secondary_read_view_reclaimer_stopping_ = true;
|
||||
}
|
||||
retired_secondary_read_views_cv_.notify_all();
|
||||
TEST_SYNC_POINT("DBImplSecondary::StopSecondaryReadViewReclaimer:BeforeJoin");
|
||||
secondary_read_view_reclaimer_->join();
|
||||
secondary_read_view_reclaimer_.reset();
|
||||
}
|
||||
|
||||
void DBImplSecondary::ResetSecondaryReadView() {
|
||||
std::shared_ptr<const SecondaryReadView> read_view;
|
||||
{
|
||||
// mutex_ makes this read-modify-write atomic against
|
||||
// PublishSecondaryReadView(), which holds it too. Without that, a catch-up
|
||||
// racing close can read the same outgoing view and retire it twice, leaving
|
||||
// two queue entries that each see use_count() == 2 so the drain below never
|
||||
// finishes.
|
||||
InstrumentedMutexLock lock_guard(&mutex_);
|
||||
secondary_read_view_closed_ = true;
|
||||
read_view =
|
||||
AtomicSharedPtrLoad(&secondary_read_view_, std::memory_order_acquire);
|
||||
AtomicSharedPtrStore(&secondary_read_view_,
|
||||
std::shared_ptr<const SecondaryReadView>(),
|
||||
std::memory_order_release);
|
||||
}
|
||||
RetireSecondaryReadView(std::move(read_view));
|
||||
DrainRetiredSecondaryReadViews();
|
||||
}
|
||||
|
||||
Status DBImplSecondary::CloseImpl() {
|
||||
// The view's SuperVersions pin their column families, so release them before
|
||||
// DBImpl::CloseImpl() destroys the VersionSet.
|
||||
ResetSecondaryReadView();
|
||||
StopSecondaryReadViewReclaimer();
|
||||
return DBImpl::CloseImpl();
|
||||
}
|
||||
|
||||
Status DBImplSecondary::Recover(
|
||||
const std::vector<ColumnFamilyDescriptor>& column_families,
|
||||
@@ -521,6 +733,42 @@ void DBImplSecondary::DeleteResolvedRecoveredTransactions() {
|
||||
}
|
||||
}
|
||||
|
||||
void DBImplSecondary::MultiGetWithMetadata(
|
||||
const ReadOptions& options, const size_t num_keys,
|
||||
ColumnFamilyHandle* const* column_families, const Slice* keys,
|
||||
PinnableSlice* values, Status* statuses,
|
||||
MultiGetOutputMetadata* output_metadata, const bool sorted_input) {
|
||||
// The only secondary-specific policy here is rejecting newer-version
|
||||
// tracking for explicit snapshots: catch-up can advance a secondary's view.
|
||||
// The remaining code initializes requested metadata and forwards to the
|
||||
// existing MultiGet implementation.
|
||||
std::vector<std::string>* timestamps = GetOutputTimestamps(output_metadata);
|
||||
if (timestamps != nullptr) {
|
||||
timestamps->resize(num_keys);
|
||||
}
|
||||
std::vector<uint8_t>* newer_version_present =
|
||||
GetOutputNewerVersionPresent(output_metadata);
|
||||
if (newer_version_present != nullptr) {
|
||||
newer_version_present->assign(num_keys, false);
|
||||
if (options.snapshot != nullptr) {
|
||||
const Status s = Status::NotSupported(
|
||||
"MultiGetWithMetadata is not supported in secondary DB mode");
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
statuses[i] = s;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
autovector<ColumnFamilyHandle*, MultiGetContext::MAX_BATCH_SIZE>
|
||||
stack_column_families;
|
||||
std::vector<ColumnFamilyHandle*> heap_column_families;
|
||||
ColumnFamilyHandle** mutable_column_families = MakeMutableCfHandles(
|
||||
column_families, num_keys, &stack_column_families, &heap_column_families);
|
||||
DBImpl::MultiGet(options, num_keys, mutable_column_families, keys, values,
|
||||
timestamps != nullptr ? timestamps->data() : nullptr,
|
||||
statuses, sorted_input);
|
||||
}
|
||||
|
||||
Iterator* DBImplSecondary::NewIterator(const ReadOptions& _read_options,
|
||||
ColumnFamilyHandle* column_family) {
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
@@ -585,8 +833,9 @@ ArenaWrappedDBIter* DBImplSecondary::NewIteratorImpl(
|
||||
SuperVersion* super_version, SequenceNumber snapshot,
|
||||
ReadCallback* read_callback, bool expose_blob_index, bool allow_refresh) {
|
||||
assert(nullptr != cfh);
|
||||
assert(snapshot == kMaxSequenceNumber);
|
||||
snapshot = versions_->LastSequence();
|
||||
if (snapshot == kMaxSequenceNumber) {
|
||||
snapshot = versions_->LastSequence();
|
||||
}
|
||||
assert(snapshot != kMaxSequenceNumber);
|
||||
return NewArenaWrappedDbIterator(env_, read_options, cfh, super_version,
|
||||
snapshot, read_callback, this,
|
||||
@@ -598,76 +847,165 @@ Status DBImplSecondary::NewIterators(
|
||||
const ReadOptions& _read_options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
std::vector<Iterator*>* iterators) {
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
_read_options.io_activity != Env::IOActivity::kDBIterator) {
|
||||
if (column_families.empty()) {
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
_read_options.io_activity != Env::IOActivity::kDBIterator) {
|
||||
return Status::InvalidArgument(
|
||||
"Can only call NewIterators with `ReadOptions::io_activity` is "
|
||||
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kDBIterator`");
|
||||
}
|
||||
if (_read_options.read_tier == kPersistedTier) {
|
||||
return Status::NotSupported(
|
||||
"ReadTier::kPersistedData is not yet supported in iterators.");
|
||||
}
|
||||
if (_read_options.tailing) {
|
||||
return Status::NotSupported(
|
||||
"tailing iterator not supported in secondary mode");
|
||||
}
|
||||
if (_read_options.snapshot != nullptr) {
|
||||
return Status::NotSupported("snapshot not supported in secondary mode");
|
||||
}
|
||||
}
|
||||
return NewIteratorsImpl(
|
||||
std::vector<ReadOptions>(column_families.size(), _read_options),
|
||||
column_families, iterators);
|
||||
}
|
||||
|
||||
Status DBImplSecondary::NewIterators(
|
||||
const std::vector<ReadOptions>& read_options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
std::vector<Iterator*>* iterators) {
|
||||
return NewIteratorsImpl(read_options, column_families, iterators);
|
||||
}
|
||||
|
||||
Status DBImplSecondary::NewIteratorsImpl(
|
||||
const std::vector<ReadOptions>& read_options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
std::vector<Iterator*>* iterators) {
|
||||
if (read_options.size() != column_families.size()) {
|
||||
return Status::InvalidArgument(
|
||||
"Can only call NewIterators with `ReadOptions::io_activity` is "
|
||||
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kDBIterator`");
|
||||
"read_options and column_families must have the same size");
|
||||
}
|
||||
ReadOptions read_options(_read_options);
|
||||
if (read_options.io_activity == Env::IOActivity::kUnknown) {
|
||||
read_options.io_activity = Env::IOActivity::kDBIterator;
|
||||
}
|
||||
if (read_options.read_tier == kPersistedTier) {
|
||||
return Status::NotSupported(
|
||||
"ReadTier::kPersistedData is not yet supported in iterators.");
|
||||
}
|
||||
ReadCallback* read_callback = nullptr; // No read callback provided.
|
||||
if (iterators == nullptr) {
|
||||
return Status::InvalidArgument("iterators not allowed to be nullptr");
|
||||
}
|
||||
|
||||
if (read_options.timestamp) {
|
||||
for (auto* cf : column_families) {
|
||||
assert(cf);
|
||||
const Status s = FailIfTsMismatchCf(cf, *(read_options.timestamp));
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
if (column_families.empty()) {
|
||||
iterators->clear();
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
const Snapshot* const snapshot = read_options.front().snapshot;
|
||||
const bool tailing = read_options.front().tailing;
|
||||
std::vector<ReadOptions> normalized_read_options;
|
||||
normalized_read_options.reserve(read_options.size());
|
||||
for (size_t i = 0; i < read_options.size(); ++i) {
|
||||
const ReadOptions& options = read_options[i];
|
||||
if (options.io_activity != Env::IOActivity::kUnknown &&
|
||||
options.io_activity != Env::IOActivity::kDBIterator) {
|
||||
return Status::InvalidArgument(
|
||||
"Can only call NewIterators with `ReadOptions::io_activity` is "
|
||||
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kDBIterator`");
|
||||
}
|
||||
} else {
|
||||
for (auto* cf : column_families) {
|
||||
assert(cf);
|
||||
const Status s = FailIfCfHasTs(cf);
|
||||
if (options.read_tier == kPersistedTier) {
|
||||
return Status::NotSupported(
|
||||
"ReadTier::kPersistedData is not yet supported in iterators.");
|
||||
}
|
||||
if (options.snapshot != snapshot) {
|
||||
return Status::InvalidArgument(
|
||||
"All ReadOptions must use the same snapshot");
|
||||
}
|
||||
if (options.tailing != tailing) {
|
||||
return Status::InvalidArgument(
|
||||
"All ReadOptions must use the same tailing setting");
|
||||
}
|
||||
if (options.tailing) {
|
||||
return Status::NotSupported(
|
||||
"tailing iterator not supported in secondary mode");
|
||||
}
|
||||
if (options.snapshot != nullptr) {
|
||||
return Status::NotSupported("snapshot not supported in secondary mode");
|
||||
}
|
||||
|
||||
auto* cf = column_families[i];
|
||||
assert(cf);
|
||||
Status s;
|
||||
if (options.timestamp) {
|
||||
s = FailIfTsMismatchCf(cf, *(options.timestamp));
|
||||
} else {
|
||||
s = FailIfCfHasTs(cf);
|
||||
}
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
normalized_read_options.emplace_back(options);
|
||||
if (normalized_read_options.back().io_activity ==
|
||||
Env::IOActivity::kUnknown) {
|
||||
normalized_read_options.back().io_activity = Env::IOActivity::kDBIterator;
|
||||
}
|
||||
}
|
||||
|
||||
iterators->clear();
|
||||
iterators->reserve(column_families.size());
|
||||
|
||||
std::shared_ptr<const SecondaryReadView> read_view =
|
||||
AtomicSharedPtrLoad(&secondary_read_view_, std::memory_order_acquire);
|
||||
if (read_view == nullptr) {
|
||||
return Status::InvalidArgument("Secondary read view is not initialized");
|
||||
}
|
||||
TEST_SYNC_POINT("DBImplSecondary::NewIterators:AfterLoadReadView");
|
||||
|
||||
const SequenceNumber read_seq = read_view->sequence;
|
||||
autovector<ColumnFamilySuperVersionPair, MultiGetContext::MAX_BATCH_SIZE>
|
||||
cf_sv_pairs;
|
||||
const auto cleanup_super_versions = [&]() {
|
||||
for (const auto& cf_sv_pair : cf_sv_pairs) {
|
||||
CleanupSuperVersion(cf_sv_pair.super_version);
|
||||
}
|
||||
};
|
||||
for (ColumnFamilyHandle* cf : column_families) {
|
||||
ColumnFamilyHandleImpl* cfh =
|
||||
static_cast_with_check<ColumnFamilyHandleImpl>(cf);
|
||||
const SecondaryReadView::SuperVersions::const_iterator sv_iter =
|
||||
read_view->super_versions.find(cfh->GetID());
|
||||
if (sv_iter == read_view->super_versions.end()) {
|
||||
if (!cfh->cfd()->IsDropped()) {
|
||||
cleanup_super_versions();
|
||||
ReleaseSecondaryReadView(&read_view);
|
||||
return Status::InvalidArgument(
|
||||
"Column family is not part of the secondary read view");
|
||||
}
|
||||
// A retained handle remains readable after its column family is dropped.
|
||||
// Its immutable final SuperVersion is intentionally not published, since
|
||||
// doing so would keep the dropped column family alive indefinitely.
|
||||
cf_sv_pairs.emplace_back(cfh,
|
||||
cfh->cfd()->GetReferencedSuperVersion(this));
|
||||
} else {
|
||||
cf_sv_pairs.emplace_back(cfh, sv_iter->second->Ref());
|
||||
}
|
||||
}
|
||||
ReleaseSecondaryReadView(&read_view);
|
||||
TEST_SYNC_POINT("DBImplSecondary::NewIterators:AfterReleaseReadView");
|
||||
for (size_t i = 0; i < cf_sv_pairs.size(); ++i) {
|
||||
const auto& cf_sv_pair = cf_sv_pairs[i];
|
||||
const ReadOptions& options = normalized_read_options[i];
|
||||
if (options.timestamp && !options.timestamp->empty()) {
|
||||
const Status s = FailIfReadCollapsedHistory(
|
||||
cf_sv_pair.cfd, cf_sv_pair.super_version, *(options.timestamp));
|
||||
if (!s.ok()) {
|
||||
cleanup_super_versions();
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
iterators->clear();
|
||||
iterators->reserve(column_families.size());
|
||||
if (read_options.tailing) {
|
||||
return Status::NotSupported(
|
||||
"tailing iterator not supported in secondary mode");
|
||||
} else if (read_options.snapshot != nullptr) {
|
||||
// TODO (yanqin) support snapshot.
|
||||
return Status::NotSupported("snapshot not supported in secondary mode");
|
||||
} else {
|
||||
SequenceNumber read_seq(kMaxSequenceNumber);
|
||||
autovector<std::tuple<ColumnFamilyHandleImpl*, SuperVersion*>> cfh_to_sv;
|
||||
const bool check_read_ts =
|
||||
read_options.timestamp && read_options.timestamp->size() > 0;
|
||||
for (auto cf : column_families) {
|
||||
auto cfh = static_cast_with_check<ColumnFamilyHandleImpl>(cf);
|
||||
auto cfd = cfh->cfd();
|
||||
SuperVersion* sv = cfd->GetReferencedSuperVersion(this);
|
||||
cfh_to_sv.emplace_back(cfh, sv);
|
||||
if (check_read_ts) {
|
||||
const Status s =
|
||||
FailIfReadCollapsedHistory(cfd, sv, *(read_options.timestamp));
|
||||
if (!s.ok()) {
|
||||
for (auto prev_entry : cfh_to_sv) {
|
||||
CleanupSuperVersion(std::get<1>(prev_entry));
|
||||
}
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
assert(cfh_to_sv.size() == column_families.size());
|
||||
for (auto [cfh, sv] : cfh_to_sv) {
|
||||
iterators->push_back(
|
||||
NewIteratorImpl(read_options, cfh, sv, read_seq, read_callback));
|
||||
}
|
||||
for (size_t i = 0; i < cf_sv_pairs.size(); ++i) {
|
||||
const auto& cf_sv_pair = cf_sv_pairs[i];
|
||||
iterators->push_back(NewIteratorImpl(
|
||||
normalized_read_options[i], cf_sv_pair.cfh, cf_sv_pair.super_version,
|
||||
read_seq, nullptr /* read_callback */, false /* expose_blob_index */,
|
||||
false /* allow_refresh */));
|
||||
TEST_SYNC_POINT("DBImplSecondary::NewIterators:AfterCreateIterator");
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
@@ -776,8 +1114,14 @@ Status DBImplSecondary::TryCatchUpWithPrimary() {
|
||||
// completions; an outstanding prepare stops the scan.
|
||||
(void)logs_with_prep_tracker_.FindMinLogContainingOutstandingPrep();
|
||||
}
|
||||
TEST_SYNC_POINT(
|
||||
"DBImplSecondary::TryCatchUpWithPrimary:BeforePublishReadView");
|
||||
TEST_SYNC_POINT(
|
||||
"DBImplSecondary::TryCatchUpWithPrimary:AllowPublishReadView");
|
||||
PublishSecondaryReadView();
|
||||
}
|
||||
}
|
||||
CleanupRetiredSecondaryReadViews();
|
||||
job_context.Clean();
|
||||
|
||||
// Cleanup unused, obsolete files.
|
||||
@@ -910,9 +1254,11 @@ Status DBImplSecondary::OpenAsSecondaryImpl(
|
||||
sv_context.NewSuperVersion();
|
||||
cfd->InstallSuperVersion(&sv_context, &impl->mutex_);
|
||||
}
|
||||
impl->PublishSecondaryReadView();
|
||||
impl->MarkAsyncFileOpenNotNeeded();
|
||||
}
|
||||
impl->mutex_.Unlock();
|
||||
impl->CleanupRetiredSecondaryReadViews();
|
||||
sv_context.Clean();
|
||||
job_context.Clean();
|
||||
if (s.ok()) {
|
||||
@@ -1186,27 +1532,48 @@ Status DBImplSecondary::CleanupPhysicalCompactionOutputFiles(
|
||||
}
|
||||
|
||||
Status DBImplSecondary::InitializeCompactionWorkspace(
|
||||
bool allow_resumption, std::unique_ptr<FSDirectory>* output_dir,
|
||||
bool allow_resumption, bool resumption_requested,
|
||||
std::unique_ptr<FSDirectory>* output_dir,
|
||||
std::unique_ptr<log::Writer>* compaction_progress_writer) {
|
||||
// Create output directory if it doest exist yet
|
||||
Status s = CreateAndNewDirectory(fs_.get(), secondary_path_, output_dir);
|
||||
if (!s.ok() || !allow_resumption) {
|
||||
return s;
|
||||
}
|
||||
|
||||
s = PrepareCompactionProgressState();
|
||||
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
s = FinalizeCompactionProgressWriter(compaction_progress_writer);
|
||||
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
if (allow_resumption) {
|
||||
s = PrepareCompactionProgressState();
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
return FinalizeCompactionProgressWriter(compaction_progress_writer);
|
||||
}
|
||||
|
||||
return Status::OK();
|
||||
if (resumption_requested) {
|
||||
// Resumption was requested by the caller but has been disabled internally
|
||||
// (see CompactWithoutInstallation: incompatible with output hash
|
||||
// verification). The caller therefore did not empty output_directory (it
|
||||
// expects the resumption path to own that state), so honor the
|
||||
// OpenAndCompactOptions::allow_resumption=true fallback contract by
|
||||
// cleaning any leftover progress and output files here, starting the fresh
|
||||
// compaction from a clean directory. Without this, output files left by a
|
||||
// previously interrupted attempt collide with the file numbers the fresh
|
||||
// compaction reuses.
|
||||
CompactionProgressFilesScan scan_result;
|
||||
s = ScanCompactionProgressFiles(&scan_result);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
s = CleanupOldAndTemporaryCompactionProgressFiles(
|
||||
/*preserve_latest=*/false, scan_result);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
s = HandleInvalidOrNoCompactionProgress(
|
||||
/*compaction_progress_file_path=*/std::nullopt, scan_result);
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
// PrepareCompactionProgressState() manages compaction progress files and output
|
||||
@@ -1434,8 +1801,9 @@ Status DBImplSecondary::CompactWithoutInstallation(
|
||||
|
||||
mutex_.Unlock();
|
||||
|
||||
s = InitializeCompactionWorkspace(allow_resumption, &output_dir,
|
||||
&compaction_progress_writer);
|
||||
s = InitializeCompactionWorkspace(
|
||||
allow_resumption, /*resumption_requested=*/options.allow_resumption,
|
||||
&output_dir, &compaction_progress_writer);
|
||||
|
||||
mutex_.Lock();
|
||||
|
||||
@@ -1623,6 +1991,10 @@ Status DB::OpenAndCompact(
|
||||
db_options.compaction_service = nullptr;
|
||||
db_options.info_log = override_options.info_log;
|
||||
|
||||
const std::string output_path = RemoteCompactionJobDir(
|
||||
name, db_options.use_session_tmp_dir_for_remote_compaction,
|
||||
output_directory);
|
||||
|
||||
// 4. Filter CFs that are needed for OpenAndCompact()
|
||||
// We do not need to open all column families for the remote compaction.
|
||||
// Only open default CF + target CF. If target CF == default CF, we will open
|
||||
@@ -1674,10 +2046,35 @@ Status DB::OpenAndCompact(
|
||||
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,
|
||||
/*trust_manifest_recovery=*/floor_provided);
|
||||
// This opens the *live* primary's directory read-only, and the primary keeps
|
||||
// rotating its MANIFEST underneath us: each rotation writes a new MANIFEST
|
||||
// and renames a freshly written CURRENT over the old one. POSIX keeps an
|
||||
// already-opened CURRENT/MANIFEST readable after such a replacement, but
|
||||
// filesystems without read-after-unlink semantics (remote or object stores)
|
||||
// report the replaced object as gone instead (Status::PathNotFound /
|
||||
// NotFound, or Status::TryAgain from
|
||||
// ReactiveVersionSet::MaybeSwitchManifest for the MANIFEST equivalent). That
|
||||
// is a transient race, not a broken DB: the next attempt reads the
|
||||
// replacement. Retry a bounded number of times so a concurrent rotation does
|
||||
// not fail the compaction -- a CompactionService that does not fall back to a
|
||||
// local compaction turns such a failure into a primary background error.
|
||||
for (uint32_t retry_count = 0;; ++retry_count) {
|
||||
s = DBImplSecondary::OpenAsSecondaryImpl(
|
||||
db_options, name, output_path, column_families, &handles, &db,
|
||||
/*recover_wal=*/false,
|
||||
/*trust_manifest_recovery=*/floor_provided);
|
||||
if (s.ok() || !(s.IsTryAgain() || s.IsPathNotFound() || s.IsNotFound()) ||
|
||||
retry_count == options.max_secondary_open_retries) {
|
||||
break;
|
||||
}
|
||||
ROCKS_LOG_WARN(db_options.info_log,
|
||||
"OpenAndCompact: secondary open of %s failed with %s "
|
||||
"(retry %" PRIu32 " of %" PRIu32
|
||||
"); the primary may have replaced "
|
||||
"CURRENT/MANIFEST concurrently, retrying",
|
||||
name.c_str(), s.ToString().c_str(), retry_count + 1,
|
||||
options.max_secondary_open_retries);
|
||||
}
|
||||
RecordTimeToHistogram(db_options.statistics.get(),
|
||||
OPEN_AND_COMPACT_DB_OPEN_MICROS,
|
||||
db_options.env->NowMicros() - db_open_start_micros);
|
||||
@@ -1751,7 +2148,7 @@ Status DB::OpenAndCompact(
|
||||
assert(cfh);
|
||||
|
||||
// 7. Run the compaction without installation.
|
||||
// Output will be stored in the directory specified by output_directory
|
||||
// Output will be stored under the DB-owned staging directory.
|
||||
CompactionServiceResult compaction_result;
|
||||
DBImplSecondary* db_secondary =
|
||||
static_cast_with_check<DBImplSecondary>(db.get());
|
||||
|
||||
@@ -5,14 +5,19 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <condition_variable>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "db/db_impl/db_impl.h"
|
||||
#include "logging/logging.h"
|
||||
#include "port/port.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
@@ -104,6 +109,14 @@ class DBImplSecondary : public DBImpl {
|
||||
const Slice& key,
|
||||
GetImplOptions& get_impl_options);
|
||||
|
||||
using DBImpl::MultiGetWithMetadata;
|
||||
void MultiGetWithMetadata(const ReadOptions& options, const size_t num_keys,
|
||||
ColumnFamilyHandle* const* column_families,
|
||||
const Slice* keys, PinnableSlice* values,
|
||||
Status* statuses,
|
||||
MultiGetOutputMetadata* output_metadata,
|
||||
const bool sorted_input = false) override;
|
||||
|
||||
using DBImpl::NewIterator;
|
||||
// Operations on the created iterators can return IOError due to files being
|
||||
// deleted by the primary. To avoid IOError in this case, application can
|
||||
@@ -127,6 +140,9 @@ class DBImplSecondary : public DBImpl {
|
||||
Status NewIterators(const ReadOptions& _read_options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
std::vector<Iterator*>* iterators) override;
|
||||
Status NewIterators(const std::vector<ReadOptions>& read_options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
std::vector<Iterator*>* iterators) override;
|
||||
|
||||
using DBImpl::Put;
|
||||
Status Put(const WriteOptions& /*options*/,
|
||||
@@ -277,6 +293,8 @@ class DBImplSecondary : public DBImpl {
|
||||
#endif // NDEBUG
|
||||
|
||||
protected:
|
||||
Status CloseImpl() override;
|
||||
|
||||
Status FlushForGetLiveFiles(bool /*force_atomic_flush*/) override {
|
||||
// No-op for read-only DB
|
||||
return Status::OK();
|
||||
@@ -311,9 +329,55 @@ class DBImplSecondary : public DBImpl {
|
||||
std::unique_ptr<log::Reader::Reporter> manifest_reporter_;
|
||||
std::unique_ptr<Status> manifest_reader_status_;
|
||||
|
||||
// Publishes a view of every initialized, non-dropped column family while
|
||||
// mutex_ protects their SuperVersions. The previous view is retained for
|
||||
// cleanup outside mutex_ and outside reader threads. No-op once the view has
|
||||
// been torn down for close.
|
||||
// REQUIRES: mutex_ held
|
||||
void PublishSecondaryReadView();
|
||||
|
||||
// Reclaims retired views that have no readers. This must be called without
|
||||
// mutex_ because releasing a view's final SuperVersion can lock it.
|
||||
void CleanupRetiredSecondaryReadViews();
|
||||
|
||||
private:
|
||||
friend class DB;
|
||||
|
||||
struct SecondaryReadView {
|
||||
using SuperVersions = std::unordered_map<uint32_t, SuperVersion*>;
|
||||
|
||||
SecondaryReadView(DBImplSecondary* db_arg, SequenceNumber sequence_arg)
|
||||
: db(db_arg), sequence(sequence_arg) {}
|
||||
~SecondaryReadView();
|
||||
SecondaryReadView(const SecondaryReadView&) = delete;
|
||||
SecondaryReadView& operator=(const SecondaryReadView&) = delete;
|
||||
SecondaryReadView(SecondaryReadView&&) = delete;
|
||||
SecondaryReadView& operator=(SecondaryReadView&&) = delete;
|
||||
|
||||
DBImplSecondary* db;
|
||||
SequenceNumber sequence;
|
||||
SuperVersions super_versions;
|
||||
};
|
||||
|
||||
void RetireSecondaryReadView(
|
||||
std::shared_ptr<const SecondaryReadView> read_view);
|
||||
// REQUIRES: retired_secondary_read_views_mutex_ held
|
||||
bool HasReclaimableSecondaryReadView() const;
|
||||
// REQUIRES: retired_secondary_read_views_mutex_ held
|
||||
bool AllSecondaryReadViewsReclaimed() const;
|
||||
bool ReclaimRetiredSecondaryReadViews();
|
||||
void ReclaimRetiredSecondaryReadViewsLoop();
|
||||
void DrainRetiredSecondaryReadViews();
|
||||
void StopSecondaryReadViewReclaimer();
|
||||
void ReleaseSecondaryReadView(
|
||||
std::shared_ptr<const SecondaryReadView>* read_view);
|
||||
void ResetSecondaryReadView();
|
||||
|
||||
Status NewIteratorsImpl(
|
||||
const std::vector<ReadOptions>& read_options,
|
||||
const std::vector<ColumnFamilyHandle*>& column_families,
|
||||
std::vector<Iterator*>* iterators);
|
||||
|
||||
// No copying allowed
|
||||
DBImplSecondary(const DBImplSecondary&);
|
||||
void operator=(const DBImplSecondary&);
|
||||
@@ -420,7 +484,8 @@ class DBImplSecondary : public DBImpl {
|
||||
};
|
||||
|
||||
Status InitializeCompactionWorkspace(
|
||||
bool allow_resumption, std::unique_ptr<FSDirectory>* output_dir,
|
||||
bool allow_resumption, bool resumption_requested,
|
||||
std::unique_ptr<FSDirectory>* output_dir,
|
||||
std::unique_ptr<log::Writer>* compaction_progress_writer);
|
||||
|
||||
Status PrepareCompactionProgressState();
|
||||
@@ -502,6 +567,22 @@ class DBImplSecondary : public DBImpl {
|
||||
std::unordered_map<uint32_t, std::pair<uint64_t, int>>
|
||||
cf_id_to_retention_warning_;
|
||||
|
||||
// Mutated only under mutex_, so that load-then-store stays atomic between
|
||||
// publication and teardown. Readers load it without mutex_, so every access
|
||||
// must still go through AtomicSharedPtrLoad/Store.
|
||||
std::shared_ptr<const SecondaryReadView> secondary_read_view_;
|
||||
// Set by ResetSecondaryReadView() so a later catch-up cannot publish a view
|
||||
// that would outlive the drain close waits on.
|
||||
// Guarded by mutex_, which every mutation of secondary_read_view_ holds.
|
||||
bool secondary_read_view_closed_ = false;
|
||||
std::mutex retired_secondary_read_views_mutex_;
|
||||
std::condition_variable retired_secondary_read_views_cv_;
|
||||
std::vector<std::shared_ptr<const SecondaryReadView>>
|
||||
retired_secondary_read_views_;
|
||||
size_t secondary_read_view_reclaims_in_flight_ = 0;
|
||||
bool secondary_read_view_reclaimer_stopping_ = false;
|
||||
std::unique_ptr<port::Thread> secondary_read_view_reclaimer_;
|
||||
|
||||
const std::string secondary_path_;
|
||||
|
||||
CompactionProgress compaction_progress_;
|
||||
|
||||
@@ -19,6 +19,14 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImplSecondary::GetImpl)
|
||||
get_impl_options.merge_operands != nullptr);
|
||||
assert(get_impl_options.column_family);
|
||||
|
||||
if (get_impl_options.newer_version_present != nullptr) {
|
||||
if (read_options.snapshot != nullptr) {
|
||||
CO_RETURN Status::NotSupported(
|
||||
"GetWithMetadata is not supported in secondary DB mode");
|
||||
}
|
||||
get_impl_options.newer_version_present = nullptr;
|
||||
}
|
||||
|
||||
Status s;
|
||||
|
||||
if (read_options.timestamp) {
|
||||
@@ -48,7 +56,12 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImplSecondary::GetImpl)
|
||||
|
||||
const Comparator* ucmp = get_impl_options.column_family->GetComparator();
|
||||
assert(ucmp);
|
||||
SequenceNumber snapshot = versions_->LastSequence();
|
||||
// For a batched lazy read (MultiGetEntityLazy) use the sequence number the
|
||||
// batch fixed (see GetImplOptions::lazy_columns_shared_sv); otherwise this
|
||||
// instance's last sequence.
|
||||
SequenceNumber snapshot = get_impl_options.lazy_columns_shared_sv != nullptr
|
||||
? get_impl_options.lazy_columns_snapshot_seq
|
||||
: versions_->LastSequence();
|
||||
GetWithTimestampReadCallback read_cb(snapshot);
|
||||
auto cfh = static_cast_with_check<ColumnFamilyHandleImpl>(
|
||||
get_impl_options.column_family);
|
||||
@@ -74,21 +87,32 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImplSecondary::GetImpl)
|
||||
}
|
||||
}
|
||||
|
||||
// Acquire SuperVersion
|
||||
// Acquire SuperVersion, or use the batch's shared one for a batched lazy read
|
||||
// (see GetImplOptions::lazy_columns_shared_sv), skipping the per-key
|
||||
// reference and its release.
|
||||
const bool own_super_version =
|
||||
get_impl_options.lazy_columns_shared_sv == nullptr;
|
||||
SuperVersion* super_version;
|
||||
if (own_super_version) {
|
||||
#if defined(WITH_COROUTINES)
|
||||
SuperVersion* super_version = cfd->GetReferencedSuperVersion(this);
|
||||
super_version = cfd->GetReferencedSuperVersion(this);
|
||||
#else
|
||||
SuperVersion* super_version = GetAndRefSuperVersion(cfd);
|
||||
super_version = GetAndRefSuperVersion(cfd);
|
||||
#endif
|
||||
} else {
|
||||
super_version = get_impl_options.lazy_columns_shared_sv;
|
||||
}
|
||||
if (read_options.timestamp && read_options.timestamp->size() > 0) {
|
||||
s = FailIfReadCollapsedHistory(cfd, super_version,
|
||||
*(read_options.timestamp));
|
||||
if (!s.ok()) {
|
||||
if (own_super_version) {
|
||||
#if defined(WITH_COROUTINES)
|
||||
CleanupSuperVersion(super_version);
|
||||
CleanupSuperVersion(super_version);
|
||||
#else
|
||||
ReturnAndCleanupSuperVersion(cfd, super_version);
|
||||
ReturnAndCleanupSuperVersion(cfd, super_version);
|
||||
#endif
|
||||
}
|
||||
CO_RETURN s;
|
||||
}
|
||||
}
|
||||
@@ -169,11 +193,13 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImplSecondary::GetImpl)
|
||||
}
|
||||
if (!s.ok() && !s.IsMergeInProgress() && !s.IsNotFound()) {
|
||||
assert(done);
|
||||
if (own_super_version) {
|
||||
#if defined(WITH_COROUTINES)
|
||||
CleanupSuperVersion(super_version);
|
||||
CleanupSuperVersion(super_version);
|
||||
#else
|
||||
ReturnAndCleanupSuperVersion(cfd, super_version);
|
||||
ReturnAndCleanupSuperVersion(cfd, super_version);
|
||||
#endif
|
||||
}
|
||||
CO_RETURN s;
|
||||
}
|
||||
if (!done) {
|
||||
@@ -191,21 +217,27 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImplSecondary::GetImpl)
|
||||
}
|
||||
{
|
||||
PERF_TIMER_GUARD(get_post_process_time);
|
||||
if (get_impl_options.lazy_columns_version != nullptr && s.ok()) {
|
||||
// Lazy result (GetEntityLazy / MultiGetEntityLazy): hand back the Version
|
||||
// the entity's blob references must be resolved against (both the
|
||||
// single-key and batched paths).
|
||||
*get_impl_options.lazy_columns_version = super_version->current;
|
||||
}
|
||||
if (get_impl_options.lazy_columns_pin != nullptr && s.ok()) {
|
||||
// Lazy result (GetEntityLazy): hand back the resolution Version and take
|
||||
// a SuperVersion pin for the result before the borrowed reference below
|
||||
// is released, so the result -- and deferred blob reads -- stay valid
|
||||
// after this call (as an iterator's pin does).
|
||||
if (get_impl_options.lazy_columns_version != nullptr) {
|
||||
*get_impl_options.lazy_columns_version = super_version->current;
|
||||
}
|
||||
// Single-key GetEntityLazy: take a SuperVersion pin for the result before
|
||||
// the borrowed reference below is released, so the result -- and deferred
|
||||
// blob reads -- stay valid after this call (as an iterator's pin does).
|
||||
// The batched path passes no pin (it uses lazy_columns_shared_sv); the
|
||||
// batch holds one shared pin per column family instead.
|
||||
TransferSuperVersionPin(super_version, get_impl_options.lazy_columns_pin);
|
||||
}
|
||||
if (own_super_version) {
|
||||
#if defined(WITH_COROUTINES)
|
||||
CleanupSuperVersion(super_version);
|
||||
CleanupSuperVersion(super_version);
|
||||
#else
|
||||
ReturnAndCleanupSuperVersion(cfd, super_version);
|
||||
ReturnAndCleanupSuperVersion(cfd, super_version);
|
||||
#endif
|
||||
}
|
||||
RecordTick(stats_, NUMBER_KEYS_READ);
|
||||
size_t size = 0;
|
||||
// Mirror DBImpl::GetImpl: only produce merge-operand output and count bytes
|
||||
|
||||
@@ -6,10 +6,6 @@
|
||||
|
||||
#include "util/coro_utils.h"
|
||||
|
||||
#if defined(USE_COROUTINES) && defined(WITH_COROUTINES)
|
||||
#include "util/coro_stats_util.h"
|
||||
#endif // USE_COROUTINES && WITH_COROUTINES
|
||||
|
||||
#if defined(WITHOUT_COROUTINES) || \
|
||||
(defined(USE_COROUTINES) && defined(WITH_COROUTINES))
|
||||
|
||||
@@ -25,10 +21,6 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
DEFINE_SYNC_AND_ASYNC(Status, DBImpl::Get)
|
||||
(const ReadOptions& _read_options, ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableSlice* value, std::string* timestamp) {
|
||||
#ifdef WITH_COROUTINES
|
||||
INSTALL_COROUTINE_STATS_CONTEXT_SCOPE(
|
||||
immutable_db_options_.fs->GetReadExecutor(), immutable_db_options_.env);
|
||||
#endif
|
||||
assert(value != nullptr);
|
||||
value->Reset();
|
||||
|
||||
@@ -48,6 +40,106 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::Get)
|
||||
timestamp);
|
||||
}
|
||||
|
||||
DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetEntity)
|
||||
(const ReadOptions& _read_options, ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableWideColumns* columns) {
|
||||
if (!column_family) {
|
||||
CO_RETURN Status::InvalidArgument(
|
||||
"Cannot call GetEntity without a column family handle");
|
||||
}
|
||||
if (!columns) {
|
||||
CO_RETURN Status::InvalidArgument(
|
||||
"Cannot call GetEntity without a PinnableWideColumns object");
|
||||
}
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
_read_options.io_activity != Env::IOActivity::kGetEntity) {
|
||||
CO_RETURN Status::InvalidArgument(
|
||||
"Can only call GetEntity with `ReadOptions::io_activity` set to "
|
||||
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kGetEntity`");
|
||||
}
|
||||
ReadOptions read_options(_read_options);
|
||||
if (read_options.io_activity == Env::IOActivity::kUnknown) {
|
||||
read_options.io_activity = Env::IOActivity::kGetEntity;
|
||||
}
|
||||
columns->Reset();
|
||||
|
||||
GetImplOptions get_impl_options;
|
||||
get_impl_options.column_family = column_family;
|
||||
get_impl_options.columns = columns;
|
||||
|
||||
CO_RETURN CO_AWAIT(GetImpl, read_options, key, get_impl_options);
|
||||
}
|
||||
|
||||
DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetEntity)
|
||||
(const ReadOptions& _read_options, const Slice& key,
|
||||
PinnableAttributeGroups* result) {
|
||||
if (!result) {
|
||||
CO_RETURN Status::InvalidArgument(
|
||||
"Cannot call GetEntity without PinnableAttributeGroups object");
|
||||
}
|
||||
Status s;
|
||||
const size_t num_column_families = result->size();
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
_read_options.io_activity != Env::IOActivity::kGetEntity) {
|
||||
s = Status::InvalidArgument(
|
||||
"Can only call GetEntity with `ReadOptions::io_activity` set to "
|
||||
"`Env::IOActivity::kUnknown` or `Env::IOActivity::kGetEntity`");
|
||||
for (size_t i = 0; i < num_column_families; ++i) {
|
||||
(*result)[i].SetStatus(s);
|
||||
}
|
||||
CO_RETURN s;
|
||||
}
|
||||
// return early if no CF was passed in
|
||||
if (num_column_families == 0) {
|
||||
CO_RETURN s;
|
||||
}
|
||||
ReadOptions read_options(_read_options);
|
||||
if (read_options.io_activity == Env::IOActivity::kUnknown) {
|
||||
read_options.io_activity = Env::IOActivity::kGetEntity;
|
||||
}
|
||||
std::vector<Slice> keys;
|
||||
std::vector<ColumnFamilyHandle*> column_families;
|
||||
for (size_t i = 0; i < num_column_families; ++i) {
|
||||
// If any of the CFH is null, break early since the entire query will fail
|
||||
if (!(*result)[i].column_family()) {
|
||||
s = Status::InvalidArgument(
|
||||
"DB failed to query because one or more group(s) have null column "
|
||||
"family handle");
|
||||
(*result)[i].SetStatus(
|
||||
Status::InvalidArgument("Column family handle cannot be null"));
|
||||
break;
|
||||
}
|
||||
// Adding the same key slice for different CFs
|
||||
keys.emplace_back(key);
|
||||
column_families.emplace_back((*result)[i].column_family());
|
||||
}
|
||||
if (!s.ok()) {
|
||||
for (size_t i = 0; i < num_column_families; ++i) {
|
||||
if ((*result)[i].status().ok()) {
|
||||
(*result)[i].SetStatus(
|
||||
Status::Incomplete("DB not queried due to invalid argument(s) in "
|
||||
"one or more of the attribute groups"));
|
||||
}
|
||||
}
|
||||
CO_RETURN s;
|
||||
}
|
||||
std::vector<PinnableWideColumns> columns(num_column_families);
|
||||
std::vector<Status> statuses(num_column_families);
|
||||
CO_AWAIT(MultiGetCommon, read_options, num_column_families,
|
||||
column_families.data(), keys.data(),
|
||||
/* values */ nullptr, columns.data(),
|
||||
/* timestamps */ nullptr, statuses.data(),
|
||||
/* newer_version_present */ nullptr,
|
||||
/* sorted_input */ false);
|
||||
// Set results
|
||||
for (size_t i = 0; i < num_column_families; ++i) {
|
||||
(*result)[i].Reset();
|
||||
(*result)[i].SetStatus(statuses[i]);
|
||||
(*result)[i].SetColumns(std::move(columns[i]));
|
||||
}
|
||||
CO_RETURN s;
|
||||
}
|
||||
|
||||
DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
(const ReadOptions& read_options, ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableSlice* value, std::string* timestamp) {
|
||||
@@ -69,6 +161,13 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
|
||||
assert(get_impl_options.column_family);
|
||||
|
||||
if (get_impl_options.newer_version_present != nullptr &&
|
||||
read_options.snapshot != nullptr &&
|
||||
get_impl_options.callback != nullptr) {
|
||||
CO_RETURN Status::NotSupported(
|
||||
"Newer-version metadata is not supported with a read callback");
|
||||
}
|
||||
|
||||
if (read_options.timestamp) {
|
||||
const Status s = FailIfTsMismatchCf(get_impl_options.column_family,
|
||||
*(read_options.timestamp));
|
||||
@@ -118,13 +217,33 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
}
|
||||
}
|
||||
|
||||
// Acquire SuperVersion
|
||||
// Acquire SuperVersion (or, for a batched lazy read, use the shared one the
|
||||
// batch already referenced -- see GetImplOptions::lazy_columns_shared_sv).
|
||||
const bool own_super_version =
|
||||
get_impl_options.lazy_columns_shared_sv == nullptr;
|
||||
SuperVersion* sv;
|
||||
#if defined(WITH_COROUTINES)
|
||||
SuperVersion* sv = cfd->GetReferencedSuperVersion(this);
|
||||
Defer sv_cleanup([&] { CleanupSuperVersion(sv); });
|
||||
if (own_super_version) {
|
||||
sv = cfd->GetReferencedSuperVersion(this);
|
||||
} else {
|
||||
sv = get_impl_options.lazy_columns_shared_sv;
|
||||
}
|
||||
Defer sv_cleanup([&] {
|
||||
if (own_super_version) {
|
||||
CleanupSuperVersion(sv);
|
||||
}
|
||||
});
|
||||
#else
|
||||
SuperVersion* sv = GetAndRefSuperVersion(cfd);
|
||||
Defer sv_cleanup([&] { ReturnAndCleanupSuperVersion(cfd, sv); });
|
||||
if (own_super_version) {
|
||||
sv = GetAndRefSuperVersion(cfd);
|
||||
} else {
|
||||
sv = get_impl_options.lazy_columns_shared_sv;
|
||||
}
|
||||
Defer sv_cleanup([&] {
|
||||
if (own_super_version) {
|
||||
ReturnAndCleanupSuperVersion(cfd, sv);
|
||||
}
|
||||
});
|
||||
#endif
|
||||
if (read_options.timestamp && read_options.timestamp->size() > 0) {
|
||||
const Status s =
|
||||
@@ -139,7 +258,11 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
TEST_SYNC_POINT("DBImpl::GetImpl:2");
|
||||
|
||||
SequenceNumber snapshot;
|
||||
if (read_options.snapshot != nullptr) {
|
||||
if (!own_super_version) {
|
||||
// Batched lazy read: use the batch's consistent sequence number (derived
|
||||
// once, with the shared SuperVersion, by MultiCFSnapshot).
|
||||
snapshot = get_impl_options.lazy_columns_snapshot_seq;
|
||||
} else if (read_options.snapshot != nullptr) {
|
||||
if (get_impl_options.callback) {
|
||||
// Already calculated based on read_options.snapshot
|
||||
snapshot = get_impl_options.callback->max_visible_seq();
|
||||
@@ -173,6 +296,17 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
snapshot = get_impl_options.callback->max_visible_seq();
|
||||
}
|
||||
}
|
||||
SequenceNumber lookup_snapshot = snapshot;
|
||||
const bool newer_version_present_requested =
|
||||
get_impl_options.newer_version_present != nullptr &&
|
||||
read_options.snapshot != nullptr;
|
||||
const SequenceNumber newer_version_upper_bound_seq =
|
||||
newer_version_present_requested ? GetLastPublishedSequence() : snapshot;
|
||||
const bool track_newer_versions = newer_version_present_requested &&
|
||||
snapshot < newer_version_upper_bound_seq;
|
||||
if (track_newer_versions) {
|
||||
lookup_snapshot = newer_version_upper_bound_seq;
|
||||
}
|
||||
// If timestamp is used, we use read callback to ensure <key,t,s> is returned
|
||||
// only if t <= read_opts.timestamp and s <= snapshot.
|
||||
// HACK: temporarily overwrite input struct field but restore
|
||||
@@ -184,6 +318,13 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
.callback); // timestamp with callback is not supported
|
||||
read_cb.Refresh(snapshot);
|
||||
get_impl_options.callback = &read_cb;
|
||||
} else if (track_newer_versions && get_impl_options.callback == nullptr) {
|
||||
read_cb.Refresh(snapshot);
|
||||
get_impl_options.callback = &read_cb;
|
||||
}
|
||||
if (track_newer_versions) {
|
||||
read_cb.EnableNewerVersionTracking(snapshot, newer_version_upper_bound_seq,
|
||||
get_impl_options.newer_version_present);
|
||||
}
|
||||
TEST_SYNC_POINT("DBImpl::GetImpl:3");
|
||||
TEST_SYNC_POINT("DBImpl::GetImpl:4");
|
||||
@@ -198,7 +339,7 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
// First look in the memtable, then in the immutable memtable (if any).
|
||||
// s is both in/out. When in, s could either be OK or MergeInProgress.
|
||||
// merge_operands will contain the sequence of merges in the latter case.
|
||||
LookupKey lkey(key, snapshot, read_options.timestamp);
|
||||
LookupKey lkey(key, lookup_snapshot, read_options.timestamp);
|
||||
PERF_TIMER_STOP(get_snapshot_time);
|
||||
|
||||
bool skip_memtable = (read_options.read_tier == kPersistedTier &&
|
||||
@@ -403,17 +544,21 @@ DEFINE_SYNC_AND_ASYNC(Status, DBImpl::GetImpl)
|
||||
PERF_COUNTER_ADD(get_read_bytes, size);
|
||||
}
|
||||
|
||||
if (get_impl_options.lazy_columns_version != nullptr && s.ok()) {
|
||||
// Lazy result (GetEntityLazy / MultiGetEntityLazy): the entity's blob
|
||||
// references were left unresolved. Hand back the Version they must be
|
||||
// resolved against (for both the single-key and batched paths).
|
||||
*get_impl_options.lazy_columns_version = sv->current;
|
||||
}
|
||||
if (get_impl_options.lazy_columns_pin != nullptr && s.ok()) {
|
||||
// Lazy result (GetEntityLazy): the entity's blob references were left
|
||||
// unresolved. Hand back the Version they must be resolved against and
|
||||
// transfer an extra SuperVersion reference into the result's pin, so the
|
||||
// result -- and thus deferred blob reads -- stay valid after this call
|
||||
// returns (as an iterator's SuperVersion pin does). The reference is
|
||||
// released when the pin's cleanup runs (result destruction); the
|
||||
// borrowed reference for this call is dropped by the sv_cleanup Defer.
|
||||
if (get_impl_options.lazy_columns_version != nullptr) {
|
||||
*get_impl_options.lazy_columns_version = sv->current;
|
||||
}
|
||||
// Single-key GetEntityLazy: transfer an extra SuperVersion reference into
|
||||
// the result's pin, so the result -- and thus deferred blob reads -- stay
|
||||
// valid after this call returns (as an iterator's SuperVersion pin does).
|
||||
// The reference is released when the pin's cleanup runs (result
|
||||
// destruction); the borrowed reference for this call is dropped by the
|
||||
// sv_cleanup Defer. The batched path passes no pin here (it uses
|
||||
// lazy_columns_shared_sv) because the batch holds one shared pin per
|
||||
// column family instead.
|
||||
TransferSuperVersionPin(sv, get_impl_options.lazy_columns_pin);
|
||||
}
|
||||
RecordInHistogram(stats_, BYTES_PER_READ, size);
|
||||
@@ -635,7 +780,7 @@ DEFINE_SYNC_AND_ASYNC(void, DBImpl::MultiGetCommon)
|
||||
(const ReadOptions& read_options, const size_t num_keys,
|
||||
ColumnFamilyHandle** column_families, const Slice* keys, PinnableSlice* values,
|
||||
PinnableWideColumns* columns, std::string* timestamps, Status* statuses,
|
||||
const bool sorted_input) {
|
||||
std::vector<uint8_t>* newer_version_present, const bool sorted_input) {
|
||||
if (num_keys == 0) {
|
||||
CO_RETURN;
|
||||
}
|
||||
@@ -754,8 +899,26 @@ DEFINE_SYNC_AND_ASYNC(void, DBImpl::MultiGetCommon)
|
||||
|
||||
GetWithTimestampReadCallback timestamp_read_callback(0);
|
||||
ReadCallback* read_callback = nullptr;
|
||||
if (read_options.timestamp && read_options.timestamp->size() > 0) {
|
||||
SequenceNumber lookup_seqnum = consistent_seqnum;
|
||||
const bool newer_version_present_requested =
|
||||
newer_version_present != nullptr && read_options.snapshot != nullptr;
|
||||
const SequenceNumber newer_version_upper_bound_seq =
|
||||
newer_version_present_requested ? GetLastPublishedSequence()
|
||||
: consistent_seqnum;
|
||||
const bool track_newer_versions =
|
||||
newer_version_present_requested &&
|
||||
consistent_seqnum < newer_version_upper_bound_seq;
|
||||
if (track_newer_versions) {
|
||||
lookup_seqnum = newer_version_upper_bound_seq;
|
||||
}
|
||||
if ((read_options.timestamp && read_options.timestamp->size() > 0) ||
|
||||
track_newer_versions) {
|
||||
timestamp_read_callback.Refresh(consistent_seqnum);
|
||||
if (track_newer_versions) {
|
||||
timestamp_read_callback.EnableNewerVersionTracking(
|
||||
consistent_seqnum, newer_version_upper_bound_seq,
|
||||
/*single_key_result=*/nullptr);
|
||||
}
|
||||
read_callback = ×tamp_read_callback;
|
||||
}
|
||||
|
||||
@@ -766,8 +929,7 @@ DEFINE_SYNC_AND_ASYNC(void, DBImpl::MultiGetCommon)
|
||||
cf_sv_pair_iter != cf_sv_pairs.end()) {
|
||||
s = CO_AWAIT(MultiGetImpl, read_options, key_range_per_cf_iter->start,
|
||||
key_range_per_cf_iter->num_keys, &sorted_keys,
|
||||
cf_sv_pair_iter->super_version, consistent_seqnum,
|
||||
read_callback);
|
||||
cf_sv_pair_iter->super_version, lookup_seqnum, read_callback);
|
||||
if (!s.ok()) {
|
||||
break;
|
||||
}
|
||||
@@ -795,16 +957,13 @@ DEFINE_SYNC_AND_ASYNC(void, DBImpl::MultiGetCommon)
|
||||
CleanupSuperVersion(cf_sv_pair.super_version);
|
||||
}
|
||||
}
|
||||
CopyNewerVersionPresent(key_context, newer_version_present);
|
||||
}
|
||||
|
||||
DEFINE_SYNC_AND_ASYNC(void, DBImpl::MultiGet)
|
||||
(const ReadOptions& _read_options, const size_t num_keys,
|
||||
ColumnFamilyHandle** column_families, const Slice* keys, PinnableSlice* values,
|
||||
std::string* timestamps, Status* statuses, const bool sorted_input) {
|
||||
#ifdef WITH_COROUTINES
|
||||
INSTALL_COROUTINE_STATS_CONTEXT_SCOPE(
|
||||
immutable_db_options_.fs->GetReadExecutor(), immutable_db_options_.env);
|
||||
#endif
|
||||
if (_read_options.io_activity != Env::IOActivity::kUnknown &&
|
||||
_read_options.io_activity != Env::IOActivity::kMultiGet) {
|
||||
Status s = Status::InvalidArgument(
|
||||
@@ -823,7 +982,8 @@ DEFINE_SYNC_AND_ASYNC(void, DBImpl::MultiGet)
|
||||
}
|
||||
CO_AWAIT(MultiGetCommon, read_options, num_keys, column_families, keys,
|
||||
values,
|
||||
/* columns */ nullptr, timestamps, statuses, sorted_input);
|
||||
/* columns */ nullptr, timestamps, statuses,
|
||||
/* newer_version_present */ nullptr, sorted_input);
|
||||
CO_RETURN;
|
||||
}
|
||||
|
||||
|
||||
@@ -4308,6 +4308,109 @@ TEST_F(DBIteratorTest, IteratorsConsistentViewExplicitSnapshot) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBIteratorTest, NewIteratorsWithPerColumnFamilyReadOptions) {
|
||||
Options options = GetDefaultOptions();
|
||||
options.prefix_extractor.reset(NewFixedPrefixTransform(1));
|
||||
BlockBasedTableOptions table_options;
|
||||
table_options.filter_policy.reset(NewBloomFilterPolicy(10));
|
||||
table_options.whole_key_filtering = false;
|
||||
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
|
||||
CreateAndReopenWithCF({"cf_1"}, options);
|
||||
|
||||
ASSERT_OK(Put(0, "b1", "default_value"));
|
||||
ASSERT_OK(Put(1, "b1", "cf_1_value"));
|
||||
ASSERT_OK(Flush(0));
|
||||
ASSERT_OK(Flush(1));
|
||||
|
||||
ReadOptions prefix_read_options;
|
||||
prefix_read_options.total_order_seek = false;
|
||||
ReadOptions total_order_read_options;
|
||||
total_order_read_options.total_order_seek = true;
|
||||
std::vector<ReadOptions> read_options = {prefix_read_options,
|
||||
total_order_read_options};
|
||||
std::vector<Iterator*> iterators;
|
||||
ASSERT_OK(db_->NewIterators(read_options, handles_, &iterators));
|
||||
ASSERT_EQ(2U, iterators.size());
|
||||
|
||||
iterators[0]->Seek("a9");
|
||||
ASSERT_FALSE(iterators[0]->Valid());
|
||||
ASSERT_OK(iterators[0]->status());
|
||||
iterators[1]->Seek("a9");
|
||||
ASSERT_TRUE(iterators[1]->Valid());
|
||||
ASSERT_EQ("b1", iterators[1]->key());
|
||||
ASSERT_OK(iterators[1]->status());
|
||||
|
||||
for (auto* iterator : iterators) {
|
||||
delete iterator;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBIteratorTest, NewIteratorsPerColumnFamilyOptionsValidation) {
|
||||
Options options = GetDefaultOptions();
|
||||
CreateAndReopenWithCF({"cf_1"}, options);
|
||||
|
||||
std::vector<Iterator*> iterators;
|
||||
ASSERT_TRUE(
|
||||
db_->NewIterators(std::vector<ReadOptions>(1), handles_, &iterators)
|
||||
.IsInvalidArgument());
|
||||
|
||||
std::vector<ReadOptions> read_options(2);
|
||||
read_options[1].tailing = true;
|
||||
ASSERT_TRUE(db_->NewIterators(read_options, handles_, &iterators)
|
||||
.IsInvalidArgument());
|
||||
|
||||
read_options[1].tailing = false;
|
||||
const Snapshot* first_snapshot = db_->GetSnapshot();
|
||||
ASSERT_OK(Put(0, "key", "value"));
|
||||
const Snapshot* second_snapshot = db_->GetSnapshot();
|
||||
read_options[0].snapshot = second_snapshot;
|
||||
read_options[1].snapshot = second_snapshot;
|
||||
ASSERT_OK(db_->NewIterators(read_options, handles_, &iterators));
|
||||
for (auto* iterator : iterators) {
|
||||
delete iterator;
|
||||
}
|
||||
|
||||
read_options[0].snapshot = first_snapshot;
|
||||
read_options[1].snapshot = second_snapshot;
|
||||
ASSERT_TRUE(db_->NewIterators(read_options, handles_, &iterators)
|
||||
.IsInvalidArgument());
|
||||
db_->ReleaseSnapshot(first_snapshot);
|
||||
db_->ReleaseSnapshot(second_snapshot);
|
||||
}
|
||||
|
||||
TEST_F(DBIteratorTest,
|
||||
ReadOnlyNewIteratorsAllowsTableFilterWithRangeConversion) {
|
||||
Options options = CurrentOptions();
|
||||
options.min_tombstones_for_range_conversion = 2;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(Put("key", "value"));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(ReadOnlyReopen(options));
|
||||
|
||||
std::function<bool(const TableProperties&)> table_filter =
|
||||
[](const TableProperties&) { return true; };
|
||||
ReadOptions read_options;
|
||||
read_options.table_filter = &table_filter;
|
||||
const std::vector<ReadOptions> per_cf_read_options = {read_options};
|
||||
const std::vector<ColumnFamilyHandle*> column_families = {
|
||||
db_->DefaultColumnFamily()};
|
||||
for (bool use_per_cf_read_options : {false, true}) {
|
||||
std::vector<Iterator*> iterators;
|
||||
const Status s =
|
||||
use_per_cf_read_options
|
||||
? db_->NewIterators(per_cf_read_options, column_families,
|
||||
&iterators)
|
||||
: db_->NewIterators(read_options, column_families, &iterators);
|
||||
ASSERT_OK(s);
|
||||
ASSERT_EQ(1U, iterators.size());
|
||||
iterators[0]->SeekToFirst();
|
||||
ASSERT_OK(iterators[0]->status());
|
||||
ASSERT_TRUE(iterators[0]->Valid());
|
||||
ASSERT_EQ("key", iterators[0]->key());
|
||||
delete iterators[0];
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(DBIteratorTest, MemtableOpsScanFlushTriggerWithSeek) {
|
||||
// Tests that option memtable_op_scan_flush_trigger works when the limit
|
||||
// is reached during a Seek() operation.
|
||||
|
||||
+589
-1
@@ -7,6 +7,10 @@
|
||||
// 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 <condition_variable>
|
||||
#include <mutex>
|
||||
#include <thread>
|
||||
|
||||
#include "db/blob/blob_index.h"
|
||||
#include "db/db_impl/db_impl_secondary.h"
|
||||
#include "db/db_test_util.h"
|
||||
@@ -173,6 +177,19 @@ class DBSecondaryTestBase : public DBBasicTestWithTimestampBase {
|
||||
ASSERT_EQ(expected, iter->value().ToString());
|
||||
}
|
||||
|
||||
void VerifySecondaryBatchValue(ColumnFamilyHandle* cfh,
|
||||
const std::string& key,
|
||||
const std::string& expected) {
|
||||
std::vector<Iterator*> iterators;
|
||||
ASSERT_OK(db_secondary_->NewIterators(ReadOptions(), {cfh}, &iterators));
|
||||
ASSERT_EQ(1U, iterators.size());
|
||||
std::unique_ptr<Iterator> iterator(iterators[0]);
|
||||
iterator->Seek(key);
|
||||
ASSERT_OK(iterator->status());
|
||||
ASSERT_TRUE(iterator->Valid());
|
||||
ASSERT_EQ(expected, iterator->value());
|
||||
}
|
||||
|
||||
// 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;
|
||||
@@ -324,6 +341,19 @@ TEST_F(DBSecondaryTest, ReopenAsSecondary) {
|
||||
ASSERT_OK(ReopenAsSecondary(options));
|
||||
ASSERT_EQ("foo_value", Get("foo"));
|
||||
ASSERT_EQ("bar_value", Get("bar"));
|
||||
|
||||
MultiGetOutputMetadata output_metadata;
|
||||
output_metadata.WantNewerVersionPresent();
|
||||
std::vector<Slice> metadata_keys{"foo", "missing"};
|
||||
std::vector<std::string> metadata_values;
|
||||
const std::vector<Status> metadata_statuses = db_->MultiGetWithMetadata(
|
||||
ReadOptions(), metadata_keys, &metadata_values, &output_metadata);
|
||||
ASSERT_OK(metadata_statuses[0]);
|
||||
ASSERT_EQ("foo_value", metadata_values[0]);
|
||||
ASSERT_FALSE((*output_metadata.newer_version_present)[0]);
|
||||
ASSERT_TRUE(metadata_statuses[1].IsNotFound());
|
||||
ASSERT_FALSE((*output_metadata.newer_version_present)[1]);
|
||||
|
||||
PinnableWideColumns result;
|
||||
ASSERT_OK(db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(), "baz",
|
||||
&result));
|
||||
@@ -914,11 +944,16 @@ TEST_F(DBSecondaryTest, OptionsOverrideTest) {
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
ASSERT_OK(DB::OpenAndCompact(OpenAndCompactOptions(), dbname_,
|
||||
secondary_path_, compaction_input_binary,
|
||||
"options_override", compaction_input_binary,
|
||||
&compaction_result_binary, override_options));
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
ASSERT_TRUE(verified);
|
||||
|
||||
CompactionServiceResult result;
|
||||
ASSERT_OK(CompactionServiceResult::Read(compaction_result_binary, &result));
|
||||
ASSERT_OK(result.status);
|
||||
ASSERT_EQ(result.output_path, dbname_ + "/options_override");
|
||||
}
|
||||
|
||||
namespace {
|
||||
@@ -1197,8 +1232,11 @@ TEST_F(DBSecondaryCatchUpFaultTest, ReconcilesMemtableAfterFailedRound) {
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
fault_fs_->SetFilesystemActive(true);
|
||||
|
||||
VerifySecondaryBatchValue(db_secondary_->DefaultColumnFamily(), "foo", "v1");
|
||||
|
||||
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
|
||||
VerifySecondaryValue("foo", "v2");
|
||||
VerifySecondaryBatchValue(db_secondary_->DefaultColumnFamily(), "foo", "v2");
|
||||
}
|
||||
|
||||
// A round whose WAL discovery reports a purged path replays nothing, yet
|
||||
@@ -1730,6 +1768,164 @@ TEST_F(DBSecondaryTest, CatchUpKeepsUnflushedWalData) {
|
||||
VerifySecondaryValue(handles_secondary_[1], "bar", "v1");
|
||||
}
|
||||
|
||||
TEST_F(DBSecondaryTest, NewIteratorsConsistentViewDuringCatchUp) {
|
||||
Options options;
|
||||
options.env = env_;
|
||||
options.disable_auto_compactions = true;
|
||||
CreateAndReopenWithCF({"cf1"}, options);
|
||||
|
||||
ASSERT_OK(Put(0, "key", "old"));
|
||||
ASSERT_OK(Put(1, "key", "old"));
|
||||
ASSERT_OK(Flush(0));
|
||||
ASSERT_OK(Flush(1));
|
||||
|
||||
Options secondary_options = options;
|
||||
secondary_options.max_open_files = -1;
|
||||
OpenSecondaryWithColumnFamilies({"cf1"}, secondary_options);
|
||||
ASSERT_EQ(2, handles_secondary_.size());
|
||||
|
||||
WriteOptions write_options;
|
||||
write_options.disableWAL = true;
|
||||
ASSERT_OK(db_->Put(write_options, handles_[0], "key", "new"));
|
||||
ASSERT_OK(db_->Put(write_options, handles_[1], "key", "new"));
|
||||
ASSERT_OK(Flush(0));
|
||||
ASSERT_OK(Flush(1));
|
||||
ASSERT_OK(
|
||||
db_->CompactRange(CompactRangeOptions(), handles_[0], nullptr, nullptr));
|
||||
ASSERT_OK(
|
||||
db_->CompactRange(CompactRangeOptions(), handles_[1], nullptr, nullptr));
|
||||
|
||||
Status catch_up_status;
|
||||
SyncPoint::GetInstance()->LoadDependency({
|
||||
{"DBImplSecondary::TryCatchUpWithPrimary:BeforePublishReadView",
|
||||
"DBImplSecondary::NewIterators:AfterLoadReadView"},
|
||||
{"DBImplSecondary::NewIterators:AfterLoadReadView",
|
||||
"DBImplSecondary::TryCatchUpWithPrimary:AllowPublishReadView"},
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
port::Thread catch_up_thread(
|
||||
[&]() { catch_up_status = db_secondary_->TryCatchUpWithPrimary(); });
|
||||
|
||||
std::vector<Iterator*> iterators;
|
||||
const Status new_iterators_status = db_secondary_->NewIterators(
|
||||
ReadOptions(), handles_secondary_, &iterators);
|
||||
catch_up_thread.join();
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearTrace();
|
||||
|
||||
std::vector<std::unique_ptr<Iterator>> owned_iterators;
|
||||
owned_iterators.reserve(iterators.size());
|
||||
for (Iterator* iterator : iterators) {
|
||||
owned_iterators.emplace_back(iterator);
|
||||
}
|
||||
|
||||
ASSERT_OK(catch_up_status);
|
||||
ASSERT_OK(new_iterators_status);
|
||||
ASSERT_EQ(2, owned_iterators.size());
|
||||
for (const auto& iterator : owned_iterators) {
|
||||
iterator->Seek("key");
|
||||
ASSERT_OK(iterator->status());
|
||||
ASSERT_TRUE(iterator->Valid());
|
||||
ASSERT_EQ("old", iterator->value());
|
||||
}
|
||||
|
||||
VerifySecondaryValue(handles_secondary_[0], "key", "new");
|
||||
VerifySecondaryValue(handles_secondary_[1], "key", "new");
|
||||
}
|
||||
|
||||
TEST_F(DBSecondaryTest, RetiredReadViewIsNotReclaimedByReader) {
|
||||
Options options;
|
||||
options.env = env_;
|
||||
options.disable_auto_compactions = true;
|
||||
CreateAndReopenWithCF({"cf1"}, options);
|
||||
|
||||
ASSERT_OK(Put(0, "key", "old"));
|
||||
ASSERT_OK(Put(1, "key", "old"));
|
||||
ASSERT_OK(Flush(0));
|
||||
ASSERT_OK(Flush(1));
|
||||
|
||||
Options secondary_options = options;
|
||||
secondary_options.max_open_files = -1;
|
||||
OpenSecondaryWithColumnFamilies({"cf1"}, secondary_options);
|
||||
|
||||
WriteOptions write_options;
|
||||
write_options.disableWAL = true;
|
||||
ASSERT_OK(db_->Put(write_options, handles_[1], "key", "new"));
|
||||
ASSERT_OK(Flush(1));
|
||||
|
||||
std::mutex state_mutex;
|
||||
std::condition_variable state_cv;
|
||||
bool reader_loaded = false;
|
||||
bool allow_reader = false;
|
||||
bool second_catch_up_locked = false;
|
||||
bool reader_done = false;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImplSecondary::NewIterators:AfterLoadReadView", [&](void* /*arg*/) {
|
||||
std::unique_lock<std::mutex> lock(state_mutex);
|
||||
reader_loaded = true;
|
||||
state_cv.notify_all();
|
||||
state_cv.wait(lock, [&]() { return allow_reader; });
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
Status reader_status;
|
||||
std::vector<Iterator*> iterators;
|
||||
port::Thread reader_thread([&]() {
|
||||
reader_status = db_secondary_->NewIterators(
|
||||
ReadOptions(), {handles_secondary_[0]}, &iterators);
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
reader_done = true;
|
||||
state_cv.notify_all();
|
||||
});
|
||||
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(state_mutex);
|
||||
state_cv.wait(lock, [&]() { return reader_loaded; });
|
||||
}
|
||||
const Status first_catch_up_status = db_secondary_->TryCatchUpWithPrimary();
|
||||
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImplSecondary::TryCatchUpWithPrimary:BeforePublishReadView",
|
||||
[&](void* /*arg*/) {
|
||||
std::unique_lock<std::mutex> lock(state_mutex);
|
||||
second_catch_up_locked = true;
|
||||
state_cv.notify_all();
|
||||
state_cv.wait(lock, [&]() { return reader_done; });
|
||||
});
|
||||
Status second_catch_up_status;
|
||||
port::Thread second_catch_up_thread([&]() {
|
||||
second_catch_up_status = db_secondary_->TryCatchUpWithPrimary();
|
||||
});
|
||||
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(state_mutex);
|
||||
state_cv.wait(lock, [&]() { return second_catch_up_locked; });
|
||||
allow_reader = true;
|
||||
state_cv.notify_all();
|
||||
}
|
||||
|
||||
reader_thread.join();
|
||||
second_catch_up_thread.join();
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
std::vector<std::unique_ptr<Iterator>> owned_iterators;
|
||||
owned_iterators.reserve(iterators.size());
|
||||
for (Iterator* iterator : iterators) {
|
||||
owned_iterators.emplace_back(iterator);
|
||||
}
|
||||
ASSERT_OK(first_catch_up_status);
|
||||
ASSERT_OK(second_catch_up_status);
|
||||
ASSERT_OK(reader_status);
|
||||
ASSERT_EQ(1U, owned_iterators.size());
|
||||
owned_iterators[0]->Seek("key");
|
||||
ASSERT_OK(owned_iterators[0]->status());
|
||||
ASSERT_TRUE(owned_iterators[0]->Valid());
|
||||
ASSERT_EQ("old", owned_iterators[0]->value());
|
||||
}
|
||||
|
||||
TEST_F(DBSecondaryTest, RefreshIterator) {
|
||||
Options options;
|
||||
options.env = env_;
|
||||
@@ -1983,6 +2179,334 @@ TEST_F(DBSecondaryTest, PrimaryDropColumnFamily) {
|
||||
value.clear();
|
||||
ASSERT_OK(db_secondary_->Get(ropts, handles_secondary_[1], "foo", &value));
|
||||
ASSERT_EQ("foo_val_1", value);
|
||||
|
||||
std::vector<Iterator*> raw_iterators;
|
||||
Status new_iterators_status = db_secondary_->NewIterators(
|
||||
ropts, {handles_secondary_[1]}, &raw_iterators);
|
||||
std::vector<std::unique_ptr<Iterator>> iterators;
|
||||
for (Iterator* iterator : raw_iterators) {
|
||||
iterators.emplace_back(iterator);
|
||||
}
|
||||
ASSERT_OK(new_iterators_status);
|
||||
ASSERT_EQ(1U, iterators.size());
|
||||
iterators[0]->SeekToFirst();
|
||||
ASSERT_TRUE(iterators[0]->Valid());
|
||||
ASSERT_EQ("foo", iterators[0]->key().ToString());
|
||||
ASSERT_EQ("foo_val_1", iterators[0]->value().ToString());
|
||||
|
||||
// A later publication must keep skipping the dropped column family after
|
||||
// releasing the previous view's SuperVersion.
|
||||
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
|
||||
|
||||
raw_iterators.clear();
|
||||
new_iterators_status = db_secondary_->NewIterators(
|
||||
ropts, {handles_secondary_[0], handles_secondary_[1]}, &raw_iterators);
|
||||
iterators.clear();
|
||||
for (Iterator* iterator : raw_iterators) {
|
||||
iterators.emplace_back(iterator);
|
||||
}
|
||||
ASSERT_OK(new_iterators_status);
|
||||
ASSERT_EQ(2U, iterators.size());
|
||||
iterators[0]->SeekToFirst();
|
||||
ASSERT_FALSE(iterators[0]->Valid());
|
||||
ASSERT_OK(iterators[0]->status());
|
||||
iterators[1]->SeekToFirst();
|
||||
ASSERT_TRUE(iterators[1]->Valid());
|
||||
ASSERT_EQ("foo", iterators[1]->key().ToString());
|
||||
ASSERT_EQ("foo_val_1", iterators[1]->value().ToString());
|
||||
}
|
||||
|
||||
TEST_F(DBSecondaryTest, ReclaimsRetiredReadViewAfterLastReader) {
|
||||
Options options;
|
||||
options.env = env_;
|
||||
CreateAndReopenWithCF({"cf1"}, options);
|
||||
|
||||
options.max_open_files = -1;
|
||||
OpenSecondaryWithColumnFamilies({"cf1"}, options);
|
||||
|
||||
std::mutex state_mutex;
|
||||
std::condition_variable state_cv;
|
||||
bool reader_loaded = false;
|
||||
bool allow_reader = false;
|
||||
bool view_reclaimed = false;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImplSecondary::NewIterators:AfterLoadReadView", [&](void* /*arg*/) {
|
||||
std::unique_lock<std::mutex> lock(state_mutex);
|
||||
reader_loaded = true;
|
||||
state_cv.notify_all();
|
||||
state_cv.wait(lock, [&]() { return allow_reader; });
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImplSecondary::ReclaimRetiredReadViews:AfterReclaim",
|
||||
[&](void* /*arg*/) {
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
view_reclaimed = true;
|
||||
state_cv.notify_all();
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
Status reader_status;
|
||||
std::vector<Iterator*> iterators;
|
||||
port::Thread reader_thread([&]() {
|
||||
reader_status = db_secondary_->NewIterators(ReadOptions(),
|
||||
handles_secondary_, &iterators);
|
||||
});
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(state_mutex);
|
||||
state_cv.wait(lock, [&]() { return reader_loaded; });
|
||||
}
|
||||
|
||||
const Status catch_up_status = db_secondary_->TryCatchUpWithPrimary();
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(state_mutex);
|
||||
allow_reader = true;
|
||||
state_cv.notify_all();
|
||||
if (catch_up_status.ok()) {
|
||||
state_cv.wait(lock, [&]() { return view_reclaimed; });
|
||||
}
|
||||
}
|
||||
|
||||
reader_thread.join();
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
std::vector<std::unique_ptr<Iterator>> owned_iterators;
|
||||
for (Iterator* iterator : iterators) {
|
||||
owned_iterators.emplace_back(iterator);
|
||||
}
|
||||
ASSERT_OK(catch_up_status);
|
||||
ASSERT_OK(reader_status);
|
||||
ASSERT_EQ(handles_secondary_.size(), owned_iterators.size());
|
||||
}
|
||||
|
||||
TEST_F(DBSecondaryTest, CloseWaitsForInFlightReadViewReclamation) {
|
||||
Options options;
|
||||
options.env = env_;
|
||||
CreateAndReopenWithCF({"cf1"}, options);
|
||||
|
||||
options.max_open_files = -1;
|
||||
OpenSecondaryWithColumnFamilies({"cf1"}, options);
|
||||
|
||||
std::mutex state_mutex;
|
||||
std::condition_variable state_cv;
|
||||
std::thread::id reclaimer_thread_id;
|
||||
bool reader_loaded = false;
|
||||
bool allow_reader = false;
|
||||
bool reclaimer_ready = false;
|
||||
bool allow_reclaimer = false;
|
||||
bool reclamation_in_flight = false;
|
||||
bool allow_reclamation = false;
|
||||
bool close_waiting = false;
|
||||
bool close_reached_stop = false;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImplSecondary::NewIterators:AfterLoadReadView", [&](void* /*arg*/) {
|
||||
std::unique_lock<std::mutex> lock(state_mutex);
|
||||
reader_loaded = true;
|
||||
state_cv.notify_all();
|
||||
state_cv.wait(lock, [&]() { return allow_reader; });
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImplSecondary::ReclaimRetiredReadViewsLoop:BeforeReclaim",
|
||||
[&](void* /*arg*/) {
|
||||
std::unique_lock<std::mutex> lock(state_mutex);
|
||||
reclaimer_thread_id = std::this_thread::get_id();
|
||||
reclaimer_ready = true;
|
||||
state_cv.notify_all();
|
||||
state_cv.wait(lock, [&]() { return allow_reclaimer; });
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImplSecondary::ReclaimRetiredReadViews:BeforeReclaim",
|
||||
[&](void* /*arg*/) {
|
||||
std::unique_lock<std::mutex> lock(state_mutex);
|
||||
if (std::this_thread::get_id() != reclaimer_thread_id) {
|
||||
return;
|
||||
}
|
||||
reclamation_in_flight = true;
|
||||
state_cv.notify_all();
|
||||
state_cv.wait(lock, [&]() { return allow_reclamation; });
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImplSecondary::DrainRetiredReadViews:Wait", [&](void* /*arg*/) {
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
close_waiting = true;
|
||||
state_cv.notify_all();
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImplSecondary::StopSecondaryReadViewReclaimer:BeforeJoin",
|
||||
[&](void* /*arg*/) {
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
close_reached_stop = true;
|
||||
state_cv.notify_all();
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
Status reader_status;
|
||||
std::vector<Iterator*> iterators;
|
||||
port::Thread reader_thread([&]() {
|
||||
reader_status = db_secondary_->NewIterators(ReadOptions(),
|
||||
handles_secondary_, &iterators);
|
||||
});
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(state_mutex);
|
||||
state_cv.wait(lock, [&]() { return reader_loaded; });
|
||||
}
|
||||
const Status catch_up_status = db_secondary_->TryCatchUpWithPrimary();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
allow_reader = true;
|
||||
state_cv.notify_all();
|
||||
}
|
||||
reader_thread.join();
|
||||
|
||||
for (Iterator* iterator : iterators) {
|
||||
delete iterator;
|
||||
}
|
||||
iterators.clear();
|
||||
Status destroy_handles_status;
|
||||
for (ColumnFamilyHandle* handle : handles_secondary_) {
|
||||
const Status s = db_secondary_->DestroyColumnFamilyHandle(handle);
|
||||
if (!s.ok() && destroy_handles_status.ok()) {
|
||||
destroy_handles_status = s;
|
||||
}
|
||||
}
|
||||
handles_secondary_.clear();
|
||||
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(state_mutex);
|
||||
state_cv.wait(lock, [&]() { return reclaimer_ready; });
|
||||
allow_reclaimer = true;
|
||||
state_cv.notify_all();
|
||||
state_cv.wait(lock, [&]() { return reclamation_in_flight; });
|
||||
}
|
||||
|
||||
Status close_status;
|
||||
port::Thread close_thread([&]() { close_status = db_secondary_->Close(); });
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(state_mutex);
|
||||
state_cv.wait(lock, [&]() { return close_waiting || close_reached_stop; });
|
||||
EXPECT_TRUE(close_waiting);
|
||||
EXPECT_FALSE(close_reached_stop);
|
||||
allow_reclamation = true;
|
||||
state_cv.notify_all();
|
||||
}
|
||||
close_thread.join();
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
ASSERT_OK(catch_up_status);
|
||||
ASSERT_OK(reader_status);
|
||||
ASSERT_OK(destroy_handles_status);
|
||||
ASSERT_OK(close_status);
|
||||
db_secondary_.reset();
|
||||
}
|
||||
|
||||
TEST_F(DBSecondaryTest, ExplicitCloseReleasesPublishedReadView) {
|
||||
Options options;
|
||||
options.env = env_;
|
||||
CreateAndReopenWithCF({"cf1"}, options);
|
||||
|
||||
options.max_open_files = -1;
|
||||
OpenSecondaryWithColumnFamilies({"cf1"}, options);
|
||||
ASSERT_OK(Put(0, "key", "value"));
|
||||
ASSERT_OK(Put(1, "key", "value"));
|
||||
ASSERT_OK(db_->FlushWAL(/*sync=*/true));
|
||||
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
|
||||
|
||||
for (ColumnFamilyHandle* handle : handles_secondary_) {
|
||||
ASSERT_OK(db_secondary_->DestroyColumnFamilyHandle(handle));
|
||||
}
|
||||
handles_secondary_.clear();
|
||||
ASSERT_OK(db_secondary_->Close());
|
||||
db_secondary_.reset();
|
||||
}
|
||||
|
||||
TEST_F(DBSecondaryTest, ExplicitCloseWaitsForRetiredReadView) {
|
||||
Options options;
|
||||
options.env = env_;
|
||||
CreateAndReopenWithCF({"cf1"}, options);
|
||||
|
||||
options.max_open_files = -1;
|
||||
OpenSecondaryWithColumnFamilies({"cf1"}, options);
|
||||
|
||||
std::mutex state_mutex;
|
||||
std::condition_variable state_cv;
|
||||
bool reader_loaded = false;
|
||||
bool allow_reader = false;
|
||||
bool close_waiting = false;
|
||||
bool close_reclaiming = false;
|
||||
bool allow_close = false;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImplSecondary::NewIterators:AfterLoadReadView", [&](void* /*arg*/) {
|
||||
std::unique_lock<std::mutex> lock(state_mutex);
|
||||
reader_loaded = true;
|
||||
state_cv.notify_all();
|
||||
state_cv.wait(lock, [&]() { return allow_reader; });
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImplSecondary::DrainRetiredReadViews:Wait", [&](void* /*arg*/) {
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
close_waiting = true;
|
||||
state_cv.notify_all();
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImplSecondary::DrainRetiredReadViews:AfterWait", [&](void* /*arg*/) {
|
||||
std::unique_lock<std::mutex> lock(state_mutex);
|
||||
close_reclaiming = true;
|
||||
state_cv.notify_all();
|
||||
state_cv.wait(lock, [&]() { return allow_close; });
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
Status reader_status;
|
||||
std::vector<Iterator*> iterators;
|
||||
port::Thread reader_thread([&]() {
|
||||
reader_status = db_secondary_->NewIterators(ReadOptions(),
|
||||
handles_secondary_, &iterators);
|
||||
});
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(state_mutex);
|
||||
state_cv.wait(lock, [&]() { return reader_loaded; });
|
||||
}
|
||||
|
||||
Status close_status;
|
||||
port::Thread close_thread([&]() { close_status = db_secondary_->Close(); });
|
||||
{
|
||||
std::unique_lock<std::mutex> lock(state_mutex);
|
||||
state_cv.wait(lock, [&]() { return close_waiting; });
|
||||
allow_reader = true;
|
||||
state_cv.notify_all();
|
||||
state_cv.wait(lock, [&]() { return close_reclaiming; });
|
||||
}
|
||||
|
||||
reader_thread.join();
|
||||
const size_t num_iterators = iterators.size();
|
||||
for (Iterator* iterator : iterators) {
|
||||
delete iterator;
|
||||
}
|
||||
iterators.clear();
|
||||
Status destroy_handles_status;
|
||||
for (ColumnFamilyHandle* handle : handles_secondary_) {
|
||||
const Status s = db_secondary_->DestroyColumnFamilyHandle(handle);
|
||||
if (!s.ok() && destroy_handles_status.ok()) {
|
||||
destroy_handles_status = s;
|
||||
}
|
||||
}
|
||||
handles_secondary_.clear();
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(state_mutex);
|
||||
allow_close = true;
|
||||
state_cv.notify_all();
|
||||
}
|
||||
close_thread.join();
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
ASSERT_OK(reader_status);
|
||||
ASSERT_EQ(2U, num_iterators);
|
||||
ASSERT_OK(destroy_handles_status);
|
||||
ASSERT_OK(close_status);
|
||||
db_secondary_.reset();
|
||||
}
|
||||
|
||||
TEST_F(DBSecondaryTest, SwitchManifest) {
|
||||
@@ -2261,6 +2785,70 @@ TEST_F(DBSecondaryTest, CatchUpAfterFlush) {
|
||||
ASSERT_OK(iter3->status());
|
||||
}
|
||||
|
||||
TEST_F(DBSecondaryTest, NewIteratorsPerColumnFamilyOptionsConsistentView) {
|
||||
const std::string kCFName = "cf_1";
|
||||
Options options;
|
||||
options.env = env_;
|
||||
options.disable_auto_compactions = true;
|
||||
CreateAndReopenWithCF({kCFName}, options);
|
||||
|
||||
ASSERT_OK(Put(0, "key", "old_default"));
|
||||
ASSERT_OK(Put(1, "key", "old_cf_1"));
|
||||
ASSERT_OK(Flush(0));
|
||||
ASSERT_OK(Flush(1));
|
||||
|
||||
Options secondary_options = options;
|
||||
secondary_options.max_open_files = -1;
|
||||
OpenSecondaryWithColumnFamilies({kCFName}, secondary_options);
|
||||
|
||||
ASSERT_OK(Put(0, "key", "new_default"));
|
||||
ASSERT_OK(Put(1, "key", "new_cf_1"));
|
||||
ASSERT_OK(db_->FlushWAL(/*sync=*/true));
|
||||
|
||||
bool caught_up = false;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImplSecondary::NewIterators:AfterCreateIterator", [&](void* /*arg*/) {
|
||||
if (!caught_up) {
|
||||
ASSERT_OK(db_secondary_->TryCatchUpWithPrimary());
|
||||
caught_up = true;
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
std::vector<ReadOptions> read_options(2);
|
||||
read_options[0].total_order_seek = false;
|
||||
read_options[1].total_order_seek = true;
|
||||
std::vector<Iterator*> iterators;
|
||||
ASSERT_OK(db_secondary_->NewIterators(read_options, handles_secondary_,
|
||||
&iterators));
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
ASSERT_TRUE(caught_up);
|
||||
ASSERT_EQ(2U, iterators.size());
|
||||
const std::vector<std::string> expected_values = {"old_default", "old_cf_1"};
|
||||
for (size_t i = 0; i < iterators.size(); ++i) {
|
||||
iterators[i]->Seek("key");
|
||||
ASSERT_OK(iterators[i]->status());
|
||||
ASSERT_TRUE(iterators[i]->Valid());
|
||||
ASSERT_EQ(expected_values[i], iterators[i]->value());
|
||||
ASSERT_TRUE(iterators[i]->Refresh().IsNotSupported());
|
||||
delete iterators[i];
|
||||
}
|
||||
|
||||
iterators.clear();
|
||||
ASSERT_OK(db_secondary_->NewIterators(read_options, handles_secondary_,
|
||||
&iterators));
|
||||
const std::vector<std::string> new_values = {"new_default", "new_cf_1"};
|
||||
for (size_t i = 0; i < iterators.size(); ++i) {
|
||||
iterators[i]->Seek("key");
|
||||
ASSERT_OK(iterators[i]->status());
|
||||
ASSERT_TRUE(iterators[i]->Valid());
|
||||
ASSERT_EQ(new_values[i], iterators[i]->value());
|
||||
delete iterators[i];
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBSecondaryTest, StartFromInconsistent) {
|
||||
Options options = CurrentOptions();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/advanced_compression.h"
|
||||
#include "rocksdb/db.h"
|
||||
#include "rocksdb/sst_file_reader.h"
|
||||
#include "rocksdb/sst_file_writer.h"
|
||||
#include "rocksdb/types.h"
|
||||
#include "rocksdb/utilities/object_registry.h"
|
||||
#include "rocksdb/utilities/table_properties_collectors.h"
|
||||
@@ -851,6 +853,156 @@ TEST_F(DBTablePropertiesTest, KeyLargestSmallestSeqno) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBTablePropertiesTest, LsmInfoAtCreationPacking) {
|
||||
using Applicability = LsmInfoAtCreation::Applicability;
|
||||
auto round_trip = [](const LsmInfoAtCreation& info) {
|
||||
return LsmInfoAtCreation::DecodeFrom(info.Encode());
|
||||
};
|
||||
|
||||
// Default is "unknown", which encodes to 0 and is distinct from every other
|
||||
// state.
|
||||
LsmInfoAtCreation unknown;
|
||||
EXPECT_EQ(unknown.applicability, Applicability::kUnknown);
|
||||
EXPECT_EQ(unknown.Encode(), 0U);
|
||||
EXPECT_EQ(round_trip(unknown), unknown);
|
||||
|
||||
// "not applicable" is non-zero (so it persists) and distinct from "unknown".
|
||||
LsmInfoAtCreation na = LsmInfoAtCreation::NotApplicable();
|
||||
EXPECT_EQ(na.applicability, Applicability::kNotApplicable);
|
||||
EXPECT_NE(na.Encode(), 0U);
|
||||
EXPECT_NE(na.Encode(), unknown.Encode());
|
||||
EXPECT_EQ(round_trip(na), na);
|
||||
|
||||
// "applicable" round-trips natural level values and bottommost. Level 0 / not
|
||||
// bottommost is still non-zero and thus distinct from "unknown"/"N/A".
|
||||
for (int level : {0, 1, 5, 63, 200}) {
|
||||
for (bool bottommost : {false, true}) {
|
||||
LsmInfoAtCreation info = LsmInfoAtCreation::Applicable(level, bottommost);
|
||||
EXPECT_NE(info.Encode(), 0U);
|
||||
LsmInfoAtCreation decoded = round_trip(info);
|
||||
EXPECT_EQ(decoded.applicability, Applicability::kApplicable);
|
||||
EXPECT_EQ(decoded.level_at_creation, level);
|
||||
EXPECT_EQ(decoded.is_bottommost, bottommost);
|
||||
EXPECT_EQ(decoded, info);
|
||||
}
|
||||
}
|
||||
|
||||
// Applicable but with an unknown level (e.g. the DB repairer): still
|
||||
// applicable, bottommost preserved, level decodes back to kUnknownLevel.
|
||||
LsmInfoAtCreation unknown_level = LsmInfoAtCreation::Applicable(
|
||||
LsmInfoAtCreation::kUnknownLevel, /*is_bottommost=*/true);
|
||||
LsmInfoAtCreation decoded = round_trip(unknown_level);
|
||||
EXPECT_EQ(decoded.applicability, Applicability::kApplicable);
|
||||
EXPECT_EQ(decoded.level_at_creation, LsmInfoAtCreation::kUnknownLevel);
|
||||
EXPECT_TRUE(decoded.is_bottommost);
|
||||
}
|
||||
|
||||
TEST_F(DBTablePropertiesTest, UncompressedDataSizeAndLsmInfoPersisted) {
|
||||
Options options = CurrentOptions();
|
||||
options.compression = kNoCompression;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "key1", "value1"));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "key2", "value2"));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "key3", "value3"));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
|
||||
// Freshly flushed L0 file. uncompressed_data_size is now persisted (it used
|
||||
// to always read back as 0), and LSM-at-creation info is populated.
|
||||
{
|
||||
TablePropertiesCollection props;
|
||||
ASSERT_OK(db_->GetPropertiesOfAllTables(&props));
|
||||
ASSERT_EQ(1U, props.size());
|
||||
auto table_props = props.begin()->second;
|
||||
|
||||
EXPECT_GT(table_props->uncompressed_data_size, 0U);
|
||||
LsmInfoAtCreation info =
|
||||
LsmInfoAtCreation::DecodeFrom(table_props->lsm_info_at_creation);
|
||||
EXPECT_EQ(info.applicability,
|
||||
LsmInfoAtCreation::Applicability::kApplicable);
|
||||
EXPECT_EQ(info.level_at_creation, 0);
|
||||
EXPECT_FALSE(info.is_bottommost);
|
||||
}
|
||||
|
||||
// After forcing a bottommost compaction, the (re)created file records
|
||||
// is_bottommost = true and a deeper creation level.
|
||||
{
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
|
||||
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
|
||||
|
||||
TablePropertiesCollection props;
|
||||
ASSERT_OK(db_->GetPropertiesOfAllTables(&props));
|
||||
ASSERT_EQ(1U, props.size());
|
||||
auto table_props = props.begin()->second;
|
||||
|
||||
EXPECT_GT(table_props->uncompressed_data_size, 0U);
|
||||
LsmInfoAtCreation info =
|
||||
LsmInfoAtCreation::DecodeFrom(table_props->lsm_info_at_creation);
|
||||
EXPECT_EQ(info.applicability,
|
||||
LsmInfoAtCreation::Applicability::kApplicable);
|
||||
EXPECT_TRUE(info.is_bottommost);
|
||||
EXPECT_GT(info.level_at_creation, 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBTablePropertiesTest, LsmInfoNotApplicableForSstFileWriter) {
|
||||
// A file written by SstFileWriter has no LSM position, so its
|
||||
// lsm_info_at_creation records "not applicable" (distinct from "unknown").
|
||||
Options options = CurrentOptions();
|
||||
options.compression = kNoCompression;
|
||||
std::string file = dbname_ + "/for_ingestion.sst";
|
||||
{
|
||||
SstFileWriter writer(EnvOptions(), options);
|
||||
ASSERT_OK(writer.Open(file));
|
||||
ASSERT_OK(writer.Put("a", "1"));
|
||||
ASSERT_OK(writer.Put("b", "2"));
|
||||
ASSERT_OK(writer.Finish());
|
||||
}
|
||||
|
||||
SstFileReader reader(options);
|
||||
ASSERT_OK(reader.Open(file));
|
||||
ASSERT_OK(reader.VerifyChecksum());
|
||||
std::shared_ptr<const TableProperties> table_props =
|
||||
reader.GetTableProperties();
|
||||
ASSERT_NE(table_props, nullptr);
|
||||
|
||||
LsmInfoAtCreation info =
|
||||
LsmInfoAtCreation::DecodeFrom(table_props->lsm_info_at_creation);
|
||||
EXPECT_EQ(info.applicability,
|
||||
LsmInfoAtCreation::Applicability::kNotApplicable);
|
||||
EXPECT_EQ(info.level_at_creation, LsmInfoAtCreation::kUnknownLevel);
|
||||
EXPECT_FALSE(info.is_bottommost);
|
||||
// uncompressed_data_size is persisted for SstFileWriter files too.
|
||||
EXPECT_GT(table_props->uncompressed_data_size, 0U);
|
||||
}
|
||||
|
||||
TEST_F(DBTablePropertiesTest, CompressionManagerFullDetailsPersisted) {
|
||||
// With an explicit CompressionManager, compression_options records the
|
||||
// manager's full details via ToString() (id plus any configured options),
|
||||
// not just GetId(). This manager has no options, so ToString() is its id.
|
||||
Options options = CurrentOptions();
|
||||
options.compression = kNoCompression;
|
||||
options.compression_manager =
|
||||
std::make_shared<ParseCompressionDisplayNameManager>();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "k", "v"));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
|
||||
TablePropertiesCollection props;
|
||||
ASSERT_OK(db_->GetPropertiesOfAllTables(&props));
|
||||
ASSERT_EQ(1U, props.size());
|
||||
auto table_props = props.begin()->second;
|
||||
|
||||
EXPECT_NE(
|
||||
table_props->compression_options.find(
|
||||
"_compression_manager=" +
|
||||
std::string(ParseCompressionDisplayNameManager::kCompatibilityName)),
|
||||
std::string::npos)
|
||||
<< table_props->compression_options;
|
||||
}
|
||||
|
||||
TEST_F(DBTablePropertiesTest, ParseCompressionNameForDisplay) {
|
||||
auto custom_mgr = std::make_shared<ParseCompressionDisplayNameManager>();
|
||||
|
||||
|
||||
@@ -3600,6 +3600,11 @@ class ModelDB : public DB {
|
||||
std::vector<Iterator*>* /*iterators*/) override {
|
||||
return Status::NotSupported("Not supported yet");
|
||||
}
|
||||
Status NewIterators(const std::vector<ReadOptions>& /*options*/,
|
||||
const std::vector<ColumnFamilyHandle*>& /*column_family*/,
|
||||
std::vector<Iterator*>* /*iterators*/) override {
|
||||
return Status::NotSupported("Not supported yet");
|
||||
}
|
||||
|
||||
std::unique_ptr<Iterator> NewCoalescingIterator(
|
||||
const ReadOptions& /*options*/,
|
||||
|
||||
@@ -32,6 +32,46 @@ class DBBasicTestWithTimestamp : public DBBasicTestWithTimestampBase {
|
||||
: DBBasicTestWithTimestampBase("db_basic_test_with_timestamp") {}
|
||||
};
|
||||
|
||||
TEST_F(DBBasicTestWithTimestamp, GetWithMetadataTimestampAndNewerVersion) {
|
||||
Options options = CurrentOptions();
|
||||
options.comparator = test::BytewiseComparatorWithU64TsWrapper();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string old_timestamp = EncodeAsUint64(1);
|
||||
const std::string new_timestamp = EncodeAsUint64(2);
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "key", old_timestamp, "old"));
|
||||
const Snapshot* snapshot = db_->GetSnapshot();
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "key", new_timestamp, "new"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
Slice read_timestamp(new_timestamp);
|
||||
ReadOptions read_options;
|
||||
read_options.snapshot = snapshot;
|
||||
read_options.timestamp = &read_timestamp;
|
||||
|
||||
OutputMetadata output_metadata;
|
||||
output_metadata.WantTimestamp().WantNewerVersionPresent();
|
||||
std::string value;
|
||||
ASSERT_OK(
|
||||
db_->GetWithMetadata(read_options, "key", &value, &output_metadata));
|
||||
ASSERT_EQ("old", value);
|
||||
ASSERT_EQ(old_timestamp, *output_metadata.timestamp);
|
||||
ASSERT_TRUE(*output_metadata.newer_version_present);
|
||||
|
||||
MultiGetOutputMetadata multiget_output_metadata;
|
||||
multiget_output_metadata.WantTimestamps().WantNewerVersionPresent();
|
||||
std::vector<Slice> keys{"key"};
|
||||
std::vector<std::string> values;
|
||||
const std::vector<Status> statuses = db_->MultiGetWithMetadata(
|
||||
read_options, keys, &values, &multiget_output_metadata);
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ("old", values[0]);
|
||||
ASSERT_EQ(old_timestamp, (*multiget_output_metadata.timestamps)[0]);
|
||||
ASSERT_TRUE((*multiget_output_metadata.newer_version_present)[0]);
|
||||
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTestWithTimestamp, SanityChecks) {
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
|
||||
+6
-4
@@ -201,7 +201,7 @@ void EventHelpers::LogAndNotifyTableFileCreationFinished(
|
||||
void EventHelpers::LogAndNotifyTableFileDeletion(
|
||||
EventLogger* event_logger, int job_id, uint64_t file_number,
|
||||
const std::string& file_path, const Status& status,
|
||||
const std::string& dbname,
|
||||
const std::string& dbname, const std::string& db_session_id,
|
||||
const std::vector<std::shared_ptr<EventListener>>& listeners) {
|
||||
if (!event_logger && listeners.empty()) {
|
||||
status.PermitUncheckedError();
|
||||
@@ -213,7 +213,8 @@ void EventHelpers::LogAndNotifyTableFileDeletion(
|
||||
AppendCurrentTime(&jwriter);
|
||||
|
||||
jwriter << "job" << job_id << "event" << "table_file_deletion"
|
||||
<< "file_number" << file_number << "status" << status.ToString();
|
||||
<< "file_number" << file_number << "db_session_id" << db_session_id
|
||||
<< "status" << status.ToString();
|
||||
|
||||
jwriter.EndObject();
|
||||
|
||||
@@ -322,7 +323,7 @@ void EventHelpers::LogAndNotifyBlobFileDeletion(
|
||||
EventLogger* event_logger,
|
||||
const std::vector<std::shared_ptr<EventListener>>& listeners, int job_id,
|
||||
uint64_t file_number, const std::string& file_path, const Status& status,
|
||||
const std::string& dbname) {
|
||||
const std::string& dbname, const std::string& db_session_id) {
|
||||
if (!event_logger && listeners.empty()) {
|
||||
status.PermitUncheckedError();
|
||||
return;
|
||||
@@ -333,7 +334,8 @@ void EventHelpers::LogAndNotifyBlobFileDeletion(
|
||||
AppendCurrentTime(&jwriter);
|
||||
|
||||
jwriter << "job" << job_id << "event" << "blob_file_deletion"
|
||||
<< "file_number" << file_number << "status" << status.ToString();
|
||||
<< "file_number" << file_number << "db_session_id" << db_session_id
|
||||
<< "status" << status.ToString();
|
||||
|
||||
jwriter.EndObject();
|
||||
event_logger->Log(jwriter);
|
||||
|
||||
+2
-2
@@ -39,7 +39,7 @@ class EventHelpers {
|
||||
static void LogAndNotifyTableFileDeletion(
|
||||
EventLogger* event_logger, int job_id, uint64_t file_number,
|
||||
const std::string& file_path, const Status& status,
|
||||
const std::string& db_name,
|
||||
const std::string& db_name, const std::string& db_session_id,
|
||||
const std::vector<std::shared_ptr<EventListener>>& listeners);
|
||||
static void NotifyOnErrorRecoveryEnd(
|
||||
const std::vector<std::shared_ptr<EventListener>>& listeners,
|
||||
@@ -66,7 +66,7 @@ class EventHelpers {
|
||||
EventLogger* event_logger,
|
||||
const std::vector<std::shared_ptr<EventListener>>& listeners, int job_id,
|
||||
uint64_t file_number, const std::string& file_path, const Status& status,
|
||||
const std::string& db_name);
|
||||
const std::string& db_name, const std::string& db_session_id);
|
||||
|
||||
private:
|
||||
static void LogAndNotifyTableFileCreation(
|
||||
|
||||
@@ -224,6 +224,24 @@ class ExternalSSTFileBasicTest
|
||||
bool random_rwfile_supported_;
|
||||
};
|
||||
|
||||
TEST_F(ExternalSSTFileBasicTest, ReopenWithoutFinishIsRejected) {
|
||||
Options options = CurrentOptions();
|
||||
SstFileWriter sst_file_writer(EnvOptions(), options);
|
||||
|
||||
ASSERT_OK(sst_file_writer.Open(sst_files_dir_ + "reopen1.sst"));
|
||||
ASSERT_OK(sst_file_writer.Put(Key(0), "val"));
|
||||
|
||||
// Re-opening would drop a TableBuilder that has had neither Finish() nor
|
||||
// Abandon() called on it.
|
||||
ASSERT_TRUE(
|
||||
sst_file_writer.Open(sst_files_dir_ + "reopen2.sst").IsInvalidArgument());
|
||||
|
||||
// The first file is still the open one and can still be finished.
|
||||
ExternalSstFileInfo file_info;
|
||||
ASSERT_OK(sst_file_writer.Finish(&file_info));
|
||||
ASSERT_EQ(file_info.num_entries, 1);
|
||||
}
|
||||
|
||||
TEST_F(ExternalSSTFileBasicTest, Basic) {
|
||||
Options options = CurrentOptions();
|
||||
|
||||
|
||||
+152
-42
@@ -1307,6 +1307,8 @@ struct Saver {
|
||||
bool* is_blob_index;
|
||||
bool allow_data_in_errors;
|
||||
uint32_t protection_bytes_per_key;
|
||||
// MultiGet's per-key result. Single-key Get stores its result on callback_.
|
||||
bool* newer_version_present;
|
||||
bool CheckCallback(SequenceNumber _seq) {
|
||||
if (callback_) {
|
||||
return callback_->IsVisible(_seq);
|
||||
@@ -1366,6 +1368,12 @@ static bool SaveValue(void* arg, const char* entry) {
|
||||
ValueType type;
|
||||
SequenceNumber seq;
|
||||
UnPackSequenceAndType(tag, &seq, &type);
|
||||
if (UNLIKELY(
|
||||
s->callback_ != nullptr &&
|
||||
s->callback_->NeedToTrackNewerVersions(s->newer_version_present))) {
|
||||
s->callback_->MaybeRecordNewerVersion(seq, type,
|
||||
s->newer_version_present);
|
||||
}
|
||||
// If the value is not in the snapshot, skip it
|
||||
if (!s->CheckCallback(seq)) {
|
||||
return true; // to continue to the next seq
|
||||
@@ -1581,9 +1589,32 @@ bool MemTable::Get(const LookupKey& key, std::string* value,
|
||||
|
||||
PERF_TIMER_GUARD(get_from_memtable_time);
|
||||
|
||||
// Metadata reads widen the lookup seq to detect newer range tombstones. The
|
||||
// regular masking iterator must stay at the user's snapshot seq so such
|
||||
// tombstones only set metadata and do not hide snapshot-visible values.
|
||||
const SequenceNumber lookup_seq = GetInternalKeySeqno(key.internal_key());
|
||||
const MetadataReadBounds* metadata_read_bounds =
|
||||
callback != nullptr ? callback->GetMetadataReadBounds() : nullptr;
|
||||
const SequenceNumber range_del_read_seq =
|
||||
metadata_read_bounds != nullptr ? metadata_read_bounds->read_snapshot_seq
|
||||
: lookup_seq;
|
||||
assert(range_del_read_seq <= lookup_seq);
|
||||
if (callback != nullptr && callback->NeedToTrackNewerVersions() &&
|
||||
!is_range_del_table_empty_.LoadRelaxed()) {
|
||||
std::unique_ptr<FragmentedRangeTombstoneIterator> latest_range_del_iter(
|
||||
NewRangeTombstoneIteratorInternal(read_opts, lookup_seq,
|
||||
immutable_memtable));
|
||||
const SequenceNumber covering_seq =
|
||||
latest_range_del_iter != nullptr
|
||||
? latest_range_del_iter->MaxCoveringTombstoneSeqnum(key.user_key(),
|
||||
callback)
|
||||
: 0;
|
||||
if (covering_seq != 0) {
|
||||
callback->MaybeRecordNewerVersion(covering_seq, kTypeRangeDeletion);
|
||||
}
|
||||
}
|
||||
std::unique_ptr<FragmentedRangeTombstoneIterator> range_del_iter(
|
||||
NewRangeTombstoneIterator(read_opts,
|
||||
GetInternalKeySeqno(key.internal_key()),
|
||||
NewRangeTombstoneIterator(read_opts, range_del_read_seq,
|
||||
immutable_memtable));
|
||||
if (range_del_iter != nullptr) {
|
||||
SequenceNumber covering_seq =
|
||||
@@ -1646,15 +1677,13 @@ bool MemTable::Get(const LookupKey& key, std::string* value,
|
||||
return found_final_value;
|
||||
}
|
||||
|
||||
void MemTable::GetFromTable(const LookupKey& key,
|
||||
SequenceNumber max_covering_tombstone_seq,
|
||||
bool do_merge, ReadCallback* callback,
|
||||
bool* is_blob_index, std::string* value,
|
||||
PinnableWideColumns* columns,
|
||||
std::string* timestamp, Status* s,
|
||||
MergeContext* merge_context, SequenceNumber* seq,
|
||||
bool* found_final_value, bool* merge_in_progress,
|
||||
const BlobFetcher* blob_fetcher) {
|
||||
void MemTable::GetFromTable(
|
||||
const LookupKey& key, SequenceNumber max_covering_tombstone_seq,
|
||||
bool do_merge, ReadCallback* callback, bool* is_blob_index,
|
||||
std::string* value, PinnableWideColumns* columns, std::string* timestamp,
|
||||
Status* s, MergeContext* merge_context, SequenceNumber* seq,
|
||||
bool* found_final_value, bool* merge_in_progress,
|
||||
const BlobFetcher* blob_fetcher, bool* newer_version_present) {
|
||||
Saver saver;
|
||||
saver.status = s;
|
||||
saver.found_final_value = found_final_value;
|
||||
@@ -1678,6 +1707,7 @@ void MemTable::GetFromTable(const LookupKey& key,
|
||||
saver.do_merge = do_merge;
|
||||
saver.allow_data_in_errors = moptions_.allow_data_in_errors;
|
||||
saver.protection_bytes_per_key = moptions_.protection_bytes_per_key;
|
||||
saver.newer_version_present = newer_version_present;
|
||||
|
||||
if (!moptions_.paranoid_memory_checks &&
|
||||
!moptions_.memtable_verify_per_key_checksum_on_seek) {
|
||||
@@ -1711,13 +1741,25 @@ void MemTable::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
||||
}
|
||||
PERF_TIMER_GUARD(get_from_memtable_time);
|
||||
|
||||
// For now, memtable Bloom filter is effectively disabled if there are any
|
||||
// range tombstones. This is the simplest way to ensure range tombstones are
|
||||
// handled. TODO: allow Bloom checks where max_covering_tombstone_seq==0
|
||||
bool no_range_del = read_options.ignore_range_deletions ||
|
||||
is_range_del_table_empty_.LoadRelaxed();
|
||||
MultiGetRange temp_range(*range, range->begin(), range->end());
|
||||
if (bloom_filter_ && no_range_del) {
|
||||
const bool has_range_del = !is_range_del_table_empty_.LoadRelaxed();
|
||||
const bool apply_range_del =
|
||||
has_range_del && !read_options.ignore_range_deletions;
|
||||
bool track_range_del_metadata = false;
|
||||
if (has_range_del && callback != nullptr &&
|
||||
callback->GetMetadataReadBounds() != nullptr) {
|
||||
for (auto iter = temp_range.begin(); iter != temp_range.end(); ++iter) {
|
||||
if (callback->NeedToTrackNewerVersions(&iter->newer_version_present)) {
|
||||
track_range_del_metadata = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For now, memtable Bloom filter is effectively disabled if there are range
|
||||
// tombstones that must affect either read results or metadata.
|
||||
// TODO: allow Bloom checks where max_covering_tombstone_seq==0.
|
||||
if (bloom_filter_ && !apply_range_del && !track_range_del_metadata) {
|
||||
bool whole_key =
|
||||
!prefix_extractor_ || moptions_.memtable_whole_key_filtering;
|
||||
std::array<Slice, MultiGetContext::MAX_BATCH_SIZE> bloom_keys;
|
||||
@@ -1750,6 +1792,25 @@ void MemTable::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
||||
bool validate = moptions_.paranoid_memory_checks ||
|
||||
moptions_.memtable_verify_per_key_checksum_on_seek;
|
||||
|
||||
// For metadata-tracking reads, allocate one "latest" range tombstone
|
||||
// iterator per memtable batch (highest seq across the keys that requested
|
||||
// metadata that has not already observed newer data) instead of one per key.
|
||||
std::unique_ptr<FragmentedRangeTombstoneIterator> latest_range_del_iter;
|
||||
if (track_range_del_metadata) {
|
||||
SequenceNumber latest_range_del_read_seq = 0;
|
||||
for (auto iter = temp_range.begin(); iter != temp_range.end(); ++iter) {
|
||||
if (callback->NeedToTrackNewerVersions(&iter->newer_version_present)) {
|
||||
latest_range_del_read_seq =
|
||||
std::max(latest_range_del_read_seq,
|
||||
GetInternalKeySeqno(iter->lkey->internal_key()));
|
||||
}
|
||||
}
|
||||
if (latest_range_del_read_seq > 0) {
|
||||
latest_range_del_iter.reset(NewRangeTombstoneIteratorInternal(
|
||||
read_options, latest_range_del_read_seq, immutable_memtable));
|
||||
}
|
||||
}
|
||||
|
||||
if (use_batch_optimization) {
|
||||
// Phase 1: Handle range tombstones and set up Savers for batched lookup
|
||||
std::array<Saver, MultiGetContext::MAX_BATCH_SIZE> savers{};
|
||||
@@ -1760,18 +1821,39 @@ void MemTable::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
||||
size_t num_keys = 0;
|
||||
|
||||
for (auto iter = temp_range.begin(); iter != temp_range.end(); ++iter) {
|
||||
if (!no_range_del) {
|
||||
std::unique_ptr<FragmentedRangeTombstoneIterator> range_del_iter(
|
||||
NewRangeTombstoneIteratorInternal(
|
||||
read_options, GetInternalKeySeqno(iter->lkey->internal_key()),
|
||||
immutable_memtable));
|
||||
SequenceNumber covering_seq =
|
||||
range_del_iter->MaxCoveringTombstoneSeqnum(iter->lkey->user_key());
|
||||
if (covering_seq > iter->max_covering_tombstone_seq) {
|
||||
iter->max_covering_tombstone_seq = covering_seq;
|
||||
if (iter->timestamp) {
|
||||
iter->timestamp->assign(range_del_iter->timestamp().data(),
|
||||
range_del_iter->timestamp().size());
|
||||
if (apply_range_del || latest_range_del_iter != nullptr) {
|
||||
if (latest_range_del_iter != nullptr &&
|
||||
callback->NeedToTrackNewerVersions(&iter->newer_version_present)) {
|
||||
const SequenceNumber covering_seq =
|
||||
latest_range_del_iter->MaxCoveringTombstoneSeqnum(
|
||||
iter->lkey->user_key(), callback);
|
||||
if (covering_seq != 0) {
|
||||
callback->MaybeRecordNewerVersion(covering_seq, kTypeRangeDeletion,
|
||||
&iter->newer_version_present);
|
||||
}
|
||||
}
|
||||
if (apply_range_del) {
|
||||
const SequenceNumber lookup_seq =
|
||||
GetInternalKeySeqno(iter->lkey->internal_key());
|
||||
const MetadataReadBounds* metadata_read_bounds =
|
||||
callback != nullptr ? callback->GetMetadataReadBounds() : nullptr;
|
||||
const SequenceNumber range_del_read_seq =
|
||||
metadata_read_bounds != nullptr
|
||||
? metadata_read_bounds->read_snapshot_seq
|
||||
: lookup_seq;
|
||||
assert(range_del_read_seq <= lookup_seq);
|
||||
std::unique_ptr<FragmentedRangeTombstoneIterator> range_del_iter(
|
||||
NewRangeTombstoneIteratorInternal(
|
||||
read_options, range_del_read_seq, immutable_memtable));
|
||||
SequenceNumber covering_seq =
|
||||
range_del_iter->MaxCoveringTombstoneSeqnum(
|
||||
iter->lkey->user_key());
|
||||
if (covering_seq > iter->max_covering_tombstone_seq) {
|
||||
iter->max_covering_tombstone_seq = covering_seq;
|
||||
if (iter->timestamp) {
|
||||
iter->timestamp->assign(range_del_iter->timestamp().data(),
|
||||
range_del_iter->timestamp().size());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1801,6 +1883,10 @@ void MemTable::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
||||
saver.do_merge = true;
|
||||
saver.allow_data_in_errors = moptions_.allow_data_in_errors;
|
||||
saver.protection_bytes_per_key = moptions_.protection_bytes_per_key;
|
||||
saver.newer_version_present =
|
||||
callback != nullptr && callback->GetMetadataReadBounds() != nullptr
|
||||
? &iter->newer_version_present
|
||||
: nullptr;
|
||||
|
||||
memtable_keys[num_keys] = iter->lkey->memtable_key().data();
|
||||
callback_args[num_keys] = &savers[num_keys];
|
||||
@@ -1867,18 +1953,39 @@ void MemTable::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
||||
for (auto iter = temp_range.begin(); iter != temp_range.end(); ++iter) {
|
||||
bool found_final_value{false};
|
||||
bool merge_in_progress = iter->s->IsMergeInProgress();
|
||||
if (!no_range_del) {
|
||||
std::unique_ptr<FragmentedRangeTombstoneIterator> range_del_iter(
|
||||
NewRangeTombstoneIteratorInternal(
|
||||
read_options, GetInternalKeySeqno(iter->lkey->internal_key()),
|
||||
immutable_memtable));
|
||||
SequenceNumber covering_seq =
|
||||
range_del_iter->MaxCoveringTombstoneSeqnum(iter->lkey->user_key());
|
||||
if (covering_seq > iter->max_covering_tombstone_seq) {
|
||||
iter->max_covering_tombstone_seq = covering_seq;
|
||||
if (iter->timestamp) {
|
||||
iter->timestamp->assign(range_del_iter->timestamp().data(),
|
||||
range_del_iter->timestamp().size());
|
||||
if (apply_range_del || latest_range_del_iter != nullptr) {
|
||||
if (latest_range_del_iter != nullptr &&
|
||||
callback->NeedToTrackNewerVersions(&iter->newer_version_present)) {
|
||||
const SequenceNumber covering_seq =
|
||||
latest_range_del_iter->MaxCoveringTombstoneSeqnum(
|
||||
iter->lkey->user_key(), callback);
|
||||
if (covering_seq != 0) {
|
||||
callback->MaybeRecordNewerVersion(covering_seq, kTypeRangeDeletion,
|
||||
&iter->newer_version_present);
|
||||
}
|
||||
}
|
||||
if (apply_range_del) {
|
||||
const SequenceNumber lookup_seq =
|
||||
GetInternalKeySeqno(iter->lkey->internal_key());
|
||||
const MetadataReadBounds* metadata_read_bounds =
|
||||
callback != nullptr ? callback->GetMetadataReadBounds() : nullptr;
|
||||
const SequenceNumber range_del_read_seq =
|
||||
metadata_read_bounds != nullptr
|
||||
? metadata_read_bounds->read_snapshot_seq
|
||||
: lookup_seq;
|
||||
assert(range_del_read_seq <= lookup_seq);
|
||||
std::unique_ptr<FragmentedRangeTombstoneIterator> range_del_iter(
|
||||
NewRangeTombstoneIteratorInternal(
|
||||
read_options, range_del_read_seq, immutable_memtable));
|
||||
SequenceNumber covering_seq =
|
||||
range_del_iter->MaxCoveringTombstoneSeqnum(
|
||||
iter->lkey->user_key());
|
||||
if (covering_seq > iter->max_covering_tombstone_seq) {
|
||||
iter->max_covering_tombstone_seq = covering_seq;
|
||||
if (iter->timestamp) {
|
||||
iter->timestamp->assign(range_del_iter->timestamp().data(),
|
||||
range_del_iter->timestamp().size());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1887,7 +1994,10 @@ void MemTable::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
||||
*(iter->lkey), iter->max_covering_tombstone_seq, true, callback,
|
||||
&iter->is_blob_index, iter->value ? iter->value->GetSelf() : nullptr,
|
||||
iter->columns, iter->timestamp, iter->s, &(iter->merge_context),
|
||||
&dummy_seq, &found_final_value, &merge_in_progress, blob_fetcher);
|
||||
&dummy_seq, &found_final_value, &merge_in_progress, blob_fetcher,
|
||||
callback != nullptr && callback->GetMetadataReadBounds() != nullptr
|
||||
? &iter->newer_version_present
|
||||
: nullptr);
|
||||
|
||||
if (!found_final_value && merge_in_progress) {
|
||||
if (iter->s->ok()) {
|
||||
|
||||
+2
-1
@@ -1001,7 +1001,8 @@ class MemTable final : public ReadOnlyMemTable {
|
||||
std::string* timestamp, Status* s,
|
||||
MergeContext* merge_context, SequenceNumber* seq,
|
||||
bool* found_final_value, bool* merge_in_progress,
|
||||
const BlobFetcher* blob_fetcher);
|
||||
const BlobFetcher* blob_fetcher,
|
||||
bool* newer_version_present = nullptr);
|
||||
|
||||
// Always returns non-null and assumes certain pre-checks (e.g.,
|
||||
// is_range_del_table_empty_) are done. This is only valid during the lifetime
|
||||
|
||||
+5
-4
@@ -240,10 +240,11 @@ bool MemTableListVersion::GetFromList(
|
||||
assert(memtable->IsFragmentedRangeTombstonesConstructed());
|
||||
SequenceNumber current_seq = kMaxSequenceNumber;
|
||||
|
||||
bool done = memtable->Get(key, value, columns, timestamp, s, merge_context,
|
||||
max_covering_tombstone_seq, ¤t_seq,
|
||||
read_opts, true /* immutable_memtable */,
|
||||
callback, is_blob_index, true, blob_fetcher);
|
||||
bool done =
|
||||
memtable->Get(key, value, columns, timestamp, s, merge_context,
|
||||
max_covering_tombstone_seq, ¤t_seq, read_opts,
|
||||
true /* immutable_memtable */, callback, is_blob_index,
|
||||
true /* do_merge */, blob_fetcher);
|
||||
if (*seq == kMaxSequenceNumber) {
|
||||
// Store the most recent sequence number of any operation on this key.
|
||||
// Since we only care about the most recent change, we only need to
|
||||
|
||||
+36
-25
@@ -37,6 +37,7 @@
|
||||
#include "folly/coro/CurrentExecutor.h"
|
||||
#include "folly/coro/Task.h"
|
||||
#include "folly/executors/IOThreadPoolExecutor.h"
|
||||
#include "folly/executors/ManualExecutor.h"
|
||||
#include "folly/io/async/EventBase.h"
|
||||
#include "folly/io/async/Request.h"
|
||||
#include "util/coro_stats_util.h"
|
||||
@@ -178,16 +179,16 @@ TEST_F(PerfContextTest, CoroutineStatsContextsRemainIsolatedWhenInterleaved) {
|
||||
get_perf_context()->EnablePerLevelPerfContext();
|
||||
get_iostats_context()->disable_iostats = true;
|
||||
CoroutineStatsConfig second_stats_config = CaptureCoroutineStatsConfig();
|
||||
CoroutineStatsConfig disabled_stats_config;
|
||||
disabled_stats_config.perf_level = PerfLevel::kDisable;
|
||||
disabled_stats_config.iostats_disabled = true;
|
||||
|
||||
folly::IOThreadPoolExecutor executor(1);
|
||||
folly::EventBase* event_base = executor.getEventBase();
|
||||
ASSERT_NE(nullptr, event_base);
|
||||
|
||||
folly::coro::blockingWait(folly::coro::co_withExecutor(
|
||||
folly::Executor::getKeepAliveToken(event_base),
|
||||
folly::ManualExecutor executor;
|
||||
folly::coro::blockingWait(
|
||||
[first_stats_config = std::move(first_stats_config),
|
||||
second_stats_config = std::move(
|
||||
second_stats_config)]() mutable -> folly::coro::Task<void> {
|
||||
second_stats_config = std::move(second_stats_config),
|
||||
disabled_stats_config = std::move(
|
||||
disabled_stats_config)]() mutable -> folly::coro::Task<void> {
|
||||
get_perf_context()->Reset();
|
||||
get_perf_context()->ClearPerLevelPerfContext();
|
||||
get_perf_context()->block_read_count = 100;
|
||||
@@ -200,6 +201,7 @@ TEST_F(PerfContextTest, CoroutineStatsContextsRemainIsolatedWhenInterleaved) {
|
||||
const bool expected_per_level_perf_context_enabled =
|
||||
stats_config.per_level_perf_context_enabled;
|
||||
const bool expected_iostats_disabled = stats_config.iostats_disabled;
|
||||
const bool stats_enabled = IsCoroutineStatsEnabled(stats_config);
|
||||
{
|
||||
CoroutineStatsContextScope scope(std::move(stats_config),
|
||||
Env::Default());
|
||||
@@ -209,11 +211,12 @@ TEST_F(PerfContextTest, CoroutineStatsContextsRemainIsolatedWhenInterleaved) {
|
||||
get_perf_context()->per_level_perf_context_enabled);
|
||||
EXPECT_EQ(expected_iostats_disabled,
|
||||
get_iostats_context()->disable_iostats);
|
||||
EXPECT_EQ(0, get_perf_context()->block_read_count);
|
||||
EXPECT_EQ(0, get_iostats_context()->bytes_read);
|
||||
|
||||
get_perf_context()->block_read_count += first;
|
||||
get_iostats_context()->bytes_read += first;
|
||||
if (stats_enabled) {
|
||||
EXPECT_EQ(0, get_perf_context()->block_read_count);
|
||||
EXPECT_EQ(0, get_iostats_context()->bytes_read);
|
||||
get_perf_context()->block_read_count += first;
|
||||
get_iostats_context()->bytes_read += first;
|
||||
}
|
||||
co_await folly::coro::co_reschedule_on_current_executor;
|
||||
|
||||
EXPECT_EQ(expected_perf_level, GetPerfLevel());
|
||||
@@ -221,6 +224,9 @@ TEST_F(PerfContextTest, CoroutineStatsContextsRemainIsolatedWhenInterleaved) {
|
||||
get_perf_context()->per_level_perf_context_enabled);
|
||||
EXPECT_EQ(expected_iostats_disabled,
|
||||
get_iostats_context()->disable_iostats);
|
||||
if (!stats_enabled) {
|
||||
co_return 0;
|
||||
}
|
||||
EXPECT_EQ(first, get_perf_context()->block_read_count);
|
||||
EXPECT_EQ(first, get_iostats_context()->bytes_read);
|
||||
get_perf_context()->block_read_count += second;
|
||||
@@ -231,18 +237,22 @@ TEST_F(PerfContextTest, CoroutineStatsContextsRemainIsolatedWhenInterleaved) {
|
||||
co_return get_perf_context()->block_read_count;
|
||||
};
|
||||
|
||||
std::vector<folly::coro::Task<uint64_t>> tasks;
|
||||
tasks.push_back(make_task(std::move(first_stats_config), 1, 10));
|
||||
tasks.push_back(make_task(std::move(second_stats_config), 2, 20));
|
||||
std::vector<uint64_t> results =
|
||||
co_await folly::coro::collectAllRange(std::move(tasks));
|
||||
|
||||
EXPECT_EQ((std::vector<uint64_t>{11, 22}), results);
|
||||
auto [disabled_result, first_result, second_result] =
|
||||
co_await folly::coro::collectAll(
|
||||
make_task(std::move(disabled_stats_config), 0, 0),
|
||||
make_task(std::move(first_stats_config), 1, 10),
|
||||
make_task(std::move(second_stats_config), 2, 20));
|
||||
EXPECT_EQ(0, disabled_result);
|
||||
EXPECT_EQ(11, first_result);
|
||||
EXPECT_EQ(22, second_result);
|
||||
|
||||
get_perf_context()->Reset();
|
||||
get_iostats_context()->Reset();
|
||||
co_return;
|
||||
}()));
|
||||
}(),
|
||||
&executor);
|
||||
EXPECT_EQ(PerfLevel::kDisable, GetPerfLevel());
|
||||
EXPECT_TRUE(get_iostats_context()->disable_iostats);
|
||||
}
|
||||
|
||||
TEST_F(PerfContextTest, CoroutineStatsContextScopeCollectsGetCpuNanos) {
|
||||
@@ -326,10 +336,9 @@ TEST_F(PerfContextTest, AsyncCallbackStatsStartEmptyOnSyncFallback) {
|
||||
|
||||
class StatsCallback final : public DB::AsyncCallback {
|
||||
public:
|
||||
bool EnableStats() const override { return true; }
|
||||
|
||||
void OnComplete(const PerfContext* callback_perf_context,
|
||||
const IOStatsContext* callback_iostats_context) override {
|
||||
void OnComplete() override {
|
||||
const PerfContext* callback_perf_context = get_perf_context();
|
||||
const IOStatsContext* callback_iostats_context = get_iostats_context();
|
||||
ASSERT_NE(nullptr, callback_perf_context);
|
||||
ASSERT_NE(nullptr, callback_iostats_context);
|
||||
called = true;
|
||||
@@ -368,6 +377,8 @@ TEST_F(PerfContextTest, AsyncCallbackStatsStartEmptyOnSyncFallback) {
|
||||
|
||||
get_perf_context()->Reset();
|
||||
get_perf_context()->multiget_read_bytes = 29;
|
||||
SetPerfLevel(PerfLevel::kEnableCount);
|
||||
get_iostats_context()->disable_iostats = false;
|
||||
|
||||
StatsCallback multiget_callback;
|
||||
ColumnFamilyHandle* column_families[] = {db->DefaultColumnFamily(),
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <functional>
|
||||
#include <set>
|
||||
|
||||
#include "db/read_callback.h"
|
||||
#include "util/autovector.h"
|
||||
#include "util/kv_map.h"
|
||||
#include "util/vector_iterator.h"
|
||||
@@ -490,6 +491,29 @@ SequenceNumber FragmentedRangeTombstoneIterator::MaxCoveringTombstoneSeqnum(
|
||||
: 0;
|
||||
}
|
||||
|
||||
SequenceNumber FragmentedRangeTombstoneIterator::MaxCoveringTombstoneSeqnum(
|
||||
const Slice& target_user_key, ReadCallback* callback) {
|
||||
assert(callback != nullptr);
|
||||
SeekToCoveringTombstone(target_user_key);
|
||||
if (!ValidPos() ||
|
||||
ucmp_->CompareWithoutTimestamp(start_key(), target_user_key) > 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const auto seq_end = tombstones_->seq_iter(pos_->seq_end_idx);
|
||||
// Sequence numbers are ordered newest to oldest and seq_pos_ is already
|
||||
// bounded by upper_bound_. Return the first tombstone the metadata callback
|
||||
// considers newer than the read snapshot.
|
||||
while (seq_pos_ != seq_end && *seq_pos_ >= lower_bound_) {
|
||||
const SequenceNumber candidate_seq = *seq_pos_;
|
||||
if (callback->IsNewerVisibleForMetadataRead(candidate_seq)) {
|
||||
return candidate_seq;
|
||||
}
|
||||
++seq_pos_;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::map<SequenceNumber, std::unique_ptr<FragmentedRangeTombstoneIterator>>
|
||||
FragmentedRangeTombstoneIterator::SplitBySnapshot(
|
||||
const std::vector<SequenceNumber>& snapshots) {
|
||||
|
||||
@@ -17,6 +17,9 @@
|
||||
#include "table/internal_iterator.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
class ReadCallback;
|
||||
|
||||
struct FragmentedRangeTombstoneList {
|
||||
public:
|
||||
// A compact representation of a "stack" of range tombstone fragments, which
|
||||
@@ -236,6 +239,12 @@ class FragmentedRangeTombstoneIterator : public InternalIterator {
|
||||
// If there is no covering tombstone, then 0 is returned.
|
||||
SequenceNumber MaxCoveringTombstoneSeqnum(const Slice& user_key);
|
||||
|
||||
// Return the max sequence number of a covering range tombstone that should
|
||||
// be reported as newer-version metadata. If there is no such tombstone, then
|
||||
// 0 is returned.
|
||||
SequenceNumber MaxCoveringTombstoneSeqnum(const Slice& user_key,
|
||||
ReadCallback* callback);
|
||||
|
||||
// Splits the iterator into n+1 iterators (where n is the number of
|
||||
// snapshots), each providing a view over a "stripe" of sequence numbers. The
|
||||
// iterators are keyed by the upper bound of their ranges (the provided
|
||||
|
||||
@@ -10,6 +10,14 @@
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
struct MetadataReadBounds {
|
||||
// The snapshot-visible sequence and the upper bound sampled for this read.
|
||||
// Lookups scan through the upper bound but return data only through the
|
||||
// snapshot sequence.
|
||||
SequenceNumber read_snapshot_seq;
|
||||
SequenceNumber newer_version_upper_bound_seq;
|
||||
};
|
||||
|
||||
class ReadCallback {
|
||||
public:
|
||||
explicit ReadCallback(SequenceNumber last_visible_seq)
|
||||
@@ -38,6 +46,25 @@ class ReadCallback {
|
||||
}
|
||||
}
|
||||
|
||||
virtual const MetadataReadBounds* GetMetadataReadBounds() const {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
virtual bool NeedToTrackNewerVersions(
|
||||
const bool* /*per_key_result*/ = nullptr) const {
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual bool IsNewerVisibleForMetadataRead(SequenceNumber /*seq*/) {
|
||||
// Metadata reads with custom visibility callbacks are unsupported. The
|
||||
// timestamp callback overrides this method for regular DB reads.
|
||||
return false;
|
||||
}
|
||||
|
||||
virtual void MaybeRecordNewerVersion(SequenceNumber /*seq*/,
|
||||
ValueType /*type*/,
|
||||
bool* /*per_key_result*/ = nullptr) {}
|
||||
|
||||
inline SequenceNumber max_visible_seq() { return max_visible_seq_; }
|
||||
|
||||
inline SequenceNumber min_uncommitted() const { return min_uncommitted_; }
|
||||
|
||||
+65
-6
@@ -9,6 +9,8 @@
|
||||
|
||||
#include "db/table_cache.h"
|
||||
|
||||
#include <algorithm>
|
||||
|
||||
#include "db/dbformat.h"
|
||||
#include "db/range_tombstone_fragmenter.h"
|
||||
#include "db/snapshot_impl.h"
|
||||
@@ -548,7 +550,11 @@ uint64_t TableCache::CreateRowCacheKeyPrefix(const ReadOptions& options,
|
||||
// We should consider to use options.snapshot->GetSequenceNumber()
|
||||
// instead of GetInternalKeySeqno(k), which will make the code
|
||||
// easier to understand.
|
||||
cache_entry_seq_no = 1 + GetInternalKeySeqno(internal_key);
|
||||
const MetadataReadBounds* metadata_read_bounds =
|
||||
get_context->metadata_read_bounds();
|
||||
cache_entry_seq_no = 1 + (metadata_read_bounds != nullptr
|
||||
? metadata_read_bounds->read_snapshot_seq
|
||||
: GetInternalKeySeqno(internal_key));
|
||||
}
|
||||
|
||||
// Compute row cache key.
|
||||
@@ -597,10 +603,52 @@ bool TableCache::GetFromRowCache(const Slice& user_key, IterKey& row_cache_key,
|
||||
void TableCache::UpdateRangeTombstoneSeqnums(
|
||||
const ReadOptions& options, TableReader* t,
|
||||
MultiGetContext::Range& table_range) {
|
||||
std::unique_ptr<FragmentedRangeTombstoneIterator> range_del_iter(
|
||||
t->NewRangeTombstoneIterator(options));
|
||||
if (range_del_iter != nullptr) {
|
||||
std::unique_ptr<FragmentedRangeTombstoneIterator> latest_range_del_iter;
|
||||
bool track_newer_versions = false;
|
||||
if (!table_range.empty()) {
|
||||
ReadCallback* callback = table_range.begin()->get_context->read_callback();
|
||||
if (callback != nullptr && callback->GetMetadataReadBounds() != nullptr) {
|
||||
for (auto iter = table_range.begin(); iter != table_range.end(); ++iter) {
|
||||
if (iter->get_context->NeedToTrackNewerVersions()) {
|
||||
track_newer_versions = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (track_newer_versions) {
|
||||
SequenceNumber latest_range_del_read_seq = 0;
|
||||
for (auto iter = table_range.begin(); iter != table_range.end(); ++iter) {
|
||||
if (iter->get_context->NeedToTrackNewerVersions()) {
|
||||
latest_range_del_read_seq = std::max(latest_range_del_read_seq,
|
||||
GetInternalKeySeqno(iter->ikey));
|
||||
}
|
||||
}
|
||||
if (latest_range_del_read_seq > 0) {
|
||||
latest_range_del_iter.reset(t->NewRangeTombstoneIterator(
|
||||
latest_range_del_read_seq, options.timestamp));
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<FragmentedRangeTombstoneIterator> range_del_iter;
|
||||
if (!options.ignore_range_deletions) {
|
||||
range_del_iter.reset(t->NewRangeTombstoneIterator(options));
|
||||
}
|
||||
if (range_del_iter != nullptr || latest_range_del_iter != nullptr) {
|
||||
for (auto iter = table_range.begin(); iter != table_range.end(); ++iter) {
|
||||
if (latest_range_del_iter != nullptr &&
|
||||
iter->get_context->NeedToTrackNewerVersions()) {
|
||||
const SequenceNumber covering_seq =
|
||||
latest_range_del_iter->MaxCoveringTombstoneSeqnum(
|
||||
iter->ukey_with_ts, iter->get_context->read_callback());
|
||||
if (covering_seq != 0) {
|
||||
iter->get_context->RecordNewerVersionIfNeeded(covering_seq,
|
||||
kTypeRangeDeletion);
|
||||
}
|
||||
}
|
||||
if (range_del_iter == nullptr) {
|
||||
continue;
|
||||
}
|
||||
SequenceNumber* max_covering_tombstone_seq =
|
||||
iter->get_context->max_covering_tombstone_seq();
|
||||
SequenceNumber seq =
|
||||
@@ -630,7 +678,18 @@ Status TableCache::MultiGetFilter(
|
||||
// filtering here, since the filtering needs to happen after the row cache
|
||||
// lookup.
|
||||
KeyContext& first_key = *mget_range->begin();
|
||||
if (ioptions_.row_cache && !first_key.get_context->NeedToReadSequence()) {
|
||||
bool track_newer_versions = false;
|
||||
ReadCallback* callback = first_key.get_context->read_callback();
|
||||
if (callback != nullptr && callback->GetMetadataReadBounds() != nullptr) {
|
||||
for (auto iter = mget_range->begin(); iter != mget_range->end(); ++iter) {
|
||||
if (iter->get_context->NeedToTrackNewerVersions()) {
|
||||
track_newer_versions = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (ioptions_.row_cache && !first_key.get_context->NeedToReadSequence() &&
|
||||
!track_newer_versions) {
|
||||
return Status::NotSupported();
|
||||
}
|
||||
Status s;
|
||||
@@ -649,7 +708,7 @@ Status TableCache::MultiGetFilter(
|
||||
s = t->MultiGetFilter(options, mutable_cf_options.prefix_extractor.get(),
|
||||
mget_range);
|
||||
}
|
||||
if (s.ok() && !options.ignore_range_deletions) {
|
||||
if (s.ok() && (!options.ignore_range_deletions || track_newer_versions)) {
|
||||
// Update the range tombstone sequence numbers for the keys here
|
||||
// as TableCache::MultiGet may or may not be called, and even if it
|
||||
// is, it may be called with fewer keys in the rangedue to filtering.
|
||||
|
||||
@@ -25,7 +25,8 @@ DEFINE_SYNC_AND_ASYNC(Status, TableCache::Get)
|
||||
// Check row cache if enabled.
|
||||
// Reuse row_cache_key sequence number when row cache hits.
|
||||
Status s;
|
||||
if (ioptions_.row_cache && !get_context->NeedToReadSequence()) {
|
||||
if (ioptions_.row_cache && !get_context->NeedToReadSequence() &&
|
||||
!get_context->NeedToTrackNewerVersions()) {
|
||||
auto user_key = ExtractUserKey(k);
|
||||
uint64_t cache_entry_seq_no =
|
||||
CreateRowCacheKeyPrefix(options, fd, k, get_context, row_cache_key);
|
||||
@@ -49,18 +50,37 @@ DEFINE_SYNC_AND_ASYNC(Status, TableCache::Get)
|
||||
should_pin_table_handles_);
|
||||
SequenceNumber* max_covering_tombstone_seq =
|
||||
get_context->max_covering_tombstone_seq();
|
||||
if (s.ok() && max_covering_tombstone_seq != nullptr &&
|
||||
!options.ignore_range_deletions) {
|
||||
std::unique_ptr<FragmentedRangeTombstoneIterator> range_del_iter(
|
||||
t->NewRangeTombstoneIterator(options));
|
||||
if (range_del_iter != nullptr) {
|
||||
SequenceNumber seq =
|
||||
range_del_iter->MaxCoveringTombstoneSeqnum(ExtractUserKey(k));
|
||||
if (seq > *max_covering_tombstone_seq) {
|
||||
*max_covering_tombstone_seq = seq;
|
||||
if (get_context->NeedTimestamp()) {
|
||||
get_context->SetTimestampFromRangeTombstone(
|
||||
range_del_iter->timestamp());
|
||||
const bool track_newer_versions = get_context->NeedToTrackNewerVersions();
|
||||
if (s.ok() && ((max_covering_tombstone_seq != nullptr &&
|
||||
!options.ignore_range_deletions) ||
|
||||
track_newer_versions)) {
|
||||
if (track_newer_versions) {
|
||||
std::unique_ptr<FragmentedRangeTombstoneIterator> latest_range_del_iter(
|
||||
t->NewRangeTombstoneIterator(GetInternalKeySeqno(k),
|
||||
options.timestamp));
|
||||
const SequenceNumber covering_seq =
|
||||
latest_range_del_iter != nullptr
|
||||
? latest_range_del_iter->MaxCoveringTombstoneSeqnum(
|
||||
ExtractUserKey(k), get_context->read_callback())
|
||||
: 0;
|
||||
if (covering_seq != 0) {
|
||||
get_context->RecordNewerVersionIfNeeded(covering_seq,
|
||||
kTypeRangeDeletion);
|
||||
}
|
||||
}
|
||||
if (max_covering_tombstone_seq != nullptr &&
|
||||
!options.ignore_range_deletions) {
|
||||
std::unique_ptr<FragmentedRangeTombstoneIterator> range_del_iter(
|
||||
t->NewRangeTombstoneIterator(options));
|
||||
if (range_del_iter != nullptr) {
|
||||
SequenceNumber seq =
|
||||
range_del_iter->MaxCoveringTombstoneSeqnum(ExtractUserKey(k));
|
||||
if (seq > *max_covering_tombstone_seq) {
|
||||
*max_covering_tombstone_seq = seq;
|
||||
if (get_context->NeedTimestamp()) {
|
||||
get_context->SetTimestampFromRangeTombstone(
|
||||
range_del_iter->timestamp());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -116,15 +136,30 @@ DEFINE_SYNC_AND_ASYNC(Status, TableCache::MultiGet)
|
||||
IterKey row_cache_key;
|
||||
size_t row_cache_key_prefix_size = 0;
|
||||
KeyContext& first_key = *table_range.begin();
|
||||
bool lookup_row_cache =
|
||||
ioptions_.row_cache && !first_key.get_context->NeedToReadSequence();
|
||||
bool track_newer_versions = false;
|
||||
ReadCallback* callback = first_key.get_context->read_callback();
|
||||
const bool is_metadata_read =
|
||||
callback != nullptr && callback->GetMetadataReadBounds() != nullptr;
|
||||
if (is_metadata_read) {
|
||||
for (auto iter = table_range.begin(); iter != table_range.end(); ++iter) {
|
||||
if (iter->get_context->NeedToTrackNewerVersions()) {
|
||||
track_newer_versions = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
bool lookup_row_cache = ioptions_.row_cache &&
|
||||
!first_key.get_context->NeedToReadSequence() &&
|
||||
!track_newer_versions;
|
||||
|
||||
// Check row cache if enabled. Since row cache does not currently store
|
||||
// sequence numbers, we cannot use it if we need to fetch the sequence.
|
||||
if (lookup_row_cache) {
|
||||
GetContext* first_context = first_key.get_context;
|
||||
CreateRowCacheKeyPrefix(options, fd, first_key.ikey, first_context,
|
||||
row_cache_key);
|
||||
const SequenceNumber row_cache_entry_seq_no = CreateRowCacheKeyPrefix(
|
||||
options, fd, first_key.ikey, first_context, row_cache_key);
|
||||
const SequenceNumber replay_seq_no =
|
||||
is_metadata_read ? row_cache_entry_seq_no : kMaxSequenceNumber;
|
||||
row_cache_key_prefix_size = row_cache_key.Size();
|
||||
|
||||
for (auto miter = table_range.begin(); miter != table_range.end();
|
||||
@@ -136,7 +171,7 @@ DEFINE_SYNC_AND_ASYNC(Status, TableCache::MultiGet)
|
||||
Status read_status;
|
||||
bool ret =
|
||||
GetFromRowCache(user_key, row_cache_key, row_cache_key_prefix_size,
|
||||
get_context, &read_status);
|
||||
get_context, &read_status, replay_seq_no);
|
||||
if (!read_status.ok()) {
|
||||
CO_RETURN read_status;
|
||||
}
|
||||
@@ -164,7 +199,8 @@ DEFINE_SYNC_AND_ASYNC(Status, TableCache::MultiGet)
|
||||
TEST_SYNC_POINT_CALLBACK("TableCache::MultiGet:FindTable", &s);
|
||||
assert(!s.ok() || t);
|
||||
}
|
||||
if (s.ok() && !options.ignore_range_deletions && !skip_range_deletions) {
|
||||
if (s.ok() && !skip_range_deletions &&
|
||||
(!options.ignore_range_deletions || track_newer_versions)) {
|
||||
UpdateRangeTombstoneSeqnums(options, t, table_range);
|
||||
}
|
||||
if (s.ok()) {
|
||||
|
||||
+104
-2
@@ -2846,6 +2846,107 @@ Status Version::GetBlobRange(const ReadOptions& read_options,
|
||||
blob_index.compression(), range_offset, range_length, value, bytes_read);
|
||||
}
|
||||
|
||||
void Version::MultiGetBlobLazy(const ReadOptions& read_options,
|
||||
autovector<LazyBlobReadRequest>& reqs) const {
|
||||
assert(blob_source_);
|
||||
|
||||
// Turn the flat list of separate-file blob references into per-blob-file
|
||||
// batches that BlobSource can each resolve with a single coalesced MultiRead.
|
||||
// Whole-value and sub-range requests use different BlobSource entry points,
|
||||
// so they are grouped independently:
|
||||
// whole_reqs -- batches of whole-value reads (BlobReadRequest)
|
||||
// range_reqs -- batches of sub-range reads (BlobRangeReadRequest)
|
||||
// Each batch holds at most MultiGetContext::MAX_BATCH_SIZE requests (see the
|
||||
// loop below), so one file may contribute several batches. whole_idx /
|
||||
// range_idx map a file number to the index, within whole_reqs / range_reqs,
|
||||
// of that file's current (most recently started, still fillable) batch.
|
||||
autovector<BlobFileReadRequests> whole_reqs;
|
||||
autovector<BlobFileRangeReadRequests> range_reqs;
|
||||
std::unordered_map<uint64_t, size_t> whole_idx;
|
||||
std::unordered_map<uint64_t, size_t> range_idx;
|
||||
|
||||
for (auto& req : reqs) {
|
||||
assert(req.user_key);
|
||||
assert(req.blob_index);
|
||||
assert(req.result);
|
||||
assert(req.status);
|
||||
const BlobIndex& blob_index = *req.blob_index;
|
||||
|
||||
if (blob_index.HasTTL() || blob_index.IsInlined()) {
|
||||
*req.status = Status::Corruption("Unexpected TTL/inlined blob index");
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint64_t file_number = blob_index.file_number();
|
||||
auto blob_file_meta = storage_info_.GetBlobFileMetaData(file_number);
|
||||
if (!blob_file_meta) {
|
||||
// INTEGRITY CHECK -- see Version::GetBlob. A same-file/embedded reference
|
||||
// (file_number 0) must be resolved via SameFileBlobReader, not here.
|
||||
*req.status = Status::Corruption("Invalid blob file number");
|
||||
continue;
|
||||
}
|
||||
const uint64_t file_size = blob_file_meta->GetBlobFileSize();
|
||||
|
||||
if (req.range_length == kWholeBlobLength) {
|
||||
// Append to this file's current batch, or start a new batch when the file
|
||||
// has none yet or its current batch is full. Capping each batch at
|
||||
// MAX_BATCH_SIZE is required, not just an optimization:
|
||||
// BlobFileReader::MultiGetBlob[Range] asserts a batch is at most
|
||||
// MAX_BATCH_SIZE, and BlobSource tracks a batch's cache hits in a 64-bit
|
||||
// mask -- while MultiGetEntityLazy does not otherwise bound how many keys
|
||||
// can reference a single blob file.
|
||||
size_t idx;
|
||||
auto it = whole_idx.find(file_number);
|
||||
if (it != whole_idx.end() && std::get<2>(whole_reqs[it->second]).size() <
|
||||
MultiGetContext::MAX_BATCH_SIZE) {
|
||||
idx = it->second; // this file's current batch still has room
|
||||
} else {
|
||||
idx = whole_reqs.size(); // no batch yet for this file, or it is full
|
||||
whole_idx[file_number] = idx;
|
||||
whole_reqs.emplace_back(file_number, file_size,
|
||||
autovector<BlobReadRequest>());
|
||||
}
|
||||
std::get<2>(whole_reqs[idx])
|
||||
.emplace_back(*req.user_key, blob_index.offset(), blob_index.size(),
|
||||
blob_index.compression(), req.result, req.status);
|
||||
} else {
|
||||
// A strict sub-range of a compressed blob cannot be decompressed in
|
||||
// isolation; the caller resolves such columns whole and slices instead.
|
||||
if (blob_index.compression() != kNoCompression) {
|
||||
*req.status = Status::Corruption("Cannot range-read a compressed blob");
|
||||
continue;
|
||||
}
|
||||
// Same per-file batching and MAX_BATCH_SIZE cap as the whole-value path
|
||||
// above.
|
||||
size_t idx;
|
||||
auto it = range_idx.find(file_number);
|
||||
if (it != range_idx.end() && std::get<2>(range_reqs[it->second]).size() <
|
||||
MultiGetContext::MAX_BATCH_SIZE) {
|
||||
idx = it->second; // this file's current batch still has room
|
||||
} else {
|
||||
idx = range_reqs.size(); // no batch yet for this file, or it is full
|
||||
range_idx[file_number] = idx;
|
||||
range_reqs.emplace_back(file_number, file_size,
|
||||
autovector<BlobRangeReadRequest>());
|
||||
}
|
||||
std::get<2>(range_reqs[idx])
|
||||
.emplace_back(*req.user_key, blob_index.offset(), blob_index.size(),
|
||||
req.range_offset, req.range_length, req.result,
|
||||
req.status);
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve every batch; BlobSource issues one coalesced MultiRead per batch.
|
||||
if (!whole_reqs.empty()) {
|
||||
blob_source_->MultiGetBlob(read_options, whole_reqs,
|
||||
/*bytes_read=*/nullptr);
|
||||
}
|
||||
if (!range_reqs.empty()) {
|
||||
blob_source_->MultiGetBlobRange(read_options, range_reqs,
|
||||
/*bytes_read=*/nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
void Version::MultiGetBlob(
|
||||
const ReadOptions& read_options, MultiGetRange& range,
|
||||
std::unordered_map<uint64_t, BlobReadContexts>& blob_ctxs) {
|
||||
@@ -6187,9 +6288,10 @@ Status VersionSet::ProcessManifestWrites(
|
||||
if (new_descriptor_log) {
|
||||
ROCKS_LOG_INFO(db_options_->info_log,
|
||||
"Created manifest %" PRIu64
|
||||
", compacted+appended from %" PRIu64 " to %" PRIu64 "\n",
|
||||
", compacted+appended from %" PRIu64 " to %" PRIu64
|
||||
", db_session_id=%s\n",
|
||||
pending_manifest_file_number_, prev_manifest_file_size,
|
||||
new_manifest_file_size);
|
||||
new_manifest_file_size, db_session_id_.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1033,6 +1033,28 @@ class Version {
|
||||
size_t range_length, PinnableSlice* value,
|
||||
uint64_t* bytes_read) const;
|
||||
|
||||
// A single separate-file blob reference to resolve via MultiGetBlobLazy:
|
||||
// whole value (range_length == kWholeBlobLength) or a byte sub-range.
|
||||
struct LazyBlobReadRequest {
|
||||
const Slice* user_key = nullptr;
|
||||
const BlobIndex* blob_index = nullptr;
|
||||
uint64_t range_offset = 0;
|
||||
size_t range_length = 0; // kWholeBlobLength => whole value
|
||||
PinnableSlice* result = nullptr;
|
||||
Status* status = nullptr;
|
||||
};
|
||||
|
||||
// Resolves a batch of separate-file blob references for the lazy (cross-key
|
||||
// coalescing) read path, grouping by blob file so each file's reads are
|
||||
// coalesced into one MultiRead. Whole-value requests (range_length ==
|
||||
// kWholeBlobLength) go through BlobSource::MultiGetBlob (blob-cache fill +
|
||||
// checksum per read_options); sub-range requests go through
|
||||
// BlobSource::MultiGetBlobRange (uncompressed, no checksum, no cache fill).
|
||||
// Per-request outcome is written to *reqs[i].status; a TTL/inlined index or
|
||||
// an unknown blob file number yields Corruption on that request.
|
||||
void MultiGetBlobLazy(const ReadOptions& read_options,
|
||||
autovector<LazyBlobReadRequest>& reqs) const;
|
||||
|
||||
struct BlobReadContext {
|
||||
BlobReadContext(const BlobIndex& blob_idx, KeyContext* key_ctx)
|
||||
: blob_index(blob_idx), key_context(key_ctx) {}
|
||||
|
||||
@@ -415,6 +415,9 @@ DEFINE_SYNC_AND_ASYNC(void, Version::MultiGet)
|
||||
&iter->max_covering_tombstone_seq, clock_, nullptr,
|
||||
merge_operator_ ? &pinned_iters_mgr : nullptr, callback,
|
||||
&iter->is_blob_index, tracing_mget_id, &blob_fetcher);
|
||||
if (callback != nullptr && callback->GetMetadataReadBounds() != nullptr) {
|
||||
get_ctx.back().SetNewerVersionResult(&iter->newer_version_present);
|
||||
}
|
||||
// MergeInProgress status, if set, has been transferred to the get_context
|
||||
// state, so we set status to ok here. From now on, the iter status will
|
||||
// be used for IO errors, and get_context state will be used for any
|
||||
|
||||
+832
-13
@@ -59,8 +59,11 @@ class BlobReadIOActivityFS : public FileSystemWrapper {
|
||||
}
|
||||
const bool is_blob_file =
|
||||
fname.size() >= 5 && fname.compare(fname.size() - 5, 5, ".blob") == 0;
|
||||
if (is_blob_file) {
|
||||
*result = std::make_unique<RecordingFile>(std::move(file), this);
|
||||
const bool is_sst_file =
|
||||
fname.size() >= 4 && fname.compare(fname.size() - 4, 4, ".sst") == 0;
|
||||
if (is_blob_file || is_sst_file) {
|
||||
*result = std::make_unique<RecordingFile>(std::move(file), this,
|
||||
/*is_blob=*/is_blob_file);
|
||||
} else {
|
||||
*result = std::move(file);
|
||||
}
|
||||
@@ -75,28 +78,67 @@ class BlobReadIOActivityFS : public FileSystemWrapper {
|
||||
return static_cast<Env::IOActivity>(last_blob_read_io_activity_.load());
|
||||
}
|
||||
|
||||
// Counters distinguishing a coalesced blob read (one MultiRead over N blobs)
|
||||
// from per-blob reads (N separate Read calls). A coalesced batch of blobs in
|
||||
// one blob file (separate-file) or one SST (embedded) issues a single
|
||||
// MultiRead; the non-coalesced single-read paths (GetBlob / GetBlobRange /
|
||||
// GetSimpleGen2Blob*) issue individual Read calls. (Reads issued *inside* the
|
||||
// underlying FS's MultiRead go to the wrapped file, not this wrapper's Read,
|
||||
// so they are not double-counted here.) Separate-file blob reads hit .blob
|
||||
// files; embedded (same-file) reads hit .sst files, hence separate counters.
|
||||
void ResetBlobReadCounts() {
|
||||
blob_read_count_.store(0);
|
||||
blob_multiread_count_.store(0);
|
||||
sst_multiread_count_.store(0);
|
||||
}
|
||||
uint64_t blob_read_count() const { return blob_read_count_.load(); }
|
||||
uint64_t blob_multiread_count() const { return blob_multiread_count_.load(); }
|
||||
uint64_t sst_multiread_count() const { return sst_multiread_count_.load(); }
|
||||
|
||||
private:
|
||||
class RecordingFile : public FSRandomAccessFileOwnerWrapper {
|
||||
public:
|
||||
RecordingFile(std::unique_ptr<FSRandomAccessFile>&& file,
|
||||
BlobReadIOActivityFS* fs)
|
||||
: FSRandomAccessFileOwnerWrapper(std::move(file)), fs_(fs) {}
|
||||
BlobReadIOActivityFS* fs, bool is_blob)
|
||||
: FSRandomAccessFileOwnerWrapper(std::move(file)),
|
||||
fs_(fs),
|
||||
is_blob_(is_blob) {}
|
||||
|
||||
IOStatus Read(uint64_t offset, size_t n, const IOOptions& options,
|
||||
Slice* result, char* scratch,
|
||||
IODebugContext* dbg) const override {
|
||||
fs_->last_blob_read_io_activity_.store(
|
||||
static_cast<uint8_t>(options.io_activity));
|
||||
if (is_blob_) {
|
||||
fs_->last_blob_read_io_activity_.store(
|
||||
static_cast<uint8_t>(options.io_activity));
|
||||
fs_->blob_read_count_.fetch_add(1);
|
||||
}
|
||||
return FSRandomAccessFileOwnerWrapper::Read(offset, n, options, result,
|
||||
scratch, dbg);
|
||||
}
|
||||
|
||||
IOStatus MultiRead(FSReadRequest* reqs, size_t num_reqs,
|
||||
const IOOptions& options, IODebugContext* dbg) override {
|
||||
if (is_blob_) {
|
||||
fs_->last_blob_read_io_activity_.store(
|
||||
static_cast<uint8_t>(options.io_activity));
|
||||
fs_->blob_multiread_count_.fetch_add(1);
|
||||
} else {
|
||||
fs_->sst_multiread_count_.fetch_add(1);
|
||||
}
|
||||
return FSRandomAccessFileOwnerWrapper::MultiRead(reqs, num_reqs, options,
|
||||
dbg);
|
||||
}
|
||||
|
||||
private:
|
||||
BlobReadIOActivityFS* fs_;
|
||||
bool is_blob_;
|
||||
};
|
||||
|
||||
std::atomic<uint8_t> last_blob_read_io_activity_{
|
||||
static_cast<uint8_t>(Env::IOActivity::kUnknown)};
|
||||
std::atomic<uint64_t> blob_read_count_{0};
|
||||
std::atomic<uint64_t> blob_multiread_count_{0};
|
||||
std::atomic<uint64_t> sst_multiread_count_{0};
|
||||
};
|
||||
} // namespace
|
||||
|
||||
@@ -174,6 +216,26 @@ class DBLazyEntityTest : public DBTestBase {
|
||||
ASSERT_OK(writer.Finish());
|
||||
ASSERT_OK(db_->IngestExternalFile({sst_path}, IngestExternalFileOptions()));
|
||||
}
|
||||
|
||||
// Writes a single SST holding several embedded (same-file blob) wide-column
|
||||
// entities (keys must be given in sorted order) and ingests it, so all their
|
||||
// embedded blob references live in one SST -- used to exercise cross-key
|
||||
// coalescing of embedded reads within a single SameFileBlobReader.
|
||||
void IngestEmbeddedEntities(
|
||||
const Options& options,
|
||||
const std::vector<std::pair<std::string, WideColumns>>& entities,
|
||||
uint64_t min_blob_size = 64) {
|
||||
const std::string sst_path = dbname_ + "/embedded_multi.sst";
|
||||
SstFileWriterEmbeddedBlobOptions embedded_blob_options;
|
||||
embedded_blob_options.min_blob_size = min_blob_size;
|
||||
SstFileWriter writer(EnvOptions(), options);
|
||||
ASSERT_OK(writer.OpenWithEmbeddedBlobs(sst_path, embedded_blob_options));
|
||||
for (const auto& entity : entities) {
|
||||
ASSERT_OK(writer.PutEntity(entity.first, entity.second));
|
||||
}
|
||||
ASSERT_OK(writer.Finish());
|
||||
ASSERT_OK(db_->IngestExternalFile({sst_path}, IngestExternalFileOptions()));
|
||||
}
|
||||
};
|
||||
|
||||
// Enumerate a lazily-read entity without triggering any blob I/O: inline
|
||||
@@ -972,8 +1034,635 @@ TEST_F(DBLazyEntityTest, BatchResolveAcrossKeysInOneCall) {
|
||||
ASSERT_EQ(results[1], v2);
|
||||
}
|
||||
|
||||
// The lazy result outlives the producing call: it pins the SuperVersion, so a
|
||||
// deferred read still works after GetEntityLazy has returned and even after the
|
||||
// Cross-key coalescing: N whole-column separate-file reads across keys (all in
|
||||
// one blob file) are served by a single coalesced MultiRead, and the fetched
|
||||
// values are cached (a repeat read does no further blob I/O).
|
||||
TEST_F(DBLazyEntityTest, BatchCoalescesWholeColumnSeparateFileReads) {
|
||||
auto fs = std::make_shared<BlobReadIOActivityFS>(FileSystem::Default());
|
||||
std::unique_ptr<Env> env(NewCompositeEnv(fs));
|
||||
Defer close_db_on_exit([this]() { Close(); });
|
||||
|
||||
Options options = GetLazyTestOptions();
|
||||
options.env = env.get();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
constexpr size_t kNumKeys = 3;
|
||||
std::vector<std::string> keys;
|
||||
std::vector<std::string> values;
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
keys.push_back("k" + std::to_string(i));
|
||||
values.emplace_back(2000 + i, static_cast<char>('a' + i));
|
||||
ASSERT_OK(db_->PutEntity(
|
||||
WriteOptions(), db_->DefaultColumnFamily(), keys.back(),
|
||||
{{kDefaultWideColumnName, "inline"}, {"data", values.back()}}));
|
||||
}
|
||||
ASSERT_OK(Flush()); // one flush -> one blob file holding all N blobs
|
||||
|
||||
std::vector<Slice> key_slices(keys.begin(), keys.end());
|
||||
LazyWideColumnsBatch batch;
|
||||
std::vector<Status> get_statuses(kNumKeys);
|
||||
db_->MultiGetEntityLazy(ReadOptions(), db_->DefaultColumnFamily(), kNumKeys,
|
||||
key_slices.data(), &batch, get_statuses.data());
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
ASSERT_OK(get_statuses[i]);
|
||||
}
|
||||
ASSERT_EQ(batch.size(), kNumKeys);
|
||||
|
||||
// Warm the (shared) blob-file reader so the coalescing assertion below counts
|
||||
// only the value read, not the one-time header/footer open.
|
||||
PinnableSlice warm;
|
||||
ASSERT_OK(batch[0].ResolveColumnRange(batch[0][1], /*offset=*/0, /*length=*/1,
|
||||
&warm));
|
||||
fs->ResetBlobReadCounts();
|
||||
|
||||
std::vector<PinnableSlice> results(kNumKeys);
|
||||
std::vector<Status> statuses(kNumKeys);
|
||||
std::vector<LazyColumnReadRequest> reads(kNumKeys);
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
reads[i].column = &batch[i][1]; // "data" column of entity i
|
||||
reads[i].result = &results[i];
|
||||
reads[i].status = &statuses[i];
|
||||
}
|
||||
ASSERT_OK(batch.MultiResolve(reads.size(), reads.data()));
|
||||
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
ASSERT_OK(statuses[i]);
|
||||
ASSERT_EQ(results[i], values[i]);
|
||||
}
|
||||
// All N whole-column reads coalesced into one MultiRead over the blob file;
|
||||
// no per-blob Read calls.
|
||||
ASSERT_EQ(fs->blob_multiread_count(), 1U);
|
||||
ASSERT_EQ(fs->blob_read_count(), 0U);
|
||||
|
||||
// The coalesced whole reads were cached: reading the same columns again does
|
||||
// no further blob I/O.
|
||||
fs->ResetBlobReadCounts();
|
||||
std::vector<PinnableSlice> results2(kNumKeys);
|
||||
std::vector<Status> statuses2(kNumKeys);
|
||||
std::vector<LazyColumnReadRequest> reads2(kNumKeys);
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
reads2[i].column = &batch[i][1];
|
||||
reads2[i].result = &results2[i];
|
||||
reads2[i].status = &statuses2[i];
|
||||
}
|
||||
ASSERT_OK(batch.MultiResolve(reads2.size(), reads2.data()));
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
ASSERT_OK(statuses2[i]);
|
||||
ASSERT_EQ(results2[i], values[i]);
|
||||
}
|
||||
ASSERT_EQ(fs->blob_multiread_count(), 0U);
|
||||
ASSERT_EQ(fs->blob_read_count(), 0U);
|
||||
}
|
||||
|
||||
// Cross-key coalescing of byte-range (partial) separate-file reads: N sub-range
|
||||
// reads across keys in one blob file become a single coalesced MultiRead, save
|
||||
// the un-read bytes, and (being partial) never populate the blob cache.
|
||||
TEST_F(DBLazyEntityTest, BatchCoalescesRangeSeparateFileReads) {
|
||||
auto fs = std::make_shared<BlobReadIOActivityFS>(FileSystem::Default());
|
||||
std::unique_ptr<Env> env(NewCompositeEnv(fs));
|
||||
Defer close_db_on_exit([this]() { Close(); });
|
||||
|
||||
Options options = GetLazyTestOptionsWithBlobCache();
|
||||
options.env = env.get();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
constexpr size_t kNumKeys = 3;
|
||||
constexpr size_t kValueSize = 3000;
|
||||
constexpr size_t kRangeLen = 100;
|
||||
std::vector<std::string> keys;
|
||||
std::vector<std::string> values;
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
keys.push_back("k" + std::to_string(i));
|
||||
values.emplace_back(kValueSize, static_cast<char>('a' + i));
|
||||
ASSERT_OK(db_->PutEntity(
|
||||
WriteOptions(), db_->DefaultColumnFamily(), keys.back(),
|
||||
{{kDefaultWideColumnName, "inline"}, {"data", values.back()}}));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(options.statistics->Reset());
|
||||
|
||||
std::vector<Slice> key_slices(keys.begin(), keys.end());
|
||||
LazyWideColumnsBatch batch;
|
||||
std::vector<Status> get_statuses(kNumKeys);
|
||||
db_->MultiGetEntityLazy(ReadOptions(), db_->DefaultColumnFamily(), kNumKeys,
|
||||
key_slices.data(), &batch, get_statuses.data());
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
ASSERT_OK(get_statuses[i]);
|
||||
}
|
||||
|
||||
// Warm the shared blob-file reader (opening it reads header/footer).
|
||||
PinnableSlice warm;
|
||||
ASSERT_OK(batch[0].ResolveColumnRange(batch[0][1], /*offset=*/0, /*length=*/1,
|
||||
&warm));
|
||||
fs->ResetBlobReadCounts();
|
||||
// Reset stats after warming so the assertions below count only the coalesced
|
||||
// batch reads (not the 1-byte warm partial read).
|
||||
ASSERT_OK(options.statistics->Reset());
|
||||
const uint64_t cache_adds_before = BlobCacheAdds(options);
|
||||
|
||||
std::vector<PinnableSlice> results(kNumKeys);
|
||||
std::vector<Status> statuses(kNumKeys);
|
||||
std::vector<LazyColumnReadRequest> reads(kNumKeys);
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
reads[i].column = &batch[i][1];
|
||||
reads[i].offset = 0;
|
||||
reads[i].length = kRangeLen;
|
||||
reads[i].result = &results[i];
|
||||
reads[i].status = &statuses[i];
|
||||
}
|
||||
ASSERT_OK(batch.MultiResolve(reads.size(), reads.data()));
|
||||
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
ASSERT_OK(statuses[i]);
|
||||
ASSERT_EQ(results[i], values[i].substr(0, kRangeLen));
|
||||
}
|
||||
// All N range reads coalesced into a single MultiRead; no per-blob Reads.
|
||||
ASSERT_EQ(fs->blob_multiread_count(), 1U);
|
||||
ASSERT_EQ(fs->blob_read_count(), 0U);
|
||||
// Partial reads never fill the blob cache.
|
||||
ASSERT_EQ(BlobCacheAdds(options), cache_adds_before);
|
||||
// Bytes saved: each read fetched kRangeLen instead of the whole value.
|
||||
ASSERT_EQ(LazyPartialBytesSaved(options),
|
||||
kNumKeys * (kValueSize - kRangeLen));
|
||||
}
|
||||
|
||||
// Cross-key coalescing of embedded (same-file) reads: several keys whose
|
||||
// embedded blobs live in one SST are resolved by a single coalesced MultiRead
|
||||
// over that SST.
|
||||
TEST_F(DBLazyEntityTest, BatchCoalescesEmbeddedReads) {
|
||||
auto fs = std::make_shared<BlobReadIOActivityFS>(FileSystem::Default());
|
||||
std::unique_ptr<Env> env(NewCompositeEnv(fs));
|
||||
Defer close_db_on_exit([this]() { Close(); });
|
||||
|
||||
Options options = GetLazyTestOptions();
|
||||
options.env = env.get();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
constexpr size_t kNumKeys = 3;
|
||||
std::vector<std::string> keys;
|
||||
std::vector<std::string> values;
|
||||
std::vector<std::pair<std::string, WideColumns>> entities;
|
||||
entities.reserve(kNumKeys);
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
keys.push_back("ek" + std::to_string(i));
|
||||
values.emplace_back(2000 + i, static_cast<char>('a' + i));
|
||||
}
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
entities.emplace_back(
|
||||
keys[i],
|
||||
WideColumns{{kDefaultWideColumnName, "inline"}, {"data", values[i]}});
|
||||
}
|
||||
IngestEmbeddedEntities(options, entities);
|
||||
|
||||
std::vector<Slice> key_slices(keys.begin(), keys.end());
|
||||
LazyWideColumnsBatch batch;
|
||||
std::vector<Status> get_statuses(kNumKeys);
|
||||
db_->MultiGetEntityLazy(ReadOptions(), db_->DefaultColumnFamily(), kNumKeys,
|
||||
key_slices.data(), &batch, get_statuses.data());
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
ASSERT_OK(get_statuses[i]);
|
||||
}
|
||||
ASSERT_EQ(batch.size(), kNumKeys);
|
||||
|
||||
// Reset after the point lookups; the embedded records are read only now.
|
||||
fs->ResetBlobReadCounts();
|
||||
|
||||
std::vector<PinnableSlice> results(kNumKeys);
|
||||
std::vector<Status> statuses(kNumKeys);
|
||||
std::vector<LazyColumnReadRequest> reads(kNumKeys);
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
reads[i].column = &batch[i][1]; // embedded "data" column
|
||||
reads[i].result = &results[i];
|
||||
reads[i].status = &statuses[i];
|
||||
}
|
||||
ASSERT_OK(batch.MultiResolve(reads.size(), reads.data()));
|
||||
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
ASSERT_OK(statuses[i]);
|
||||
ASSERT_EQ(results[i], values[i]);
|
||||
}
|
||||
// All embedded records (same SST) coalesced into one MultiRead over the SST.
|
||||
ASSERT_EQ(fs->sst_multiread_count(), 1U);
|
||||
}
|
||||
|
||||
// Regression test: the batch may present reads in an order that does not match
|
||||
// ascending record offset. RandomAccessFileReader::MultiRead requires ascending
|
||||
// offsets (it asserts, and in direct-I/O mode merges only consecutive
|
||||
// requests), so the embedded batch reader must sort internally. Here the reads
|
||||
// are issued in reverse key order (descending record offset); the values must
|
||||
// still resolve correctly in one coalesced read.
|
||||
TEST_F(DBLazyEntityTest, BatchCoalescesEmbeddedReadsOutOfOrder) {
|
||||
auto fs = std::make_shared<BlobReadIOActivityFS>(FileSystem::Default());
|
||||
std::unique_ptr<Env> env(NewCompositeEnv(fs));
|
||||
Defer close_db_on_exit([this]() { Close(); });
|
||||
|
||||
Options options = GetLazyTestOptions();
|
||||
options.env = env.get();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
constexpr size_t kNumKeys = 4;
|
||||
std::vector<std::string> keys;
|
||||
std::vector<std::string> values;
|
||||
std::vector<std::pair<std::string, WideColumns>> entities;
|
||||
entities.reserve(kNumKeys);
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
keys.push_back("ek" + std::to_string(i));
|
||||
values.emplace_back(2000 + i, static_cast<char>('a' + i));
|
||||
}
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
entities.emplace_back(
|
||||
keys[i],
|
||||
WideColumns{{kDefaultWideColumnName, "inline"}, {"data", values[i]}});
|
||||
}
|
||||
// Keys are written in ascending order, so their embedded records have
|
||||
// ascending file offsets.
|
||||
IngestEmbeddedEntities(options, entities);
|
||||
|
||||
std::vector<Slice> key_slices(keys.begin(), keys.end());
|
||||
LazyWideColumnsBatch batch;
|
||||
std::vector<Status> get_statuses(kNumKeys);
|
||||
db_->MultiGetEntityLazy(ReadOptions(), db_->DefaultColumnFamily(), kNumKeys,
|
||||
key_slices.data(), &batch, get_statuses.data());
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
ASSERT_OK(get_statuses[i]);
|
||||
}
|
||||
|
||||
fs->ResetBlobReadCounts();
|
||||
|
||||
// Issue the reads in reverse key order, i.e. descending record offset, so the
|
||||
// batch reader receives them unsorted.
|
||||
std::vector<PinnableSlice> results(kNumKeys);
|
||||
std::vector<Status> statuses(kNumKeys);
|
||||
std::vector<LazyColumnReadRequest> reads(kNumKeys);
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
const size_t key_index = kNumKeys - 1 - i;
|
||||
reads[i].column = &batch[key_index][1]; // embedded "data" column
|
||||
reads[i].result = &results[i];
|
||||
reads[i].status = &statuses[i];
|
||||
}
|
||||
ASSERT_OK(batch.MultiResolve(reads.size(), reads.data()));
|
||||
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
const size_t key_index = kNumKeys - 1 - i;
|
||||
ASSERT_OK(statuses[i]);
|
||||
ASSERT_EQ(results[i], values[key_index]);
|
||||
}
|
||||
// Still one coalesced MultiRead over the SST despite the unsorted input.
|
||||
ASSERT_EQ(fs->sst_multiread_count(), 1U);
|
||||
}
|
||||
|
||||
// A single batch MultiResolve mixing an inline column (served immediately), a
|
||||
// whole blob read, and a byte-range blob read across keys returns the right
|
||||
// bytes for each.
|
||||
TEST_F(DBLazyEntityTest, BatchMixedResolveAcrossKeys) {
|
||||
Options options = GetLazyTestOptions();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string v0(2000, 'a');
|
||||
const std::string v1(2500, 'b');
|
||||
ASSERT_OK(
|
||||
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), "k0",
|
||||
{{kDefaultWideColumnName, "inline0"}, {"data", v0}}));
|
||||
ASSERT_OK(
|
||||
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), "k1",
|
||||
{{kDefaultWideColumnName, "inline1"}, {"data", v1}}));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
const std::array<Slice, 2> keys{Slice("k0"), Slice("k1")};
|
||||
LazyWideColumnsBatch batch;
|
||||
std::array<Status, 2> get_statuses;
|
||||
db_->MultiGetEntityLazy(ReadOptions(), db_->DefaultColumnFamily(),
|
||||
keys.size(), keys.data(), &batch,
|
||||
get_statuses.data());
|
||||
ASSERT_OK(get_statuses[0]);
|
||||
ASSERT_OK(get_statuses[1]);
|
||||
|
||||
// Reads: k0 inline default column, k0 whole "data", k1 "data" range [10,60).
|
||||
std::array<PinnableSlice, 3> results;
|
||||
std::array<Status, 3> statuses;
|
||||
std::array<LazyColumnReadRequest, 3> reads;
|
||||
reads[0].column = &batch[0][0]; // inline default column
|
||||
reads[0].result = &results[0];
|
||||
reads[0].status = &statuses[0];
|
||||
reads[1].column = &batch[0][1]; // k0 whole "data"
|
||||
reads[1].result = &results[1];
|
||||
reads[1].status = &statuses[1];
|
||||
reads[2].column = &batch[1][1]; // k1 "data" range
|
||||
reads[2].offset = 10;
|
||||
reads[2].length = 50;
|
||||
reads[2].result = &results[2];
|
||||
reads[2].status = &statuses[2];
|
||||
|
||||
ASSERT_OK(batch.MultiResolve(reads.size(), reads.data()));
|
||||
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ(results[0], "inline0");
|
||||
ASSERT_OK(statuses[1]);
|
||||
ASSERT_EQ(results[1], v0);
|
||||
ASSERT_OK(statuses[2]);
|
||||
ASSERT_EQ(results[2], v1.substr(10, 50));
|
||||
}
|
||||
|
||||
// Batched MultiGetEntityLazy + one cross-key MultiResolve returns the same
|
||||
// bytes as resolving each key with a separate GetEntityLazy.
|
||||
TEST_F(DBLazyEntityTest, BatchedResolveMatchesPerKey) {
|
||||
Options options = GetLazyTestOptions();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
constexpr size_t kNumKeys = 4;
|
||||
std::vector<std::string> keys;
|
||||
std::vector<std::string> values;
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
keys.push_back("k" + std::to_string(i));
|
||||
values.emplace_back(1500 + 37 * i, static_cast<char>('a' + i));
|
||||
ASSERT_OK(db_->PutEntity(
|
||||
WriteOptions(), db_->DefaultColumnFamily(), keys.back(),
|
||||
{{kDefaultWideColumnName, "inline"}, {"data", values.back()}}));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Per-key reference via separate GetEntityLazy calls.
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
LazyWideColumns single;
|
||||
ASSERT_OK(db_->GetEntityLazy(ReadOptions(), db_->DefaultColumnFamily(),
|
||||
keys[i], &single));
|
||||
PinnableSlice ref;
|
||||
ASSERT_OK(single.ResolveColumnRange(single[1], /*offset=*/5, /*length=*/200,
|
||||
&ref));
|
||||
ASSERT_EQ(ref, values[i].substr(5, 200));
|
||||
}
|
||||
|
||||
// Batched path.
|
||||
std::vector<Slice> key_slices(keys.begin(), keys.end());
|
||||
LazyWideColumnsBatch batch;
|
||||
std::vector<Status> get_statuses(kNumKeys);
|
||||
db_->MultiGetEntityLazy(ReadOptions(), db_->DefaultColumnFamily(), kNumKeys,
|
||||
key_slices.data(), &batch, get_statuses.data());
|
||||
std::vector<PinnableSlice> results(kNumKeys);
|
||||
std::vector<Status> statuses(kNumKeys);
|
||||
std::vector<LazyColumnReadRequest> reads(kNumKeys);
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
ASSERT_OK(get_statuses[i]);
|
||||
reads[i].column = &batch[i][1];
|
||||
reads[i].offset = 5;
|
||||
reads[i].length = 200;
|
||||
reads[i].result = &results[i];
|
||||
reads[i].status = &statuses[i];
|
||||
}
|
||||
ASSERT_OK(batch.MultiResolve(reads.size(), reads.data()));
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
ASSERT_OK(statuses[i]);
|
||||
ASSERT_EQ(results[i], values[i].substr(5, 200));
|
||||
}
|
||||
}
|
||||
|
||||
// Regression test: a single blob file can hold more references than the
|
||||
// MultiGet batch limit (MultiGetContext::MAX_BATCH_SIZE).
|
||||
// Version::MultiGetBlobLazy must split per-file requests into batches no larger
|
||||
// than that limit, since the lower-level BlobFileReader::MultiGetBlob[Range]
|
||||
// asserts it and BlobSource tracks cache hits in a 64-bit mask. Here all keys
|
||||
// are flushed into one blob file and resolved (whole, then byte-range) in one
|
||||
// batch each; kNumKeys exceeds both 32 and 64.
|
||||
TEST_F(DBLazyEntityTest, BatchManyKeysInOneBlobFileExceedsBatchLimit) {
|
||||
Options options = GetLazyTestOptions();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
constexpr size_t kNumKeys = 70; // > MAX_BATCH_SIZE (32) and > 64
|
||||
std::vector<std::string> keys;
|
||||
std::vector<std::string> values;
|
||||
keys.reserve(kNumKeys);
|
||||
values.reserve(kNumKeys);
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
keys.push_back("k" + std::to_string(i));
|
||||
values.emplace_back(1000 + i, static_cast<char>('a' + (i % 26)));
|
||||
ASSERT_OK(db_->PutEntity(
|
||||
WriteOptions(), db_->DefaultColumnFamily(), keys.back(),
|
||||
{{kDefaultWideColumnName, "inline"}, {"data", values.back()}}));
|
||||
}
|
||||
ASSERT_OK(Flush()); // one flush -> one blob file holding all kNumKeys blobs
|
||||
|
||||
std::vector<Slice> key_slices(keys.begin(), keys.end());
|
||||
LazyWideColumnsBatch batch;
|
||||
std::vector<Status> get_statuses(kNumKeys);
|
||||
db_->MultiGetEntityLazy(ReadOptions(), db_->DefaultColumnFamily(), kNumKeys,
|
||||
key_slices.data(), &batch, get_statuses.data());
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
ASSERT_OK(get_statuses[i]);
|
||||
}
|
||||
ASSERT_EQ(batch.size(), kNumKeys);
|
||||
|
||||
// Whole-column reads of every key in one batch.
|
||||
{
|
||||
std::vector<PinnableSlice> results(kNumKeys);
|
||||
std::vector<Status> statuses(kNumKeys);
|
||||
std::vector<LazyColumnReadRequest> reads(kNumKeys);
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
reads[i].column = &batch[i][1];
|
||||
reads[i].result = &results[i];
|
||||
reads[i].status = &statuses[i];
|
||||
}
|
||||
ASSERT_OK(batch.MultiResolve(reads.size(), reads.data()));
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
ASSERT_OK(statuses[i]);
|
||||
ASSERT_EQ(results[i], values[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Byte-range reads of every key in one batch.
|
||||
{
|
||||
std::vector<PinnableSlice> results(kNumKeys);
|
||||
std::vector<Status> statuses(kNumKeys);
|
||||
std::vector<LazyColumnReadRequest> reads(kNumKeys);
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
reads[i].column = &batch[i][1];
|
||||
reads[i].offset = 10;
|
||||
reads[i].length = 50;
|
||||
reads[i].result = &results[i];
|
||||
reads[i].status = &statuses[i];
|
||||
}
|
||||
ASSERT_OK(batch.MultiResolve(reads.size(), reads.data()));
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
ASSERT_OK(statuses[i]);
|
||||
ASSERT_EQ(results[i], values[i].substr(10, 50));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Regression test: a force_verify read must not be silently skipped when the
|
||||
// same column is also read (whole, unverified) in the same batch. Coalescing
|
||||
// must not adopt unverified bytes into the resolver cache ahead of the
|
||||
// force_verify read. A corrupted record is detected because the force_verify
|
||||
// read runs a full verified read rather than slicing the coalesced cache entry.
|
||||
TEST_F(DBLazyEntityTest, BatchForceVerifyNotSkippedByCoalescedRead) {
|
||||
Options options = GetLazyTestOptions(); // no blob cache: reads hit the file
|
||||
DestroyAndReopen(options);
|
||||
|
||||
constexpr char key[] = "entity";
|
||||
const std::string big(4000, 'a');
|
||||
const WideColumns columns{{kDefaultWideColumnName, "inline"}, {"data", big}};
|
||||
ASSERT_OK(
|
||||
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Corrupt the value bytes after they are read from the file so a checksum
|
||||
// verification (if it runs) fails.
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"BlobFileReader::GetBlob:TamperWithResult", [](void* arg) {
|
||||
Slice* const record_slice = static_cast<Slice*>(arg);
|
||||
ASSERT_NE(record_slice, nullptr);
|
||||
ASSERT_FALSE(record_slice->empty());
|
||||
char* const data = const_cast<char*>(record_slice->data());
|
||||
data[record_slice->size() - 1] ^= 0xff;
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
ReadOptions ro;
|
||||
ro.verify_checksums = false; // globally off; force_verify overrides per-read
|
||||
const std::array<Slice, 1> key_slices{Slice(key)};
|
||||
LazyWideColumnsBatch batch;
|
||||
std::array<Status, 1> get_statuses;
|
||||
db_->MultiGetEntityLazy(ro, db_->DefaultColumnFamily(), key_slices.size(),
|
||||
key_slices.data(), &batch, get_statuses.data());
|
||||
ASSERT_OK(get_statuses[0]);
|
||||
|
||||
// reads[0] forces verification of the "data" column; reads[1] reads the same
|
||||
// column whole without verification. reads[1] must not cause reads[0] to be
|
||||
// served from an unverified coalesced cache entry.
|
||||
std::array<PinnableSlice, 2> results;
|
||||
std::array<Status, 2> statuses;
|
||||
std::array<LazyColumnReadRequest, 2> reads;
|
||||
reads[0].column = &batch[0][1];
|
||||
reads[0].force_verify = true;
|
||||
reads[0].result = &results[0];
|
||||
reads[0].status = &statuses[0];
|
||||
reads[1].column = &batch[0][1]; // same column, whole, unverified
|
||||
reads[1].result = &results[1];
|
||||
reads[1].status = &statuses[1];
|
||||
ASSERT_OK(batch.MultiResolve(reads.size(), reads.data()));
|
||||
|
||||
// The force_verify read detected the corruption.
|
||||
ASSERT_TRUE(statuses[0].IsCorruption()) << statuses[0].ToString();
|
||||
// The unverified whole read of the same column runs no verification, so it
|
||||
// still succeeds (returning the unverified bytes) rather than surfacing the
|
||||
// corruption.
|
||||
ASSERT_OK(statuses[1]);
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
// Regression test: a force_verify read must not be served from the shared blob
|
||||
// cache after a non-verifying coalesced read of a *duplicate key* filled it.
|
||||
// The two keys are distinct entities that resolve to the same physical blob
|
||||
// record and blob-cache key, so the non-verifying whole fetch must not run
|
||||
// (caching unverified bytes) ahead of the force_verify read. Requires a blob
|
||||
// cache (that is the shared state at risk); without one each entity's resolver
|
||||
// is independent and the reordering is harmless.
|
||||
TEST_F(DBLazyEntityTest, BatchForceVerifyNotSkippedAcrossDuplicateKeys) {
|
||||
Options options = GetLazyTestOptionsWithBlobCache();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
constexpr char key[] = "entity";
|
||||
const std::string big(4000, 'a');
|
||||
const WideColumns columns{{kDefaultWideColumnName, "inline"}, {"data", big}};
|
||||
ASSERT_OK(
|
||||
db_->PutEntity(WriteOptions(), db_->DefaultColumnFamily(), key, columns));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Corrupt the value bytes on each file read so a checksum verification (if it
|
||||
// runs) fails; a cache hit that skips verification would not surface it.
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"BlobFileReader::GetBlob:TamperWithResult", [](void* arg) {
|
||||
Slice* const record_slice = static_cast<Slice*>(arg);
|
||||
ASSERT_NE(record_slice, nullptr);
|
||||
ASSERT_FALSE(record_slice->empty());
|
||||
char* const data = const_cast<char*>(record_slice->data());
|
||||
data[record_slice->size() - 1] ^= 0xff;
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
ReadOptions ro;
|
||||
ro.verify_checksums = false; // globally off; force_verify overrides per-read
|
||||
// Duplicate keys: two distinct entities that resolve to the same physical
|
||||
// blob record and cache key.
|
||||
const std::array<Slice, 2> key_slices{Slice(key), Slice(key)};
|
||||
LazyWideColumnsBatch batch;
|
||||
std::array<Status, 2> get_statuses;
|
||||
db_->MultiGetEntityLazy(ro, db_->DefaultColumnFamily(), key_slices.size(),
|
||||
key_slices.data(), &batch, get_statuses.data());
|
||||
ASSERT_OK(get_statuses[0]);
|
||||
ASSERT_OK(get_statuses[1]);
|
||||
ASSERT_EQ(batch.size(), 2U);
|
||||
|
||||
// reads[0] force-verifies entity 0's "data"; reads[1] reads entity 1's "data"
|
||||
// whole without verification. The duplicate key shares the physical record,
|
||||
// so reads[1] must not coalesce a non-verifying fetch that caches unverified
|
||||
// bytes ahead of reads[0].
|
||||
std::array<PinnableSlice, 2> results;
|
||||
std::array<Status, 2> statuses;
|
||||
std::array<LazyColumnReadRequest, 2> reads;
|
||||
reads[0].column = &batch[0][1];
|
||||
reads[0].force_verify = true;
|
||||
reads[0].result = &results[0];
|
||||
reads[0].status = &statuses[0];
|
||||
reads[1].column = &batch[1][1]; // duplicate key's same column, unverified
|
||||
reads[1].result = &results[1];
|
||||
reads[1].status = &statuses[1];
|
||||
ASSERT_OK(batch.MultiResolve(reads.size(), reads.data()));
|
||||
|
||||
// The force_verify read ran a real verified read and caught the corruption
|
||||
// rather than being served unverified bytes from the shared blob cache.
|
||||
ASSERT_TRUE(statuses[0].IsCorruption()) << statuses[0].ToString();
|
||||
// The unverified whole read runs no verification, so it still succeeds
|
||||
// (returning unverified bytes).
|
||||
ASSERT_OK(statuses[1]);
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
// A read that omits its status out-param is a caller error (there is nowhere to
|
||||
// report that read's outcome), so MultiResolve fails the whole call with
|
||||
// InvalidArgument rather than silently resolving.
|
||||
TEST_F(DBLazyEntityTest, BatchNullStatusReturnsInvalidArgument) {
|
||||
Options options = GetLazyTestOptions();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
constexpr size_t kNumKeys = 3;
|
||||
std::vector<std::string> keys;
|
||||
std::vector<std::string> values;
|
||||
keys.reserve(kNumKeys);
|
||||
values.reserve(kNumKeys);
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
keys.push_back("k" + std::to_string(i));
|
||||
values.emplace_back(3000, static_cast<char>('a' + i));
|
||||
ASSERT_OK(db_->PutEntity(
|
||||
WriteOptions(), db_->DefaultColumnFamily(), keys.back(),
|
||||
{{kDefaultWideColumnName, "inline"}, {"data", values.back()}}));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
std::vector<Slice> key_slices(keys.begin(), keys.end());
|
||||
LazyWideColumnsBatch batch;
|
||||
std::vector<Status> get_statuses(kNumKeys);
|
||||
db_->MultiGetEntityLazy(ReadOptions(), db_->DefaultColumnFamily(), kNumKeys,
|
||||
key_slices.data(), &batch, get_statuses.data());
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
ASSERT_OK(get_statuses[i]);
|
||||
}
|
||||
|
||||
std::vector<PinnableSlice> results(kNumKeys);
|
||||
std::vector<LazyColumnReadRequest> reads(kNumKeys);
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
reads[i].column = &batch[i][1];
|
||||
reads[i].offset = 100;
|
||||
reads[i].length = 80;
|
||||
reads[i].result = &results[i];
|
||||
reads[i].status = nullptr; // caller error: no status out-param
|
||||
}
|
||||
const Status s = batch.MultiResolve(reads.size(), reads.data());
|
||||
ASSERT_TRUE(s.IsInvalidArgument()) << s.ToString();
|
||||
}
|
||||
// result has been moved.
|
||||
TEST_F(DBLazyEntityTest, ResultOutlivesGetEntityLazyCall) {
|
||||
Options options = GetLazyTestOptions();
|
||||
@@ -1474,6 +2163,46 @@ TEST_F(DBLazyEntityTest, UserTimestampParityWithGetEntity) {
|
||||
ASSERT_EQ(Slice(value), "plain");
|
||||
}
|
||||
|
||||
// A wrong-width read timestamp must be rejected up front (InvalidArgument for
|
||||
// every key), before MultiCFSnapshot's collapsed-history check -- which assumes
|
||||
// a correctly-sized timestamp and would otherwise assert in debug builds (or
|
||||
// read past the timestamp buffer in release) when full_history_ts_low is set.
|
||||
TEST_F(DBLazyEntityTest, BatchWrongWidthTimestampRejected) {
|
||||
Options options = GetLazyTestOptions();
|
||||
options.comparator = test::BytewiseComparatorWithU64TsWrapper();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ColumnFamilyHandle* const cfh = db_->DefaultColumnFamily();
|
||||
|
||||
// Write a plain value with a valid (8-byte) timestamp.
|
||||
std::string write_ts;
|
||||
PutFixed64(&write_ts, 1);
|
||||
ASSERT_OK(db_->Put(WriteOptions(), cfh, "k", write_ts, "plain"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// Set full_history_ts_low so the collapsed-history check would run (and, on a
|
||||
// wrong-width timestamp, trip the size assertion) if reached before the
|
||||
// width validation this test covers.
|
||||
std::string ts_low;
|
||||
PutFixed64(&ts_low, 1);
|
||||
ASSERT_OK(db_->IncreaseFullHistoryTsLow(cfh, ts_low));
|
||||
|
||||
// A 1-byte timestamp does not match the column family's 8-byte width.
|
||||
const std::string bad_ts_storage(1, '\x01');
|
||||
const Slice bad_ts(bad_ts_storage);
|
||||
ReadOptions ro;
|
||||
ro.timestamp = &bad_ts;
|
||||
|
||||
const std::array<Slice, 2> keys{Slice("k"), Slice("k")};
|
||||
LazyWideColumnsBatch batch;
|
||||
std::array<Status, 2> statuses;
|
||||
db_->MultiGetEntityLazy(ro, cfh, keys.size(), keys.data(), &batch,
|
||||
statuses.data());
|
||||
for (const Status& s : statuses) {
|
||||
ASSERT_TRUE(s.IsInvalidArgument()) << s.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
// The lazy read API must be forwarded through StackableDB wrappers (e.g.
|
||||
// TransactionDB) to the underlying DB, not fall through to the DB base class
|
||||
// default (Status::NotSupported). Exercises committed data read outside a
|
||||
@@ -1611,11 +2340,101 @@ TEST_F(DBLazyEntityTest, SecondaryInstance) {
|
||||
ASSERT_EQ(Slice(value), big);
|
||||
}
|
||||
|
||||
// Verify that MultiGetEntityLazy sets IOOptions::io_activity to
|
||||
// kMultiGetEntity (not kGetEntity or kUnknown) so that the stress test's
|
||||
// CheckIOActivity assertion in db_stress_env_wrapper.h passes. This
|
||||
// reproduces the scenario from T283693234 where the thread operation was left
|
||||
// stale at OP_GETENTITY after a consistency check, causing a mismatch with the
|
||||
// Batched MultiGetEntityLazy works on a read-only instance: the read-only
|
||||
// GetImpl override must honor the batch's shared SuperVersion / sequence number
|
||||
// and hand back the resolution Version even though no per-key pin is taken.
|
||||
TEST_F(DBLazyEntityTest, ReadOnlyInstanceBatched) {
|
||||
Options options = GetLazyTestOptions();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
constexpr size_t kNumKeys = 3;
|
||||
std::vector<std::string> keys;
|
||||
std::vector<std::string> values;
|
||||
keys.reserve(kNumKeys);
|
||||
values.reserve(kNumKeys);
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
keys.push_back("k" + std::to_string(i));
|
||||
values.emplace_back(200 + i, static_cast<char>('a' + i));
|
||||
ASSERT_OK(db_->PutEntity(
|
||||
WriteOptions(), db_->DefaultColumnFamily(), keys.back(),
|
||||
{{kDefaultWideColumnName, "inline"}, {"data", values.back()}}));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
ASSERT_OK(ReadOnlyReopen(options));
|
||||
|
||||
std::vector<Slice> key_slices(keys.begin(), keys.end());
|
||||
LazyWideColumnsBatch batch;
|
||||
std::vector<Status> get_statuses(kNumKeys);
|
||||
db_->MultiGetEntityLazy(ReadOptions(), db_->DefaultColumnFamily(), kNumKeys,
|
||||
key_slices.data(), &batch, get_statuses.data());
|
||||
std::vector<PinnableSlice> results(kNumKeys);
|
||||
std::vector<Status> statuses(kNumKeys);
|
||||
std::vector<LazyColumnReadRequest> reads(kNumKeys);
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
ASSERT_OK(get_statuses[i]);
|
||||
reads[i].column = &batch[i][1];
|
||||
reads[i].result = &results[i];
|
||||
reads[i].status = &statuses[i];
|
||||
}
|
||||
ASSERT_OK(batch.MultiResolve(reads.size(), reads.data()));
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
ASSERT_OK(statuses[i]);
|
||||
ASSERT_EQ(results[i], values[i]);
|
||||
}
|
||||
}
|
||||
|
||||
// Batched MultiGetEntityLazy works on a secondary instance (same requirement on
|
||||
// its GetImpl override as read-only).
|
||||
TEST_F(DBLazyEntityTest, SecondaryInstanceBatched) {
|
||||
Options options = GetLazyTestOptions();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
constexpr size_t kNumKeys = 3;
|
||||
std::vector<std::string> keys;
|
||||
std::vector<std::string> values;
|
||||
keys.reserve(kNumKeys);
|
||||
values.reserve(kNumKeys);
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
keys.push_back("k" + std::to_string(i));
|
||||
values.emplace_back(250 + i, static_cast<char>('b' + i));
|
||||
ASSERT_OK(db_->PutEntity(
|
||||
WriteOptions(), db_->DefaultColumnFamily(), keys.back(),
|
||||
{{kDefaultWideColumnName, "inline"}, {"data", values.back()}}));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
const std::string secondary_path = dbname_ + "_secondary";
|
||||
ASSERT_OK(DestroyDB(secondary_path, options));
|
||||
std::unique_ptr<DB> secondary;
|
||||
ASSERT_OK(DB::OpenAsSecondary(options, dbname_, secondary_path, &secondary));
|
||||
ASSERT_OK(secondary->TryCatchUpWithPrimary());
|
||||
|
||||
std::vector<Slice> key_slices(keys.begin(), keys.end());
|
||||
LazyWideColumnsBatch batch;
|
||||
std::vector<Status> get_statuses(kNumKeys);
|
||||
secondary->MultiGetEntityLazy(ReadOptions(), secondary->DefaultColumnFamily(),
|
||||
kNumKeys, key_slices.data(), &batch,
|
||||
get_statuses.data());
|
||||
std::vector<PinnableSlice> results(kNumKeys);
|
||||
std::vector<Status> statuses(kNumKeys);
|
||||
std::vector<LazyColumnReadRequest> reads(kNumKeys);
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
ASSERT_OK(get_statuses[i]);
|
||||
reads[i].column = &batch[i][1];
|
||||
reads[i].result = &results[i];
|
||||
reads[i].status = &statuses[i];
|
||||
}
|
||||
ASSERT_OK(batch.MultiResolve(reads.size(), reads.data()));
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
ASSERT_OK(statuses[i]);
|
||||
ASSERT_EQ(results[i], values[i]);
|
||||
}
|
||||
}
|
||||
// Verify that MultiGetEntityLazy sets IOOptions::io_activity to kMultiGetEntity
|
||||
// for its reads, so it matches the thread's operation type. This reproduces the
|
||||
// scenario from T283693234 where the thread operation was left stale at
|
||||
// OP_GETENTITY after a consistency check, causing a mismatch with the
|
||||
// kMultiGetEntity activity that MultiGetEntityLazy correctly propagates.
|
||||
TEST_F(DBLazyEntityTest, MultiGetEntityLazyIOActivity) {
|
||||
Options options = GetLazyTestOptions();
|
||||
|
||||
+379
-42
@@ -9,11 +9,15 @@
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "db/blob/blob_index.h"
|
||||
#include "db/blob/same_file_blob_reader.h"
|
||||
#include "db/version_set.h"
|
||||
#include "db/wide/blob_column_resolver_util.h"
|
||||
#include "db/wide/lazy_wide_columns_helper.h"
|
||||
#include "db/wide/read_path_blob_resolver.h"
|
||||
@@ -23,6 +27,7 @@
|
||||
#include "rocksdb/cleanable.h"
|
||||
#include "rocksdb/options.h"
|
||||
#include "rocksdb/wide_columns.h"
|
||||
#include "util/autovector.h"
|
||||
|
||||
// Current implementation of the lazy blob resolution API. Enumeration and
|
||||
// whole-column resolution are functional. Byte-range reads of an uncompressed
|
||||
@@ -30,9 +35,11 @@
|
||||
// in the SST -- are served by reading only the requested bytes from storage
|
||||
// (skipping checksum verification and cache-fill); other cases (compressed,
|
||||
// whole-column, already-cached, or force_verify) resolve the whole column and
|
||||
// slice it. Cross-key coalescing and async execution are future work; here
|
||||
// LazyWideColumnsBatch::MultiResolve simply routes each read to its owning
|
||||
// per-key result, and MultiGetEntityLazy fills the batch key-by-key.
|
||||
// slice it. LazyWideColumnsBatch::MultiResolve coalesces reads across keys:
|
||||
// classifies each read, then groups the storage reads per (Version, blob file)
|
||||
// for separate-file references and per SST for embedded references, issuing one
|
||||
// coalesced MultiRead per group (whole and byte-range). Async execution is
|
||||
// future work.
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
@@ -67,6 +74,24 @@ class LazyResolveThreadOpScope {
|
||||
LazyResolveThreadOpScope(const LazyResolveThreadOpScope&) = delete;
|
||||
LazyResolveThreadOpScope& operator=(const LazyResolveThreadOpScope&) = delete;
|
||||
};
|
||||
|
||||
// Identity of a physical blob record, shared across the batch: two reads that
|
||||
// resolve to the same on-disk record and blob-cache key (e.g. duplicate keys
|
||||
// in the batch, whose distinct entities reference the same record) map to the
|
||||
// same id. The batch planner uses this to keep every read of a force-verified
|
||||
// record on the individual path, so a non-verifying coalesced fetch cannot
|
||||
// populate the shared blob cache ahead of the force_verify read.
|
||||
using PhysicalBlobId = std::tuple<const void*, uint64_t, uint64_t>;
|
||||
PhysicalBlobId MakePhysicalBlobId(const ReadPathBlobResolver& resolver,
|
||||
const BlobIndex& blob_index) {
|
||||
if (blob_index.IsSameFile()) {
|
||||
// Embedded record: keyed by the owning SST reader + in-file offset.
|
||||
return PhysicalBlobId(resolver.same_file_reader(), 0, blob_index.offset());
|
||||
}
|
||||
// Separate blob file: keyed by Version + blob file number + offset.
|
||||
return PhysicalBlobId(resolver.version(), blob_index.file_number(),
|
||||
blob_index.offset());
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// Internal representation. Owns the serialized-entity backing buffer + inline
|
||||
@@ -176,9 +201,8 @@ class LazyWideColumns::Rep {
|
||||
const Status s = resolver_->ResolveColumnRange(
|
||||
index, read.offset, read.length, read.force_verify, read.result);
|
||||
|
||||
if (read.status) {
|
||||
*read.status = s;
|
||||
}
|
||||
assert(read.status); // callers reject a null status up front
|
||||
*read.status = s;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -201,11 +225,18 @@ const LazyWideColumn& LazyWideColumns::operator[](size_t i) const {
|
||||
Status LazyWideColumns::MultiResolve(size_t num_reads,
|
||||
LazyColumnReadRequest* reads) {
|
||||
LazyResolveThreadOpScope thread_op_scope;
|
||||
// Each read must supply a status out-param; a null one is a caller error with
|
||||
// nowhere to report that read's outcome, so fail the whole call (see
|
||||
// LazyColumnReadRequest::status).
|
||||
for (size_t i = 0; i < num_reads; ++i) {
|
||||
if (reads[i].status == nullptr) {
|
||||
return Status::InvalidArgument(
|
||||
"LazyColumnReadRequest::status must not be null");
|
||||
}
|
||||
}
|
||||
for (size_t i = 0; i < num_reads; ++i) {
|
||||
LazyColumnReadRequest& read = reads[i];
|
||||
if (read.status) {
|
||||
*read.status = Status::OK();
|
||||
}
|
||||
*read.status = Status::OK();
|
||||
if (read.result) {
|
||||
read.result->Reset(); // failure paths below leave an empty result
|
||||
}
|
||||
@@ -214,10 +245,8 @@ Status LazyWideColumns::MultiResolve(size_t num_reads,
|
||||
if (read.column == nullptr ||
|
||||
static_cast<const Rep::ColumnImpl*>(read.column)->parent_rep_ !=
|
||||
rep_.get()) {
|
||||
if (read.status) {
|
||||
*read.status = Status::InvalidArgument(
|
||||
"Column does not belong to this LazyWideColumns");
|
||||
}
|
||||
*read.status = Status::InvalidArgument(
|
||||
"Column does not belong to this LazyWideColumns");
|
||||
continue;
|
||||
}
|
||||
rep_->ResolveOneRead(read.column->index(), read);
|
||||
@@ -253,18 +282,45 @@ void LazyWideColumns::Reset() { rep_.reset(); }
|
||||
|
||||
// ---- LazyWideColumnsBatch ----
|
||||
|
||||
// Internal representation. Owns the per-key LazyWideColumns. (In the current
|
||||
// phase each entity holds its own SuperVersion pin; cf_pins is reserved for the
|
||||
// future shared-pin, cross-CF design and is currently unused.)
|
||||
// Internal representation. Owns the shared SuperVersion pin(s) and the per-key
|
||||
// LazyWideColumns. Each entity's resolver is bound to the batch's shared
|
||||
// Version but takes no per-entity pin; the batch holds one shared pin per
|
||||
// column family (`cf_pins`, a single entry for the common single-CF call),
|
||||
// transferred in by the batched MultiGetEntityLazy.
|
||||
class LazyWideColumnsBatch::Rep {
|
||||
public:
|
||||
// Shared SuperVersion pin per column family. Declared before `entities` so
|
||||
// the entities (whose resolvers reference the pinned Version) are destroyed
|
||||
// before these pins are released.
|
||||
std::map<uint32_t /* column_family_id */, Cleanable> cf_pins;
|
||||
|
||||
// One result per key of the MultiGetEntityLazy call, in key order.
|
||||
std::vector<LazyWideColumns> entities;
|
||||
|
||||
// TODO(lazy-blob-resolution-phase3): switch to one shared SuperVersion pin
|
||||
// per column family here (instead of one self-pin per entity), populated by a
|
||||
// batched MultiGetEntityLazy.
|
||||
std::map<uint32_t /* column_family_id */, Cleanable> cf_pins;
|
||||
Rep() = default;
|
||||
// Not copyable or movable: the entities' resolvers hold pointers back into
|
||||
// this Rep, and the custom destructor below has teardown-ordering semantics.
|
||||
Rep(const Rep&) = delete;
|
||||
Rep& operator=(const Rep&) = delete;
|
||||
Rep(Rep&&) = delete;
|
||||
Rep& operator=(Rep&&) = delete;
|
||||
|
||||
~Rep() {
|
||||
// Releasing a shared SuperVersion pin can trigger obsolete-file cleanup I/O
|
||||
// (e.g. FindObsoleteFiles) when the batch held the last reference. Mirror
|
||||
// LazyWideColumns::Rep::~Rep / DBIter::~DBIter and run teardown with the
|
||||
// thread operation reset to OP_UNKNOWN so that incidental I/O is not
|
||||
// misattributed to whatever read op is active on the destroying thread.
|
||||
// Destroy the entities (their resolvers reference the pinned Version)
|
||||
// before releasing the pins.
|
||||
const ThreadStatus::OperationType saved_op =
|
||||
ThreadStatusUtil::GetThreadOperation();
|
||||
ThreadStatusUtil::SetThreadOperation(
|
||||
ThreadStatus::OperationType::OP_UNKNOWN);
|
||||
entities.clear();
|
||||
cf_pins.clear();
|
||||
ThreadStatusUtil::SetThreadOperation(saved_op);
|
||||
}
|
||||
};
|
||||
|
||||
LazyWideColumnsBatch::LazyWideColumnsBatch() = default;
|
||||
@@ -293,19 +349,110 @@ LazyWideColumns& LazyWideColumnsBatch::operator[](size_t i) {
|
||||
Status LazyWideColumnsBatch::MultiResolve(size_t num_reads,
|
||||
LazyColumnReadRequest* reads) {
|
||||
LazyResolveThreadOpScope thread_op_scope;
|
||||
// Each read must supply a status out-param; a null one is a caller error with
|
||||
// nowhere to report that read's outcome, so fail the whole call (see
|
||||
// LazyColumnReadRequest::status).
|
||||
for (size_t i = 0; i < num_reads; ++i) {
|
||||
if (reads[i].status == nullptr) {
|
||||
return Status::InvalidArgument(
|
||||
"LazyColumnReadRequest::status must not be null");
|
||||
}
|
||||
}
|
||||
const void* this_rep = rep_.get();
|
||||
|
||||
using EntityRep = LazyWideColumns::Rep;
|
||||
using Plan = ReadPathBlobResolver::LazyColumnReadPlan;
|
||||
|
||||
// A whole-value fetch, deduplicated per owning (entity, column): fetched into
|
||||
// `temp`, then adopted into the entity's resolver cache and sliced per read.
|
||||
struct WholeFetch {
|
||||
EntityRep* entity_rep = nullptr;
|
||||
size_t column_index = 0;
|
||||
bool same_file = false;
|
||||
const BlobIndex* blob_index = nullptr;
|
||||
PinnableSlice temp;
|
||||
Status status;
|
||||
};
|
||||
// A sub-range fetch: read straight into the user's output slice/status.
|
||||
struct RangeFetch {
|
||||
EntityRep* entity_rep = nullptr;
|
||||
bool same_file = false;
|
||||
const BlobIndex* blob_index = nullptr;
|
||||
uint64_t range_offset = 0;
|
||||
size_t range_length = 0;
|
||||
PinnableSlice* result = nullptr;
|
||||
Status* status = nullptr; // the caller's status (never null; validated)
|
||||
};
|
||||
// Binds an original whole read to the whole fetch that satisfies it.
|
||||
struct WholeBinding {
|
||||
size_t fetch_idx = 0;
|
||||
LazyColumnReadRequest* read = nullptr;
|
||||
};
|
||||
|
||||
std::vector<WholeFetch> whole_fetches;
|
||||
std::vector<RangeFetch> range_fetches;
|
||||
std::vector<WholeBinding> whole_bindings;
|
||||
std::vector<LazyColumnReadRequest*> serve_individually;
|
||||
std::map<std::pair<EntityRep*, size_t>, size_t> whole_index;
|
||||
// Each read contributes at most one entry to one of these plans; reserve up
|
||||
// front to avoid repeated reallocation on the batch path.
|
||||
whole_fetches.reserve(num_reads);
|
||||
range_fetches.reserve(num_reads);
|
||||
whole_bindings.reserve(num_reads);
|
||||
serve_individually.reserve(num_reads);
|
||||
|
||||
// Any column with a force_verify read must be resolved entirely on the
|
||||
// individual (sequential) path, never coalesced: otherwise a non-verified
|
||||
// coalesced whole fetch for that column could adopt its bytes into the
|
||||
// resolver cache before the force_verify read runs, so that read would hit
|
||||
// the cache and skip checksum verification. Collect those columns first (a
|
||||
// read requesting verification may follow a non-verifying read of the same
|
||||
// column in the batch) so classification below can route all their reads to
|
||||
// the individual path, preserving the "verify on the first actual read"
|
||||
// semantics of resolving key-by-key.
|
||||
std::set<std::pair<EntityRep*, size_t>> force_verify_columns;
|
||||
// Physical blob records force-verified anywhere in this batch. Complements
|
||||
// force_verify_columns to also cover reads from a *different* entity that
|
||||
// resolve to the same on-disk record and shared blob-cache key -- e.g. a
|
||||
// duplicate key in the batch. Without this, such a read's non-verifying
|
||||
// coalesced whole fetch could populate the shared blob cache before the
|
||||
// force_verify read runs, and that read would then hit the cache and skip
|
||||
// checksum verification.
|
||||
std::set<PhysicalBlobId> force_verify_blobs;
|
||||
for (size_t i = 0; i < num_reads; ++i) {
|
||||
const LazyColumnReadRequest& read = reads[i];
|
||||
if (read.force_verify && read.column != nullptr) {
|
||||
EntityRep* entity_rep =
|
||||
static_cast<const EntityRep::ColumnImpl*>(read.column)->parent_rep_;
|
||||
if (entity_rep != nullptr) {
|
||||
const size_t column_index = read.column->index();
|
||||
force_verify_columns.emplace(entity_rep, column_index);
|
||||
if (entity_rep->resolver_) {
|
||||
const BlobIndex* blob_index = blob_resolver_util::FindBlobColumn(
|
||||
&entity_rep->blob_columns_, column_index);
|
||||
if (blob_index != nullptr && !blob_index->IsInlined()) {
|
||||
force_verify_blobs.insert(
|
||||
MakePhysicalBlobId(*entity_rep->resolver_, *blob_index));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The (single-CF, shared) lazy ReadOptions used for every coalesced dispatch;
|
||||
// captured from the first classified entity. A future cross-CF batch would
|
||||
// need per-group ReadOptions instead.
|
||||
const ReadOptions* resolve_ro = nullptr;
|
||||
|
||||
// Pass 1: reset outputs, validate ownership, classify.
|
||||
for (size_t i = 0; i < num_reads; ++i) {
|
||||
LazyColumnReadRequest& read = reads[i];
|
||||
if (read.status) {
|
||||
*read.status = Status::OK();
|
||||
}
|
||||
*read.status = Status::OK();
|
||||
if (read.result) {
|
||||
read.result->Reset(); // failure paths below leave an empty result
|
||||
}
|
||||
if (read.column == nullptr) {
|
||||
if (read.status) {
|
||||
*read.status = Status::InvalidArgument("Null column in batch read");
|
||||
}
|
||||
*read.status = Status::InvalidArgument("Null column in batch read");
|
||||
continue;
|
||||
}
|
||||
// Route the read to the entity that owns its column, and require that
|
||||
@@ -314,21 +461,189 @@ Status LazyWideColumnsBatch::MultiResolve(size_t num_reads,
|
||||
// owning_batch_rep_ == nullptr, and an empty/default-constructed batch has
|
||||
// this_rep == nullptr, so without it a foreign standalone column would slip
|
||||
// through the nullptr == nullptr comparison instead of being rejected.
|
||||
// TODO(lazy-blob-resolution-phase3): group reads per (CF, Version, blob
|
||||
// file) across entities and coalesce them; the request shape is unchanged.
|
||||
LazyWideColumns::Rep* entity_rep =
|
||||
static_cast<const LazyWideColumns::Rep::ColumnImpl*>(read.column)
|
||||
->parent_rep_;
|
||||
EntityRep* entity_rep =
|
||||
static_cast<const EntityRep::ColumnImpl*>(read.column)->parent_rep_;
|
||||
if (entity_rep == nullptr || entity_rep->owning_batch_rep_ == nullptr ||
|
||||
entity_rep->owning_batch_rep_ != this_rep) {
|
||||
if (read.status) {
|
||||
*read.status =
|
||||
Status::InvalidArgument("Column does not belong to this batch");
|
||||
}
|
||||
*read.status =
|
||||
Status::InvalidArgument("Column does not belong to this batch");
|
||||
continue;
|
||||
}
|
||||
entity_rep->ResolveOneRead(read.column->index(), read);
|
||||
// A read with no output buffer only wants to surface an I/O/integrity
|
||||
// error; resolve it individually (ResolveColumnRange handles the
|
||||
// null-result case), and defensively serve entities with no resolver
|
||||
// individually too.
|
||||
if (read.result == nullptr || !entity_rep->resolver_) {
|
||||
serve_individually.push_back(&read);
|
||||
continue;
|
||||
}
|
||||
|
||||
const size_t column_index = read.column->index();
|
||||
// A column with any force_verify read in this batch is resolved entirely on
|
||||
// the individual path (see force_verify_columns above) so verification is
|
||||
// never skipped by a coalesced adoption.
|
||||
if (force_verify_columns.count({entity_rep, column_index}) != 0) {
|
||||
serve_individually.push_back(&read);
|
||||
continue;
|
||||
}
|
||||
const ReadPathBlobResolver::LazyColumnReadClassification cls =
|
||||
entity_rep->resolver_->ClassifyColumnRange(
|
||||
column_index, read.offset, read.length, read.force_verify);
|
||||
// If some force_verify read in this batch targets the same physical blob
|
||||
// (e.g. this read is for a duplicate key referencing that record), keep
|
||||
// this read on the individual path too, so its non-verifying coalesced
|
||||
// fetch cannot fill the shared blob cache ahead of the force_verify read
|
||||
// (which would then hit the cache and skip verification).
|
||||
if (cls.blob_index != nullptr &&
|
||||
force_verify_blobs.count(
|
||||
MakePhysicalBlobId(*entity_rep->resolver_, *cls.blob_index)) != 0) {
|
||||
serve_individually.push_back(&read);
|
||||
continue;
|
||||
}
|
||||
if (resolve_ro == nullptr) {
|
||||
resolve_ro = &entity_rep->resolver_->read_options();
|
||||
}
|
||||
switch (cls.plan) {
|
||||
case Plan::kServeIndividually:
|
||||
serve_individually.push_back(&read);
|
||||
break;
|
||||
case Plan::kFetchWholeSeparateFile:
|
||||
case Plan::kFetchWholeSameFile: {
|
||||
const std::pair<EntityRep*, size_t> key{entity_rep, column_index};
|
||||
auto it = whole_index.find(key);
|
||||
size_t idx;
|
||||
if (it == whole_index.end()) {
|
||||
idx = whole_fetches.size();
|
||||
whole_index.emplace(key, idx);
|
||||
whole_fetches.emplace_back();
|
||||
WholeFetch& wf = whole_fetches.back();
|
||||
wf.entity_rep = entity_rep;
|
||||
wf.column_index = column_index;
|
||||
wf.same_file = (cls.plan == Plan::kFetchWholeSameFile);
|
||||
wf.blob_index = cls.blob_index;
|
||||
} else {
|
||||
idx = it->second;
|
||||
}
|
||||
whole_bindings.push_back(WholeBinding{idx, &read});
|
||||
break;
|
||||
}
|
||||
case Plan::kFetchRangeSeparateFile:
|
||||
case Plan::kFetchRangeSameFile: {
|
||||
RangeFetch rf;
|
||||
rf.entity_rep = entity_rep;
|
||||
rf.same_file = (cls.plan == Plan::kFetchRangeSameFile);
|
||||
rf.blob_index = cls.blob_index;
|
||||
rf.range_offset = cls.range_offset;
|
||||
rf.range_length = cls.range_length;
|
||||
rf.result = read.result;
|
||||
rf.status = read.status;
|
||||
range_fetches.push_back(std::move(rf));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 2: build one dispatch list per group and issue a coalesced read.
|
||||
// Separate-file reads (whole + range) go through Version::MultiGetBlobLazy
|
||||
// (grouped by Version, coalesced per blob file inside); same-file reads
|
||||
// through SameFileBlobReader::MultiGetSameFileBlob (grouped by SST, coalesced
|
||||
// there).
|
||||
if (resolve_ro != nullptr) {
|
||||
std::map<const Version*, autovector<Version::LazyBlobReadRequest>>
|
||||
separate_groups;
|
||||
std::map<const SameFileBlobReader*, std::vector<SameFileBlobReadRequest>>
|
||||
same_groups;
|
||||
// Whole reads are never force-verify here (those are served individually),
|
||||
// so the verify policy is just today's global verify_checksums.
|
||||
const BlobVerifyPolicy whole_policy =
|
||||
resolve_ro->verify_checksums
|
||||
? BlobVerifyPolicy::kVerifyIfNoAmplification
|
||||
: BlobVerifyPolicy::kSkip;
|
||||
|
||||
for (WholeFetch& wf : whole_fetches) {
|
||||
ReadPathBlobResolver& resolver = *wf.entity_rep->resolver_;
|
||||
if (wf.same_file) {
|
||||
SameFileBlobReadRequest req;
|
||||
req.blob_index = wf.blob_index;
|
||||
req.range_offset = 0;
|
||||
req.range_length = kWholeBlobLength;
|
||||
req.verify_policy = whole_policy;
|
||||
req.result = &wf.temp;
|
||||
req.status = &wf.status;
|
||||
same_groups[resolver.same_file_reader()].push_back(req);
|
||||
} else {
|
||||
Version::LazyBlobReadRequest req;
|
||||
req.user_key = &resolver.user_key();
|
||||
req.blob_index = wf.blob_index;
|
||||
req.range_offset = 0;
|
||||
req.range_length = kWholeBlobLength;
|
||||
req.result = &wf.temp;
|
||||
req.status = &wf.status;
|
||||
separate_groups[resolver.version()].emplace_back(req);
|
||||
}
|
||||
}
|
||||
for (RangeFetch& rf : range_fetches) {
|
||||
ReadPathBlobResolver& resolver = *rf.entity_rep->resolver_;
|
||||
if (rf.same_file) {
|
||||
SameFileBlobReadRequest req;
|
||||
req.blob_index = rf.blob_index;
|
||||
req.range_offset = rf.range_offset;
|
||||
req.range_length = rf.range_length;
|
||||
req.verify_policy =
|
||||
whole_policy; // range never verifies; policy unused
|
||||
req.result = rf.result;
|
||||
req.status = rf.status;
|
||||
same_groups[resolver.same_file_reader()].push_back(req);
|
||||
} else {
|
||||
Version::LazyBlobReadRequest req;
|
||||
req.user_key = &resolver.user_key();
|
||||
req.blob_index = rf.blob_index;
|
||||
req.range_offset = rf.range_offset;
|
||||
req.range_length = rf.range_length;
|
||||
req.result = rf.result;
|
||||
req.status = rf.status;
|
||||
separate_groups[resolver.version()].emplace_back(req);
|
||||
}
|
||||
}
|
||||
|
||||
for (auto& [version, reqs] : separate_groups) {
|
||||
version->MultiGetBlobLazy(*resolve_ro, reqs);
|
||||
}
|
||||
for (auto& [reader, reqs] : same_groups) {
|
||||
reader->MultiGetSameFileBlob(*resolve_ro, reqs.size(), reqs.data());
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 3: adopt the fetched whole values into their resolvers' caches so the
|
||||
// slice below (and any later read of the same column) does no further I/O.
|
||||
for (WholeFetch& wf : whole_fetches) {
|
||||
if (wf.status.ok()) {
|
||||
wf.entity_rep->resolver_->AdoptResolvedWholeColumn(wf.column_index,
|
||||
std::move(wf.temp));
|
||||
}
|
||||
}
|
||||
|
||||
// Pass 4: finalize each whole read by slicing the requested range out of the
|
||||
// (now cached) whole value; a failed fetch leaves an empty result.
|
||||
for (WholeBinding& b : whole_bindings) {
|
||||
WholeFetch& wf = whole_fetches[b.fetch_idx];
|
||||
if (!wf.status.ok()) {
|
||||
*b.read->status = wf.status;
|
||||
continue;
|
||||
}
|
||||
const Status s = wf.entity_rep->resolver_->ResolveColumnRange(
|
||||
b.read->column->index(), b.read->offset, b.read->length,
|
||||
b.read->force_verify, b.read->result);
|
||||
*b.read->status = s;
|
||||
}
|
||||
|
||||
// Pass 5: serve the individual (non-coalesced) reads.
|
||||
for (LazyColumnReadRequest* read : serve_individually) {
|
||||
EntityRep* entity_rep =
|
||||
static_cast<const EntityRep::ColumnImpl*>(read->column)->parent_rep_;
|
||||
entity_rep->ResolveOneRead(read->column->index(), *read);
|
||||
}
|
||||
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
@@ -352,6 +667,21 @@ Status LazyWideColumnsHelper::Finalize(
|
||||
Cleanable&& pin) {
|
||||
assert(result);
|
||||
assert(result->rep_);
|
||||
// Take ownership of the per-result SuperVersion pin, then set up the resolver
|
||||
// exactly as the batched path does.
|
||||
result->rep_->pin_ = std::move(pin);
|
||||
return FinalizeInBatch(result, user_key, version, read_options,
|
||||
blob_file_cache, allow_write_path_fallback,
|
||||
same_file_reader);
|
||||
}
|
||||
|
||||
Status LazyWideColumnsHelper::FinalizeInBatch(
|
||||
LazyWideColumns* result, const Slice& user_key, const Version* version,
|
||||
const ReadOptions& read_options, BlobFileCache* blob_file_cache,
|
||||
bool allow_write_path_fallback,
|
||||
const SameFileBlobReader* same_file_reader) {
|
||||
assert(result);
|
||||
assert(result->rep_);
|
||||
LazyWideColumns::Rep& rep = *result->rep_;
|
||||
|
||||
// Decode the blob references (if the entity still has unresolved ones -- it
|
||||
@@ -396,12 +726,12 @@ Status LazyWideColumnsHelper::Finalize(
|
||||
rep.columns_.emplace_back(&rep, i, columns[i].name(), inline_data, size);
|
||||
}
|
||||
|
||||
// Take ownership of the SuperVersion pin and stand up the resolver bound to
|
||||
// the (address-stable, since Rep is heap-allocated) entity columns + blob
|
||||
// references. The resolver's deferred blob-byte reads are attributed to
|
||||
// Env::IOActivity::kLazyResolve (distinct from the kGetEntity/kMultiGetEntity
|
||||
// of the initial entity read that already completed via GetImpl).
|
||||
rep.pin_ = std::move(pin);
|
||||
// Stand up the resolver bound to the (address-stable, since Rep is
|
||||
// heap-allocated) entity columns + blob references. The resolver's deferred
|
||||
// blob-byte reads are attributed to Env::IOActivity::kLazyResolve (distinct
|
||||
// from the kGetEntity/kMultiGetEntity of the initial entity read that already
|
||||
// completed via GetImpl). A standalone result took ownership of its pin in
|
||||
// Finalize; a batched entity relies on its enclosing batch's shared pin.
|
||||
rep.user_key_.assign(user_key.data(), user_key.size());
|
||||
ReadOptions resolve_read_options(read_options);
|
||||
resolve_read_options.io_activity = Env::IOActivity::kLazyResolve;
|
||||
@@ -422,6 +752,13 @@ void LazyWideColumnsHelper::InitBatch(LazyWideColumnsBatch* batch,
|
||||
batch->rep_->entities.resize(num_entities);
|
||||
}
|
||||
|
||||
Cleanable* LazyWideColumnsHelper::BatchCfPin(LazyWideColumnsBatch* batch,
|
||||
uint32_t cf_id) {
|
||||
assert(batch);
|
||||
assert(batch->rep_);
|
||||
return &batch->rep_->cf_pins[cf_id];
|
||||
}
|
||||
|
||||
void LazyWideColumnsHelper::FinalizeBatch(LazyWideColumnsBatch* batch) {
|
||||
assert(batch);
|
||||
if (!batch->rep_) {
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#include "rocksdb/cleanable.h"
|
||||
#include "rocksdb/lazy_wide_columns.h"
|
||||
@@ -46,11 +47,29 @@ class LazyWideColumnsHelper {
|
||||
const SameFileBlobReader* same_file_reader,
|
||||
Cleanable&& pin);
|
||||
|
||||
// Batched-read counterpart of Finalize: sets up the entity's on-demand
|
||||
// resolver bound to `version` WITHOUT taking a per-entity SuperVersion pin --
|
||||
// the enclosing LazyWideColumnsBatch owns one shared pin per column family
|
||||
// (see BatchCfPin). Otherwise identical to Finalize.
|
||||
static Status FinalizeInBatch(LazyWideColumns* result, const Slice& user_key,
|
||||
const Version* version,
|
||||
const ReadOptions& read_options,
|
||||
BlobFileCache* blob_file_cache,
|
||||
bool allow_write_path_fallback,
|
||||
const SameFileBlobReader* same_file_reader);
|
||||
|
||||
// Prepares `batch` to hold `num_entities` empty per-key results (creating the
|
||||
// batch's representation). Use (*batch)[i] to access each for filling, then
|
||||
// call FinalizeBatch() once all entities are populated.
|
||||
static void InitBatch(LazyWideColumnsBatch* batch, size_t num_entities);
|
||||
|
||||
// Returns the Cleanable in `batch` that owns the shared SuperVersion pin for
|
||||
// column family `cf_id` (creating the entry). The batched read transfers a
|
||||
// SuperVersion reference into it (DBImpl::TransferSuperVersionPin) so the
|
||||
// batch keeps that CF's Version -- and the entities' deferred blob reads --
|
||||
// valid for the batch's lifetime.
|
||||
static Cleanable* BatchCfPin(LazyWideColumnsBatch* batch, uint32_t cf_id);
|
||||
|
||||
// Links every populated entity of `batch` back to the batch, so a batch read
|
||||
// can validate that a column belongs to this batch. Call after all entities
|
||||
// have been filled (and individually Finalize()d).
|
||||
|
||||
@@ -223,6 +223,84 @@ Status ReadPathBlobResolver::ResolveColumnRange(size_t column_index,
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
ReadPathBlobResolver::LazyColumnReadClassification
|
||||
ReadPathBlobResolver::ClassifyColumnRange(size_t column_index,
|
||||
uint64_t range_offset,
|
||||
size_t range_length,
|
||||
bool force_verify) const {
|
||||
LazyColumnReadClassification c;
|
||||
|
||||
if (columns_ == nullptr || column_index >= columns_->size()) {
|
||||
return c; // kServeIndividually (ResolveColumnRange -> InvalidArgument)
|
||||
}
|
||||
const BlobIndex* blob_index_ptr =
|
||||
blob_resolver_util::FindBlobColumn(blob_columns_, column_index);
|
||||
if (blob_index_ptr == nullptr) {
|
||||
return c; // inline column
|
||||
}
|
||||
if (blob_resolver_util::FindInCache(resolved_cache_, column_index) !=
|
||||
nullptr) {
|
||||
return c; // already resolved -> slice from cache
|
||||
}
|
||||
const BlobIndex& blob_index = *blob_index_ptr;
|
||||
if (blob_index.IsInlined()) {
|
||||
return c; // TTL-inlined -> ResolveColumnRange caches inlined + slices
|
||||
}
|
||||
const BlobVerifyPolicy policy = DeriveVerifyPolicy(
|
||||
blob_fetcher_.read_options().verify_checksums, force_verify);
|
||||
if (policy == BlobVerifyPolicy::kVerifyIfPresent) {
|
||||
// Force-verify: served individually to avoid a mixed-verify coalesced read.
|
||||
return c;
|
||||
}
|
||||
|
||||
const bool same_file = blob_index.IsSameFile();
|
||||
// A read is coalesceable only if it is servable the same way
|
||||
// ResolveColumnRange would serve it without falling back to a whole
|
||||
// individual read: same-file needs the SST reader; separate-file needs a
|
||||
// Version-backed range-capable fetcher (SupportsRangeRead() also excludes
|
||||
// direct-write fallback mode, whose reads are not manifest-visible and cannot
|
||||
// go through Version::MultiGetBlobLazy).
|
||||
const bool servable = same_file ? (same_file_reader_ != nullptr)
|
||||
: blob_fetcher_.SupportsRangeRead();
|
||||
if (!servable) {
|
||||
return c; // kServeIndividually
|
||||
}
|
||||
|
||||
if (blob_index.compression() == kNoCompression) {
|
||||
const uint64_t value_size = blob_index.size();
|
||||
const bool strict_subrange = range_offset > 0 || range_length < value_size;
|
||||
if (strict_subrange) {
|
||||
if (range_offset >= value_size || range_length == 0) {
|
||||
// Empty range -> ResolveColumnRange returns empty with no I/O.
|
||||
return c; // kServeIndividually
|
||||
}
|
||||
const size_t avail = static_cast<size_t>(value_size - range_offset);
|
||||
c.blob_index = &blob_index;
|
||||
c.range_offset = range_offset;
|
||||
c.range_length = range_length > avail ? avail : range_length;
|
||||
c.plan = same_file ? LazyColumnReadPlan::kFetchRangeSameFile
|
||||
: LazyColumnReadPlan::kFetchRangeSeparateFile;
|
||||
return c;
|
||||
}
|
||||
}
|
||||
|
||||
// Whole fetch (whole-column read, or a compressed reference resolved whole).
|
||||
c.blob_index = &blob_index;
|
||||
c.plan = same_file ? LazyColumnReadPlan::kFetchWholeSameFile
|
||||
: LazyColumnReadPlan::kFetchWholeSeparateFile;
|
||||
return c;
|
||||
}
|
||||
|
||||
void ReadPathBlobResolver::AdoptResolvedWholeColumn(size_t column_index,
|
||||
PinnableSlice&& value) {
|
||||
if (blob_resolver_util::FindInCache(resolved_cache_, column_index) !=
|
||||
nullptr) {
|
||||
return; // already resolved; keep the existing entry (idempotent)
|
||||
}
|
||||
resolved_cache_.emplace_back(
|
||||
column_index, std::make_unique<PinnableSlice>(std::move(value)));
|
||||
}
|
||||
|
||||
Status ReadPathBlobResolver::ResolveColumns(
|
||||
const std::vector<size_t>& column_indices,
|
||||
std::vector<Slice>* resolved_values) {
|
||||
|
||||
@@ -96,6 +96,61 @@ class ReadPathBlobResolver {
|
||||
size_t range_length, bool force_verify,
|
||||
PinnableSlice* result);
|
||||
|
||||
// How the cross-key batch coalescing path
|
||||
// (LazyWideColumnsBatch::MultiResolve) should resolve a byte-range read of
|
||||
// one column.
|
||||
enum class LazyColumnReadPlan {
|
||||
// Resolve individually via ResolveColumnRange (inline column, already
|
||||
// resolved, TTL-inlined blob, empty range, force-verify, or an unservable
|
||||
// reference); no cross-key coalescing benefit.
|
||||
kServeIndividually,
|
||||
// Fetch the whole blob value, then cache + slice; coalesce per blob file.
|
||||
kFetchWholeSeparateFile,
|
||||
// Fetch the whole embedded value, then cache + slice; coalesce per SST.
|
||||
kFetchWholeSameFile,
|
||||
// Fetch only the (uncompressed) sub-range; coalesce per blob file.
|
||||
kFetchRangeSeparateFile,
|
||||
// Fetch only the (uncompressed) embedded sub-range; coalesce per SST.
|
||||
kFetchRangeSameFile,
|
||||
};
|
||||
|
||||
struct LazyColumnReadClassification {
|
||||
LazyColumnReadPlan plan = LazyColumnReadPlan::kServeIndividually;
|
||||
// For fetch plans: the reference to read (owned by blob_columns_).
|
||||
const BlobIndex* blob_index = nullptr;
|
||||
// For range-fetch plans: the effective (clamped) sub-range to read.
|
||||
uint64_t range_offset = 0;
|
||||
size_t range_length = 0;
|
||||
};
|
||||
|
||||
// Classifies how a byte-range read of `column_index` should be resolved by
|
||||
// the batch coalescing path. The decision mirrors ResolveColumnRange exactly,
|
||||
// so that pre-fetching the whole/range values (per the returned plan) and
|
||||
// then calling ResolveColumnRange for the whole reads yields the same bytes
|
||||
// with no redundant I/O. Out-of-bounds / non-blob / cached / inlined /
|
||||
// empty-range / force-verify / unservable reads return kServeIndividually.
|
||||
LazyColumnReadClassification ClassifyColumnRange(size_t column_index,
|
||||
uint64_t range_offset,
|
||||
size_t range_length,
|
||||
bool force_verify) const;
|
||||
|
||||
// Inserts an externally fetched whole column value into the resolver's cache
|
||||
// (idempotent: if the column is already cached the passed value is dropped
|
||||
// and the existing entry kept). Used by the batch coalescing path, which
|
||||
// fetches whole blob values in bulk; a subsequent ResolveColumnRange of that
|
||||
// column then slices from the cache with no I/O.
|
||||
void AdoptResolvedWholeColumn(size_t column_index, PinnableSlice&& value);
|
||||
|
||||
// Accessors used by the batch coalescing path to group and dispatch reads.
|
||||
const Version* version() const { return blob_fetcher_.version(); }
|
||||
const SameFileBlobReader* same_file_reader() const {
|
||||
return same_file_reader_;
|
||||
}
|
||||
const ReadOptions& read_options() const {
|
||||
return blob_fetcher_.read_options();
|
||||
}
|
||||
const Slice& user_key() const { return user_key_; }
|
||||
|
||||
// Resolve multiple columns in the order provided by `column_indices`.
|
||||
// Resolved blob values are cached exactly as if ResolveColumn() were called
|
||||
// repeatedly.
|
||||
|
||||
@@ -163,7 +163,20 @@ class BatchedOpsStressTest : public StressTest {
|
||||
for (int i = 0; i < 10; i++) {
|
||||
keys[i] += key.ToString();
|
||||
key_slices[i] = keys[i];
|
||||
s = DbStressGet(db_, readoptionscopy, cfh, key_slices[i], &from_db);
|
||||
if (i == 0) {
|
||||
OutputMetadata output_metadata;
|
||||
output_metadata.WantNewerVersionPresent();
|
||||
s = db_->GetWithMetadata(readoptionscopy, cfh, key_slices[i], &from_db,
|
||||
&output_metadata);
|
||||
if (s.IsNotSupported()) {
|
||||
// Transaction DBs that require custom visibility callbacks cannot
|
||||
// provide newer-version metadata. Preserve the ordinary batched
|
||||
// stress coverage for those configurations.
|
||||
s = DbStressGet(db_, readoptionscopy, cfh, key_slices[i], &from_db);
|
||||
}
|
||||
} else {
|
||||
s = DbStressGet(db_, readoptionscopy, cfh, key_slices[i], &from_db);
|
||||
}
|
||||
if (!s.ok() && !s.IsNotFound()) {
|
||||
fprintf(stderr, "get error: %s\n", s.ToString().c_str());
|
||||
values[i] = "";
|
||||
@@ -235,8 +248,20 @@ class BatchedOpsStressTest : public StressTest {
|
||||
key_str.emplace_back(keys[key] + Key(rand_keys[rand_key]));
|
||||
key_slices.emplace_back(key_str.back());
|
||||
}
|
||||
DbStressMultiGet(db_, readoptionscopy, cfh, num_prefixes,
|
||||
key_slices.data(), values.data(), statuses.data());
|
||||
MultiGetOutputMetadata output_metadata;
|
||||
output_metadata.WantNewerVersionPresent();
|
||||
db_->MultiGetWithMetadata(readoptionscopy, cfh, num_prefixes,
|
||||
key_slices.data(), values.data(),
|
||||
statuses.data(), &output_metadata);
|
||||
const bool metadata_not_supported = std::all_of(
|
||||
statuses.begin(), statuses.end(),
|
||||
[](const Status& status) { return status.IsNotSupported(); });
|
||||
if (metadata_not_supported) {
|
||||
// Preserve compatibility with transaction DBs whose custom visibility
|
||||
// callbacks make newer-version tracking unsupported.
|
||||
DbStressMultiGet(db_, readoptionscopy, cfh, num_prefixes,
|
||||
key_slices.data(), values.data(), statuses.data());
|
||||
}
|
||||
for (size_t i = 0; i < num_prefixes; i++) {
|
||||
Status s = statuses[i];
|
||||
if (!s.ok() && !s.IsNotFound()) {
|
||||
|
||||
@@ -1204,17 +1204,6 @@ class CfConsistencyStressTest : public StressTest {
|
||||
SharedState* shared = thread->shared;
|
||||
assert(shared);
|
||||
|
||||
if (secondary_db_) {
|
||||
status = secondary_db_->TryCatchUpWithPrimary();
|
||||
if (!status.ok()) {
|
||||
fprintf(stderr, "TryCatchUpWithPrimary: %s\n",
|
||||
status.ToString().c_str());
|
||||
shared->SetShouldStopTest();
|
||||
assert(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const auto checksum_column_family = [](Iterator* iter,
|
||||
uint32_t* checksum) -> Status {
|
||||
assert(nullptr != checksum);
|
||||
@@ -1238,46 +1227,78 @@ class CfConsistencyStressTest : public StressTest {
|
||||
// `FLAGS_rate_limit_user_ops` to avoid slowing any validation.
|
||||
ReadOptions ropts(FLAGS_verify_checksum, true);
|
||||
ropts.total_order_seek = true;
|
||||
if (nullptr == secondary_db_ || FLAGS_auto_refresh_iterator_with_snapshot) {
|
||||
if (nullptr == secondary_db_) {
|
||||
ropts.snapshot = snapshot_guard.snapshot();
|
||||
ropts.auto_refresh_iterator_with_snapshot = true;
|
||||
}
|
||||
uint32_t crc = 0;
|
||||
{
|
||||
// Compute crc for all key-values of default column family.
|
||||
std::unique_ptr<Iterator> it(db_ptr->NewIterator(ropts));
|
||||
status = checksum_column_family(it.get(), &crc);
|
||||
if (!status.ok()) {
|
||||
fprintf(stderr, "Computing checksum of default cf: %s\n",
|
||||
status.ToString().c_str());
|
||||
assert(false);
|
||||
}
|
||||
|
||||
std::vector<Iterator*> raw_iterators;
|
||||
Status catch_up_status;
|
||||
if (secondary_db_) {
|
||||
// The separate thread intentionally overlaps catch-up with
|
||||
// NewIterators(). Fault injection is disabled separately because its
|
||||
// state is thread-local.
|
||||
port::Thread catch_up_thread([&]() {
|
||||
if (db_fault_injection_fs_) {
|
||||
db_fault_injection_fs_->DisableAllThreadLocalErrorInjection();
|
||||
}
|
||||
catch_up_status = secondary_db_->TryCatchUpWithPrimary();
|
||||
if (db_fault_injection_fs_) {
|
||||
db_fault_injection_fs_->EnableAllThreadLocalErrorInjection();
|
||||
}
|
||||
});
|
||||
status = db_ptr->NewIterators(ropts, cfhs, &raw_iterators);
|
||||
catch_up_thread.join();
|
||||
} else {
|
||||
status = db_ptr->NewIterators(ropts, cfhs, &raw_iterators);
|
||||
}
|
||||
// Since we currently intentionally disallow reading from the secondary
|
||||
// instance with snapshot, we cannot achieve cross-cf consistency if WAL is
|
||||
// enabled because there is no guarantee that secondary instance replays
|
||||
// the primary's WAL to a consistent point where all cfs have the same
|
||||
// data.
|
||||
if (status.ok() && FLAGS_disable_wal) {
|
||||
uint32_t tmp_crc = 0;
|
||||
for (ColumnFamilyHandle* cfh : cfhs) {
|
||||
if (cfh == db_ptr->DefaultColumnFamily()) {
|
||||
continue;
|
||||
}
|
||||
std::unique_ptr<Iterator> it(db_ptr->NewIterator(ropts, cfh));
|
||||
status = checksum_column_family(it.get(), &tmp_crc);
|
||||
if (!status.ok() || tmp_crc != crc) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::unique_ptr<Iterator>> iterators;
|
||||
iterators.reserve(raw_iterators.size());
|
||||
for (Iterator* iterator : raw_iterators) {
|
||||
iterators.emplace_back(iterator);
|
||||
}
|
||||
|
||||
if (secondary_db_ && !catch_up_status.ok()) {
|
||||
fprintf(stderr, "TryCatchUpWithPrimary: %s\n",
|
||||
catch_up_status.ToString().c_str());
|
||||
shared->SetShouldStopTest();
|
||||
assert(false);
|
||||
return;
|
||||
}
|
||||
if (!status.ok()) {
|
||||
fprintf(stderr, "NewIterators: %s\n", status.ToString().c_str());
|
||||
shared->SetShouldStopTest();
|
||||
assert(false);
|
||||
return;
|
||||
}
|
||||
|
||||
// With WAL enabled, secondary MANIFEST recovery can expose flushed state
|
||||
// beyond the point reached by WAL replay. NewIterators() keeps one
|
||||
// published view but cannot make those column families logically
|
||||
// consistent.
|
||||
const size_t num_cfs_to_verify =
|
||||
FLAGS_disable_wal ? iterators.size()
|
||||
: std::min<size_t>(1, iterators.size());
|
||||
uint32_t expected_crc = 0;
|
||||
for (size_t i = 0; i < num_cfs_to_verify; ++i) {
|
||||
uint32_t crc = 0;
|
||||
status = checksum_column_family(iterators[i].get(), &crc);
|
||||
if (!status.ok()) {
|
||||
fprintf(stderr, "status: %s\n", status.ToString().c_str());
|
||||
fprintf(stderr, "Computing checksum of cf %zu: %s\n", i,
|
||||
status.ToString().c_str());
|
||||
shared->SetShouldStopTest();
|
||||
assert(false);
|
||||
} else if (tmp_crc != crc) {
|
||||
fprintf(stderr, "tmp_crc=%" PRIu32 " crc=%" PRIu32 "\n", tmp_crc, crc);
|
||||
return;
|
||||
}
|
||||
if (i == 0) {
|
||||
expected_crc = crc;
|
||||
} else if (FLAGS_disable_wal && crc != expected_crc) {
|
||||
fprintf(stderr, "cf %zu crc=%" PRIu32 " expected=%" PRIu32 "\n", i, crc,
|
||||
expected_crc);
|
||||
shared->SetShouldStopTest();
|
||||
assert(false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,10 +15,17 @@ int main() {
|
||||
return 1;
|
||||
}
|
||||
#else
|
||||
#ifdef USE_FOLLY
|
||||
#include <glog/logging.h>
|
||||
#endif
|
||||
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/db_stress_tool.h"
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
#ifdef USE_FOLLY
|
||||
google::InitGoogleLogging(argv[0]);
|
||||
#endif
|
||||
ROCKSDB_NAMESPACE::port::InstallStackTraceHandler();
|
||||
return ROCKSDB_NAMESPACE::db_stress_tool(argc, argv);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
#include "db_stress_tool/db_stress_test_base.h"
|
||||
#include "file/file_util.h"
|
||||
#include "file/filename.h"
|
||||
#include "rocksdb/secondary_cache.h"
|
||||
#include "util/file_checksum_helper.h"
|
||||
#include "util/xxhash.h"
|
||||
@@ -45,8 +46,7 @@ namespace {
|
||||
|
||||
class BlockingAsyncCallback : public DB::AsyncCallback {
|
||||
public:
|
||||
void OnComplete(const PerfContext* /*perf_context*/,
|
||||
const IOStatsContext* /*iostats_context*/) override {
|
||||
void OnComplete() override {
|
||||
std::lock_guard<std::mutex> lock(mu_);
|
||||
done_ = true;
|
||||
cv_.notify_one();
|
||||
@@ -722,15 +722,18 @@ static void ProcessCompactionResult(
|
||||
|
||||
static void ProcessRemoteCompactionJob(
|
||||
const std::string& job_id, const CompactionServiceJobInfo& job_info,
|
||||
const std::string& serialized_input, const std::string& output_directory,
|
||||
bool was_canceled, SharedState* shared, StressTest* stress_test,
|
||||
Random& rand, uint64_t& successful_compaction_end_to_end_micros) {
|
||||
const std::string& serialized_input,
|
||||
const std::string& output_directory_name, bool was_canceled,
|
||||
SharedState* shared, StressTest* stress_test, Random& rand,
|
||||
uint64_t& successful_compaction_end_to_end_micros) {
|
||||
auto options = stress_test->GetOptions(job_info.cf_id);
|
||||
assert(options.env != nullptr);
|
||||
|
||||
auto override_options = CreateOverrideOptions(options, job_info);
|
||||
|
||||
OpenAndCompactOptions open_compact_options;
|
||||
open_compact_options.max_secondary_open_retries =
|
||||
FLAGS_openandcompact_max_secondary_open_retries;
|
||||
if (FLAGS_allow_resumption_one_in > 0) {
|
||||
open_compact_options.allow_resumption =
|
||||
rand.OneIn(FLAGS_allow_resumption_one_in);
|
||||
@@ -739,7 +742,9 @@ static void ProcessRemoteCompactionJob(
|
||||
}
|
||||
|
||||
if (!open_compact_options.allow_resumption) {
|
||||
CleanupOutputDirectory(output_directory);
|
||||
CleanupOutputDirectory(RemoteCompactionJobDir(
|
||||
job_info.db_name, options.use_session_tmp_dir_for_remote_compaction,
|
||||
output_directory_name));
|
||||
}
|
||||
|
||||
std::shared_ptr<std::atomic<bool>> canceled = nullptr;
|
||||
@@ -752,11 +757,11 @@ static void ProcessRemoteCompactionJob(
|
||||
uint64_t start_micros = options.env->NowMicros();
|
||||
|
||||
Status s = DB::OpenAndCompact(open_compact_options, job_info.db_name,
|
||||
output_directory, serialized_input,
|
||||
output_directory_name, serialized_input,
|
||||
&serialized_output, override_options);
|
||||
|
||||
ProcessCompactionResult(s, job_id, job_info, serialized_input,
|
||||
output_directory, serialized_output, shared,
|
||||
output_directory_name, serialized_output, shared,
|
||||
successful_compaction_end_to_end_micros, start_micros,
|
||||
options.env);
|
||||
}
|
||||
|
||||
@@ -107,6 +107,9 @@ DECLARE_bool(destroy_db_and_exit);
|
||||
DECLARE_string(delete_dir_and_exit);
|
||||
DECLARE_bool(verbose);
|
||||
DECLARE_bool(progress_reports);
|
||||
DECLARE_string(stress_diagnostics_dir);
|
||||
DECLARE_bool(stress_diagnostics_breadcrumbs);
|
||||
DECLARE_uint64(stress_diagnostics_breadcrumb_entries);
|
||||
DECLARE_uint64(db_write_buffer_size);
|
||||
DECLARE_int32(write_buffer_size);
|
||||
DECLARE_int32(max_write_buffer_number);
|
||||
@@ -477,6 +480,7 @@ DECLARE_bool(track_and_verify_wals);
|
||||
DECLARE_int32(remote_compaction_worker_threads);
|
||||
DECLARE_int32(remote_compaction_worker_interval);
|
||||
DECLARE_bool(remote_compaction_failure_fall_back_to_local);
|
||||
DECLARE_uint32(openandcompact_max_secondary_open_retries);
|
||||
DECLARE_int32(allow_resumption_one_in);
|
||||
DECLARE_bool(auto_refresh_iterator_with_snapshot);
|
||||
DECLARE_uint32(memtable_op_scan_flush_trigger);
|
||||
|
||||
@@ -6,8 +6,11 @@
|
||||
#ifdef GFLAGS
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
#include "db/compaction/compaction_job.h"
|
||||
#include "db_stress_shared_state.h"
|
||||
#include "file/file_util.h"
|
||||
#include "rocksdb/options.h"
|
||||
#include "utilities/fault_injection_fs.h"
|
||||
|
||||
@@ -27,7 +30,6 @@ class DbStressCompactionService : public CompactionService {
|
||||
const char* Name() const override { return kClassName(); }
|
||||
|
||||
static constexpr uint64_t kWaitIntervalInMicros = 10 * 1000; // 10ms
|
||||
|
||||
static constexpr const char* kTempOutputDirectoryPrefix = "tmp_output_";
|
||||
|
||||
CompactionServiceScheduleResponse Schedule(
|
||||
@@ -40,12 +42,12 @@ class DbStressCompactionService : public CompactionService {
|
||||
return CompactionServiceScheduleResponse(
|
||||
job_id, CompactionServiceJobStatus::kUseLocal);
|
||||
}
|
||||
std::string output_directory = info.db_name + "/" +
|
||||
kTempOutputDirectoryPrefix +
|
||||
Env::Default()->GenerateUniqueId();
|
||||
|
||||
const std::string output_directory_name =
|
||||
kTempOutputDirectoryPrefix + Env::Default()->GenerateUniqueId();
|
||||
|
||||
shared_->EnqueueRemoteCompaction(
|
||||
job_id, info, compaction_service_input, output_directory,
|
||||
job_id, info, compaction_service_input, output_directory_name,
|
||||
false /* was_cancelled */); // Not canceled initially
|
||||
CompactionServiceScheduleResponse response(
|
||||
job_id, CompactionServiceJobStatus::kSuccess);
|
||||
@@ -57,28 +59,14 @@ class DbStressCompactionService : public CompactionService {
|
||||
|
||||
void OnInstallation(const std::string& scheduled_job_id,
|
||||
CompactionServiceJobStatus /*status*/) override {
|
||||
// Clean up tmp directory
|
||||
std::string serialized;
|
||||
CompactionServiceResult result;
|
||||
if (shared_->GetRemoteCompactionResult(scheduled_job_id, &serialized)
|
||||
.has_value()) {
|
||||
if (CompactionServiceResult::Read(serialized, &result).ok()) {
|
||||
std::vector<std::string> filenames;
|
||||
Status s = Env::Default()->GetChildren(result.output_path, &filenames);
|
||||
for (size_t i = 0; s.ok() && i < filenames.size(); ++i) {
|
||||
s = Env::Default()->DeleteFile(result.output_path + "/" +
|
||||
filenames[i]);
|
||||
if (!s.ok()) {
|
||||
// TODO - Handle clean up failure?
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (s.ok()) {
|
||||
Env::Default()->DeleteDir(result.output_path).PermitUncheckedError();
|
||||
}
|
||||
}
|
||||
shared_->RemoveRemoteCompactionResult(scheduled_job_id);
|
||||
.has_value() &&
|
||||
CompactionServiceResult::Read(serialized, &result).ok()) {
|
||||
DestroyDir(Env::Default(), result.output_path).PermitUncheckedError();
|
||||
}
|
||||
shared_->RemoveRemoteCompactionResult(scheduled_job_id);
|
||||
}
|
||||
|
||||
void CancelAwaitingJobs() override { aborted_.store(true); }
|
||||
|
||||
@@ -22,6 +22,17 @@ static bool ValidateUint32Range(const char* flagname, uint64_t value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ValidateStressDiagnosticEntryCount(const char* flagname,
|
||||
uint64_t value) {
|
||||
constexpr uint64_t kMaxStressDiagnosticEntries = 64 * 1024;
|
||||
if (value > kMaxStressDiagnosticEntries) {
|
||||
fprintf(stderr, "Invalid value for --%s: %lu, maximum is %lu\n", flagname,
|
||||
(unsigned long)value, (unsigned long)kMaxStressDiagnosticEntries);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
DEFINE_uint64(seed, 2341234,
|
||||
"Seed for PRNG. When --nooverwritepercent is "
|
||||
"nonzero and --expected_values_dir is nonempty, this value "
|
||||
@@ -166,6 +177,24 @@ DEFINE_uint64(
|
||||
"If non-zero with --liveness_check_interval_sec, terminate db_stress when "
|
||||
"no operation completes for this many seconds during the operation phase.");
|
||||
|
||||
DEFINE_string(stress_diagnostics_dir, "",
|
||||
"Directory where db_stress writes optional diagnostic artifacts. "
|
||||
"If empty, diagnostics that require file output are disabled.");
|
||||
|
||||
DEFINE_bool(stress_diagnostics_breadcrumbs, false,
|
||||
"If true, keep a bounded per-thread memory-mapped ring of recent "
|
||||
"stress operations in --stress_diagnostics_dir when set.");
|
||||
|
||||
DEFINE_uint64(stress_diagnostics_breadcrumb_entries, 256,
|
||||
"Number of recent operation breadcrumbs retained per stress "
|
||||
"thread when --stress_diagnostics_breadcrumbs is true. Maximum "
|
||||
"is 65536.");
|
||||
// NOLINTNEXTLINE(cert-err58-cpp)
|
||||
static const bool FLAGS_stress_diagnostics_breadcrumb_entries_dummy
|
||||
__attribute__((__unused__)) =
|
||||
RegisterFlagValidator(&FLAGS_stress_diagnostics_breadcrumb_entries,
|
||||
&ValidateStressDiagnosticEntryCount);
|
||||
|
||||
DEFINE_uint64(db_write_buffer_size,
|
||||
ROCKSDB_NAMESPACE::Options().db_write_buffer_size,
|
||||
"Number of bytes to buffer in all memtables before compacting");
|
||||
@@ -1060,6 +1089,12 @@ DEFINE_bool(remote_compaction_failure_fall_back_to_local, true,
|
||||
"If true, remote compaction failures will be ignored and "
|
||||
"compactions will fall back to local and retried");
|
||||
|
||||
DEFINE_uint32(
|
||||
openandcompact_max_secondary_open_retries,
|
||||
ROCKSDB_NAMESPACE::OpenAndCompactOptions().max_secondary_open_retries,
|
||||
"Maximum number of retries when OpenAndCompact opens the source DB as a "
|
||||
"secondary");
|
||||
|
||||
DEFINE_int32(allow_resumption_one_in, 0,
|
||||
"If non-zero, enable resumable compaction with 1/N probability "
|
||||
"for each OpenAndCompact call.Requires "
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
|
||||
#include "db_stress_tool/db_stress_compaction_service.h"
|
||||
#include "db_stress_tool/db_stress_shared_state.h"
|
||||
#include "file/filename.h"
|
||||
#include "file/writable_file_writer.h"
|
||||
#include "rocksdb/db.h"
|
||||
#include "rocksdb/env.h"
|
||||
@@ -421,7 +420,7 @@ class DbStressListener : public EventListener {
|
||||
}
|
||||
}
|
||||
}
|
||||
// We can't do exact matching since remote workers use dynamic temp paths
|
||||
// We can't do exact matching since remote workers use dynamic temp paths.
|
||||
if (file_dir.find(DbStressCompactionService::kTempOutputDirectoryPrefix) !=
|
||||
std::string::npos) {
|
||||
return;
|
||||
|
||||
@@ -11,10 +11,79 @@
|
||||
#ifdef GFLAGS
|
||||
#include "db_stress_tool/db_stress_shared_state.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
#include <charconv>
|
||||
#include <cstring>
|
||||
#include <new>
|
||||
|
||||
#include "db_stress_tool/db_stress_test_base.h"
|
||||
#include "port/port.h"
|
||||
#include "rocksdb/env.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
namespace {
|
||||
|
||||
using BreadcrumbRecordState = std::atomic<char>;
|
||||
|
||||
constexpr size_t kOperationBreadcrumbRecordBytes = 512;
|
||||
constexpr size_t kOperationBreadcrumbHeaderBytes =
|
||||
kOperationBreadcrumbRecordBytes;
|
||||
constexpr size_t kOperationBreadcrumbRecordsPerInitializationWrite = 128;
|
||||
|
||||
std::atomic<uint64_t> next_operation_breadcrumb_file_id{0};
|
||||
|
||||
std::string SanitizePathComponent(const std::string& value) {
|
||||
std::string result;
|
||||
result.reserve(value.size());
|
||||
for (char c : value) {
|
||||
if ((c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') ||
|
||||
(c >= '0' && c <= '9') || c == '_' || c == '-' || c == '.') {
|
||||
result.push_back(c);
|
||||
} else {
|
||||
result.push_back('_');
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
template <typename Integer>
|
||||
void AppendInteger(Integer value, std::string* output) {
|
||||
char buffer[32];
|
||||
const auto result = std::to_chars(buffer, buffer + sizeof(buffer), value);
|
||||
assert(result.ec == std::errc());
|
||||
output->append(buffer, result.ptr);
|
||||
}
|
||||
|
||||
void AppendSanitizedRecordValue(const std::string& value, size_t length,
|
||||
std::string* output) {
|
||||
assert(length <= value.size());
|
||||
for (size_t i = 0; i < length; ++i) {
|
||||
const char c = value[i];
|
||||
output->push_back(c == '\n' || c == '\r' || c == '\t' ? ' ' : c);
|
||||
}
|
||||
}
|
||||
|
||||
std::string OperationBreadcrumbFilePath(const ThreadState& thread,
|
||||
uint64_t file_id) {
|
||||
std::string path = FLAGS_stress_diagnostics_dir;
|
||||
if (!path.empty() && path.back() != '/') {
|
||||
path.push_back('/');
|
||||
}
|
||||
const StressTest* const stress_test = thread.shared->GetStressTest();
|
||||
const std::string db_label =
|
||||
stress_test ? stress_test->GetDbLabel() : "unknown_db";
|
||||
path.append(SanitizePathComponent(db_label));
|
||||
path.append(".pid_");
|
||||
path.append(std::to_string(port::GetProcessID()));
|
||||
path.append(".thread_");
|
||||
path.append(std::to_string(file_id));
|
||||
path.append(".breadcrumbs.txt");
|
||||
return path;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
thread_local bool SharedState::ignore_read_error;
|
||||
|
||||
SharedState::SharedState(Env* env, StressTest* stress_test)
|
||||
@@ -134,6 +203,260 @@ bool SharedState::BeginOperation(uint32_t tid, StressOperationType type) {
|
||||
return true;
|
||||
}
|
||||
|
||||
ThreadState::ThreadState(uint32_t index, SharedState* _shared)
|
||||
: tid(index),
|
||||
rand(1000 + index + _shared->GetSeed()),
|
||||
shared(_shared),
|
||||
operation_breadcrumb_pos(0),
|
||||
operation_breadcrumb_sequence(0),
|
||||
operation_breadcrumb_file_id(0),
|
||||
operation_breadcrumb_setup_attempted(false),
|
||||
diagnostic_io_disabled(false),
|
||||
operation_ordinal(0),
|
||||
current_operation_ordinal(0) {}
|
||||
|
||||
ThreadState::~ThreadState() {
|
||||
if (operation_breadcrumb_mapping == nullptr) {
|
||||
return;
|
||||
}
|
||||
const size_t entry_count = (operation_breadcrumb_mapping->GetLen() -
|
||||
kOperationBreadcrumbHeaderBytes) /
|
||||
kOperationBreadcrumbRecordBytes;
|
||||
char* const base =
|
||||
static_cast<char*>(operation_breadcrumb_mapping->GetBase());
|
||||
for (size_t i = 0; i < entry_count; ++i) {
|
||||
char* const slot = base + kOperationBreadcrumbHeaderBytes +
|
||||
i * kOperationBreadcrumbRecordBytes;
|
||||
BreadcrumbRecordState* const state =
|
||||
std::launder(reinterpret_cast<BreadcrumbRecordState*>(slot));
|
||||
state->~BreadcrumbRecordState();
|
||||
}
|
||||
}
|
||||
|
||||
bool ThreadState::OperationBreadcrumbsEnabled() const {
|
||||
return FLAGS_stress_diagnostics_breadcrumbs &&
|
||||
FLAGS_stress_diagnostics_breadcrumb_entries > 0 &&
|
||||
!FLAGS_stress_diagnostics_dir.empty() && !diagnostic_io_disabled &&
|
||||
(!operation_breadcrumb_setup_attempted ||
|
||||
operation_breadcrumb_mapping != nullptr);
|
||||
}
|
||||
|
||||
bool ThreadState::InitializeOperationBreadcrumbs() {
|
||||
if (operation_breadcrumb_mapping != nullptr) {
|
||||
return true;
|
||||
}
|
||||
if (!OperationBreadcrumbsEnabled()) {
|
||||
return false;
|
||||
}
|
||||
operation_breadcrumb_setup_attempted = true;
|
||||
|
||||
if (sizeof(BreadcrumbRecordState) != sizeof(char) ||
|
||||
kOperationBreadcrumbRecordBytes % alignof(BreadcrumbRecordState) != 0 ||
|
||||
!BreadcrumbRecordState{}.is_lock_free()) {
|
||||
fprintf(stdout,
|
||||
"Operation breadcrumbs require a lock-free one-byte atomic "
|
||||
"record marker\n");
|
||||
return false;
|
||||
}
|
||||
|
||||
Env* const env = Env::Default();
|
||||
Status s = env->CreateDirIfMissing(FLAGS_stress_diagnostics_dir);
|
||||
if (!s.ok()) {
|
||||
// Some Env implementations report an error when another thread creates
|
||||
// the directory between their existence check and create call.
|
||||
Status exists_status = env->FileExists(FLAGS_stress_diagnostics_dir);
|
||||
if (!exists_status.ok()) {
|
||||
fprintf(stdout, "Failed to create stress diagnostics directory %s: %s\n",
|
||||
FLAGS_stress_diagnostics_dir.c_str(), s.ToString().c_str());
|
||||
diagnostic_io_disabled = true;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
operation_breadcrumb_file_id =
|
||||
next_operation_breadcrumb_file_id.fetch_add(1, std::memory_order_relaxed);
|
||||
const std::string path =
|
||||
OperationBreadcrumbFilePath(*this, operation_breadcrumb_file_id);
|
||||
std::unique_ptr<WritableFile> file;
|
||||
s = env->NewWritableFile(path, &file, EnvOptions());
|
||||
if (!s.ok()) {
|
||||
fprintf(stdout, "Failed to create operation breadcrumb file %s: %s\n",
|
||||
path.c_str(), s.ToString().c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string header =
|
||||
"# mmap_breadcrumbs_v1 record_state=V(valid),I(incomplete),E(empty) "
|
||||
"order_by=seq power_loss_durable=0 pid=";
|
||||
AppendInteger(port::GetProcessID(), &header);
|
||||
header.append(" tid=");
|
||||
AppendInteger(tid, &header);
|
||||
header.append(" file_id=");
|
||||
AppendInteger(operation_breadcrumb_file_id, &header);
|
||||
header.append(" seed=");
|
||||
AppendInteger(shared->GetSeed(), &header);
|
||||
assert(header.size() < kOperationBreadcrumbHeaderBytes);
|
||||
header.resize(kOperationBreadcrumbHeaderBytes - 1, ' ');
|
||||
header.push_back('\n');
|
||||
|
||||
Status write_status = file->Append(Slice(header));
|
||||
std::string empty_records(kOperationBreadcrumbRecordsPerInitializationWrite *
|
||||
kOperationBreadcrumbRecordBytes,
|
||||
' ');
|
||||
for (size_t i = 0; i < kOperationBreadcrumbRecordsPerInitializationWrite;
|
||||
++i) {
|
||||
empty_records[i * kOperationBreadcrumbRecordBytes] = 'E';
|
||||
empty_records[(i + 1) * kOperationBreadcrumbRecordBytes - 1] = '\n';
|
||||
}
|
||||
|
||||
const size_t entry_count = FLAGS_stress_diagnostics_breadcrumb_entries;
|
||||
for (size_t first = 0; write_status.ok() && first < entry_count;
|
||||
first += kOperationBreadcrumbRecordsPerInitializationWrite) {
|
||||
const size_t count = std::min(
|
||||
kOperationBreadcrumbRecordsPerInitializationWrite, entry_count - first);
|
||||
write_status = file->Append(
|
||||
Slice(empty_records.data(), count * kOperationBreadcrumbRecordBytes));
|
||||
}
|
||||
|
||||
Status close_status = file->Close();
|
||||
file.reset();
|
||||
if (!write_status.ok()) {
|
||||
s = write_status;
|
||||
} else if (!close_status.ok()) {
|
||||
s = close_status;
|
||||
}
|
||||
if (!s.ok()) {
|
||||
env->DeleteFile(path).PermitUncheckedError();
|
||||
fprintf(stdout, "Failed to initialize operation breadcrumb file %s: %s\n",
|
||||
path.c_str(), s.ToString().c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
std::unique_ptr<MemoryMappedFileBuffer> mapping;
|
||||
s = env->NewMemoryMappedFileBuffer(path, &mapping);
|
||||
const size_t expected_size = kOperationBreadcrumbHeaderBytes +
|
||||
entry_count * kOperationBreadcrumbRecordBytes;
|
||||
if (s.ok() && (mapping == nullptr || mapping->GetBase() == nullptr ||
|
||||
mapping->GetLen() != expected_size)) {
|
||||
s = Status::IOError("Unexpected operation breadcrumb mapping size");
|
||||
}
|
||||
if (!s.ok()) {
|
||||
mapping.reset();
|
||||
env->DeleteFile(path).PermitUncheckedError();
|
||||
fprintf(stdout, "Failed to map operation breadcrumb file %s: %s\n",
|
||||
path.c_str(), s.ToString().c_str());
|
||||
return false;
|
||||
}
|
||||
|
||||
char* const base = static_cast<char*>(mapping->GetBase());
|
||||
for (size_t i = 0; i < entry_count; ++i) {
|
||||
char* const slot = base + kOperationBreadcrumbHeaderBytes +
|
||||
i * kOperationBreadcrumbRecordBytes;
|
||||
new (slot) BreadcrumbRecordState('E');
|
||||
}
|
||||
operation_breadcrumb_mapping = std::move(mapping);
|
||||
operation_breadcrumb_details.reserve(kOperationBreadcrumbRecordBytes);
|
||||
operation_breadcrumb_record.reserve(kOperationBreadcrumbRecordBytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ThreadState::AppendOperationBreadcrumb(StressOperationType type,
|
||||
const char* phase,
|
||||
const std::string& details) {
|
||||
if (!InitializeOperationBreadcrumbs()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const uint64_t sequence = ++operation_breadcrumb_sequence;
|
||||
operation_breadcrumb_record.clear();
|
||||
operation_breadcrumb_record.append(" seq=");
|
||||
AppendInteger(sequence, &operation_breadcrumb_record);
|
||||
operation_breadcrumb_record.append(" time_micros=");
|
||||
AppendInteger(shared->GetEnv()->NowMicros(), &operation_breadcrumb_record);
|
||||
operation_breadcrumb_record.append(" tid=");
|
||||
AppendInteger(tid, &operation_breadcrumb_record);
|
||||
operation_breadcrumb_record.append(" ordinal=");
|
||||
AppendInteger(current_operation_ordinal, &operation_breadcrumb_record);
|
||||
operation_breadcrumb_record.append(" op=");
|
||||
operation_breadcrumb_record.append(StressOperationTypeName(type));
|
||||
operation_breadcrumb_record.append(" phase=");
|
||||
operation_breadcrumb_record.append(phase);
|
||||
|
||||
constexpr size_t kMaxRecordContentBytes = kOperationBreadcrumbRecordBytes - 2;
|
||||
assert(operation_breadcrumb_record.size() < kMaxRecordContentBytes);
|
||||
if (!details.empty()) {
|
||||
operation_breadcrumb_record.push_back(' ');
|
||||
const size_t available =
|
||||
kMaxRecordContentBytes - operation_breadcrumb_record.size();
|
||||
if (details.size() <= available) {
|
||||
AppendSanitizedRecordValue(details, details.size(),
|
||||
&operation_breadcrumb_record);
|
||||
} else {
|
||||
static constexpr char kTruncatedSuffix[] = " details_truncated=1";
|
||||
constexpr size_t kTruncatedSuffixBytes = sizeof(kTruncatedSuffix) - 1;
|
||||
assert(available >= kTruncatedSuffixBytes);
|
||||
AppendSanitizedRecordValue(details, available - kTruncatedSuffixBytes,
|
||||
&operation_breadcrumb_record);
|
||||
operation_breadcrumb_record.append(kTruncatedSuffix);
|
||||
}
|
||||
}
|
||||
|
||||
const size_t entry_count = (operation_breadcrumb_mapping->GetLen() -
|
||||
kOperationBreadcrumbHeaderBytes) /
|
||||
kOperationBreadcrumbRecordBytes;
|
||||
char* const base =
|
||||
static_cast<char*>(operation_breadcrumb_mapping->GetBase());
|
||||
char* const slot = base + kOperationBreadcrumbHeaderBytes +
|
||||
operation_breadcrumb_pos * kOperationBreadcrumbRecordBytes;
|
||||
BreadcrumbRecordState* const state =
|
||||
std::launder(reinterpret_cast<BreadcrumbRecordState*>(slot));
|
||||
// The acquire half prevents the following body stores from becoming visible
|
||||
// before the slot is invalidated on weakly ordered architectures.
|
||||
state->exchange('I', std::memory_order_acq_rel);
|
||||
std::memset(slot + 1, ' ', kOperationBreadcrumbRecordBytes - 2);
|
||||
slot[kOperationBreadcrumbRecordBytes - 1] = '\n';
|
||||
std::memcpy(slot + 1, operation_breadcrumb_record.data(),
|
||||
operation_breadcrumb_record.size());
|
||||
state->store('V', std::memory_order_release);
|
||||
|
||||
operation_breadcrumb_pos = (operation_breadcrumb_pos + 1) % entry_count;
|
||||
}
|
||||
|
||||
std::string* ThreadState::PrepareOperationEvent() {
|
||||
if (!InitializeOperationBreadcrumbs()) {
|
||||
return nullptr;
|
||||
}
|
||||
operation_breadcrumb_details.clear();
|
||||
return &operation_breadcrumb_details;
|
||||
}
|
||||
|
||||
void ThreadState::CommitOperationEvent(StressOperationType type) {
|
||||
AppendOperationBreadcrumb(type, "event", operation_breadcrumb_details);
|
||||
}
|
||||
|
||||
bool ThreadState::BeginOperation(StressOperationType type) {
|
||||
++operation_ordinal;
|
||||
current_operation_ordinal = operation_ordinal;
|
||||
|
||||
if (OperationBreadcrumbsEnabled()) {
|
||||
AppendOperationBreadcrumb(type, "begin", "");
|
||||
}
|
||||
|
||||
if (LivenessTrackingEnabled()) {
|
||||
return shared->BeginOperation(tid, type);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void ThreadState::RecordOperationEnd(StressOperationType type) {
|
||||
if (!OperationBreadcrumbsEnabled()) {
|
||||
return;
|
||||
}
|
||||
const bool verification_failed = shared->HasVerificationFailedYet();
|
||||
AppendOperationBreadcrumb(
|
||||
type, "end", verification_failed ? "verification_failure=1" : "");
|
||||
}
|
||||
|
||||
bool SharedState::ShouldVerifyAtBeginning() const {
|
||||
return !stress_test_->GetExpectedValuesDir().empty();
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
#include <atomic>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "db_stress_tool/db_stress_stat.h"
|
||||
#include "db_stress_tool/expected_state.h"
|
||||
@@ -37,6 +39,9 @@ DECLARE_bool(error_recovery_with_no_fault_injection);
|
||||
DECLARE_bool(sync_fault_injection);
|
||||
DECLARE_uint64(liveness_check_interval_sec);
|
||||
DECLARE_uint64(liveness_no_progress_timeout_sec);
|
||||
DECLARE_string(stress_diagnostics_dir);
|
||||
DECLARE_bool(stress_diagnostics_breadcrumbs);
|
||||
DECLARE_uint64(stress_diagnostics_breadcrumb_entries);
|
||||
DECLARE_int32(range_deletion_width);
|
||||
DECLARE_bool(disable_wal);
|
||||
DECLARE_int32(manual_wal_flush_one_in);
|
||||
@@ -56,6 +61,7 @@ DECLARE_bool(disable_auto_compactions);
|
||||
DECLARE_bool(enable_compaction_filter);
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
class MemoryMappedFileBuffer;
|
||||
class StressTest;
|
||||
|
||||
enum class StressOperationType : uint32_t {
|
||||
@@ -284,6 +290,8 @@ class SharedState {
|
||||
|
||||
bool HasVerificationFailedYet() const { return verification_failure_.load(); }
|
||||
|
||||
Env* GetEnv() const { return env_; }
|
||||
|
||||
void IncFinishedOps() {
|
||||
finished_ops_.fetch_add(1, std::memory_order_relaxed);
|
||||
}
|
||||
@@ -707,6 +715,16 @@ struct ThreadState {
|
||||
Random rand; // Has different seeds for different threads
|
||||
SharedState* shared;
|
||||
Stats stats;
|
||||
std::unique_ptr<MemoryMappedFileBuffer> operation_breadcrumb_mapping;
|
||||
std::string operation_breadcrumb_details;
|
||||
std::string operation_breadcrumb_record;
|
||||
size_t operation_breadcrumb_pos;
|
||||
uint64_t operation_breadcrumb_sequence;
|
||||
uint64_t operation_breadcrumb_file_id;
|
||||
bool operation_breadcrumb_setup_attempted;
|
||||
bool diagnostic_io_disabled;
|
||||
uint64_t operation_ordinal;
|
||||
uint64_t current_operation_ordinal;
|
||||
struct SnapshotState {
|
||||
const Snapshot* snapshot;
|
||||
// The cf from which we did a Get at this snapshot
|
||||
@@ -726,20 +744,15 @@ struct ThreadState {
|
||||
};
|
||||
std::queue<std::pair<uint64_t, SnapshotState>> snapshot_queue;
|
||||
|
||||
ThreadState(uint32_t index, SharedState* _shared)
|
||||
: tid(index), rand(1000 + index + _shared->GetSeed()), shared(_shared) {}
|
||||
ThreadState(uint32_t index, SharedState* _shared);
|
||||
~ThreadState();
|
||||
|
||||
bool LivenessTrackingEnabled() const {
|
||||
return FLAGS_liveness_check_interval_sec > 0 &&
|
||||
FLAGS_liveness_no_progress_timeout_sec > 0;
|
||||
}
|
||||
|
||||
bool BeginOperation(StressOperationType type) {
|
||||
if (LivenessTrackingEnabled()) {
|
||||
return shared->BeginOperation(tid, type);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
bool BeginOperation(StressOperationType type);
|
||||
|
||||
bool EndOperation(StressOperationType type) {
|
||||
if (LivenessTrackingEnabled()) {
|
||||
@@ -754,6 +767,14 @@ struct ThreadState {
|
||||
}
|
||||
}
|
||||
|
||||
bool OperationBreadcrumbsEnabled() const;
|
||||
|
||||
std::string* PrepareOperationEvent();
|
||||
|
||||
void CommitOperationEvent(StressOperationType type);
|
||||
|
||||
void RecordOperationEnd(StressOperationType type);
|
||||
|
||||
void CompletedOpForDiagnostics(StressOperationType type) {
|
||||
if (LivenessTrackingEnabled()) {
|
||||
shared->IncCompletedOpForDiagnostics(type);
|
||||
@@ -771,6 +792,11 @@ struct ThreadState {
|
||||
RecordSingleOpFinished();
|
||||
ClearOperation();
|
||||
}
|
||||
|
||||
private:
|
||||
bool InitializeOperationBreadcrumbs();
|
||||
void AppendOperationBreadcrumb(StressOperationType type, const char* phase,
|
||||
const std::string& details);
|
||||
};
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
#endif // GFLAGS
|
||||
@@ -37,6 +37,7 @@ class Stats {
|
||||
long iterator_size_sums_;
|
||||
long founds_;
|
||||
long iterations_;
|
||||
long seek_to_first_after_seek_;
|
||||
long range_deletions_;
|
||||
long covered_by_range_deletions_;
|
||||
long errors_;
|
||||
@@ -63,6 +64,7 @@ class Stats {
|
||||
iterator_size_sums_ = 0;
|
||||
founds_ = 0;
|
||||
iterations_ = 0;
|
||||
seek_to_first_after_seek_ = 0;
|
||||
range_deletions_ = 0;
|
||||
covered_by_range_deletions_ = 0;
|
||||
errors_ = 0;
|
||||
@@ -87,6 +89,7 @@ class Stats {
|
||||
iterator_size_sums_ += other.iterator_size_sums_;
|
||||
founds_ += other.founds_;
|
||||
iterations_ += other.iterations_;
|
||||
seek_to_first_after_seek_ += other.seek_to_first_after_seek_;
|
||||
range_deletions_ += other.range_deletions_;
|
||||
covered_by_range_deletions_ = other.covered_by_range_deletions_;
|
||||
errors_ += other.errors_;
|
||||
@@ -154,6 +157,9 @@ class Stats {
|
||||
|
||||
void AddIterations(long n) { iterations_ += n; }
|
||||
|
||||
// SeekToFirst() after Seek() on one iterator under prefix_same_as_start.
|
||||
void AddSeekToFirstAfterSeek(long n) { seek_to_first_after_seek_ += n; }
|
||||
|
||||
void AddDeletes(long n) { deletes_ += n; }
|
||||
|
||||
void AddSingleDeletes(size_t n) { single_deletes_ += n; }
|
||||
@@ -200,6 +206,10 @@ class Stats {
|
||||
fprintf(stdout, "%-12s: Iterator size sum is %ld\n", "",
|
||||
iterator_size_sums_);
|
||||
fprintf(stdout, "%-12s: Iterated %ld times\n", "", iterations_);
|
||||
fprintf(stdout,
|
||||
"%-12s: SeekToFirst() after Seek() under "
|
||||
"prefix_same_as_start %ld times\n",
|
||||
"", seek_to_first_after_seek_);
|
||||
fprintf(stdout, "%-12s: Deleted %ld key-ranges\n", "", range_deletions_);
|
||||
fprintf(stdout, "%-12s: Range deletions covered %ld keys\n", "",
|
||||
covered_by_range_deletions_);
|
||||
|
||||
@@ -9,12 +9,15 @@
|
||||
//
|
||||
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <cstdlib>
|
||||
#include <initializer_list>
|
||||
#include <iomanip>
|
||||
#include <ios>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
#include <optional>
|
||||
#include <sstream>
|
||||
#include <thread>
|
||||
#include <unordered_set>
|
||||
|
||||
@@ -78,6 +81,7 @@ class ScopedThreadOperation {
|
||||
if (tracking_) {
|
||||
thread_->CompletedOpForDiagnostics(type_);
|
||||
}
|
||||
thread_->RecordOperationEnd(type_);
|
||||
switch (finish_action_) {
|
||||
case FinishAction::kPop:
|
||||
break;
|
||||
@@ -97,6 +101,67 @@ class ScopedThreadOperation {
|
||||
bool tracking_;
|
||||
};
|
||||
|
||||
template <typename Integer>
|
||||
void AppendOperationContextInteger(Integer value, std::string* output) {
|
||||
char buffer[32];
|
||||
const auto result = std::to_chars(buffer, buffer + sizeof(buffer), value);
|
||||
assert(result.ec == std::errc());
|
||||
output->append(buffer, result.ptr);
|
||||
}
|
||||
|
||||
template <typename ColumnFamilies, typename Keys>
|
||||
void FormatOperationContext(const ColumnFamilies& column_families,
|
||||
const Keys& keys, std::string* output) {
|
||||
output->clear();
|
||||
output->append("cfs=[");
|
||||
bool first = true;
|
||||
for (int column_family : column_families) {
|
||||
if (!first) {
|
||||
output->push_back(',');
|
||||
}
|
||||
first = false;
|
||||
AppendOperationContextInteger(column_family, output);
|
||||
}
|
||||
output->append("] keys=[");
|
||||
first = true;
|
||||
for (int64_t key : keys) {
|
||||
if (!first) {
|
||||
output->push_back(',');
|
||||
}
|
||||
first = false;
|
||||
AppendOperationContextInteger(key, output);
|
||||
}
|
||||
output->push_back(']');
|
||||
}
|
||||
|
||||
template <typename ColumnFamilies, typename Keys>
|
||||
void MaybeRecordOperationContextImpl(ThreadState* thread,
|
||||
StressOperationType type,
|
||||
const ColumnFamilies& column_families,
|
||||
const Keys& keys) {
|
||||
if (!thread->OperationBreadcrumbsEnabled()) {
|
||||
return;
|
||||
}
|
||||
std::string* details = thread->PrepareOperationEvent();
|
||||
if (details == nullptr) {
|
||||
return;
|
||||
}
|
||||
FormatOperationContext(column_families, keys, details);
|
||||
thread->CommitOperationEvent(type);
|
||||
}
|
||||
|
||||
void MaybeRecordOperationContext(ThreadState* thread, StressOperationType type,
|
||||
const std::vector<int>& column_families,
|
||||
const std::vector<int64_t>& keys) {
|
||||
MaybeRecordOperationContextImpl(thread, type, column_families, keys);
|
||||
}
|
||||
|
||||
void MaybeRecordOperationContext(ThreadState* thread, StressOperationType type,
|
||||
std::initializer_list<int> column_families,
|
||||
std::initializer_list<int64_t> keys) {
|
||||
MaybeRecordOperationContextImpl(thread, type, column_families, keys);
|
||||
}
|
||||
|
||||
class StressReadScopedBlockBufferProvider
|
||||
: public ReadScopedBlockBufferProvider {
|
||||
public:
|
||||
@@ -454,15 +519,29 @@ Status VerifyCheckpointColumnFamilies(
|
||||
|
||||
} // namespace
|
||||
|
||||
bool StressTest::IsTolerableNonInjectedRemoteIOError(const Status& error_s) {
|
||||
assert(!error_s.ok());
|
||||
return FLAGS_tolerate_non_injected_io_errors_for_remote_dbs &&
|
||||
(!FLAGS_env_uri.empty() || !FLAGS_fs_uri.empty()) &&
|
||||
error_s.IsIOError() &&
|
||||
!status_to_io_status(Status(error_s)).GetDataLoss();
|
||||
}
|
||||
|
||||
bool StressTest::IsErrorInjectedAndRetryable(const Status& error_s) {
|
||||
assert(!error_s.ok());
|
||||
const IOStatus io_s = status_to_io_status(Status(error_s));
|
||||
return !io_s.GetDataLoss() &&
|
||||
((error_s.getState() &&
|
||||
FaultInjectionTestFS::IsInjectedError(error_s)) ||
|
||||
(FLAGS_tolerate_non_injected_io_errors_for_remote_dbs &&
|
||||
(!FLAGS_env_uri.empty() || !FLAGS_fs_uri.empty()) &&
|
||||
error_s.IsIOError()));
|
||||
const bool injected_and_retryable =
|
||||
error_s.getState() && FaultInjectionTestFS::IsInjectedError(error_s) &&
|
||||
!status_to_io_status(Status(error_s)).GetDataLoss();
|
||||
return injected_and_retryable || IsTolerableNonInjectedRemoteIOError(error_s);
|
||||
}
|
||||
|
||||
bool StressTest::IsRetryableOperationError(int injected_error_count,
|
||||
const Status& error_s) {
|
||||
assert(!error_s.ok());
|
||||
if (IsTolerableNonInjectedRemoteIOError(error_s)) {
|
||||
return true;
|
||||
}
|
||||
return injected_error_count > 0 && IsErrorInjectedAndRetryable(error_s);
|
||||
}
|
||||
|
||||
bool StressTest::LazyEntityReadEnabled() const {
|
||||
@@ -2345,6 +2424,8 @@ void StressTest::OperateDb(ThreadState* thread) {
|
||||
|
||||
if (thread->rand.OneInOpt(FLAGS_compact_files_one_in)) {
|
||||
ScopedThreadOperation op(thread, StressOperationType::kCompactFiles);
|
||||
MaybeRecordOperationContext(thread, StressOperationType::kCompactFiles,
|
||||
{rand_column_family}, {});
|
||||
TestCompactFiles(thread, column_family);
|
||||
}
|
||||
|
||||
@@ -2354,6 +2435,8 @@ void StressTest::OperateDb(ThreadState* thread) {
|
||||
|
||||
if (thread->rand.OneInOpt(FLAGS_compact_range_one_in)) {
|
||||
ScopedThreadOperation op(thread, StressOperationType::kCompactRange);
|
||||
MaybeRecordOperationContext(thread, StressOperationType::kCompactRange,
|
||||
{rand_column_family}, {rand_key});
|
||||
TestCompactRange(thread, rand_key, key, column_family);
|
||||
if (thread->shared->HasVerificationFailedYet()) {
|
||||
break;
|
||||
@@ -2369,6 +2452,8 @@ void StressTest::OperateDb(ThreadState* thread) {
|
||||
|
||||
if (thread->rand.OneInOpt(FLAGS_flush_one_in)) {
|
||||
ScopedThreadOperation op(thread, StressOperationType::kFlush);
|
||||
MaybeRecordOperationContext(thread, StressOperationType::kFlush,
|
||||
rand_column_families, {});
|
||||
TestFlush(thread, rand_column_families);
|
||||
}
|
||||
|
||||
@@ -2487,11 +2572,16 @@ void StressTest::OperateDb(ThreadState* thread) {
|
||||
if (thread->rand.OneInOpt(FLAGS_ingest_external_file_one_in)) {
|
||||
ScopedThreadOperation op(thread,
|
||||
StressOperationType::kIngestExternalFile);
|
||||
MaybeRecordOperationContext(thread,
|
||||
StressOperationType::kIngestExternalFile,
|
||||
rand_column_families, rand_keys);
|
||||
TestIngestExternalFile(thread, rand_column_families, rand_keys);
|
||||
}
|
||||
|
||||
if (thread->rand.OneInOpt(FLAGS_backup_one_in)) {
|
||||
ScopedThreadOperation op(thread, StressOperationType::kBackup);
|
||||
MaybeRecordOperationContext(thread, StressOperationType::kBackup,
|
||||
rand_column_families, rand_keys);
|
||||
// Beyond a certain DB size threshold, this test becomes heavier than
|
||||
// it's worth.
|
||||
uint64_t total_size = 0;
|
||||
@@ -2519,18 +2609,25 @@ void StressTest::OperateDb(ThreadState* thread) {
|
||||
|
||||
if (thread->rand.OneInOpt(FLAGS_checkpoint_one_in)) {
|
||||
ScopedThreadOperation op(thread, StressOperationType::kCheckpoint);
|
||||
MaybeRecordOperationContext(thread, StressOperationType::kCheckpoint,
|
||||
rand_column_families, rand_keys);
|
||||
Status s = TestCheckpoint(thread, rand_column_families, rand_keys);
|
||||
ProcessStatus(shared, "Checkpoint", s);
|
||||
}
|
||||
|
||||
if (thread->rand.OneInOpt(FLAGS_approximate_size_one_in)) {
|
||||
ScopedThreadOperation op(thread, StressOperationType::kApproximateSize);
|
||||
MaybeRecordOperationContext(thread,
|
||||
StressOperationType::kApproximateSize,
|
||||
rand_column_families, rand_keys);
|
||||
Status s =
|
||||
TestApproximateSize(thread, i, rand_column_families, rand_keys);
|
||||
ProcessStatus(shared, "ApproximateSize", s);
|
||||
}
|
||||
if (thread->rand.OneInOpt(FLAGS_acquire_snapshot_one_in)) {
|
||||
ScopedThreadOperation op(thread, StressOperationType::kSnapshot);
|
||||
MaybeRecordOperationContext(thread, StressOperationType::kSnapshot,
|
||||
{rand_column_family}, {rand_key});
|
||||
TestAcquireSnapshot(thread, rand_column_family, keystr, i);
|
||||
}
|
||||
|
||||
@@ -2551,6 +2648,8 @@ void StressTest::OperateDb(ThreadState* thread) {
|
||||
|
||||
if (thread->rand.OneInOpt(FLAGS_key_may_exist_one_in)) {
|
||||
ScopedThreadOperation op(thread, StressOperationType::kKeyMayExist);
|
||||
MaybeRecordOperationContext(thread, StressOperationType::kKeyMayExist,
|
||||
rand_column_families, rand_keys);
|
||||
TestKeyMayExist(thread, read_opts, rand_column_families, rand_keys);
|
||||
}
|
||||
// Historical expected-state restore replays exactly
|
||||
@@ -2578,12 +2677,16 @@ void StressTest::OperateDb(ThreadState* thread) {
|
||||
assert(i + batch_size <= ops_per_open);
|
||||
|
||||
rand_keys = GenerateNKeys(thread, static_cast<int>(batch_size), i);
|
||||
MaybeRecordOperationContext(thread, StressOperationType::kRead,
|
||||
rand_column_families, rand_keys);
|
||||
ThreadStatusUtil::SetThreadOperation(
|
||||
ThreadStatus::OperationType::OP_MULTIGETENTITY);
|
||||
TestMultiGetEntity(thread, read_opts, rand_column_families,
|
||||
rand_keys);
|
||||
i += batch_size - 1;
|
||||
} else if (FLAGS_use_get_entity) {
|
||||
MaybeRecordOperationContext(thread, StressOperationType::kRead,
|
||||
rand_column_families, rand_keys);
|
||||
ThreadStatusUtil::SetThreadOperation(
|
||||
ThreadStatus::OperationType::OP_GETENTITY);
|
||||
TestGetEntity(thread, read_opts, rand_column_families, rand_keys);
|
||||
@@ -2597,11 +2700,15 @@ void StressTest::OperateDb(ThreadState* thread) {
|
||||
// If its the last iteration, ensure that multiget_batch_size is 1
|
||||
multiget_batch_size = std::max(multiget_batch_size, 1);
|
||||
rand_keys = GenerateNKeys(thread, multiget_batch_size, i);
|
||||
MaybeRecordOperationContext(thread, StressOperationType::kRead,
|
||||
rand_column_families, rand_keys);
|
||||
ThreadStatusUtil::SetThreadOperation(
|
||||
ThreadStatus::OperationType::OP_MULTIGET);
|
||||
TestMultiGet(thread, read_opts, rand_column_families, rand_keys);
|
||||
i += multiget_batch_size - 1;
|
||||
} else {
|
||||
MaybeRecordOperationContext(thread, StressOperationType::kRead,
|
||||
rand_column_families, rand_keys);
|
||||
ThreadStatusUtil::SetThreadOperation(
|
||||
ThreadStatus::OperationType::OP_GET);
|
||||
TestGet(thread, read_opts, rand_column_families, rand_keys);
|
||||
@@ -2613,6 +2720,8 @@ void StressTest::OperateDb(ThreadState* thread) {
|
||||
ScopedThreadOperation op(
|
||||
thread, StressOperationType::kPrefixScan,
|
||||
ScopedThreadOperation::FinishAction::kFinishSingleOp);
|
||||
MaybeRecordOperationContext(thread, StressOperationType::kPrefixScan,
|
||||
rand_column_families, rand_keys);
|
||||
// keys are 8 bytes long, prefix size is FLAGS_prefix_size. There are
|
||||
// (8 - FLAGS_prefix_size) bytes besides the prefix. So there will
|
||||
// be 2 ^ ((8 - FLAGS_prefix_size) * 8) possible keys with the same
|
||||
@@ -2624,6 +2733,8 @@ void StressTest::OperateDb(ThreadState* thread) {
|
||||
ScopedThreadOperation op(
|
||||
thread, StressOperationType::kWrite,
|
||||
ScopedThreadOperation::FinishAction::kFinishSingleOp);
|
||||
MaybeRecordOperationContext(thread, StressOperationType::kWrite,
|
||||
rand_column_families, rand_keys);
|
||||
if (disable_fault_injection_during_user_write) {
|
||||
db_fault_injection_fs_->DisableAllThreadLocalErrorInjection();
|
||||
}
|
||||
@@ -2638,6 +2749,8 @@ void StressTest::OperateDb(ThreadState* thread) {
|
||||
ScopedThreadOperation op(
|
||||
thread, StressOperationType::kDelete,
|
||||
ScopedThreadOperation::FinishAction::kFinishSingleOp);
|
||||
MaybeRecordOperationContext(thread, StressOperationType::kDelete,
|
||||
rand_column_families, rand_keys);
|
||||
if (disable_fault_injection_during_user_write) {
|
||||
db_fault_injection_fs_->DisableAllThreadLocalErrorInjection();
|
||||
}
|
||||
@@ -2651,6 +2764,8 @@ void StressTest::OperateDb(ThreadState* thread) {
|
||||
ScopedThreadOperation op(
|
||||
thread, StressOperationType::kDeleteRange,
|
||||
ScopedThreadOperation::FinishAction::kFinishSingleOp);
|
||||
MaybeRecordOperationContext(thread, StressOperationType::kDeleteRange,
|
||||
rand_column_families, rand_keys);
|
||||
if (disable_fault_injection_during_user_write) {
|
||||
db_fault_injection_fs_->DisableAllThreadLocalErrorInjection();
|
||||
}
|
||||
@@ -2672,6 +2787,8 @@ void StressTest::OperateDb(ThreadState* thread) {
|
||||
// and an upper bound
|
||||
rand_keys = GenerateNKeys(thread, num_seeks * 2, i);
|
||||
i += num_seeks - 1;
|
||||
MaybeRecordOperationContext(thread, StressOperationType::kIterate,
|
||||
rand_column_families, rand_keys);
|
||||
ThreadStatusUtil::SetEnableTracking(FLAGS_enable_thread_tracking);
|
||||
ThreadStatusUtil::SetThreadOperation(
|
||||
ThreadStatus::OperationType::OP_DBITERATOR);
|
||||
@@ -2683,6 +2800,8 @@ void StressTest::OperateDb(ThreadState* thread) {
|
||||
thread->rand.OneInOpt(
|
||||
FLAGS_verify_iterator_with_expected_state_one_in)) {
|
||||
ThreadStatusUtil::SetEnableTracking(FLAGS_enable_thread_tracking);
|
||||
MaybeRecordOperationContext(thread, StressOperationType::kIterate,
|
||||
rand_column_families, rand_keys);
|
||||
ThreadStatusUtil::SetThreadOperation(
|
||||
ThreadStatus::OperationType::OP_DBITERATOR);
|
||||
TestIterateAgainstExpected(thread, read_opts, rand_column_families,
|
||||
@@ -2696,6 +2815,8 @@ void StressTest::OperateDb(ThreadState* thread) {
|
||||
static_cast<uint64_t>(1))));
|
||||
rand_keys = GenerateNKeys(thread, num_seeks, i);
|
||||
i += num_seeks - 1;
|
||||
MaybeRecordOperationContext(thread, StressOperationType::kIterate,
|
||||
rand_column_families, rand_keys);
|
||||
ThreadStatusUtil::SetEnableTracking(FLAGS_enable_thread_tracking);
|
||||
ThreadStatusUtil::SetThreadOperation(
|
||||
ThreadStatus::OperationType::OP_DBITERATOR);
|
||||
@@ -2715,6 +2836,8 @@ void StressTest::OperateDb(ThreadState* thread) {
|
||||
ScopedThreadOperation op(
|
||||
thread, StressOperationType::kCustom,
|
||||
ScopedThreadOperation::FinishAction::kFinishSingleOp);
|
||||
MaybeRecordOperationContext(thread, StressOperationType::kCustom,
|
||||
rand_column_families, rand_keys);
|
||||
TestCustomOperations(thread, rand_column_families);
|
||||
}
|
||||
}
|
||||
@@ -3195,11 +3318,23 @@ Status StressTest::TestIterateImpl(ThreadState* thread,
|
||||
expect_total_order = true;
|
||||
} else if (options_.prefix_extractor.get() == nullptr) {
|
||||
expect_total_order = true;
|
||||
} else if (thread->rand.OneIn(2)) {
|
||||
ro.prefix_same_as_start = true;
|
||||
}
|
||||
// Run SeekToFirst() after Seek() on the same table iterator under
|
||||
// prefix_same_as_start, without bounds. DBIter converts SeekToFirst() with a
|
||||
// lower bound to Seek(lower_bound). An upper bound inside the first prefix
|
||||
// ends the scan before the verifier can tell a truncated scan from a complete
|
||||
// one. prefix_hash returns nothing from a prefix SeekToFirst().
|
||||
const bool cover_seek_to_first_after_seek =
|
||||
ro.prefix_same_as_start && FLAGS_memtablerep != "prefix_hash" &&
|
||||
thread->rand.OneIn(8);
|
||||
|
||||
std::string upper_bound_str;
|
||||
Slice upper_bound;
|
||||
// Prefer no bound with no range query filtering; prefer bound with it
|
||||
if (FLAGS_use_sqfc_for_range_queries ^ thread->rand.OneIn(16)) {
|
||||
if (!cover_seek_to_first_after_seek &&
|
||||
(FLAGS_use_sqfc_for_range_queries ^ thread->rand.OneIn(16))) {
|
||||
// Note: upper_bound can be smaller than the seek key.
|
||||
const int64_t rand_upper_key = GenerateOneKey(thread, FLAGS_ops_per_thread);
|
||||
upper_bound_str = Key(rand_upper_key);
|
||||
@@ -3209,7 +3344,8 @@ Status StressTest::TestIterateImpl(ThreadState* thread,
|
||||
|
||||
std::string lower_bound_str;
|
||||
Slice lower_bound;
|
||||
if (FLAGS_use_sqfc_for_range_queries ^ thread->rand.OneIn(16)) {
|
||||
if (!cover_seek_to_first_after_seek &&
|
||||
(FLAGS_use_sqfc_for_range_queries ^ thread->rand.OneIn(16))) {
|
||||
// Note: lower_bound can be greater than the seek key.
|
||||
const int64_t rand_lower_key = GenerateOneKey(thread, FLAGS_ops_per_thread);
|
||||
lower_bound_str = Key(rand_lower_key);
|
||||
@@ -3298,8 +3434,13 @@ Status StressTest::TestIterateImpl(ThreadState* thread,
|
||||
|
||||
Slice key(key_str);
|
||||
|
||||
// DBIter converts SeekToFirst() with a lower bound to Seek(lower_bound),
|
||||
// which VerifyIterator() does not handle. SeekToLast() is only verified
|
||||
// under total order.
|
||||
const bool support_seek_to_first =
|
||||
expect_total_order && FLAGS_test_backward_scan;
|
||||
(expect_total_order && FLAGS_test_backward_scan) ||
|
||||
(ro.prefix_same_as_start && ro.iterate_lower_bound == nullptr &&
|
||||
FLAGS_memtablerep != "prefix_hash");
|
||||
const bool support_seek_to_last =
|
||||
expect_total_order && FLAGS_test_backward_scan;
|
||||
const bool support_seek_for_prev = FLAGS_test_backward_scan;
|
||||
@@ -3317,11 +3458,30 @@ Status StressTest::TestIterateImpl(ThreadState* thread,
|
||||
}
|
||||
|
||||
LastIterateOp last_op;
|
||||
if (support_seek_to_first && thread->rand.OneIn(100)) {
|
||||
// Under prefix_same_as_start a SeekToFirst() scan covers the prefix of the
|
||||
// first key.
|
||||
std::string verify_key_str;
|
||||
Slice verify_key = key;
|
||||
if (support_seek_to_first &&
|
||||
thread->rand.OneIn(cover_seek_to_first_after_seek ? 4 : 100)) {
|
||||
if (cover_seek_to_first_after_seek) {
|
||||
// If this Seek() read the first data block, the block would be cached
|
||||
// and SeekToFirst() would not read the file.
|
||||
const std::string pre_seek_key =
|
||||
Key(GenerateOneKey(thread, FLAGS_ops_per_thread));
|
||||
iter->Seek(pre_seek_key);
|
||||
cmp_iter->Seek(pre_seek_key);
|
||||
op_logs += "PreS " + Slice(pre_seek_key).ToString(true) + " ";
|
||||
thread->stats.AddSeekToFirstAfterSeek(1);
|
||||
}
|
||||
iter->SeekToFirst();
|
||||
cmp_iter->SeekToFirst();
|
||||
last_op = kLastOpSeekToFirst;
|
||||
op_logs += "STF ";
|
||||
if (ro.prefix_same_as_start && cmp_iter->Valid()) {
|
||||
verify_key_str = cmp_iter->key().ToString();
|
||||
verify_key = verify_key_str;
|
||||
}
|
||||
} else if (support_seek_to_last && thread->rand.OneIn(100)) {
|
||||
iter->SeekToLast();
|
||||
cmp_iter->SeekToLast();
|
||||
@@ -3356,11 +3516,14 @@ Status StressTest::TestIterateImpl(ThreadState* thread,
|
||||
}
|
||||
|
||||
VerifyIterator(thread, cmp_cfh, ro, iter.get(), cmp_iter.get(), last_op,
|
||||
key, rand_column_families, op_logs, verify_func, &diverged);
|
||||
verify_key, rand_column_families, op_logs, verify_func,
|
||||
&diverged);
|
||||
|
||||
// Prev() resets the table iterator's readahead lookup state.
|
||||
const bool no_reverse =
|
||||
(FLAGS_memtablerep == "prefix_hash" && !expect_total_order) ||
|
||||
!FLAGS_test_backward_scan;
|
||||
!FLAGS_test_backward_scan ||
|
||||
(cover_seek_to_first_after_seek && last_op == kLastOpSeekToFirst);
|
||||
for (uint64_t i = 0; i < FLAGS_num_iterations && iter->Valid(); ++i) {
|
||||
if (no_reverse || thread->rand.OneIn(2)) {
|
||||
iter->Next();
|
||||
@@ -3397,7 +3560,7 @@ Status StressTest::TestIterateImpl(ThreadState* thread,
|
||||
}
|
||||
|
||||
VerifyIterator(thread, cmp_cfh, ro, iter.get(), cmp_iter.get(), last_op,
|
||||
key, rand_column_families, op_logs, verify_func,
|
||||
verify_key, rand_column_families, op_logs, verify_func,
|
||||
&diverged);
|
||||
}
|
||||
|
||||
@@ -3445,13 +3608,13 @@ Status StressTest::TestGetCurrentWalFile() const {
|
||||
}
|
||||
|
||||
void StressTest::DumpIteratorDivergenceDiagnostics(
|
||||
ColumnFamilyHandle* cmp_cfh, const ReadOptions& ro, const Slice& seek_key,
|
||||
ColumnFamilyHandle* cmp_cfh, const ReadOptions& ro, const Slice& verify_key,
|
||||
const std::vector<int>& rand_column_families) const {
|
||||
fprintf(stderr,
|
||||
"Iterator divergence diagnostics: seek_key=%s, cmp_cf=%s, "
|
||||
"Iterator divergence diagnostics: verify_key=%s, cmp_cf=%s, "
|
||||
"prefix_extractor=%d, using_udi=%d, use_multi_cf_iterator=%d, "
|
||||
"selected_cf_count=%zu\n",
|
||||
seek_key.ToString(/*hex=*/true).c_str(), cmp_cfh->GetName().c_str(),
|
||||
verify_key.ToString(/*hex=*/true).c_str(), cmp_cfh->GetName().c_str(),
|
||||
static_cast<int>(options_.prefix_extractor != nullptr),
|
||||
static_cast<int>(ro.table_index_factory != nullptr),
|
||||
static_cast<int>(FLAGS_use_multi_cf_iterator),
|
||||
@@ -3474,7 +3637,7 @@ void StressTest::DumpIteratorDivergenceDiagnostics(
|
||||
auto dump_debug_iter = [&](const char* label, const ReadOptions& debug_ro,
|
||||
bool use_multi_cf_iter) {
|
||||
auto debug_iter = make_debug_iter(debug_ro, use_multi_cf_iter);
|
||||
debug_iter->Seek(seek_key);
|
||||
debug_iter->Seek(verify_key);
|
||||
|
||||
std::string sv_number;
|
||||
const Status prop_s = debug_iter->GetProperty(
|
||||
@@ -3546,9 +3709,9 @@ void StressTest::DumpIteratorDivergenceDiagnostics(
|
||||
template <typename IterType, typename VerifyFuncType>
|
||||
void StressTest::VerifyIterator(
|
||||
ThreadState* thread, ColumnFamilyHandle* cmp_cfh, const ReadOptions& ro,
|
||||
IterType* iter, Iterator* cmp_iter, LastIterateOp op, const Slice& seek_key,
|
||||
const std::vector<int>& rand_column_families, const std::string& op_logs,
|
||||
VerifyFuncType verify_func, bool* diverged) {
|
||||
IterType* iter, Iterator* cmp_iter, LastIterateOp op,
|
||||
const Slice& verify_key, const std::vector<int>& rand_column_families,
|
||||
const std::string& op_logs, VerifyFuncType verify_func, bool* diverged) {
|
||||
assert(diverged);
|
||||
|
||||
if (*diverged) {
|
||||
@@ -3573,7 +3736,7 @@ void StressTest::VerifyIterator(
|
||||
return;
|
||||
} else if (op == kLastOpSeek && ro.iterate_lower_bound != nullptr &&
|
||||
(options_.comparator->CompareWithoutTimestamp(
|
||||
*ro.iterate_lower_bound, /*a_has_ts=*/false, seek_key,
|
||||
*ro.iterate_lower_bound, /*a_has_ts=*/false, verify_key,
|
||||
/*b_has_ts=*/false) >= 0 ||
|
||||
(ro.iterate_upper_bound != nullptr &&
|
||||
options_.comparator->CompareWithoutTimestamp(
|
||||
@@ -3585,7 +3748,7 @@ void StressTest::VerifyIterator(
|
||||
return;
|
||||
} else if (op == kLastOpSeekForPrev && ro.iterate_upper_bound != nullptr &&
|
||||
(options_.comparator->CompareWithoutTimestamp(
|
||||
*ro.iterate_upper_bound, /*a_has_ts=*/false, seek_key,
|
||||
*ro.iterate_upper_bound, /*a_has_ts=*/false, verify_key,
|
||||
/*b_has_ts=*/false) <= 0 ||
|
||||
(ro.iterate_lower_bound != nullptr &&
|
||||
options_.comparator->CompareWithoutTimestamp(
|
||||
@@ -3600,9 +3763,9 @@ void StressTest::VerifyIterator(
|
||||
if (!ro.total_order_seek && options_.prefix_extractor != nullptr &&
|
||||
ro.iterate_lower_bound != nullptr) {
|
||||
const SliceTransform* prefix_extractor = options_.prefix_extractor.get();
|
||||
if (!prefix_extractor->InDomain(seek_key) ||
|
||||
if (!prefix_extractor->InDomain(verify_key) ||
|
||||
!prefix_extractor->InDomain(*ro.iterate_lower_bound) ||
|
||||
prefix_extractor->Transform(seek_key) !=
|
||||
prefix_extractor->Transform(verify_key) !=
|
||||
prefix_extractor->Transform(*ro.iterate_lower_bound)) {
|
||||
// ReadOptions requires the seek target and iterate_lower_bound to share
|
||||
// a prefix when prefix iteration is enabled. Skip verification for this
|
||||
@@ -3622,6 +3785,7 @@ void StressTest::VerifyIterator(
|
||||
<< ro.background_purge_on_iterator_cleanup
|
||||
<< ", total_order_seek: " << ro.total_order_seek
|
||||
<< ", auto_prefix_mode: " << ro.auto_prefix_mode
|
||||
<< ", prefix_same_as_start: " << ro.prefix_same_as_start
|
||||
<< ", iterate_upper_bound: "
|
||||
<< (ro.iterate_upper_bound
|
||||
? ro.iterate_upper_bound->ToString(true).c_str()
|
||||
@@ -3642,7 +3806,7 @@ void StressTest::VerifyIterator(
|
||||
|
||||
if (iter->Valid() && !cmp_iter->Valid()) {
|
||||
if (pe != nullptr) {
|
||||
if (!pe->InDomain(seek_key)) {
|
||||
if (!pe->InDomain(verify_key)) {
|
||||
// Prefix seek a non-in-domain key is undefined. Skip checking for
|
||||
// this scenario.
|
||||
*diverged = true;
|
||||
@@ -3651,7 +3815,7 @@ void StressTest::VerifyIterator(
|
||||
// out of range is iterator key is not in domain anymore.
|
||||
*diverged = true;
|
||||
return;
|
||||
} else if (pe->Transform(iter->key()) != pe->Transform(seek_key)) {
|
||||
} else if (pe->Transform(iter->key()) != pe->Transform(verify_key)) {
|
||||
*diverged = true;
|
||||
return;
|
||||
}
|
||||
@@ -3670,7 +3834,7 @@ void StressTest::VerifyIterator(
|
||||
const Slice& total_order_key = cmp_iter->key();
|
||||
|
||||
if (pe != nullptr) {
|
||||
if (!pe->InDomain(seek_key)) {
|
||||
if (!pe->InDomain(verify_key)) {
|
||||
// Prefix seek a non-in-domain key is undefined. Skip checking for
|
||||
// this scenario.
|
||||
*diverged = true;
|
||||
@@ -3678,13 +3842,13 @@ void StressTest::VerifyIterator(
|
||||
}
|
||||
|
||||
if (!pe->InDomain(total_order_key) ||
|
||||
pe->Transform(total_order_key) != pe->Transform(seek_key)) {
|
||||
pe->Transform(total_order_key) != pe->Transform(verify_key)) {
|
||||
// If the prefix is exhausted, the only thing needs to check
|
||||
// is the iterator isn't return a position in prefix.
|
||||
// Either way, checking can stop from here.
|
||||
*diverged = true;
|
||||
if (!iter->Valid() || !pe->InDomain(iter->key()) ||
|
||||
pe->Transform(iter->key()) != pe->Transform(seek_key)) {
|
||||
pe->Transform(iter->key()) != pe->Transform(verify_key)) {
|
||||
return;
|
||||
}
|
||||
fprintf(stderr,
|
||||
@@ -3735,7 +3899,7 @@ void StressTest::VerifyIterator(
|
||||
}
|
||||
|
||||
if (*diverged) {
|
||||
DumpIteratorDivergenceDiagnostics(cmp_cfh, ro, seek_key,
|
||||
DumpIteratorDivergenceDiagnostics(cmp_cfh, ro, verify_key,
|
||||
rand_column_families);
|
||||
fprintf(stderr, "VerifyIterator failed. Control CF %s\n",
|
||||
cmp_cfh->GetName().c_str());
|
||||
@@ -5732,9 +5896,7 @@ void StressTest::RecordManifestStateBeforeReopen() {
|
||||
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 = !NeedsFaultInjection();
|
||||
const bool no_manifest_writes_expected =
|
||||
no_fault_injection &&
|
||||
FLAGS_avoid_flush_during_recovery && // No flush during recovery
|
||||
!FLAGS_write_dbid_to_manifest; // No DB_ID write on open
|
||||
// Note: avoid_flush_during_shutdown is NOT required for STRICT mode.
|
||||
|
||||
@@ -43,6 +43,28 @@ class StressTest {
|
||||
// header, so they are not visible here.
|
||||
static bool IsErrorInjectedAndRetryable(const Status& error_s);
|
||||
|
||||
// Returns true if `error_s` is a non-injected, non-data-loss IO error that
|
||||
// --tolerate_non_injected_io_errors_for_remote_dbs asks us to tolerate while
|
||||
// a remote backend (--env_uri / --fs_uri) is in use. Infrastructure behind a
|
||||
// remote backend can fail a read transiently with no RocksDB bug involved.
|
||||
//
|
||||
// Defined out-of-line in db_stress_test_base.cc for the same reason as
|
||||
// IsErrorInjectedAndRetryable.
|
||||
static bool IsTolerableNonInjectedRemoteIOError(const Status& error_s);
|
||||
|
||||
// Returns true if `error_s`, surfaced by an operation during which
|
||||
// `injected_error_count` faults were injected, is retryable rather than a
|
||||
// verification failure.
|
||||
//
|
||||
// An injected-error status is only trusted when this operation actually
|
||||
// injected a fault, because otherwise the status did not come from fault
|
||||
// injection. A tolerated non-injected remote IO error has no injected fault
|
||||
// to correlate with, so `injected_error_count` must not gate it -- gating it
|
||||
// would make --tolerate_non_injected_io_errors_for_remote_dbs a no-op
|
||||
// whenever fault injection is disabled.
|
||||
static bool IsRetryableOperationError(int injected_error_count,
|
||||
const Status& error_s);
|
||||
|
||||
static bool IsTolerableCompactionFailure(const Status& s) {
|
||||
// TOOD (hx235): allow an exact list of tolerable failures under stress
|
||||
// test
|
||||
@@ -429,7 +451,8 @@ class StressTest {
|
||||
// - Not in best_efforts_recovery mode
|
||||
// - avoid_flush_during_recovery=true (no flush during recovery)
|
||||
// - write_dbid_to_manifest=0 (no DB_ID write on open)
|
||||
// - all fault injection flags are disabled
|
||||
// - metadata_write_fault_one_in=0 (no fault injection)
|
||||
// - open_metadata_write_fault_one_in=0 (no fault injection)
|
||||
// - MANIFEST not corrupted, not at size limit
|
||||
// Note: avoid_flush_during_shutdown is NOT required. If it leaves data
|
||||
// in WAL but avoid_flush_during_recovery=true prevents flushing it,
|
||||
@@ -444,16 +467,19 @@ class StressTest {
|
||||
// diverged = true if the two iterator is already diverged.
|
||||
// True if verification passed, false if not.
|
||||
// op_logs is the information to print when validation fails.
|
||||
// verify_key is the seek target, or for SeekToFirst() under
|
||||
// prefix_same_as_start the first key returned. Its prefix bounds the scan.
|
||||
template <typename IterType, typename VerifyFuncType>
|
||||
void VerifyIterator(ThreadState* thread, ColumnFamilyHandle* cmp_cfh,
|
||||
const ReadOptions& ro, IterType* iter, Iterator* cmp_iter,
|
||||
LastIterateOp op, const Slice& seek_key,
|
||||
LastIterateOp op, const Slice& verify_key,
|
||||
const std::vector<int>& rand_column_families,
|
||||
const std::string& op_logs, VerifyFuncType verifyFunc,
|
||||
bool* diverged);
|
||||
|
||||
void DumpIteratorDivergenceDiagnostics(
|
||||
ColumnFamilyHandle* cmp_cfh, const ReadOptions& ro, const Slice& seek_key,
|
||||
ColumnFamilyHandle* cmp_cfh, const ReadOptions& ro,
|
||||
const Slice& verify_key,
|
||||
const std::vector<int>& rand_column_families) const;
|
||||
|
||||
virtual Status TestBackupRestore(ThreadState* thread,
|
||||
|
||||
@@ -852,7 +852,7 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
key.ToString(true).c_str(), rand_keys[0]);
|
||||
}
|
||||
}
|
||||
} else if (injected_error_count == 0 || !IsErrorInjectedAndRetryable(s)) {
|
||||
} else if (!IsRetryableOperationError(injected_error_count, s)) {
|
||||
thread->shared->SetVerificationFailure();
|
||||
fprintf(stderr, "error : Get() returns %s for key: %s (%" PRIi64 ").\n",
|
||||
s.ToString().c_str(), key.ToString(true).c_str(), rand_keys[0]);
|
||||
@@ -1069,8 +1069,10 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
ThreadStatus::OperationType::OP_MULTIGET);
|
||||
}
|
||||
if (!tmp_s.ok() && !tmp_s.IsNotFound()) {
|
||||
fprintf(stderr, "Get error: %s\n", s.ToString().c_str());
|
||||
is_consistent = false;
|
||||
if (!IsTolerableNonInjectedRemoteIOError(tmp_s)) {
|
||||
fprintf(stderr, "Get error: %s\n", tmp_s.ToString().c_str());
|
||||
is_consistent = false;
|
||||
}
|
||||
} else if (!s.ok() && tmp_s.ok()) {
|
||||
fprintf(stderr,
|
||||
"MultiGet(%d) returned different results with key %s. "
|
||||
@@ -1126,7 +1128,7 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
} else if (s.IsMergeInProgress() && use_txn) {
|
||||
// With txn this is sometimes expected.
|
||||
thread->stats.AddGets(1, 1);
|
||||
} else if (injected_error_count == 0 || !IsErrorInjectedAndRetryable(s)) {
|
||||
} else if (!IsRetryableOperationError(injected_error_count, s)) {
|
||||
fprintf(stderr, "MultiGet error: %s\n", s.ToString().c_str());
|
||||
thread->stats.AddErrors(1);
|
||||
shared->SetVerificationFailure();
|
||||
@@ -1328,7 +1330,7 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
StringToHex(key_str).c_str(), rand_keys[0]);
|
||||
}
|
||||
}
|
||||
} else if (injected_error_count == 0 || !IsErrorInjectedAndRetryable(s)) {
|
||||
} else if (!IsRetryableOperationError(injected_error_count, s)) {
|
||||
fprintf(stderr,
|
||||
"error : GetEntity() returns %s for key: %s (%" PRIi64 ").\n",
|
||||
s.ToString().c_str(), StringToHex(key_str).c_str(), rand_keys[0]);
|
||||
@@ -1495,9 +1497,11 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
ran_cmp_get_entity = true;
|
||||
|
||||
if (!cmp_s.ok() && !cmp_s.IsNotFound()) {
|
||||
fprintf(stderr, "GetEntity error: %s\n",
|
||||
cmp_s.ToString().c_str());
|
||||
is_consistent = false;
|
||||
if (!IsTolerableNonInjectedRemoteIOError(cmp_s)) {
|
||||
fprintf(stderr, "GetEntity error: %s\n",
|
||||
cmp_s.ToString().c_str());
|
||||
is_consistent = false;
|
||||
}
|
||||
} else if (cmp_s.IsNotFound()) {
|
||||
if (s.ok()) {
|
||||
fprintf(
|
||||
@@ -1616,8 +1620,7 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
thread->stats.AddGets(1, 1);
|
||||
} else if (s.IsNotFound()) {
|
||||
thread->stats.AddGets(1, 0);
|
||||
} else if (injected_error_count == 0 ||
|
||||
!IsErrorInjectedAndRetryable(s)) {
|
||||
} else if (!IsRetryableOperationError(injected_error_count, s)) {
|
||||
fprintf(stderr, "MultiGetEntity error: %s\n", s.ToString().c_str());
|
||||
thread->stats.AddErrors(1);
|
||||
thread->shared->SetVerificationFailure();
|
||||
@@ -1933,7 +1936,7 @@ class NonBatchedOpsStressTest : public StressTest {
|
||||
|
||||
if (s.ok()) {
|
||||
thread->stats.AddPrefixes(1, count);
|
||||
} else if (injected_error_count == 0 || !IsErrorInjectedAndRetryable(s)) {
|
||||
} else if (!IsRetryableOperationError(injected_error_count, s)) {
|
||||
fprintf(stderr,
|
||||
"TestPrefixScan error: %s with ReadOptions::iterate_upper_bound: "
|
||||
"%s, prefix_same_as_start: %s \n",
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
---
|
||||
title: "Native Async/Coroutine Reads in RocksDB"
|
||||
layout: post
|
||||
author: joshkang97
|
||||
category: blog
|
||||
---
|
||||
|
||||
A point lookup that misses RocksDB's block cache can spend most of its time waiting for storage. The traditional way to keep more reads in flight is to add threads. That works, but each outstanding read parks a thread, carries a stack, and adds context-switching overhead.
|
||||
|
||||
RocksDB now has experimental asynchronous `Get` and `MultiGet` APIs backed by native C++ coroutines. When a read reaches storage, RocksDB can suspend the request, let its read-executor worker run another ready task, and resume the request when the filesystem reports completion. A small executor can therefore maintain more storage queue depth without requiring one blocked application thread per read.
|
||||
|
||||
These APIs are available in RocksDB 11.10.0.
|
||||
|
||||
This is primarily a throughput feature for I/O-bound point lookups. It does not make an individual device read faster. Its benefit comes from keeping the device busy and using CPU threads for runnable work.
|
||||
|
||||
## The API surface
|
||||
|
||||
RocksDB exposes the new read path through two public interfaces:
|
||||
|
||||
* `DB::GetAsync` and `DB::MultiGetAsync` return immediately on the native path and report completion through `AsyncCallback::OnComplete`.
|
||||
* `CoroDB::CoGet` and `CoroDB::CoMultiGet` return lazy `folly::coro::Task` objects. `CoGet` produces a `Status`; `CoMultiGet` fills the same per-key values and statuses as synchronous `MultiGet`.
|
||||
|
||||
The callback APIs suit applications that do not expose Folly tasks at their boundaries. The `CoroDB` APIs let coroutine-based callers await RocksDB directly, avoiding an application-side callback-to-`Baton` adapter and its extra completion handoff.
|
||||
|
||||
Native execution requires RocksDB to be built with Folly and `USE_COROUTINES=1`.
|
||||
|
||||
Neither interface requires `ReadOptions::async_io`. That flag continues to control the older internal async-I/O optimizations for synchronous `MultiGet` and iterators.
|
||||
|
||||
The task APIs are lazy: no read begins until a task is awaited or started. Both interfaces take pointer and reference parameters, so keep the DB, column-family handles, `ReadOptions`, keys and their backing storage, output objects, and callback when applicable alive until completion. The current APIs do not provide a handle for cancelling a submitted read; outstanding filesystem requests are driven to completion.
|
||||
|
||||
## Two meanings of asynchronous
|
||||
|
||||
RocksDB has used coroutines and async I/O internally before. The [2022 asynchronous I/O work](https://rocksdb.org/blog/2022/10/07/asynchronous-io-in-rocksdb.html) lets a synchronous `MultiGet` overlap reads from multiple SST files and lets iterators prefetch in the background. The caller still waits inside the synchronous API until the whole operation finishes.
|
||||
|
||||
The new APIs make the operation itself suspendable:
|
||||
|
||||
| Interface | Caller contract | Where concurrency comes from |
|
||||
| --- | --- | --- |
|
||||
| `Get` / `MultiGet` | Returns the completed result | More calling threads |
|
||||
| `ReadOptions::async_io` with synchronous `MultiGet` | Caller still waits | Overlapping child I/O inside one call |
|
||||
| `GetAsync` / `MultiGetAsync` | Completes through a callback, which can be inline on fallback | Requests scheduled on RocksDB's read executor |
|
||||
| `CoroDB::CoGet` / `CoMultiGet` | Returns a lazy Folly task that can be awaited | Suspended tasks multiplexed on the read executor |
|
||||
|
||||
The distinction matters. An async wrapper can free the caller while still blocking a worker in the filesystem. Conversely, internal async I/O can overlap storage requests while the public API remains synchronous. The full benefit requires suspendability at both layers.
|
||||
|
||||
<div style="overflow-x: auto; margin-left: auto; margin-right: auto; width: 100%;">
|
||||
<a href="/static/images/native-coroutine-reads/sync-vs-coroutine.svg">
|
||||
<img src="/static/images/native-coroutine-reads/sync-vs-coroutine.svg" alt="Three stacked timelines comparing two-block MultiGet requests from one caller thread" style="display: block; min-width: 900px; width: 100%;" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
*Each illustrated request fetches one cache-missed block from each of two eligible SST files. `ReadOptions::async_io` overlaps those reads inside one synchronous `MultiGet`, but the caller cannot start B until A returns. In the third row, the caller already runs on a RocksDB read-executor `EventBase`: it dispatches A and B, then the event loop starts their block reads and later invokes both callbacks.*
|
||||
{: style="text-align: center"}
|
||||
|
||||
## What happens on an SST cache miss
|
||||
|
||||
A single native `Get` follows the normal RocksDB lookup path through the memtable, Version, table cache, block-based table reader, and random-access file reader. Most of that work is ordinary synchronous CPU work. The new suspension point is at the filesystem boundary:
|
||||
|
||||
1. `CoGet` discovers the concrete DB's coroutine capability and binds the inner read to a read-executor `EventBase`.
|
||||
2. RocksDB checks the memtable and caches. On an SST block miss, it submits the storage read and suspends the database coroutine.
|
||||
3. While storage is outstanding, that read no longer occupies the executor. The event loop is free to accept and run another ready request.
|
||||
4. When the filesystem reports completion, RocksDB posts the suspended read back to the same read-executor `EventBase` thread that submitted the I/O.
|
||||
5. RocksDB verifies and decodes the block, populates the cache and user output, and completes the inner task.
|
||||
6. The awaiting caller continues on its own executor after the inner task completes.
|
||||
|
||||
The database coroutine itself does not migrate between read-executor threads: its pre-I/O and post-I/O slices run on the same selected `EventBase` thread. This affinity avoids an extra cross-thread handoff and its context-switch cost when the read completes. Returning the completed result to the awaiting caller is a separate executor boundary.
|
||||
|
||||
<div style="overflow-x: auto; margin-left: auto; margin-right: auto; width: 100%;">
|
||||
<a href="/static/images/native-coroutine-reads/thread-handoff.svg">
|
||||
<img src="/static/images/native-coroutine-reads/thread-handoff.svg" alt="Top-to-bottom sequence showing SubmitReadAsync, coroutine suspension, storage completion, and resumption" style="display: block; min-width: 960px; width: 100%;" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
*After `SubmitReadAsync` returns, the database coroutine suspends and the read executor can accept another request. The filesystem completion makes the read runnable again, and RocksDB resumes it to verify and decode the block.*
|
||||
{: style="text-align: center"}
|
||||
|
||||
The filesystem callback makes the suspended read runnable by posting back to its selected event loop. The same `EventBase` thread that submitted the read performs the post-I/O RocksDB work.
|
||||
|
||||
The callback API has a different final handoff. On the native async path, `AsyncCallback::OnComplete` runs inside the detached task on the RocksDB read executor. If there is no coroutine capability or no read executor, RocksDB uses the synchronous API and may invoke `OnComplete` inline before `GetAsync` returns. Callback users must be correct in both cases and must not start another async read from `OnComplete`.
|
||||
|
||||
For `MultiGet`, RocksDB additionally creates child tasks for eligible, non-overlapping SST files within a level and awaits them together. This is logical concurrency on an `EventBase`, not one OS thread per SST. Each child can submit one or more block reads, suspend, and let other children or requests run.
|
||||
|
||||
## Filesystem integration
|
||||
|
||||
Filesystem implementers support two related paths:
|
||||
|
||||
* Synchronous `MultiGet` with `ReadOptions::async_io` uses the existing `FSRandomAccessFile::ReadAsync`, `FileSystem::Poll`, and `FileSystem::AbortIO` APIs. The filesystem also advertises `FSSupportedOps::kAsyncIO`.
|
||||
* Native `GetAsync`, `MultiGetAsync`, `CoGet`, and `CoMultiGet` additionally require `FileSystem::GetReadExecutor` and `FSRandomAccessFile::SubmitReadAsync`. `SetReadIOExecutorThreads` lets RocksDB apply `DBOptions::read_io_executor_threads` to that executor.
|
||||
|
||||
The event-loop-backed `IOExecutor` is necessary because an I/O completion only makes a suspended coroutine ready; it does not resume the coroutine by itself. RocksDB runs each read coroutine on one of the executor's `EventBase`s. After the filesystem invokes the `SubmitReadAsync` callback, RocksDB posts the continuation back to that same event loop. Because an `EventBase` is thread-affine, the coroutine resumes on the thread that submitted the read, avoiding an additional cross-thread context switch.
|
||||
|
||||
`SubmitReadAsync` must populate the `FSReadRequest` and invoke its callback exactly once. It returns `true` for a non-blocking submission and `false` when it used the synchronous fallback. If `GetReadExecutor` returns null, the native APIs use their outer synchronous fallback instead.
|
||||
|
||||
On supported Linux builds, the default POSIX filesystem supplies these hooks with a Folly `IOExecutor`. Each read-executor `EventBase` attempts to use `folly::IoUringBackend`, which submits and receives completions through io_uring.
|
||||
|
||||
## Statistics across suspension
|
||||
|
||||
`PerfContext` and `IOStatsContext` traditionally live in thread-local storage (TLS), which works while one operation owns a thread from entry to return. A coroutine read stays on one `EventBase` thread, but it does not own that thread while suspended: request B can reuse the same worker and its TLS before request A resumes.
|
||||
|
||||
RocksDB gives each coroutine read its own `PerfContext` and `IOStatsContext`. A Folly `RequestContext` loads those contexts into TLS only while that request is active, saves them before suspension, and restores them on resumption. It also carries the submitting thread's statistics configuration.
|
||||
|
||||
Collecting and transferring these request-local statistics adds request-context and TLS bookkeeping. Applications that do not consume them should disable both `PerfContext` and `IOStatsContext` on every read-executor thread for optimal performance.
|
||||
|
||||
<div style="overflow-x: auto; margin-left: auto; margin-right: auto; width: 100%;">
|
||||
<a href="/static/images/native-coroutine-reads/tls-across-suspension.svg">
|
||||
<img src="/static/images/native-coroutine-reads/tls-across-suspension.svg" alt="Per-request statistics and CPU accounting survive coroutine interleaving" style="display: block; min-width: 900px; width: 100%;" />
|
||||
</a>
|
||||
</div>
|
||||
|
||||
*Request contexts prevent two reads interleaved on one event-loop worker from sharing counters. CPU time is accumulated only during the slices in which that request is running.*
|
||||
{: style="text-align: center"}
|
||||
|
||||
### Wall-clock timers
|
||||
|
||||
Suspension does not pause RocksDB's ordinary latency timers. A timer's start point remains in the coroutine frame while the request context saves its counters. After resumption, RocksDB restores those counters and the timer records its full start-to-finish duration. Consequently `PerfContext::get_from_output_files_time`, `PerfContext::block_read_time`, and `IOStatsContext::read_nanos` include storage wait.
|
||||
|
||||
### CPU time
|
||||
|
||||
Wall time and CPU time need different treatment around a suspension. End-to-end wall time should include the device wait. A CPU timer left running across `co_await`, however, could charge request A for time spent running request B on the same event-loop worker.
|
||||
|
||||
At `PerfLevel::kEnableTimeAndCPUTimeExceptForMutex`, RocksDB starts the thread CPU clock when a request's context is installed and stops it before that context is removed. `PerfContext::get_cpu_nanos` is the sum of those active slices. It excludes time waiting for I/O, CPU consumed by another coroutine between A's slices, and work performed later in `OnComplete`.
|
||||
|
||||
Some narrower CPU timers that rely on a synchronous scope cannot safely span asynchronous I/O. In particular, `PerfContext::block_read_cpu_time` and `IOStatsContext::cpu_read_nanos` are not supported for async reads.
|
||||
|
||||
## Why not increase application threads?
|
||||
|
||||
More application threads can also keep more reads in flight, but each cache miss occupies a thread until storage responds. Raising the thread count therefore grows per-thread stack memory, scheduler work, and context switching even though the parked threads are not consuming CPU while they wait. At high concurrency, those costs compete with the CPU work needed to process completed reads.
|
||||
|
||||
Thread pools are also difficult to resize dynamically. The useful count changes with device latency, cache-hit ratio, request rate, and the amount of CPU work after each read. Increasing a pool under load can create a burst of runnable work and contention; reducing it safely requires waiting for workers and their in-flight requests to drain. A fixed count chosen for the worst case wastes resources during ordinary operation, while a smaller count can leave storage queue depth unused during a latency spike.
|
||||
|
||||
Coroutines separate **concurrency** from **threads**. The application can vary the number of in-flight tasks without continually resizing its OS-thread pool. Suspended reads retain their coroutine frames, while a small read executor stays focused on runnable work. Coroutines still require backpressure, but that limit can describe outstanding requests instead of parked threads.
|
||||
|
||||
## Performance
|
||||
|
||||
I generated an 11 GB database with 11 million 16-byte keys and 1024-byte values. Compression was disabled, the block size was 4 KiB, reads used direct I/O, and the RocksDB block cache was disabled.
|
||||
|
||||
I compared 4, 20, 40, and 60 concurrent tasks on four pinned cores. In the synchronous version, each task is an OS thread. In the coroutine version, the same number of coroutines runs on a four-thread read executor. I tested `MultiGet` batches of 1, 2, 4, and 8 keys; results are key lookups per second.
|
||||
|
||||
The coroutine columns include the throughput change relative to the matching synchronous result:
|
||||
|
||||
| Tasks | Batch 1 sync | Batch 1 coro | Batch 2 sync | Batch 2 coro | Batch 4 sync | Batch 4 coro | Batch 8 sync | Batch 8 coro |
|
||||
| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| 4 | 29,198 | 25,199 (-14%) | 30,087 | 50,262 (+67%) | 29,140 | 81,288 (+179%) | 26,019 | 105,366 (+305%) |
|
||||
| 20 | 75,094 | 97,137 (+29%) | 85,481 | 127,620 (+49%) | 80,865 | 148,247 (+83%) | 85,569 | 174,112 (+103%) |
|
||||
| 40 | 90,827 | 134,714 (+48%) | 94,392 | 162,181 (+72%) | 92,818 | 166,160 (+79%) | 90,856 | 163,306 (+80%) |
|
||||
| 60 | 88,461 | 147,062 (+66%) | 89,034 | 170,045 (+91%) | 75,060 | 178,424 (+138%) | 86,409 | 177,344 (+105%) |
|
||||
|
||||
For the batch-1 synchronous workload, an OS thread cannot use its core while blocked on I/O. Adding more OS threads overlaps those waits and increases CPU utilization, but also increases context switching.
|
||||
|
||||
| Tasks | Average CPU cores | Four-core CPU utilization | Context switches/second |
|
||||
| ---: | ---: | ---: | ---: |
|
||||
| 4 | 1.03 | 25.9% | 61,192 |
|
||||
| 20 | 3.72 | 93.0% | 210,756 |
|
||||
| 40 | 3.77 | 94.3% | 212,801 |
|
||||
| 60 | 3.81 | 95.2% | 211,355 |
|
||||
|
||||
With four threads on four cores, the workload uses only 25.9% of the available CPU. Multiplexing 20 or more threads on the same cores raises utilization above 93%, while increasing context switches from about 61,000 to 211,000 per second.
|
||||
|
||||
When there is no extra I/O parallelism to exploit, as with four batch-1 tasks, the synchronous version is faster because it avoids coroutine overhead. Larger batches let `CoMultiGet` expose more concurrent reads, so the coroutine path overtakes the synchronous version. At higher concurrency the gains flatten as the workload approaches saturating the CPUs.
|
||||
|
||||
### Async I/O comparison
|
||||
|
||||
I also compared synchronous `MultiGet` with `ReadOptions::async_io` against `CoMultiGet`. Async I/O does not support direct I/O, so this comparison used buffered reads and cleared the Linux page cache before every run. The RocksDB block cache was also disabled.
|
||||
|
||||
| Tasks | Batch 1 async I/O | Batch 1 coro | Batch 2 async I/O | Batch 2 coro | Batch 4 async I/O | Batch 4 coro | Batch 8 async I/O | Batch 8 coro |
|
||||
| ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |
|
||||
| 4 | 35,598 | 34,455 (-3.2%) | 59,121 | 58,214 (-1.5%) | 90,923 | 87,929 (-3.3%) | 133,098 | 116,565 (-12.4%) |
|
||||
| 20 | 108,945 | 120,807 (+10.9%) | 123,399 | 140,163 (+13.6%) | 134,297 | 148,535 (+10.6%) | 154,842 | 166,865 (+7.8%) |
|
||||
| 40 | 116,002 | 144,819 (+24.8%) | 125,451 | 151,117 (+20.5%) | 136,350 | 190,499 (+39.7%) | 142,375 | 188,398 (+32.3%) |
|
||||
| 60 | 113,170 | 154,815 (+36.8%) | 120,759 | 186,808 (+54.7%) | 129,513 | 195,746 (+51.1%) | 145,191 | 200,781 (+38.3%) |
|
||||
@@ -128,7 +128,7 @@ When `ReadOptions::prefix_same_as_start` is true:
|
||||
- On `Seek(key)`, the prefix of the seek key is saved in `prefix_` (via `prefix_extractor`)
|
||||
- On each `Next()`, if the current key's prefix differs from `prefix_`, `Valid()` returns false
|
||||
- Enables prefix bloom filter checks in SST file iterators via `CheckPrefixMayMatch()` in `BlockBasedTableIterator`
|
||||
- Also enables readahead trimming to avoid prefetching blocks outside the prefix boundary
|
||||
- Also enables readahead trimming to avoid prefetching blocks outside the prefix boundary, for iterators positioned with `Seek()`. A `SeekToFirst()` trims by prefix only when `iterate_lower_bound` is set, because `DBIter::SeekToFirst()` then delegates to `Seek(*iterate_lower_bound)`
|
||||
|
||||
### total_order_seek
|
||||
|
||||
|
||||
@@ -110,7 +110,7 @@ A `readaheadsize_cb` callback bound to `BlockBasedTableIterator::BlockCacheLooku
|
||||
|
||||
1. **Cache hit trimming**: Iterates ahead in the index, looks up blocks in the block cache, and trims readahead to avoid re-fetching blocks already cached
|
||||
2. **Upper bound trimming**: Trims readahead so data beyond `iterate_upper_bound` is not prefetched
|
||||
3. **Prefix trimming**: When `prefix_same_as_start` is true, trims readahead at the prefix boundary
|
||||
3. **Prefix trimming**: When `prefix_same_as_start` is true and the iterator was positioned with `Seek()`, trims readahead at the seek key's prefix boundary. A bare `SeekToFirst()` does not trim by prefix, because its prefix is not known when the first readahead decision is made; with `iterate_lower_bound` set it does, since `DBIter::SeekToFirst()` delegates to `Seek(*iterate_lower_bound)`
|
||||
|
||||
`DecreaseReadAheadIfEligible()` reduces readahead size when prefetched blocks were already in cache (wasted prefetch).
|
||||
|
||||
|
||||
@@ -0,0 +1,138 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1100" height="1070" viewBox="0 0 1100 1070" font-family="Helvetica, Arial, sans-serif">
|
||||
<title>Two MultiGet requests from one caller thread</title>
|
||||
<desc>Three stacked timelines compare plain MultiGet, synchronous MultiGet with async_io, and the callback-based MultiGetAsync API. Requests A and B each fetch two cache-missed blocks from separate eligible SST files.</desc>
|
||||
<defs>
|
||||
<marker id="arrow-row" markerWidth="10" markerHeight="10" refX="8" refY="3.5" orient="auto">
|
||||
<path d="M0,0 L8,3.5 L0,7 z" fill="#607d8b"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect width="1100" height="1070" fill="#ffffff"/>
|
||||
<text x="550" y="28" text-anchor="middle" font-size="21" font-weight="bold" fill="#263238">Two MultiGet requests from one caller thread</text>
|
||||
<text x="550" y="51" text-anchor="middle" font-size="12" fill="#546e7a">A and B each miss one block in each of two eligible SST files.</text>
|
||||
|
||||
<!-- Row 1: plain synchronous MultiGet. -->
|
||||
<rect x="20" y="70" width="1060" height="285" rx="12" fill="#fafafa" stroke="#cfd8dc" stroke-width="1.5"/>
|
||||
<text x="45" y="101" font-size="18" font-weight="bold" fill="#263238">1. Plain MultiGet</text>
|
||||
<text x="1048" y="101" text-anchor="end" font-size="11" font-weight="bold" fill="#b71c1c">synchronous public API</text>
|
||||
|
||||
<rect x="45" y="119" width="320" height="91" rx="7" fill="#eceff1" stroke="#90a4ae"/>
|
||||
<text x="61" y="141" font-size="10" font-family="monospace" fill="#263238">read_options.async_io = false;</text>
|
||||
<text x="61" y="165" font-size="9.5" font-family="monospace" fill="#0d47a1">db->MultiGet(...A...); // fetches blocks A1, A2</text>
|
||||
<text x="61" y="189" font-size="9.5" font-family="monospace" fill="#4527a0">db->MultiGet(...B...); // fetches blocks B1, B2</text>
|
||||
|
||||
<text x="395" y="136" font-size="11" font-weight="bold" fill="#455a64">Caller</text>
|
||||
<line x1="500" y1="132" x2="1040" y2="132" stroke="#b0bec5" stroke-width="2" marker-end="url(#arrow-row)"/>
|
||||
<rect x="515" y="113" width="236" height="38" rx="5" fill="#fff3e0" stroke="#1565c0" stroke-width="1.5"/>
|
||||
<text x="633" y="128" text-anchor="middle" font-size="10" font-weight="bold" fill="#0d47a1">A: MultiGet waits</text>
|
||||
<text x="633" y="143" text-anchor="middle" font-size="9" fill="#0d47a1">returns after A1 + A2</text>
|
||||
<rect x="751" y="113" width="236" height="38" rx="5" fill="#fff3e0" stroke="#5e35b1" stroke-width="1.5"/>
|
||||
<text x="869" y="128" text-anchor="middle" font-size="10" font-weight="bold" fill="#4527a0">B: MultiGet waits</text>
|
||||
<text x="869" y="143" text-anchor="middle" font-size="9" fill="#4527a0">called only after A returns</text>
|
||||
|
||||
<text x="395" y="195" font-size="11" font-weight="bold" fill="#455a64">Block I/O</text>
|
||||
<line x1="466" y1="191" x2="1040" y2="191" stroke="#cfd8dc" stroke-width="1.5"/>
|
||||
<rect x="530" y="175" width="97" height="32" rx="5" fill="#bbdefb" stroke="#1565c0"/>
|
||||
<text x="579" y="195" text-anchor="middle" font-size="10" fill="#0d47a1">block A1</text>
|
||||
<rect x="627" y="175" width="97" height="32" rx="5" fill="#bbdefb" stroke="#1565c0"/>
|
||||
<text x="676" y="195" text-anchor="middle" font-size="10" fill="#0d47a1">block A2</text>
|
||||
<rect x="766" y="175" width="97" height="32" rx="5" fill="#d1c4e9" stroke="#5e35b1"/>
|
||||
<text x="815" y="195" text-anchor="middle" font-size="10" fill="#4527a0">block B1</text>
|
||||
<rect x="863" y="175" width="97" height="32" rx="5" fill="#d1c4e9" stroke="#5e35b1"/>
|
||||
<text x="912" y="195" text-anchor="middle" font-size="10" fill="#4527a0">block B2</text>
|
||||
|
||||
<rect x="45" y="237" width="990" height="88" rx="7" fill="#ffebee" stroke="#ef9a9a"/>
|
||||
<text x="540" y="262" text-anchor="middle" font-size="13" font-weight="bold" fill="#b71c1c">A completes before the thread can submit B.</text>
|
||||
<text x="540" y="286" text-anchor="middle" font-size="11" fill="#455a64">Without async I/O, the two eligible SST-file reads for A occur one after the other;</text>
|
||||
<text x="540" y="306" text-anchor="middle" font-size="11" fill="#455a64">then the same caller thread runs B and fetches its two blocks.</text>
|
||||
|
||||
<!-- Row 2: synchronous MultiGet with internal async I/O. -->
|
||||
<rect x="20" y="375" width="1060" height="285" rx="12" fill="#fffdf8" stroke="#ffcc80" stroke-width="1.5"/>
|
||||
<text x="45" y="406" font-size="18" font-weight="bold" fill="#263238">2. MultiGet with ReadOptions::async_io</text>
|
||||
<text x="1048" y="406" text-anchor="end" font-size="11" font-weight="bold" fill="#e65100">async I/O inside a synchronous call</text>
|
||||
|
||||
<rect x="45" y="424" width="320" height="91" rx="7" fill="#fff8e1" stroke="#ffb74d"/>
|
||||
<text x="61" y="446" font-size="10" font-family="monospace" fill="#263238">read_options.async_io = true;</text>
|
||||
<text x="61" y="470" font-size="9.5" font-family="monospace" fill="#0d47a1">db->MultiGet(...A...); // fetches blocks A1, A2</text>
|
||||
<text x="61" y="494" font-size="9.5" font-family="monospace" fill="#4527a0">db->MultiGet(...B...); // fetches blocks B1, B2</text>
|
||||
|
||||
<text x="395" y="441" font-size="11" font-weight="bold" fill="#455a64">Caller</text>
|
||||
<line x1="500" y1="437" x2="1040" y2="437" stroke="#b0bec5" stroke-width="2" marker-end="url(#arrow-row)"/>
|
||||
<rect x="515" y="418" width="236" height="38" rx="5" fill="#fff3e0" stroke="#1565c0" stroke-width="1.5"/>
|
||||
<text x="633" y="433" text-anchor="middle" font-size="10" font-weight="bold" fill="#0d47a1">A: MultiGet waits</text>
|
||||
<text x="633" y="448" text-anchor="middle" font-size="9" fill="#0d47a1">A1 + A2 can overlap</text>
|
||||
<rect x="751" y="418" width="236" height="38" rx="5" fill="#fff3e0" stroke="#5e35b1" stroke-width="1.5"/>
|
||||
<text x="869" y="433" text-anchor="middle" font-size="10" font-weight="bold" fill="#4527a0">B: MultiGet waits</text>
|
||||
<text x="869" y="448" text-anchor="middle" font-size="9" fill="#4527a0">called only after A returns</text>
|
||||
|
||||
<text x="395" y="500" font-size="11" font-weight="bold" fill="#455a64">Block I/O</text>
|
||||
<line x1="466" y1="496" x2="1040" y2="496" stroke="#cfd8dc" stroke-width="1.5"/>
|
||||
<rect x="530" y="473" width="165" height="28" rx="5" fill="#bbdefb" stroke="#1565c0"/>
|
||||
<text x="613" y="491" text-anchor="middle" font-size="10" fill="#0d47a1">block A1</text>
|
||||
<rect x="552" y="503" width="165" height="28" rx="5" fill="#bbdefb" stroke="#1565c0"/>
|
||||
<text x="635" y="521" text-anchor="middle" font-size="10" fill="#0d47a1">block A2</text>
|
||||
<rect x="766" y="473" width="165" height="28" rx="5" fill="#d1c4e9" stroke="#5e35b1"/>
|
||||
<text x="849" y="491" text-anchor="middle" font-size="10" fill="#4527a0">block B1</text>
|
||||
<rect x="788" y="503" width="165" height="28" rx="5" fill="#d1c4e9" stroke="#5e35b1"/>
|
||||
<text x="871" y="521" text-anchor="middle" font-size="10" fill="#4527a0">block B2</text>
|
||||
|
||||
<rect x="45" y="542" width="990" height="88" rx="7" fill="#fff8e1" stroke="#ffb74d"/>
|
||||
<text x="540" y="567" text-anchor="middle" font-size="13" font-weight="bold" fill="#e65100">The two block reads inside A overlap, but the public call still waits.</text>
|
||||
<text x="540" y="591" text-anchor="middle" font-size="11" fill="#455a64">The caller thread does not submit B until A returns. Then async_io overlaps B1 and B2</text>
|
||||
<text x="540" y="611" text-anchor="middle" font-size="11" fill="#455a64">inside the second synchronous MultiGet.</text>
|
||||
|
||||
<!-- Row 3: callback-based async API. -->
|
||||
<rect x="20" y="680" width="1060" height="330" rx="12" fill="#f8fbff" stroke="#90caf9" stroke-width="1.5"/>
|
||||
<text x="45" y="711" font-size="18" font-weight="bold" fill="#263238">3. MultiGetAsync (callback API)</text>
|
||||
<text x="1048" y="711" text-anchor="end" font-size="11" font-weight="bold" fill="#0d47a1">caller runs on the read executor</text>
|
||||
|
||||
<rect x="45" y="729" width="350" height="114" rx="7" fill="#e3f2fd" stroke="#64b5f6"/>
|
||||
<text x="61" y="747" font-size="8.5" fill="#455a64">// Already running on a RocksDB read-executor EventBase</text>
|
||||
<text x="61" y="766" font-size="9.5" font-family="monospace" fill="#0d47a1">db->MultiGetAsync(...A..., callback_a);</text>
|
||||
<text x="78" y="783" font-size="8.5" font-family="monospace" fill="#0d47a1">// fetches blocks A1, A2</text>
|
||||
<text x="61" y="805" font-size="9.5" font-family="monospace" fill="#4527a0">db->MultiGetAsync(...B..., callback_b);</text>
|
||||
<text x="78" y="822" font-size="8.5" font-family="monospace" fill="#4527a0">// fetches blocks B1, B2</text>
|
||||
<text x="61" y="838" font-size="8" fill="#455a64">Native path shown; fallback may complete inline.</text>
|
||||
|
||||
<text x="420" y="746" font-size="11" font-weight="bold" fill="#455a64">Caller</text>
|
||||
<line x1="535" y1="742" x2="1040" y2="742" stroke="#b0bec5" stroke-width="2" marker-end="url(#arrow-row)"/>
|
||||
<rect x="550" y="723" width="90" height="38" rx="5" fill="#bbdefb" stroke="#1565c0" stroke-width="1.5"/>
|
||||
<text x="595" y="738" text-anchor="middle" font-size="9" font-weight="bold" fill="#0d47a1">submit A</text>
|
||||
<text x="595" y="753" text-anchor="middle" font-size="8" fill="#0d47a1">return</text>
|
||||
<rect x="640" y="723" width="90" height="38" rx="5" fill="#d1c4e9" stroke="#5e35b1" stroke-width="1.5"/>
|
||||
<text x="685" y="738" text-anchor="middle" font-size="9" font-weight="bold" fill="#4527a0">submit B</text>
|
||||
<text x="685" y="753" text-anchor="middle" font-size="8" fill="#4527a0">return</text>
|
||||
<rect x="730" y="723" width="190" height="38" rx="5" fill="#e8f5e9" stroke="#81c784"/>
|
||||
<text x="825" y="738" text-anchor="middle" font-size="9" font-weight="bold" fill="#2e7d32">A and B in flight</text>
|
||||
<text x="825" y="753" text-anchor="middle" font-size="8" fill="#2e7d32">callbacks complete later</text>
|
||||
<rect x="920" y="723" width="58" height="38" rx="5" fill="#bbdefb" stroke="#1565c0"/>
|
||||
<text x="949" y="738" text-anchor="middle" font-size="8" fill="#0d47a1">callback A</text>
|
||||
<text x="949" y="751" text-anchor="middle" font-size="7" fill="#0d47a1">OnComplete</text>
|
||||
<rect x="978" y="723" width="58" height="38" rx="5" fill="#d1c4e9" stroke="#5e35b1"/>
|
||||
<text x="1007" y="738" text-anchor="middle" font-size="8" fill="#4527a0">callback B</text>
|
||||
<text x="1007" y="751" text-anchor="middle" font-size="7" fill="#4527a0">OnComplete</text>
|
||||
|
||||
<text x="420" y="835" font-size="11" font-weight="bold" fill="#455a64">Block I/O</text>
|
||||
<line x1="491" y1="831" x2="1040" y2="831" stroke="#cfd8dc" stroke-width="1.5"/>
|
||||
<rect x="645" y="807" width="245" height="28" rx="5" fill="#bbdefb" stroke="#1565c0"/>
|
||||
<text x="768" y="825" text-anchor="middle" font-size="10" fill="#0d47a1">block A1</text>
|
||||
<rect x="660" y="837" width="245" height="28" rx="5" fill="#bbdefb" stroke="#1565c0"/>
|
||||
<text x="783" y="855" text-anchor="middle" font-size="10" fill="#0d47a1">block A2</text>
|
||||
<rect x="745" y="867" width="210" height="28" rx="5" fill="#d1c4e9" stroke="#5e35b1"/>
|
||||
<text x="850" y="885" text-anchor="middle" font-size="10" fill="#4527a0">block B1</text>
|
||||
<rect x="760" y="897" width="210" height="28" rx="5" fill="#d1c4e9" stroke="#5e35b1"/>
|
||||
<text x="865" y="915" text-anchor="middle" font-size="10" fill="#4527a0">block B2</text>
|
||||
|
||||
<rect x="45" y="865" width="350" height="113" rx="7" fill="#e3f2fd" stroke="#64b5f6"/>
|
||||
<text x="220" y="891" text-anchor="middle" font-size="13" font-weight="bold" fill="#0d47a1">A and B are both in flight.</text>
|
||||
<text x="220" y="916" text-anchor="middle" font-size="11" fill="#455a64">The thread submits A, then B.</text>
|
||||
<text x="220" y="939" text-anchor="middle" font-size="11" fill="#455a64">Their block reads begin after submission;</text>
|
||||
<text x="220" y="959" text-anchor="middle" font-size="11" fill="#455a64">both callbacks complete later.</text>
|
||||
|
||||
<rect x="300" y="1035" width="16" height="12" rx="2" fill="#bbdefb" stroke="#1565c0"/>
|
||||
<text x="322" y="1045" font-size="10" fill="#546e7a">request A / its blocks</text>
|
||||
<rect x="470" y="1035" width="16" height="12" rx="2" fill="#d1c4e9" stroke="#5e35b1"/>
|
||||
<text x="492" y="1045" font-size="10" fill="#546e7a">request B / its blocks</text>
|
||||
<rect x="640" y="1035" width="16" height="12" fill="#fff3e0" stroke="#fb8c00"/>
|
||||
<text x="662" y="1045" font-size="10" fill="#546e7a">caller waits synchronously</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 12 KiB |
@@ -0,0 +1,103 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="1000" height="740" viewBox="0 0 1000 740" font-family="Helvetica, Arial, sans-serif">
|
||||
<title>Suspension and resumption of a native RocksDB coroutine read</title>
|
||||
<desc>The parent coroutine starts an inner RocksDB read on a read-executor EventBase. SubmitReadAsync queues the filesystem read, then the database coroutine suspends and leaves the executor free until completion.</desc>
|
||||
<defs>
|
||||
<marker id="arrow-th" markerWidth="11" markerHeight="11" refX="9" refY="3.5" orient="auto">
|
||||
<path d="M0,0 L9,3.5 L0,7 z" fill="#455a64"/>
|
||||
</marker>
|
||||
<marker id="arrow-blue-th" markerWidth="11" markerHeight="11" refX="9" refY="3.5" orient="auto">
|
||||
<path d="M0,0 L9,3.5 L0,7 z" fill="#1565c0"/>
|
||||
</marker>
|
||||
<marker id="arrow-orange-th" markerWidth="11" markerHeight="11" refX="9" refY="3.5" orient="auto">
|
||||
<path d="M0,0 L9,3.5 L0,7 z" fill="#ef6c00"/>
|
||||
</marker>
|
||||
<marker id="arrow-purple-th" markerWidth="11" markerHeight="11" refX="9" refY="3.5" orient="auto">
|
||||
<path d="M0,0 L9,3.5 L0,7 z" fill="#5e35b1"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect x="0" y="0" width="1000" height="740" fill="#ffffff"/>
|
||||
<text x="500" y="30" text-anchor="middle" font-size="19" font-weight="bold" fill="#263238">Native CoGet: suspension and resumption</text>
|
||||
|
||||
<rect x="35" y="50" width="205" height="54" rx="8" fill="#e3f2fd" stroke="#1565c0" stroke-width="1.5"/>
|
||||
<text x="138" y="72" text-anchor="middle" font-size="13" font-weight="bold" fill="#0d47a1">Parent coroutine executor</text>
|
||||
<text x="138" y="90" text-anchor="middle" font-size="11" fill="#0d47a1">application task</text>
|
||||
|
||||
<rect x="285" y="50" width="225" height="54" rx="8" fill="#ede7f6" stroke="#5e35b1" stroke-width="1.5"/>
|
||||
<text x="398" y="72" text-anchor="middle" font-size="13" font-weight="bold" fill="#4527a0">RocksDB read executor</text>
|
||||
<text x="398" y="90" text-anchor="middle" font-size="11" fill="#4527a0">one EventBase worker</text>
|
||||
|
||||
<rect x="555" y="50" width="205" height="54" rx="8" fill="#fff3e0" stroke="#ef6c00" stroke-width="1.5"/>
|
||||
<text x="658" y="72" text-anchor="middle" font-size="13" font-weight="bold" fill="#e65100">Filesystem implementation</text>
|
||||
<text x="658" y="90" text-anchor="middle" font-size="11" fill="#e65100">submission and completion</text>
|
||||
|
||||
<rect x="805" y="50" width="160" height="54" rx="8" fill="#eceff1" stroke="#607d8b" stroke-width="1.5"/>
|
||||
<text x="885" y="72" text-anchor="middle" font-size="13" font-weight="bold" fill="#37474f">Kernel / storage</text>
|
||||
<text x="885" y="90" text-anchor="middle" font-size="11" fill="#455a64">for POSIX: io_uring</text>
|
||||
|
||||
<line x1="138" y1="104" x2="138" y2="680" stroke="#90a4ae" stroke-width="1.2" stroke-dasharray="6 5"/>
|
||||
<line x1="398" y1="104" x2="398" y2="680" stroke="#90a4ae" stroke-width="1.2" stroke-dasharray="6 5"/>
|
||||
<line x1="658" y1="104" x2="658" y2="680" stroke="#90a4ae" stroke-width="1.2" stroke-dasharray="6 5"/>
|
||||
<line x1="885" y1="104" x2="885" y2="680" stroke="#90a4ae" stroke-width="1.2" stroke-dasharray="6 5"/>
|
||||
|
||||
<rect x="74" y="128" width="128" height="34" rx="5" fill="#bbdefb" stroke="#1565c0"/>
|
||||
<text x="138" y="150" text-anchor="middle" font-size="11" font-weight="bold" fill="#0d47a1">co_await CoGet()</text>
|
||||
<line x1="202" y1="145" x2="398" y2="145" stroke="#1565c0" stroke-width="2" marker-end="url(#arrow-blue-th)"/>
|
||||
<text x="294" y="136" text-anchor="middle" font-size="10" fill="#0d47a1">bind inner GetCoroutine</text>
|
||||
|
||||
<rect x="315" y="176" width="166" height="46" rx="5" fill="#d1c4e9" stroke="#5e35b1"/>
|
||||
<text x="398" y="195" text-anchor="middle" font-size="11" font-weight="bold" fill="#4527a0">lookup starts</text>
|
||||
<text x="398" y="211" text-anchor="middle" font-size="10" fill="#4527a0">memtable and block cache</text>
|
||||
|
||||
<rect x="315" y="241" width="166" height="38" rx="5" fill="#d1c4e9" stroke="#5e35b1"/>
|
||||
<text x="398" y="265" text-anchor="middle" font-size="11" fill="#4527a0">SST block cache miss</text>
|
||||
<line x1="481" y1="260" x2="581" y2="260" stroke="#455a64" stroke-width="1.8" marker-end="url(#arrow-th)"/>
|
||||
|
||||
<rect x="581" y="235" width="154" height="50" rx="5" fill="#ffe0b2" stroke="#ef6c00" stroke-width="1.5"/>
|
||||
<text x="658" y="256" text-anchor="middle" font-size="11" font-weight="bold" font-family="monospace" fill="#e65100">SubmitReadAsync</text>
|
||||
<text x="658" y="273" text-anchor="middle" font-size="10" fill="#e65100">queue non-blocking read</text>
|
||||
|
||||
<line x1="735" y1="260" x2="885" y2="260" stroke="#ef6c00" stroke-width="1.8" marker-end="url(#arrow-orange-th)"/>
|
||||
<text x="805" y="250" text-anchor="middle" font-size="10" fill="#e65100">submit</text>
|
||||
|
||||
<rect x="315" y="309" width="166" height="52" rx="5" fill="#f3e5f5" stroke="#7e57c2" stroke-width="1.2"/>
|
||||
<text x="398" y="330" text-anchor="middle" font-size="11" font-weight="bold" fill="#4527a0">DB coroutine suspends</text>
|
||||
<text x="398" y="347" text-anchor="middle" font-size="10" fill="#4527a0">storage remains in flight</text>
|
||||
|
||||
<rect x="827" y="300" width="116" height="130" rx="5" fill="#eceff1" stroke="#607d8b"/>
|
||||
<text x="885" y="356" text-anchor="middle" font-size="11" font-weight="bold" fill="#37474f">storage I/O</text>
|
||||
<text x="885" y="374" text-anchor="middle" font-size="10" fill="#455a64">in flight</text>
|
||||
|
||||
<rect x="285" y="375" width="226" height="55" rx="7" fill="#e8f5e9" stroke="#66bb6a" stroke-width="1.5"/>
|
||||
<text x="398" y="396" text-anchor="middle" font-size="11" font-weight="bold" fill="#2e7d32">READ EXECUTOR IS FREE</text>
|
||||
<text x="398" y="414" text-anchor="middle" font-size="10" fill="#2e7d32">accept and run another request</text>
|
||||
|
||||
<rect x="827" y="445" width="116" height="40" rx="5" fill="#eceff1" stroke="#607d8b"/>
|
||||
<text x="885" y="469" text-anchor="middle" font-size="11" fill="#37474f">I/O completes</text>
|
||||
|
||||
<rect x="581" y="440" width="154" height="50" rx="5" fill="#ffe0b2" stroke="#ef6c00"/>
|
||||
<text x="658" y="461" text-anchor="middle" font-size="11" font-weight="bold" fill="#e65100">completion reported</text>
|
||||
<text x="658" y="478" text-anchor="middle" font-size="10" fill="#e65100">read becomes runnable</text>
|
||||
<line x1="827" y1="465" x2="735" y2="465" stroke="#ef6c00" stroke-width="1.8" marker-end="url(#arrow-orange-th)"/>
|
||||
<text x="781" y="455" text-anchor="middle" font-size="9" fill="#e65100">complete</text>
|
||||
<line x1="581" y1="465" x2="398" y2="465" stroke="#5e35b1" stroke-width="1.8" marker-end="url(#arrow-purple-th)"/>
|
||||
<text x="490" y="455" text-anchor="middle" font-size="9" fill="#4527a0">resume on submitting thread</text>
|
||||
|
||||
<rect x="315" y="500" width="166" height="54" rx="5" fill="#d1c4e9" stroke="#5e35b1"/>
|
||||
<text x="398" y="521" text-anchor="middle" font-size="11" font-weight="bold" fill="#4527a0">resume DB coroutine</text>
|
||||
<text x="398" y="538" text-anchor="middle" font-size="10" fill="#4527a0">verify, decode, fill output</text>
|
||||
|
||||
<line x1="398" y1="600" x2="138" y2="600" stroke="#1565c0" stroke-width="2" marker-end="url(#arrow-blue-th)"/>
|
||||
<text x="268" y="590" text-anchor="middle" font-size="10" fill="#0d47a1">return to parent executor</text>
|
||||
<rect x="57" y="615" width="162" height="52" rx="5" fill="#bbdefb" stroke="#1565c0"/>
|
||||
<text x="138" y="636" text-anchor="middle" font-size="11" font-weight="bold" fill="#0d47a1">caller continues</text>
|
||||
<text x="138" y="653" text-anchor="middle" font-size="10" fill="#0d47a1">after the RocksDB read completes</text>
|
||||
|
||||
<rect x="540" y="510" width="410" height="70" rx="7" fill="#fafafa" stroke="#b0bec5" stroke-width="1.2"/>
|
||||
<text x="555" y="531" font-size="10" font-weight="bold" fill="#37474f">POSIX fast path</text>
|
||||
<text x="555" y="549" font-size="10" fill="#455a64">io_uring completion already runs on the same read EventBase worker,</text>
|
||||
<text x="555" y="566" font-size="10" fill="#455a64">so the filesystem-completion and RocksDB lanes collapse onto one thread.</text>
|
||||
|
||||
<rect x="35" y="690" width="930" height="38" rx="6" fill="#e8f5e9" stroke="#66bb6a" stroke-width="1.2"/>
|
||||
<text x="500" y="713" text-anchor="middle" font-size="10" font-weight="bold" fill="#2e7d32">Completion returns to the submitting EventBase thread, avoiding an extra cross-thread handoff.</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 8.0 KiB |
@@ -0,0 +1,88 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="940" height="430" viewBox="0 0 940 430" font-family="Helvetica, Arial, sans-serif">
|
||||
<title>How request statistics survive coroutine suspension</title>
|
||||
<desc>A suspended request cannot leave its statistics in thread-local storage because another request can run on that worker. RocksDB swaps per-request statistics and accumulates CPU time only while each request is active.</desc>
|
||||
<defs>
|
||||
<marker id="arrow-tls" markerWidth="10" markerHeight="10" refX="8" refY="3.5" orient="auto">
|
||||
<path d="M0,0 L8,3.5 L0,7 z" fill="#546e7a"/>
|
||||
</marker>
|
||||
</defs>
|
||||
|
||||
<rect x="0" y="0" width="940" height="430" fill="#ffffff"/>
|
||||
<text x="235" y="32" text-anchor="middle" font-size="18" font-weight="bold" fill="#c62828">Unsafe: statistics belong to a thread</text>
|
||||
<text x="705" y="32" text-anchor="middle" font-size="18" font-weight="bold" fill="#1565c0">Coroutine-safe: statistics belong to a request</text>
|
||||
|
||||
<rect x="20" y="52" width="430" height="322" rx="10" fill="#fffafa" stroke="#ef9a9a" stroke-width="1.5"/>
|
||||
<rect x="490" y="52" width="430" height="322" rx="10" fill="#f8fbff" stroke="#90caf9" stroke-width="1.5"/>
|
||||
|
||||
<text x="42" y="84" font-size="12" font-weight="bold" fill="#455a64">Read-executor worker</text>
|
||||
<line x1="42" y1="100" x2="425" y2="100" stroke="#b0bec5" stroke-width="2" marker-end="url(#arrow-tls)"/>
|
||||
|
||||
<rect x="58" y="119" width="110" height="46" rx="5" fill="#bbdefb" stroke="#1565c0"/>
|
||||
<text x="113" y="138" text-anchor="middle" font-size="10" font-weight="bold" fill="#0d47a1">Request A runs</text>
|
||||
<text x="113" y="153" text-anchor="middle" font-size="9" fill="#0d47a1">TLS = A</text>
|
||||
<line x1="168" y1="142" x2="210" y2="142" stroke="#546e7a" stroke-width="1.5" marker-end="url(#arrow-tls)"/>
|
||||
<text x="189" y="132" text-anchor="middle" font-size="9" fill="#546e7a">suspend</text>
|
||||
|
||||
<rect x="218" y="119" width="110" height="46" rx="5" fill="#d1c4e9" stroke="#5e35b1"/>
|
||||
<text x="273" y="138" text-anchor="middle" font-size="10" font-weight="bold" fill="#4527a0">Request B runs</text>
|
||||
<text x="273" y="153" text-anchor="middle" font-size="9" fill="#4527a0">TLS = B</text>
|
||||
<line x1="328" y1="142" x2="370" y2="142" stroke="#546e7a" stroke-width="1.5" marker-end="url(#arrow-tls)"/>
|
||||
<text x="349" y="132" text-anchor="middle" font-size="9" fill="#546e7a">resume A</text>
|
||||
|
||||
<rect x="370" y="119" width="58" height="46" rx="5" fill="#ffcdd2" stroke="#c62828" stroke-width="1.5"/>
|
||||
<text x="399" y="138" text-anchor="middle" font-size="10" font-weight="bold" fill="#b71c1c">A?</text>
|
||||
<text x="399" y="153" text-anchor="middle" font-size="9" fill="#b71c1c">TLS = B</text>
|
||||
|
||||
<rect x="55" y="194" width="360" height="61" rx="6" fill="#ffebee" stroke="#e57373"/>
|
||||
<text x="235" y="214" text-anchor="middle" font-size="11" font-weight="bold" fill="#b71c1c">A suspension point breaks thread-local accounting</text>
|
||||
<text x="235" y="231" text-anchor="middle" font-size="10" fill="#b71c1c">Request B can overwrite A's counters while A waits.</text>
|
||||
<text x="235" y="247" text-anchor="middle" font-size="10" fill="#b71c1c">A resumes on that worker, whose TLS now contains B.</text>
|
||||
|
||||
<text x="52" y="286" font-size="11" font-weight="bold" fill="#37474f">Failures to avoid:</text>
|
||||
<text x="66" y="309" font-size="10" fill="#455a64">- A and B update the same PerfContext</text>
|
||||
<text x="66" y="329" font-size="10" fill="#455a64">- A's CPU timer charges B's work</text>
|
||||
<text x="66" y="349" font-size="10" fill="#455a64">- A's CPU timer charges suspended I/O wait</text>
|
||||
|
||||
<text x="512" y="84" font-size="12" font-weight="bold" fill="#455a64">Same interleaving, isolated state</text>
|
||||
<line x1="512" y1="100" x2="895" y2="100" stroke="#b0bec5" stroke-width="2" marker-end="url(#arrow-tls)"/>
|
||||
|
||||
<rect x="526" y="119" width="120" height="56" rx="5" fill="#bbdefb" stroke="#1565c0"/>
|
||||
<text x="586" y="138" text-anchor="middle" font-size="10" font-weight="bold" fill="#0d47a1">Request A runs</text>
|
||||
<text x="586" y="153" text-anchor="middle" font-size="9" fill="#0d47a1">load A context</text>
|
||||
<text x="586" y="167" text-anchor="middle" font-size="9" fill="#0d47a1">save before suspend</text>
|
||||
|
||||
<line x1="646" y1="147" x2="675" y2="147" stroke="#546e7a" stroke-width="1.5" marker-end="url(#arrow-tls)"/>
|
||||
|
||||
<rect x="681" y="119" width="120" height="56" rx="5" fill="#d1c4e9" stroke="#5e35b1"/>
|
||||
<text x="741" y="138" text-anchor="middle" font-size="10" font-weight="bold" fill="#4527a0">Request B runs</text>
|
||||
<text x="741" y="153" text-anchor="middle" font-size="9" fill="#4527a0">load B context</text>
|
||||
<text x="741" y="167" text-anchor="middle" font-size="9" fill="#4527a0">save before suspend</text>
|
||||
|
||||
<line x1="801" y1="147" x2="830" y2="147" stroke="#546e7a" stroke-width="1.5" marker-end="url(#arrow-tls)"/>
|
||||
|
||||
<rect x="836" y="119" width="68" height="56" rx="5" fill="#bbdefb" stroke="#1565c0"/>
|
||||
<text x="870" y="138" text-anchor="middle" font-size="10" font-weight="bold" fill="#0d47a1">A runs</text>
|
||||
<text x="870" y="153" text-anchor="middle" font-size="9" fill="#0d47a1">reload A</text>
|
||||
<text x="870" y="167" text-anchor="middle" font-size="9" fill="#0d47a1">context</text>
|
||||
|
||||
<rect x="525" y="198" width="360" height="58" rx="6" fill="#e3f2fd" stroke="#64b5f6"/>
|
||||
<text x="705" y="218" text-anchor="middle" font-size="11" font-weight="bold" fill="#0d47a1">Folly RequestContext carries request statistics</text>
|
||||
<text x="705" y="236" text-anchor="middle" font-size="10" fill="#0d47a1">and swaps them into TLS only while that request is active.</text>
|
||||
|
||||
<rect x="525" y="278" width="360" height="68" rx="6" fill="#ede7f6" stroke="#9575cd"/>
|
||||
<text x="705" y="297" text-anchor="middle" font-size="11" font-weight="bold" fill="#4527a0">CPU time includes only A's active slices</text>
|
||||
<rect x="557" y="306" width="90" height="18" rx="3" fill="#bbdefb" stroke="#1565c0"/>
|
||||
<text x="602" y="318" text-anchor="middle" font-size="9" fill="#0d47a1">A slice 1</text>
|
||||
<rect x="647" y="306" width="150" height="18" rx="3" fill="#eceff1" stroke="#90a4ae"/>
|
||||
<text x="722" y="318" text-anchor="middle" font-size="9" fill="#546e7a">I/O wait + B: not charged</text>
|
||||
<rect x="797" y="306" width="60" height="18" rx="3" fill="#bbdefb" stroke="#1565c0"/>
|
||||
<text x="827" y="318" text-anchor="middle" font-size="9" fill="#0d47a1">A slice 2</text>
|
||||
<text x="705" y="340" text-anchor="middle" font-size="10" fill="#4527a0">get_cpu_nanos(A) = slice 1 + slice 2</text>
|
||||
|
||||
<rect x="110" y="395" width="16" height="12" rx="2" fill="#bbdefb" stroke="#1565c0"/>
|
||||
<text x="132" y="405" font-size="10" fill="#546e7a">request A</text>
|
||||
<rect x="212" y="395" width="16" height="12" rx="2" fill="#d1c4e9" stroke="#5e35b1"/>
|
||||
<text x="234" y="405" font-size="10" fill="#546e7a">request B</text>
|
||||
<rect x="314" y="395" width="16" height="12" rx="2" fill="#ffcdd2" stroke="#c62828"/>
|
||||
<text x="336" y="405" font-size="10" fill="#546e7a">state collision</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 6.8 KiB |
@@ -262,6 +262,30 @@ std::string TempCompactionProgressFileName(const std::string& dbname,
|
||||
return dbname + "/" + buffer;
|
||||
}
|
||||
|
||||
std::string SessionTmpDir(const std::string& dbname) {
|
||||
std::string result = dbname;
|
||||
result.append("/").append(kSessionTmpDirName);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string RemoteCompactionJobDir(const std::string& dbname,
|
||||
bool use_output_root_dir,
|
||||
const std::string& job_dir_name) {
|
||||
if (!use_output_root_dir) {
|
||||
std::string db_prefix = dbname;
|
||||
if (db_prefix.empty() || db_prefix.back() != '/') {
|
||||
db_prefix.append("/");
|
||||
}
|
||||
if (job_dir_name.compare(0, db_prefix.size(), db_prefix) == 0) {
|
||||
return job_dir_name;
|
||||
}
|
||||
return db_prefix.append(job_dir_name);
|
||||
}
|
||||
std::string result = SessionTmpDir(dbname);
|
||||
result.append("/").append(job_dir_name);
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string MetaDatabaseName(const std::string& dbname, uint64_t number) {
|
||||
char buf[100];
|
||||
snprintf(buf, sizeof(buf), "/METADB-%llu",
|
||||
@@ -287,6 +311,11 @@ std::string IdentityFileName(const std::string& dbname) {
|
||||
// dbname/COMPACTION_PROGRESS-[timestamp]
|
||||
// dbname/COMPACTION_PROGRESS-[timestamp].dbtmp
|
||||
// Disregards / at the beginning
|
||||
//
|
||||
// The DB directory may also contain the session temporary directory (see
|
||||
// SessionTmpDir) and dbname/archive/.
|
||||
// ParseFileName deliberately rejects directories so the obsolete-file scan
|
||||
// does not treat them as deletion candidates.
|
||||
bool ParseFileName(const std::string& fname, uint64_t* number, FileType* type,
|
||||
WalFileType* log_type) {
|
||||
return ParseFileName(fname, number, "", type, log_type);
|
||||
|
||||
@@ -148,6 +148,16 @@ std::string CompactionProgressFileName(const std::string& dbname,
|
||||
std::string TempCompactionProgressFileName(const std::string& dbname,
|
||||
uint64_t timestamp);
|
||||
|
||||
// Return the DB session temporary directory.
|
||||
std::string SessionTmpDir(const std::string& dbname);
|
||||
|
||||
// Return the path for the client-selected staging directory name. When the
|
||||
// session temporary directory is not used, an already DB-rooted path is
|
||||
// returned unchanged for compatibility with legacy clients.
|
||||
std::string RemoteCompactionJobDir(const std::string& dbname,
|
||||
bool use_output_root_dir,
|
||||
const std::string& job_dir_name);
|
||||
|
||||
// Return the name to use for a metadatabase. The result will be prefixed with
|
||||
// "dbname".
|
||||
std::string MetaDatabaseName(const std::string& dbname, uint64_t number);
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "tools/io_tracer_parser_tool.h"
|
||||
#endif
|
||||
#include "rocksdb/flush_block_policy.h"
|
||||
#include "util/defer.h"
|
||||
#include "util/random.h"
|
||||
|
||||
namespace {
|
||||
@@ -1550,6 +1551,19 @@ TEST_F(PrefetchTest, PrefetchWithBlockLookupAutoTuneWithPrev) {
|
||||
Close();
|
||||
}
|
||||
|
||||
// A `MockFS` that advertises async IO support. `ArenaWrappedDBIter` clears
|
||||
// `ReadOptions::async_io` unless the file system advertises it, so without this
|
||||
// the `async_io` test variant would run the sync path.
|
||||
class AsyncIOMockFS : public MockFS {
|
||||
public:
|
||||
using MockFS::MockFS;
|
||||
|
||||
void SupportedOps(int64_t& supported_ops) override {
|
||||
MockFS::SupportedOps(supported_ops);
|
||||
supported_ops |= (1 << FSSupportedOps::kAsyncIO);
|
||||
}
|
||||
};
|
||||
|
||||
class PrefetchTrimReadaheadTestParam
|
||||
: public DBTestBase,
|
||||
public ::testing::WithParamInterface<
|
||||
@@ -1582,6 +1596,147 @@ class PrefetchTrimReadaheadTestParam
|
||||
table_options.flush_block_policy_factory.reset(
|
||||
new FlushBlockBySizePolicyFactory());
|
||||
}
|
||||
|
||||
void RunSeekToFirstAfterSeekIgnoresStalePrefix(bool async_io) {
|
||||
const bool auto_readahead_size = std::get<1>(GetParam());
|
||||
|
||||
std::shared_ptr<MockFS> fs =
|
||||
async_io ? std::make_shared<AsyncIOMockFS>(
|
||||
FileSystem::Default(), false /* support_prefetch */,
|
||||
true /* small_buffer_alignment */)
|
||||
: std::make_shared<MockFS>(FileSystem::Default(),
|
||||
false /* support_prefetch */,
|
||||
true /* small_buffer_alignment */);
|
||||
std::unique_ptr<Env> env(new CompositeEnvWrapper(env_, fs));
|
||||
// Close the DB before the local `env` is destroyed, even if an assertion
|
||||
// below exits early: ~DBTestBase() closes it after `env` is already gone.
|
||||
Defer close_db_on_exit([this]() { Close(); });
|
||||
|
||||
Options options;
|
||||
SetGenericOptions(env.get(), options);
|
||||
BlockBasedTableOptions table_options;
|
||||
SetBlockBasedTableOptions(table_options);
|
||||
// An explicit cache so both scans below start equally cold: a warm cache
|
||||
// serves every block without touching the file, which suppresses readahead
|
||||
// and would make the comparison meaningless.
|
||||
std::shared_ptr<Cache> block_cache = NewLRUCache(8 * 1024 * 1024);
|
||||
table_options.block_cache = block_cache;
|
||||
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
|
||||
|
||||
ASSERT_OK(TryReopen(options));
|
||||
|
||||
constexpr int kNumKeysUnderPrefix = 40;
|
||||
constexpr int kNumOtherPrefixes = 10;
|
||||
WriteBatch batch;
|
||||
for (int i = 0; i < kNumKeysUnderPrefix; i++) {
|
||||
ASSERT_OK(batch.Put(kPrefix + Key(i), rnd.RandomString(100)));
|
||||
}
|
||||
// Keys under other prefixes, so that a trim against a prefix other than
|
||||
// "a_prefix_" has somewhere to stop short of.
|
||||
for (int i = 0; i < kNumOtherPrefixes; i++) {
|
||||
const std::string diff_prefix =
|
||||
std::string(1, char('c' + i)) + kPrefix.substr(1);
|
||||
ASSERT_OK(batch.Put(diff_prefix + Key(i), rnd.RandomString(100)));
|
||||
}
|
||||
ASSERT_OK(db_->Write(WriteOptions(), &batch));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
|
||||
// The file must stay in L0 so the merging iterator holds and reuses this
|
||||
// table iterator across Seek()/SeekToFirst() instead of rebuilding it,
|
||||
// which would discard the stale member and hide the bug.
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(0));
|
||||
|
||||
ReadOptions ro;
|
||||
ro.prefix_same_as_start = true;
|
||||
ro.auto_readahead_size = auto_readahead_size;
|
||||
ro.async_io = async_io;
|
||||
// Small enough that the scan exhausts the buffer and re-prefetches, so the
|
||||
// trimming prefix is consulted repeatedly rather than once for the file.
|
||||
ro.readahead_size = 1024;
|
||||
|
||||
// The `async_io` variant reaches the key-loss form of the bug only because
|
||||
// `FilePrefetchBuffer` has a second buffer, prefetched with
|
||||
// `read_curr_block=false`. Count that path so this variant cannot silently
|
||||
// decay into a duplicate of the synchronous one.
|
||||
int extra_prefetch_buff_cnt = 0;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"FilePrefetchBuffer::PrefetchAsync:ExtraPrefetching",
|
||||
[&extra_prefetch_buff_cnt](void*) { ++extra_prefetch_buff_cnt; });
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
Defer disable_sync_point([]() {
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
});
|
||||
|
||||
auto count_from_seek_to_first = [this](Iterator* iter) {
|
||||
int num_keys = 0;
|
||||
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
|
||||
EXPECT_EQ(kPrefix, iter->key().ToString().substr(0, kPrefix.size()));
|
||||
++num_keys;
|
||||
}
|
||||
EXPECT_OK(iter->status());
|
||||
return num_keys;
|
||||
};
|
||||
|
||||
// Control: SeekToFirst() on an iterator with nothing stale, cold cache.
|
||||
block_cache->EraseUnRefEntries();
|
||||
ASSERT_OK(options.statistics->Reset());
|
||||
int num_keys_control = 0;
|
||||
{
|
||||
auto iter = std::unique_ptr<Iterator>(db_->NewIterator(ro));
|
||||
num_keys_control = count_from_seek_to_first(iter.get());
|
||||
}
|
||||
const uint64_t trimmed_control =
|
||||
options.statistics->getTickerCount(READAHEAD_TRIMMED);
|
||||
|
||||
// The same scan from an equally cold cache, differing only in that it
|
||||
// follows a Seek() past the last key, which assigns the trimming prefix.
|
||||
block_cache->EraseUnRefEntries();
|
||||
ASSERT_OK(options.statistics->Reset());
|
||||
extra_prefetch_buff_cnt = 0;
|
||||
int num_keys_after_seek = 0;
|
||||
{
|
||||
auto iter = std::unique_ptr<Iterator>(db_->NewIterator(ro));
|
||||
const std::string seek_key = std::string(1, 'z') + kPrefix.substr(1);
|
||||
iter->Seek(seek_key);
|
||||
ASSERT_OK(iter->status());
|
||||
ASSERT_FALSE(iter->Valid());
|
||||
// That Seek() must read no data block, or this scan does not start as
|
||||
// cold as the control and the two trim counts are not comparable.
|
||||
ASSERT_EQ(0, options.statistics->getTickerCount(BLOCK_CACHE_DATA_MISS));
|
||||
ASSERT_EQ(0, options.statistics->getTickerCount(READAHEAD_TRIMMED));
|
||||
|
||||
num_keys_after_seek = count_from_seek_to_first(iter.get());
|
||||
}
|
||||
const uint64_t trimmed_after_seek =
|
||||
options.statistics->getTickerCount(READAHEAD_TRIMMED);
|
||||
|
||||
ASSERT_EQ(kNumKeysUnderPrefix, num_keys_control);
|
||||
// Pin the trim counter down on the control side so the comparison below
|
||||
// cannot pass vacuously: without `auto_readahead_size` nothing may be
|
||||
// trimmed at all, and with it the trimming path has to be live.
|
||||
if (auto_readahead_size) {
|
||||
ASSERT_GT(trimmed_control, 0);
|
||||
} else {
|
||||
ASSERT_EQ(0, trimmed_control);
|
||||
}
|
||||
|
||||
// A preceding Seek() must not change what SeekToFirst() does: this scan
|
||||
// visits the same blocks, so it must return the same keys and trim exactly
|
||||
// as much as the control -- no more. (The control's trims come from
|
||||
// block-cache hits and index bounds, which a stale prefix does not affect.)
|
||||
ASSERT_EQ(num_keys_control, num_keys_after_seek);
|
||||
ASSERT_EQ(trimmed_control, trimmed_after_seek);
|
||||
|
||||
// Last, so the two assertions above stay the ones that report a
|
||||
// regression: the `async_io` variant is only worth having while it reaches
|
||||
// the extra-buffer prefetch path, and the synchronous one must not.
|
||||
if (async_io) {
|
||||
ASSERT_GT(extra_prefetch_buff_cnt, 0);
|
||||
} else {
|
||||
ASSERT_EQ(0, extra_prefetch_buff_cnt);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(
|
||||
@@ -1675,6 +1830,40 @@ TEST_P(PrefetchTrimReadaheadTestParam, PrefixSameAsStart) {
|
||||
Close();
|
||||
}
|
||||
|
||||
// Regression test: a `SeekToFirst()` must not trim readahead against a prefix
|
||||
// left behind by an earlier `Seek()` on the same iterator. Under
|
||||
// `prefix_same_as_start`, `DBIter::SeekToFirst()` derives its prefix from the
|
||||
// first key it finds, so an earlier seek key describes a key range this
|
||||
// positioning is not scanning, and trimming against it defeats readahead for
|
||||
// the whole scan.
|
||||
//
|
||||
// The key count is asserted as well as the trim counter because a mis-trim can
|
||||
// also end the scan early and drop keys, which the `async_io` variant below
|
||||
// exhibits and explains.
|
||||
TEST_P(PrefetchTrimReadaheadTestParam, SeekToFirstAfterSeekIgnoresStalePrefix) {
|
||||
if (mem_env_ || encrypted_env_) {
|
||||
ROCKSDB_GTEST_SKIP("Test requires non-mem or non-encrypted environment");
|
||||
return;
|
||||
}
|
||||
|
||||
RunSeekToFirstAfterSeekIgnoresStalePrefix(false /* async_io */);
|
||||
}
|
||||
|
||||
// With `async_io`, `FilePrefetchBuffer` prefetches into extra buffers with
|
||||
// `read_curr_block=false`, which skips the early return in
|
||||
// `BlockCacheLookupForReadAheadSize()` and lets a mis-trim escalate into
|
||||
// `is_index_out_of_bound_` -- ending the scan early and silently dropping keys
|
||||
// rather than only slowing it down.
|
||||
TEST_P(PrefetchTrimReadaheadTestParam,
|
||||
SeekToFirstAfterSeekIgnoresStalePrefixAsyncIO) {
|
||||
if (mem_env_ || encrypted_env_) {
|
||||
ROCKSDB_GTEST_SKIP("Test requires non-mem or non-encrypted environment");
|
||||
return;
|
||||
}
|
||||
|
||||
RunSeekToFirstAfterSeekIgnoresStalePrefix(true /* async_io */);
|
||||
}
|
||||
|
||||
TEST_P(PrefetchTrimReadaheadTestParam, IterateUpperBoundAtEndOfIndex) {
|
||||
if (mem_env_ || encrypted_env_) {
|
||||
ROCKSDB_GTEST_SKIP("Test requires non-mem or non-encrypted environment");
|
||||
@@ -3108,6 +3297,10 @@ TEST_P(PrefetchTest, SeekParallelizationTestWithPosix) {
|
||||
|
||||
{
|
||||
ASSERT_OK(options.statistics->Reset());
|
||||
// `get_perf_context()` is cumulative and thread-local; reset it so the
|
||||
// `number_async_seek` assertion below is about this iterator, as the
|
||||
// sibling tests above do.
|
||||
get_perf_context()->Reset();
|
||||
// Each block contains around 4 keys.
|
||||
auto iter = std::unique_ptr<Iterator>(db_->NewIterator(ro));
|
||||
iter->Seek(BuildKey(0)); // Prefetch data because of seek parallelization.
|
||||
|
||||
+107
-16
@@ -10,7 +10,9 @@
|
||||
#include "file/writable_file_writer.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdio>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
|
||||
#include "db/version_edit.h"
|
||||
#include "file/file_util.h"
|
||||
@@ -43,6 +45,47 @@ inline Histograms GetFileWriteHistograms(Histograms file_writer_hist,
|
||||
return Histograms::HISTOGRAM_ENUM_MAX;
|
||||
}
|
||||
|
||||
constexpr char kDestinationBufferChecksumMismatch[] =
|
||||
"Checksum handoff detected data corruption after copying data into "
|
||||
"WritableFileWriter destination buffer";
|
||||
constexpr char kLogicalAppendChecksumMismatch[] =
|
||||
"Checksum handoff detected logical append checksum mismatch after copying "
|
||||
"caller data into WritableFileWriter buffers";
|
||||
|
||||
static IOStatus ChecksumMismatchStatus(size_t size,
|
||||
Crc32cChecksum expected_checksum,
|
||||
uint32_t actual_crc32c,
|
||||
const std::string& file_name,
|
||||
const char* mismatch_message) {
|
||||
char expected_checksum_buf[11];
|
||||
char actual_crc32c_buf[11];
|
||||
snprintf(expected_checksum_buf, sizeof(expected_checksum_buf), "0x%08x",
|
||||
static_cast<unsigned int>(expected_checksum.value));
|
||||
snprintf(actual_crc32c_buf, sizeof(actual_crc32c_buf), "0x%08x",
|
||||
static_cast<unsigned int>(actual_crc32c));
|
||||
std::string message = mismatch_message;
|
||||
message.append(": file=")
|
||||
.append(file_name)
|
||||
.append(" size=")
|
||||
.append(std::to_string(size))
|
||||
.append(" expected_crc32c=")
|
||||
.append(expected_checksum_buf)
|
||||
.append(" actual_crc32c=")
|
||||
.append(actual_crc32c_buf);
|
||||
return IOStatus::Corruption(message);
|
||||
}
|
||||
|
||||
static IOStatus ValidateDataChecksum(const Slice& data, Crc32cChecksum checksum,
|
||||
const std::string& file_name,
|
||||
const char* mismatch_message) {
|
||||
const uint32_t actual_crc32c = crc32c::Value(data.data(), data.size());
|
||||
if (actual_crc32c == checksum.value) {
|
||||
return IOStatus::OK();
|
||||
}
|
||||
return ChecksumMismatchStatus(data.size(), checksum, actual_crc32c, file_name,
|
||||
mismatch_message);
|
||||
}
|
||||
|
||||
IOStatus WritableFileWriter::Create(const std::shared_ptr<FileSystem>& fs,
|
||||
const std::string& fname,
|
||||
const FileOptions& file_opts,
|
||||
@@ -63,6 +106,18 @@ IOStatus WritableFileWriter::Create(const std::shared_ptr<FileSystem>& fs,
|
||||
|
||||
IOStatus WritableFileWriter::Append(const IOOptions& opts, const Slice& data,
|
||||
uint32_t crc32c_checksum) {
|
||||
Crc32cChecksum checksum(crc32c_checksum);
|
||||
return AppendSlice(opts, data, crc32c_checksum == 0 ? nullptr : &checksum);
|
||||
}
|
||||
|
||||
IOStatus WritableFileWriter::Append(const IOOptions& opts, const Slice& data,
|
||||
Crc32cChecksum crc32c_checksum) {
|
||||
return AppendSlice(opts, data, &crc32c_checksum);
|
||||
}
|
||||
|
||||
IOStatus WritableFileWriter::AppendSlice(
|
||||
const IOOptions& opts, const Slice& data_slice,
|
||||
const Crc32cChecksum* crc32c_checksum) {
|
||||
if (seen_error()) {
|
||||
return GetWriterHasPreviousErrorStatus();
|
||||
}
|
||||
@@ -71,15 +126,14 @@ IOStatus WritableFileWriter::Append(const IOOptions& opts, const Slice& data,
|
||||
GetFileWriteHistograms(hist_type_, opts.io_activity));
|
||||
|
||||
const IOOptions io_options = FinalizeIOOptions(opts);
|
||||
const char* src = data.data();
|
||||
size_t left = data.size();
|
||||
const char* src = data_slice.data();
|
||||
size_t left = data_slice.size();
|
||||
IOStatus s;
|
||||
pending_sync_ = true;
|
||||
|
||||
TEST_KILL_RANDOM_WITH_WEIGHT("WritableFileWriter::Append:0", REDUCE_ODDS2);
|
||||
|
||||
// Calculate the checksum of appended data
|
||||
UpdateFileChecksum(data);
|
||||
UpdateFileChecksum(data_slice);
|
||||
|
||||
{
|
||||
IOSTATS_TIMER_GUARD(prepare_write_nanos);
|
||||
@@ -125,23 +179,47 @@ IOStatus WritableFileWriter::Append(const IOOptions& opts, const Slice& data,
|
||||
}
|
||||
|
||||
if (perform_data_verification_ && buffered_data_with_checksum_ &&
|
||||
crc32c_checksum != 0) {
|
||||
// Since we want to use the checksum of the input data, we cannot break it
|
||||
// into several pieces. We will only write them in the buffer when buffer
|
||||
// size is enough. Otherwise, we will directly write it down.
|
||||
crc32c_checksum != nullptr) {
|
||||
if (use_direct_io() || (buf_.Capacity() - buf_.CurrentSize()) >= left) {
|
||||
if ((buf_.Capacity() - buf_.CurrentSize()) >= left) {
|
||||
size_t copied_offset = buf_.CurrentSize();
|
||||
size_t appended = buf_.Append(src, left);
|
||||
if (appended != left) {
|
||||
s = IOStatus::Corruption("Write buffer append failure");
|
||||
}
|
||||
buffered_data_crc32c_checksum_ = crc32c::Crc32cCombine(
|
||||
buffered_data_crc32c_checksum_, crc32c_checksum, appended);
|
||||
if (s.ok()) {
|
||||
Slice copied_data(buf_.BufferStart() + copied_offset, appended);
|
||||
s = ValidateCopiedDataChecksum(copied_data, *crc32c_checksum);
|
||||
}
|
||||
if (s.ok()) {
|
||||
buffered_data_crc32c_checksum_ = crc32c::Crc32cCombine(
|
||||
buffered_data_crc32c_checksum_, crc32c_checksum->value, appended);
|
||||
} else {
|
||||
buf_.Size(0);
|
||||
buffered_data_crc32c_checksum_ = 0;
|
||||
}
|
||||
} else {
|
||||
while (left > 0) {
|
||||
size_t appended = buf_.Append(src, left);
|
||||
buffered_data_crc32c_checksum_ =
|
||||
crc32c::Extend(buffered_data_crc32c_checksum_, src, appended);
|
||||
uint32_t logical_append_crc32c = 0;
|
||||
while (s.ok() && left > 0) {
|
||||
if (buf_.CurrentSize() == buf_.Capacity()) {
|
||||
s = Flush(io_options);
|
||||
continue;
|
||||
}
|
||||
const size_t copied_offset = buf_.CurrentSize();
|
||||
const size_t append_size =
|
||||
std::min(left, buf_.Capacity() - buf_.CurrentSize());
|
||||
size_t appended = buf_.Append(src, append_size);
|
||||
if (appended != append_size) {
|
||||
s = IOStatus::Corruption("Write buffer append failure");
|
||||
break;
|
||||
}
|
||||
Slice copied_data(buf_.BufferStart() + copied_offset, appended);
|
||||
const uint32_t appended_crc32c =
|
||||
crc32c::Value(copied_data.data(), copied_data.size());
|
||||
buffered_data_crc32c_checksum_ = crc32c::Crc32cCombine(
|
||||
buffered_data_crc32c_checksum_, appended_crc32c, appended);
|
||||
logical_append_crc32c = crc32c::Crc32cCombine(
|
||||
logical_append_crc32c, appended_crc32c, appended);
|
||||
left -= appended;
|
||||
src += appended;
|
||||
|
||||
@@ -152,10 +230,17 @@ IOStatus WritableFileWriter::Append(const IOOptions& opts, const Slice& data,
|
||||
}
|
||||
}
|
||||
}
|
||||
if (s.ok() && logical_append_crc32c != crc32c_checksum->value) {
|
||||
s = ChecksumMismatchStatus(data_slice.size(), *crc32c_checksum,
|
||||
logical_append_crc32c, file_name_,
|
||||
kLogicalAppendChecksumMismatch);
|
||||
buf_.Size(0);
|
||||
buffered_data_crc32c_checksum_ = 0;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
assert(buf_.CurrentSize() == 0);
|
||||
buffered_data_crc32c_checksum_ = crc32c_checksum;
|
||||
buffered_data_crc32c_checksum_ = crc32c_checksum->value;
|
||||
s = WriteBufferedWithChecksum(io_options, src, left);
|
||||
}
|
||||
} else {
|
||||
@@ -197,7 +282,7 @@ IOStatus WritableFileWriter::Append(const IOOptions& opts, const Slice& data,
|
||||
TEST_KILL_RANDOM("WritableFileWriter::Append:1");
|
||||
if (s.ok()) {
|
||||
uint64_t cur_size = filesize_.load(std::memory_order_acquire);
|
||||
filesize_.store(cur_size + data.size(), std::memory_order_release);
|
||||
filesize_.store(cur_size + data_slice.size(), std::memory_order_release);
|
||||
} else {
|
||||
set_seen_error(s);
|
||||
}
|
||||
@@ -774,6 +859,12 @@ void WritableFileWriter::Crc32cHandoffChecksumCalculation(const char* data,
|
||||
EncodeFixed32(buf, v_crc32c);
|
||||
}
|
||||
|
||||
IOStatus WritableFileWriter::ValidateCopiedDataChecksum(
|
||||
const Slice& copied_data, Crc32cChecksum checksum) {
|
||||
return ValidateDataChecksum(copied_data, checksum, file_name_,
|
||||
kDestinationBufferChecksumMismatch);
|
||||
}
|
||||
|
||||
// This flushes the accumulated data in the buffer. We pad data with zeros if
|
||||
// necessary to the whole page.
|
||||
// However, during automatic flushes padding would not be necessary.
|
||||
|
||||
@@ -30,6 +30,12 @@ namespace ROCKSDB_NAMESPACE {
|
||||
class Statistics;
|
||||
class SystemClock;
|
||||
|
||||
struct Crc32cChecksum {
|
||||
explicit Crc32cChecksum(uint32_t _value) : value(_value) {}
|
||||
|
||||
uint32_t value;
|
||||
};
|
||||
|
||||
// WritableFileWriter is a wrapper on top of Env::WritableFile. It provides
|
||||
// facilities to:
|
||||
// - Handle Buffered and Direct writes.
|
||||
@@ -136,6 +142,10 @@ class WritableFileWriter {
|
||||
void UpdateFileChecksum(const Slice& data);
|
||||
void Crc32cHandoffChecksumCalculation(const char* data, size_t size,
|
||||
char* buf);
|
||||
IOStatus ValidateCopiedDataChecksum(const Slice& copied_data,
|
||||
Crc32cChecksum checksum);
|
||||
IOStatus AppendSlice(const IOOptions& opts, const Slice& data,
|
||||
const Crc32cChecksum* crc32c_checksum);
|
||||
|
||||
std::string file_name_;
|
||||
FSWritableFilePtr writable_file_;
|
||||
@@ -258,6 +268,8 @@ class WritableFileWriter {
|
||||
// will calculate the checksum internally.
|
||||
IOStatus Append(const IOOptions& opts, const Slice& data,
|
||||
uint32_t crc32c_checksum = 0);
|
||||
IOStatus Append(const IOOptions& opts, const Slice& data,
|
||||
Crc32cChecksum crc32c_checksum);
|
||||
|
||||
IOStatus Pad(const IOOptions& opts, const size_t pad_bytes,
|
||||
const size_t max_pad_size);
|
||||
|
||||
Loaded 100 of 214 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user