mirror of
https://github.com/facebook/rocksdb.git
synced 2026-11-06 02:52:35 -05:00
Compare commits
93
Commits
ajkr-patch-2
...
8.1.fb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d2d9518fb | ||
|
|
6a43615041 | ||
|
|
af72046d14 | ||
|
|
501f359dd1 | ||
|
|
fc1df4c688 | ||
|
|
3b6ac67f13 | ||
|
|
d3dbed0e96 | ||
|
|
f8741f1f3b | ||
|
|
cb58477185 | ||
|
|
204fcff751 | ||
|
|
eac6b6d0cd | ||
|
|
a72d55c99d | ||
|
|
291300ece8 | ||
|
|
ccaa3225b0 | ||
|
|
664dabda8f | ||
|
|
bab5f9a6f2 | ||
|
|
601efe3cf2 | ||
|
|
11cb6af6e5 | ||
|
|
2a23bee963 | ||
|
|
49881921cd | ||
|
|
648e972f30 | ||
|
|
9aa3b6f9ae | ||
|
|
fbd603d04a | ||
|
|
969d4e1dd2 | ||
|
|
7a07afe82e | ||
|
|
daeec505a4 | ||
|
|
1de697628e | ||
|
|
1d52438504 | ||
|
|
6c65bf1743 | ||
|
|
e01073252b | ||
|
|
13357de0c2 | ||
|
|
a1a3b23346 | ||
|
|
50e9b3f9c7 | ||
|
|
e168c1b1a4 | ||
|
|
ddde1e6af8 | ||
|
|
d053926fa2 | ||
|
|
95d67f3646 | ||
|
|
8dfcfd4e90 | ||
|
|
cf09917c18 | ||
|
|
b7e73501d8 | ||
|
|
3c9eed688e | ||
|
|
af7872ffd1 | ||
|
|
f007b8fdea | ||
|
|
229297d1b8 | ||
|
|
9fa9becf53 | ||
|
|
286080456c | ||
|
|
476b01579c | ||
|
|
1b48ecc2c6 | ||
|
|
ea85148b78 | ||
|
|
ebfca2cf00 | ||
|
|
83bc03a99a | ||
|
|
cfe50f7e77 | ||
|
|
142b18d00b | ||
|
|
fcd816d534 | ||
|
|
d47126875b | ||
|
|
b6640c3117 | ||
|
|
25e1365227 | ||
|
|
6aef1a05d6 | ||
|
|
68e4581c67 | ||
|
|
64a1f7670f | ||
|
|
88056ea6cb | ||
|
|
ab22e79824 | ||
|
|
94ec433833 | ||
|
|
9794acb597 | ||
|
|
6d5e8604f1 | ||
|
|
1969815fa8 | ||
|
|
c19672c187 | ||
|
|
42d6652ba2 | ||
|
|
a72f591825 | ||
|
|
ab2157fa6f | ||
|
|
3cacd4b4ec | ||
|
|
b7747bbc9f | ||
|
|
34bb3ddc43 | ||
|
|
ee5305fabb | ||
|
|
77b61abc7b | ||
|
|
876d281592 | ||
|
|
6650ca244e | ||
|
|
b5827c806c | ||
|
|
68fa90ca43 | ||
|
|
54d72085b5 | ||
|
|
9b66331388 | ||
|
|
92e8874654 | ||
|
|
4a51900700 | ||
|
|
27cf09172c | ||
|
|
cf756ed916 | ||
|
|
e17f31057b | ||
|
|
0cf1008fe3 | ||
|
|
390cc0b156 | ||
|
|
63da9cfa26 | ||
|
|
701a19cc83 | ||
|
|
fec5c8deb8 | ||
|
|
6781009ee8 | ||
|
|
6af16ac7c1 |
No files matched your search
+32
-15
@@ -152,15 +152,15 @@ commands:
|
||||
steps:
|
||||
- run:
|
||||
name: "Test low-variance benchmarks"
|
||||
command: ./tools/benchmark_ci.py --db_dir /tmp/rocksdb-benchmark-datadir --output_dir /tmp/benchmark-results --num_keys 10000000
|
||||
command: ./tools/benchmark_ci.py --db_dir /tmp/rocksdb-benchmark-datadir --output_dir /tmp/benchmark-results --num_keys 20000000
|
||||
environment:
|
||||
LD_LIBRARY_PATH: /usr/local/lib
|
||||
# How long to run parts of the test(s)
|
||||
DURATION_RO: 400
|
||||
DURATION_RW: 700
|
||||
DURATION_RO: 300
|
||||
DURATION_RW: 500
|
||||
# Keep threads within physical capacity of server (much lower than default)
|
||||
NUM_THREADS: 1
|
||||
MAX_BACKGROUND_JOBS: 3
|
||||
MAX_BACKGROUND_JOBS: 4
|
||||
# Don't run a couple of "optional" initial tests
|
||||
CI_TESTS_ONLY: "true"
|
||||
# Reduce configured size of levels to ensure more levels in the leveled compaction LSM tree
|
||||
@@ -170,7 +170,11 @@ commands:
|
||||
# The benchmark host has 32GB memory
|
||||
# The following values are tailored to work with that
|
||||
# Note, tests may not exercise the targeted issues if the memory is increased on new test hosts.
|
||||
|
||||
COMPRESSION_TYPE: "none"
|
||||
CACHE_INDEX_AND_FILTER_BLOCKS: 1
|
||||
MIN_LEVEL_TO_COMPRESS: 3
|
||||
CACHE_SIZE_MB: 10240
|
||||
MB_WRITE_PER_SEC: 2
|
||||
|
||||
post-benchmarks:
|
||||
steps:
|
||||
@@ -269,12 +273,12 @@ jobs:
|
||||
./sst_dump --help | grep -E -q 'Supported compression types: kNoCompression$' # Verify no compiled in compression
|
||||
- post-steps
|
||||
|
||||
build-linux-shared_lib-alt_namespace-status_checked:
|
||||
build-linux-static_lib-alt_namespace-status_checked:
|
||||
executor: linux-docker
|
||||
resource_class: 2xlarge
|
||||
steps:
|
||||
- pre-steps
|
||||
- run: ASSERT_STATUS_CHECKED=1 TEST_UINT128_COMPAT=1 ROCKSDB_MODIFY_NPHASH=1 LIB_MODE=shared OPT="-DROCKSDB_NAMESPACE=alternative_rocksdb_ns" make V=1 -j32 check
|
||||
- run: ASSERT_STATUS_CHECKED=1 TEST_UINT128_COMPAT=1 ROCKSDB_MODIFY_NPHASH=1 LIB_MODE=static OPT="-DROCKSDB_NAMESPACE=alternative_rocksdb_ns" make V=1 -j24 check
|
||||
- post-steps
|
||||
|
||||
build-linux-release:
|
||||
@@ -282,11 +286,21 @@ jobs:
|
||||
resource_class: 2xlarge
|
||||
steps:
|
||||
- checkout # check out the code in the project directory
|
||||
- run: make V=1 -j32 LIB_MODE=shared release
|
||||
- run: ls librocksdb.so # ensure shared lib built
|
||||
- run: ./db_stress --version # ensure with gflags
|
||||
- run: make clean
|
||||
- run: make V=1 -j32 release
|
||||
- run: ls librocksdb.a # ensure static lib built
|
||||
- run: ./db_stress --version # ensure with gflags
|
||||
- run: make clean
|
||||
- run: apt-get remove -y libgflags-dev
|
||||
- run: make V=1 -j32 LIB_MODE=shared release
|
||||
- run: ls librocksdb.so # ensure shared lib built
|
||||
- run: if ./db_stress --version; then false; else true; fi # ensure without gflags
|
||||
- run: make clean
|
||||
- run: make V=1 -j32 release
|
||||
- run: ls librocksdb.a # ensure static lib built
|
||||
- run: if ./db_stress --version; then false; else true; fi # ensure without gflags
|
||||
- post-steps
|
||||
|
||||
@@ -406,7 +420,10 @@ jobs:
|
||||
steps:
|
||||
- checkout # check out the code in the project directory
|
||||
- run: apt-get update -y && apt-get install -y libgflags-dev
|
||||
- run: make V=1 -j8 unity_test
|
||||
- run:
|
||||
name: "Unity build"
|
||||
command: make V=1 -j8 unity_test
|
||||
no_output_timeout: 20m
|
||||
- run: make V=1 -j8 -k check-headers # could be moved to a different build
|
||||
- post-steps
|
||||
|
||||
@@ -417,7 +434,7 @@ jobs:
|
||||
- pre-steps
|
||||
- setup-folly
|
||||
- build-folly
|
||||
- run: USE_FOLLY=1 CC=gcc-7 CXX=g++-7 V=1 make -j32 check
|
||||
- run: USE_FOLLY=1 LIB_MODE=static CC=gcc-7 CXX=g++-7 V=1 make -j32 check # TODO: LIB_MODE only to work around unresolved linker failures
|
||||
- post-steps
|
||||
|
||||
build-linux-gcc-7-with-folly-lite-no-test:
|
||||
@@ -463,7 +480,7 @@ jobs:
|
||||
resource_class: 2xlarge
|
||||
steps:
|
||||
- pre-steps
|
||||
- run: CC=gcc-11 CXX=g++-11 V=1 make -j32 all microbench
|
||||
- run: LIB_MODE=static CC=gcc-11 CXX=g++-11 V=1 make -j32 all microbench # TODO: LIB_MODE only to work around unresolved linker failures
|
||||
- post-steps
|
||||
|
||||
build-linux-clang-13-no_test_run:
|
||||
@@ -482,7 +499,7 @@ jobs:
|
||||
- pre-steps
|
||||
- setup-folly
|
||||
- build-folly
|
||||
- run: CC=clang-13 CXX=clang++-13 USE_CLANG=1 USE_FOLLY=1 COMPILE_WITH_UBSAN=1 COMPILE_WITH_ASAN=1 make -j32 check
|
||||
- run: CC=clang-13 CXX=clang++-13 LIB_MODE=static USE_CLANG=1 USE_FOLLY=1 COMPILE_WITH_UBSAN=1 COMPILE_WITH_ASAN=1 make -j32 check # TODO: LIB_MODE only to work around unresolved linker failures
|
||||
- post-steps
|
||||
|
||||
# This job is only to make sure the microbench tests are able to run, the benchmark result is not meaningful as the CI host is changing.
|
||||
@@ -499,7 +516,7 @@ jobs:
|
||||
resource_class: large
|
||||
steps:
|
||||
- pre-steps
|
||||
- run: ulimit -S -n `ulimit -H -n` && make V=1 -j8 CRASH_TEST_EXT_ARGS='--duration=960 --max_key=2500000' blackbox_crash_test_with_atomic_flush
|
||||
- run: ulimit -S -n `ulimit -H -n` && make V=1 -j8 CRASH_TEST_EXT_ARGS='--duration=960 --max_key=2500000 --use_io_uring=0' blackbox_crash_test_with_atomic_flush
|
||||
- post-steps
|
||||
|
||||
build-linux-crashtest-tiered-storage-bb:
|
||||
@@ -509,7 +526,7 @@ jobs:
|
||||
- pre-steps
|
||||
- run:
|
||||
name: "run crashtest"
|
||||
command: ulimit -S -n `ulimit -H -n` && make V=1 -j32 CRASH_TEST_EXT_ARGS=--duration=10800 blackbox_crash_test_with_tiered_storage
|
||||
command: ulimit -S -n `ulimit -H -n` && make V=1 -j32 CRASH_TEST_EXT_ARGS='--duration=10800 --use_io_uring=0' blackbox_crash_test_with_tiered_storage
|
||||
no_output_timeout: 100m
|
||||
- post-steps
|
||||
|
||||
@@ -520,7 +537,7 @@ jobs:
|
||||
- pre-steps
|
||||
- run:
|
||||
name: "run crashtest"
|
||||
command: ulimit -S -n `ulimit -H -n` && make V=1 -j32 CRASH_TEST_EXT_ARGS=--duration=10800 whitebox_crash_test_with_tiered_storage
|
||||
command: ulimit -S -n `ulimit -H -n` && make V=1 -j32 CRASH_TEST_EXT_ARGS='--duration=10800 --use_io_uring=0' whitebox_crash_test_with_tiered_storage
|
||||
no_output_timeout: 100m
|
||||
- post-steps
|
||||
|
||||
@@ -805,7 +822,7 @@ workflows:
|
||||
- build-linux-clang10-asan
|
||||
- build-linux-clang10-ubsan
|
||||
- build-linux-clang10-mini-tsan
|
||||
- build-linux-shared_lib-alt_namespace-status_checked
|
||||
- build-linux-static_lib-alt_namespace-status_checked
|
||||
jobs-linux-no-test-run:
|
||||
jobs:
|
||||
- build-linux-release
|
||||
|
||||
@@ -661,6 +661,7 @@ set(SOURCES
|
||||
cache/compressed_secondary_cache.cc
|
||||
cache/lru_cache.cc
|
||||
cache/secondary_cache.cc
|
||||
cache/secondary_cache_adapter.cc
|
||||
cache/sharded_cache.cc
|
||||
db/arena_wrapped_db_iter.cc
|
||||
db/blob/blob_contents.cc
|
||||
@@ -747,6 +748,7 @@ set(SOURCES
|
||||
db/write_batch.cc
|
||||
db/write_batch_base.cc
|
||||
db/write_controller.cc
|
||||
db/write_stall_stats.cc
|
||||
db/write_thread.cc
|
||||
env/composite_env.cc
|
||||
env/env.cc
|
||||
@@ -838,6 +840,7 @@ set(SOURCES
|
||||
table/get_context.cc
|
||||
table/iterator.cc
|
||||
table/merging_iterator.cc
|
||||
table/compaction_merging_iterator.cc
|
||||
table/meta_blocks.cc
|
||||
table/persistent_cache_helper.cc
|
||||
table/plain/plain_table_bloom.cc
|
||||
@@ -879,6 +882,7 @@ set(SOURCES
|
||||
util/compression_context_cache.cc
|
||||
util/concurrent_task_limiter_impl.cc
|
||||
util/crc32c.cc
|
||||
util/data_structure.cc
|
||||
util/dynamic_bloom.cc
|
||||
util/hash.cc
|
||||
util/murmurhash.cc
|
||||
@@ -1277,6 +1281,7 @@ if(WITH_TESTS OR WITH_BENCHMARK_TOOLS)
|
||||
add_subdirectory(third-party/gtest-1.8.1/fused-src/gtest)
|
||||
add_library(testharness STATIC
|
||||
test_util/mock_time_env.cc
|
||||
test_util/secondary_cache_test_util.cc
|
||||
test_util/testharness.cc)
|
||||
target_link_libraries(testharness gtest)
|
||||
endif()
|
||||
|
||||
+42
-1
@@ -1,8 +1,33 @@
|
||||
# Rocksdb Change Log
|
||||
## Unreleased
|
||||
## 8.1.1 (04/06/2023)
|
||||
### Bug Fixes
|
||||
* In the DB::VerifyFileChecksums API, ensure that file system reads of SST files are equal to the readahead_size in ReadOptions, if specified. Previously, each read was 2x the readahead_size.
|
||||
|
||||
## 8.1.0 (03/18/2023)
|
||||
### Behavior changes
|
||||
* Compaction output file cutting logic now considers range tombstone start keys. For example, SST partitioner now may receive ParitionRequest for range tombstone start keys.
|
||||
* If the async_io ReadOption is specified for MultiGet or NewIterator on a platform that doesn't support IO uring, the option is ignored and synchronous IO is used.
|
||||
|
||||
### Bug Fixes
|
||||
* Fixed an issue for backward iteration when user defined timestamp is enabled in combination with BlobDB.
|
||||
* Fixed a couple of cases where a Merge operand encountered during iteration wasn't reflected in the `internal_merge_count` PerfContext counter.
|
||||
* Fixed a bug in CreateColumnFamilyWithImport()/ExportColumnFamily() which did not support range tombstones (#11252).
|
||||
* Fixed a bug where an excluded column family from an atomic flush contains unflushed data that should've been included in this atomic flush (i.e, data of seqno less than the max seqno of this atomic flush), leading to potential data loss in this excluded column family when `WriteOptions::disableWAL == true` (#11148).
|
||||
|
||||
### New Features
|
||||
* Add statistics rocksdb.secondary.cache.filter.hits, rocksdb.secondary.cache.index.hits, and rocksdb.secondary.cache.filter.hits
|
||||
* Added a new PerfContext counter `internal_merge_point_lookup_count` which tracks the number of Merge operands applied while serving point lookup queries.
|
||||
* Add new statistics rocksdb.table.open.prefetch.tail.read.bytes, rocksdb.table.open.prefetch.tail.{miss|hit}
|
||||
* Add support for SecondaryCache with HyperClockCache (`HyperClockCacheOptions` inherits `secondary_cache` option from `ShardedCacheOptions`)
|
||||
* Add new db properties `rocksdb.cf-write-stall-stats`, `rocksdb.db-write-stall-stats`and APIs to examine them in a structured way. In particular, users of `GetMapProperty()` with property `kCFWriteStallStats`/`kDBWriteStallStats` can now use the functions in `WriteStallStatsMapKeys` to find stats in the map.
|
||||
|
||||
### Public API Changes
|
||||
* Changed various functions and features in `Cache` that are mostly relevant to custom implementations or wrappers. Especially, asychronous lookup functionality is moved from `Lookup()` to a new `StartAsyncLookup()` function.
|
||||
|
||||
## 8.0.0 (02/19/2023)
|
||||
### Behavior changes
|
||||
* `ReadOptions::verify_checksums=false` disables checksum verification for more reads of non-`CacheEntryRole::kDataBlock` blocks.
|
||||
* In case of scan with async_io enabled, if posix doesn't support IOUring, Status::NotSupported error will be returned to the users. Initially that error was swallowed and reads were switched to synchronous reads.
|
||||
|
||||
### Bug Fixes
|
||||
* Fixed a data race on `ColumnFamilyData::flush_reason` caused by concurrent flushes.
|
||||
@@ -10,6 +35,9 @@
|
||||
* Fixed some atypical behaviors for `LockWAL()` such as allowing concurrent/recursive use and not expecting `UnlockWAL()` after non-OK result. See API comments.
|
||||
* Fixed a feature interaction bug where for blobs `GetEntity` would expose the blob reference instead of the blob value.
|
||||
* Fixed `DisableManualCompaction()` and `CompactRangeOptions::canceled` to cancel compactions even when they are waiting on conflicting compactions to finish
|
||||
* Fixed a bug in which a successful `GetMergeOperands()` could transiently return `Status::MergeInProgress()`
|
||||
* Return the correct error (Status::NotSupported()) to MultiGet caller when ReadOptions::async_io flag is true and IO uring is not enabled. Previously, Status::Corruption() was being returned when the actual failure was lack of async IO support.
|
||||
* Fixed a bug in DB open/recovery from a compressed WAL that was caused due to incorrect handling of certain record fragments with the same offset within a WAL block.
|
||||
|
||||
### Feature Removal
|
||||
* Remove RocksDB Lite.
|
||||
@@ -17,9 +45,22 @@
|
||||
* Remove deprecated Env::LoadEnv(). Use Env::CreateFromString() instead.
|
||||
* Remove deprecated FileSystem::Load(). Use FileSystem::CreateFromString() instead.
|
||||
* Removed the deprecated version of these utility functions and the corresponding Java bindings: `LoadOptionsFromFile`, `LoadLatestOptions`, `CheckOptionsCompatibility`.
|
||||
* Remove the FactoryFunc from the LoadObject method from the Customizable helper methods.
|
||||
|
||||
### Public API Changes
|
||||
* Moved rarely-needed Cache class definition to new advanced_cache.h, and added a CacheWrapper class to advanced_cache.h. Minor changes to SimCache API definitions.
|
||||
* Completely removed the following deprecated/obsolete statistics: the tickers `BLOCK_CACHE_INDEX_BYTES_EVICT`, `BLOCK_CACHE_FILTER_BYTES_EVICT`, `BLOOM_FILTER_MICROS`, `NO_FILE_CLOSES`, `STALL_L0_SLOWDOWN_MICROS`, `STALL_MEMTABLE_COMPACTION_MICROS`, `STALL_L0_NUM_FILES_MICROS`, `RATE_LIMIT_DELAY_MILLIS`, `NO_ITERATORS`, `NUMBER_FILTERED_DELETES`, `WRITE_TIMEDOUT`, `BLOB_DB_GC_NUM_KEYS_OVERWRITTEN`, `BLOB_DB_GC_NUM_KEYS_EXPIRED`, `BLOB_DB_GC_BYTES_OVERWRITTEN`, `BLOB_DB_GC_BYTES_EXPIRED`, `BLOCK_CACHE_COMPRESSION_DICT_BYTES_EVICT` as well as the histograms `STALL_L0_SLOWDOWN_COUNT`, `STALL_MEMTABLE_COMPACTION_COUNT`, `STALL_L0_NUM_FILES_COUNT`, `HARD_RATE_LIMIT_DELAY_COUNT`, `SOFT_RATE_LIMIT_DELAY_COUNT`, `BLOB_DB_GC_MICROS`, and `NUM_DATA_BLOCKS_READ_PER_LEVEL`. Note that as a result, the C++ enum values of the still supported statistics have changed. Developers are advised to not rely on the actual numeric values.
|
||||
* Deprecated IngestExternalFileOptions::write_global_seqno and change default to false. This option only needs to be set to true to generate a DB compatible with RocksDB versions before 5.16.0.
|
||||
* Remove deprecated APIs `GetColumnFamilyOptionsFrom{Map|String}(const ColumnFamilyOptions&, ..)`, `GetDBOptionsFrom{Map|String}(const DBOptions&, ..)`, `GetBlockBasedTableOptionsFrom{Map|String}(const BlockBasedTableOptions& table_options, ..)` and ` GetPlainTableOptionsFrom{Map|String}(const PlainTableOptions& table_options,..)`.
|
||||
* Added a subcode of `Status::Corruption`, `Status::SubCode::kMergeOperatorFailed`, for users to identify corruption failures originating in the merge operator, as opposed to RocksDB's internally identified data corruptions
|
||||
|
||||
### Build Changes
|
||||
* The `make` build now builds a shared library by default instead of a static library. Use `LIB_MODE=static` to override.
|
||||
|
||||
### New Features
|
||||
* Compaction filters are now supported for wide-column entities by means of the `FilterV3` API. See the comment of the API for more details.
|
||||
* Added `do_not_compress_roles` to `CompressedSecondaryCacheOptions` to disable compression on certain kinds of block. Filter blocks are now not compressed by CompressedSecondaryCache by default.
|
||||
* Added a new `MultiGetEntity` API that enables batched wide-column point lookups. See the API comments for more details.
|
||||
|
||||
## 7.10.0 (01/23/2023)
|
||||
### Behavior changes
|
||||
|
||||
@@ -48,6 +48,11 @@ to build a portable binary, add `PORTABLE=1` before your make commands, like thi
|
||||
* If you wish to build the RocksJava static target, then cmake is required for building Snappy.
|
||||
|
||||
* If you wish to run microbench (e.g, `make microbench`, `make ribbon_bench` or `cmake -DWITH_BENCHMARK=1`), Google benchmark >= 1.6.0 is needed.
|
||||
* You can do the following to install Google benchmark. These commands are copied from `./build_tools/ubuntu20_image/Dockerfile`:
|
||||
|
||||
`$ git clone --depth 1 --branch v1.7.0 https://github.com/google/benchmark.git ~/benchmark`
|
||||
|
||||
`$ cd ~/benchmark && mkdir build && cd build && cmake .. -GNinja -DCMAKE_BUILD_TYPE=Release -DBENCHMARK_ENABLE_GTEST_TESTS=0 && ninja && ninja install`
|
||||
|
||||
## Supported platforms
|
||||
|
||||
|
||||
@@ -44,13 +44,6 @@ quoted_perl_command = $(subst ','\'',$(perl_command))
|
||||
# Set the default DEBUG_LEVEL to 1
|
||||
DEBUG_LEVEL?=1
|
||||
|
||||
# LIB_MODE says whether or not to use/build "shared" or "static" libraries.
|
||||
# Mode "static" means to link against static libraries (.a)
|
||||
# Mode "shared" means to link against shared libraries (.so, .sl, .dylib, etc)
|
||||
#
|
||||
# Set the default LIB_MODE to static
|
||||
LIB_MODE?=static
|
||||
|
||||
# OBJ_DIR is where the object files reside. Default to the current directory
|
||||
OBJ_DIR?=.
|
||||
|
||||
@@ -81,7 +74,19 @@ else ifneq ($(filter jtest rocksdbjava%, $(MAKECMDGOALS)),)
|
||||
endif
|
||||
endif
|
||||
|
||||
$(info $$DEBUG_LEVEL is ${DEBUG_LEVEL})
|
||||
# LIB_MODE says whether or not to use/build "shared" or "static" libraries.
|
||||
# Mode "static" means to link against static libraries (.a)
|
||||
# Mode "shared" means to link against shared libraries (.so, .sl, .dylib, etc)
|
||||
#
|
||||
ifeq ($(DEBUG_LEVEL), 0)
|
||||
# For optimized, set the default LIB_MODE to static for code size/efficiency
|
||||
LIB_MODE?=static
|
||||
else
|
||||
# For debug, set the default LIB_MODE to shared for efficient `make check` etc.
|
||||
LIB_MODE?=shared
|
||||
endif
|
||||
|
||||
$(info $$DEBUG_LEVEL is $(DEBUG_LEVEL), $$LIB_MODE is $(LIB_MODE))
|
||||
|
||||
# Figure out optimize level.
|
||||
ifneq ($(DEBUG_LEVEL), 2)
|
||||
@@ -1217,9 +1222,9 @@ clean: clean-ext-libraries-all clean-rocks clean-rocksjava
|
||||
clean-not-downloaded: clean-ext-libraries-bin clean-rocks clean-not-downloaded-rocksjava
|
||||
|
||||
clean-rocks:
|
||||
echo shared=$(ALL_SHARED_LIBS)
|
||||
echo static=$(ALL_STATIC_LIBS)
|
||||
rm -f $(BENCHMARKS) $(TOOLS) $(TESTS) $(PARALLEL_TEST) $(ALL_STATIC_LIBS) $(ALL_SHARED_LIBS) $(MICROBENCHS)
|
||||
# Not practical to exactly match all versions/variants in naming (e.g. debug or not)
|
||||
rm -f ${LIBNAME}*.so* ${LIBNAME}*.a
|
||||
rm -f $(BENCHMARKS) $(TOOLS) $(TESTS) $(PARALLEL_TEST) $(MICROBENCHS)
|
||||
rm -rf $(CLEAN_FILES) ios-x86 ios-arm scan_build_report
|
||||
$(FIND) . -name "*.[oda]" -exec rm -f {} \;
|
||||
$(FIND) . -type f \( -name "*.gcda" -o -name "*.gcno" \) -exec rm -f {} \;
|
||||
|
||||
@@ -19,6 +19,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"cache/compressed_secondary_cache.cc",
|
||||
"cache/lru_cache.cc",
|
||||
"cache/secondary_cache.cc",
|
||||
"cache/secondary_cache_adapter.cc",
|
||||
"cache/sharded_cache.cc",
|
||||
"db/arena_wrapped_db_iter.cc",
|
||||
"db/blob/blob_contents.cc",
|
||||
@@ -105,6 +106,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"db/write_batch.cc",
|
||||
"db/write_batch_base.cc",
|
||||
"db/write_controller.cc",
|
||||
"db/write_stall_stats.cc",
|
||||
"db/write_thread.cc",
|
||||
"env/composite_env.cc",
|
||||
"env/env.cc",
|
||||
@@ -200,6 +202,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"table/block_based/reader_common.cc",
|
||||
"table/block_based/uncompression_dict_reader.cc",
|
||||
"table/block_fetcher.cc",
|
||||
"table/compaction_merging_iterator.cc",
|
||||
"table/cuckoo/cuckoo_table_builder.cc",
|
||||
"table/cuckoo/cuckoo_table_factory.cc",
|
||||
"table/cuckoo/cuckoo_table_reader.cc",
|
||||
@@ -247,6 +250,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"util/concurrent_task_limiter_impl.cc",
|
||||
"util/crc32c.cc",
|
||||
"util/crc32c_arm64.cc",
|
||||
"util/data_structure.cc",
|
||||
"util/dynamic_bloom.cc",
|
||||
"util/file_checksum_helper.cc",
|
||||
"util/hash.cc",
|
||||
@@ -350,353 +354,14 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"//folly/synchronization:distributed_mutex",
|
||||
], headers=None, link_whole=False, extra_test_libs=False)
|
||||
|
||||
cpp_library_wrapper(name="rocksdb_whole_archive_lib", srcs=[
|
||||
"cache/cache.cc",
|
||||
"cache/cache_entry_roles.cc",
|
||||
"cache/cache_helpers.cc",
|
||||
"cache/cache_key.cc",
|
||||
"cache/cache_reservation_manager.cc",
|
||||
"cache/charged_cache.cc",
|
||||
"cache/clock_cache.cc",
|
||||
"cache/compressed_secondary_cache.cc",
|
||||
"cache/lru_cache.cc",
|
||||
"cache/secondary_cache.cc",
|
||||
"cache/sharded_cache.cc",
|
||||
"db/arena_wrapped_db_iter.cc",
|
||||
"db/blob/blob_contents.cc",
|
||||
"db/blob/blob_fetcher.cc",
|
||||
"db/blob/blob_file_addition.cc",
|
||||
"db/blob/blob_file_builder.cc",
|
||||
"db/blob/blob_file_cache.cc",
|
||||
"db/blob/blob_file_garbage.cc",
|
||||
"db/blob/blob_file_meta.cc",
|
||||
"db/blob/blob_file_reader.cc",
|
||||
"db/blob/blob_garbage_meter.cc",
|
||||
"db/blob/blob_log_format.cc",
|
||||
"db/blob/blob_log_sequential_reader.cc",
|
||||
"db/blob/blob_log_writer.cc",
|
||||
"db/blob/blob_source.cc",
|
||||
"db/blob/prefetch_buffer_collection.cc",
|
||||
"db/builder.cc",
|
||||
"db/c.cc",
|
||||
"db/column_family.cc",
|
||||
"db/compaction/compaction.cc",
|
||||
"db/compaction/compaction_iterator.cc",
|
||||
"db/compaction/compaction_job.cc",
|
||||
"db/compaction/compaction_outputs.cc",
|
||||
"db/compaction/compaction_picker.cc",
|
||||
"db/compaction/compaction_picker_fifo.cc",
|
||||
"db/compaction/compaction_picker_level.cc",
|
||||
"db/compaction/compaction_picker_universal.cc",
|
||||
"db/compaction/compaction_service_job.cc",
|
||||
"db/compaction/compaction_state.cc",
|
||||
"db/compaction/sst_partitioner.cc",
|
||||
"db/compaction/subcompaction_state.cc",
|
||||
"db/convenience.cc",
|
||||
"db/db_filesnapshot.cc",
|
||||
"db/db_impl/compacted_db_impl.cc",
|
||||
"db/db_impl/db_impl.cc",
|
||||
"db/db_impl/db_impl_compaction_flush.cc",
|
||||
"db/db_impl/db_impl_debug.cc",
|
||||
"db/db_impl/db_impl_experimental.cc",
|
||||
"db/db_impl/db_impl_files.cc",
|
||||
"db/db_impl/db_impl_open.cc",
|
||||
"db/db_impl/db_impl_readonly.cc",
|
||||
"db/db_impl/db_impl_secondary.cc",
|
||||
"db/db_impl/db_impl_write.cc",
|
||||
"db/db_info_dumper.cc",
|
||||
"db/db_iter.cc",
|
||||
"db/dbformat.cc",
|
||||
"db/error_handler.cc",
|
||||
"db/event_helpers.cc",
|
||||
"db/experimental.cc",
|
||||
"db/external_sst_file_ingestion_job.cc",
|
||||
"db/file_indexer.cc",
|
||||
"db/flush_job.cc",
|
||||
"db/flush_scheduler.cc",
|
||||
"db/forward_iterator.cc",
|
||||
"db/import_column_family_job.cc",
|
||||
"db/internal_stats.cc",
|
||||
"db/log_reader.cc",
|
||||
"db/log_writer.cc",
|
||||
"db/logs_with_prep_tracker.cc",
|
||||
"db/malloc_stats.cc",
|
||||
"db/memtable.cc",
|
||||
"db/memtable_list.cc",
|
||||
"db/merge_helper.cc",
|
||||
"db/merge_operator.cc",
|
||||
"db/output_validator.cc",
|
||||
"db/periodic_task_scheduler.cc",
|
||||
"db/range_del_aggregator.cc",
|
||||
"db/range_tombstone_fragmenter.cc",
|
||||
"db/repair.cc",
|
||||
"db/seqno_to_time_mapping.cc",
|
||||
"db/snapshot_impl.cc",
|
||||
"db/table_cache.cc",
|
||||
"db/table_properties_collector.cc",
|
||||
"db/transaction_log_impl.cc",
|
||||
"db/trim_history_scheduler.cc",
|
||||
"db/version_builder.cc",
|
||||
"db/version_edit.cc",
|
||||
"db/version_edit_handler.cc",
|
||||
"db/version_set.cc",
|
||||
"db/wal_edit.cc",
|
||||
"db/wal_manager.cc",
|
||||
"db/wide/wide_column_serialization.cc",
|
||||
"db/wide/wide_columns.cc",
|
||||
"db/write_batch.cc",
|
||||
"db/write_batch_base.cc",
|
||||
"db/write_controller.cc",
|
||||
"db/write_thread.cc",
|
||||
"env/composite_env.cc",
|
||||
"env/env.cc",
|
||||
"env/env_chroot.cc",
|
||||
"env/env_encryption.cc",
|
||||
"env/env_posix.cc",
|
||||
"env/file_system.cc",
|
||||
"env/file_system_tracer.cc",
|
||||
"env/fs_posix.cc",
|
||||
"env/fs_remap.cc",
|
||||
"env/io_posix.cc",
|
||||
"env/mock_env.cc",
|
||||
"env/unique_id_gen.cc",
|
||||
"file/delete_scheduler.cc",
|
||||
"file/file_prefetch_buffer.cc",
|
||||
"file/file_util.cc",
|
||||
"file/filename.cc",
|
||||
"file/line_file_reader.cc",
|
||||
"file/random_access_file_reader.cc",
|
||||
"file/read_write_util.cc",
|
||||
"file/readahead_raf.cc",
|
||||
"file/sequence_file_reader.cc",
|
||||
"file/sst_file_manager_impl.cc",
|
||||
"file/writable_file_writer.cc",
|
||||
"logging/auto_roll_logger.cc",
|
||||
"logging/event_logger.cc",
|
||||
"logging/log_buffer.cc",
|
||||
"memory/arena.cc",
|
||||
"memory/concurrent_arena.cc",
|
||||
"memory/jemalloc_nodump_allocator.cc",
|
||||
"memory/memkind_kmem_allocator.cc",
|
||||
"memory/memory_allocator.cc",
|
||||
"memtable/alloc_tracker.cc",
|
||||
"memtable/hash_linklist_rep.cc",
|
||||
"memtable/hash_skiplist_rep.cc",
|
||||
"memtable/skiplistrep.cc",
|
||||
"memtable/vectorrep.cc",
|
||||
"memtable/write_buffer_manager.cc",
|
||||
"monitoring/histogram.cc",
|
||||
"monitoring/histogram_windowing.cc",
|
||||
"monitoring/in_memory_stats_history.cc",
|
||||
"monitoring/instrumented_mutex.cc",
|
||||
"monitoring/iostats_context.cc",
|
||||
"monitoring/perf_context.cc",
|
||||
"monitoring/perf_level.cc",
|
||||
"monitoring/persistent_stats_history.cc",
|
||||
"monitoring/statistics.cc",
|
||||
"monitoring/thread_status_impl.cc",
|
||||
"monitoring/thread_status_updater.cc",
|
||||
"monitoring/thread_status_updater_debug.cc",
|
||||
"monitoring/thread_status_util.cc",
|
||||
"monitoring/thread_status_util_debug.cc",
|
||||
"options/cf_options.cc",
|
||||
"options/configurable.cc",
|
||||
"options/customizable.cc",
|
||||
"options/db_options.cc",
|
||||
"options/options.cc",
|
||||
"options/options_helper.cc",
|
||||
"options/options_parser.cc",
|
||||
"port/mmap.cc",
|
||||
"port/port_posix.cc",
|
||||
"port/stack_trace.cc",
|
||||
"port/win/env_default.cc",
|
||||
"port/win/env_win.cc",
|
||||
"port/win/io_win.cc",
|
||||
"port/win/port_win.cc",
|
||||
"port/win/win_logger.cc",
|
||||
"port/win/win_thread.cc",
|
||||
"table/adaptive/adaptive_table_factory.cc",
|
||||
"table/block_based/binary_search_index_reader.cc",
|
||||
"table/block_based/block.cc",
|
||||
"table/block_based/block_based_table_builder.cc",
|
||||
"table/block_based/block_based_table_factory.cc",
|
||||
"table/block_based/block_based_table_iterator.cc",
|
||||
"table/block_based/block_based_table_reader.cc",
|
||||
"table/block_based/block_builder.cc",
|
||||
"table/block_based/block_cache.cc",
|
||||
"table/block_based/block_prefetcher.cc",
|
||||
"table/block_based/block_prefix_index.cc",
|
||||
"table/block_based/data_block_footer.cc",
|
||||
"table/block_based/data_block_hash_index.cc",
|
||||
"table/block_based/filter_block_reader_common.cc",
|
||||
"table/block_based/filter_policy.cc",
|
||||
"table/block_based/flush_block_policy.cc",
|
||||
"table/block_based/full_filter_block.cc",
|
||||
"table/block_based/hash_index_reader.cc",
|
||||
"table/block_based/index_builder.cc",
|
||||
"table/block_based/index_reader_common.cc",
|
||||
"table/block_based/parsed_full_filter_block.cc",
|
||||
"table/block_based/partitioned_filter_block.cc",
|
||||
"table/block_based/partitioned_index_iterator.cc",
|
||||
"table/block_based/partitioned_index_reader.cc",
|
||||
"table/block_based/reader_common.cc",
|
||||
"table/block_based/uncompression_dict_reader.cc",
|
||||
"table/block_fetcher.cc",
|
||||
"table/cuckoo/cuckoo_table_builder.cc",
|
||||
"table/cuckoo/cuckoo_table_factory.cc",
|
||||
"table/cuckoo/cuckoo_table_reader.cc",
|
||||
"table/format.cc",
|
||||
"table/get_context.cc",
|
||||
"table/iterator.cc",
|
||||
"table/merging_iterator.cc",
|
||||
"table/meta_blocks.cc",
|
||||
"table/persistent_cache_helper.cc",
|
||||
"table/plain/plain_table_bloom.cc",
|
||||
"table/plain/plain_table_builder.cc",
|
||||
"table/plain/plain_table_factory.cc",
|
||||
"table/plain/plain_table_index.cc",
|
||||
"table/plain/plain_table_key_coding.cc",
|
||||
"table/plain/plain_table_reader.cc",
|
||||
"table/sst_file_dumper.cc",
|
||||
"table/sst_file_reader.cc",
|
||||
"table/sst_file_writer.cc",
|
||||
"table/table_factory.cc",
|
||||
"table/table_properties.cc",
|
||||
"table/two_level_iterator.cc",
|
||||
"table/unique_id.cc",
|
||||
"test_util/sync_point.cc",
|
||||
"test_util/sync_point_impl.cc",
|
||||
"test_util/transaction_test_util.cc",
|
||||
"tools/dump/db_dump_tool.cc",
|
||||
"tools/io_tracer_parser_tool.cc",
|
||||
"tools/ldb_cmd.cc",
|
||||
"tools/ldb_tool.cc",
|
||||
"tools/sst_dump_tool.cc",
|
||||
"trace_replay/block_cache_tracer.cc",
|
||||
"trace_replay/io_tracer.cc",
|
||||
"trace_replay/trace_record.cc",
|
||||
"trace_replay/trace_record_handler.cc",
|
||||
"trace_replay/trace_record_result.cc",
|
||||
"trace_replay/trace_replay.cc",
|
||||
"util/async_file_reader.cc",
|
||||
"util/build_version.cc",
|
||||
"util/cleanable.cc",
|
||||
"util/coding.cc",
|
||||
"util/compaction_job_stats_impl.cc",
|
||||
"util/comparator.cc",
|
||||
"util/compression.cc",
|
||||
"util/compression_context_cache.cc",
|
||||
"util/concurrent_task_limiter_impl.cc",
|
||||
"util/crc32c.cc",
|
||||
"util/crc32c_arm64.cc",
|
||||
"util/dynamic_bloom.cc",
|
||||
"util/file_checksum_helper.cc",
|
||||
"util/hash.cc",
|
||||
"util/murmurhash.cc",
|
||||
"util/random.cc",
|
||||
"util/rate_limiter.cc",
|
||||
"util/ribbon_config.cc",
|
||||
"util/slice.cc",
|
||||
"util/status.cc",
|
||||
"util/stderr_logger.cc",
|
||||
"util/string_util.cc",
|
||||
"util/thread_local.cc",
|
||||
"util/threadpool_imp.cc",
|
||||
"util/xxhash.cc",
|
||||
"utilities/agg_merge/agg_merge.cc",
|
||||
"utilities/backup/backup_engine.cc",
|
||||
"utilities/blob_db/blob_compaction_filter.cc",
|
||||
"utilities/blob_db/blob_db.cc",
|
||||
"utilities/blob_db/blob_db_impl.cc",
|
||||
"utilities/blob_db/blob_db_impl_filesnapshot.cc",
|
||||
"utilities/blob_db/blob_dump_tool.cc",
|
||||
"utilities/blob_db/blob_file.cc",
|
||||
"utilities/cache_dump_load.cc",
|
||||
"utilities/cache_dump_load_impl.cc",
|
||||
"utilities/cassandra/cassandra_compaction_filter.cc",
|
||||
"utilities/cassandra/format.cc",
|
||||
"utilities/cassandra/merge_operator.cc",
|
||||
"utilities/checkpoint/checkpoint_impl.cc",
|
||||
"utilities/compaction_filters.cc",
|
||||
"utilities/compaction_filters/remove_emptyvalue_compactionfilter.cc",
|
||||
"utilities/convenience/info_log_finder.cc",
|
||||
"utilities/counted_fs.cc",
|
||||
"utilities/debug.cc",
|
||||
"utilities/env_mirror.cc",
|
||||
"utilities/env_timed.cc",
|
||||
"utilities/fault_injection_env.cc",
|
||||
"utilities/fault_injection_fs.cc",
|
||||
"utilities/fault_injection_secondary_cache.cc",
|
||||
"utilities/leveldb_options/leveldb_options.cc",
|
||||
"utilities/memory/memory_util.cc",
|
||||
"utilities/merge_operators.cc",
|
||||
"utilities/merge_operators/bytesxor.cc",
|
||||
"utilities/merge_operators/max.cc",
|
||||
"utilities/merge_operators/put.cc",
|
||||
"utilities/merge_operators/sortlist.cc",
|
||||
"utilities/merge_operators/string_append/stringappend.cc",
|
||||
"utilities/merge_operators/string_append/stringappend2.cc",
|
||||
"utilities/merge_operators/uint64add.cc",
|
||||
"utilities/object_registry.cc",
|
||||
"utilities/option_change_migration/option_change_migration.cc",
|
||||
"utilities/options/options_util.cc",
|
||||
"utilities/persistent_cache/block_cache_tier.cc",
|
||||
"utilities/persistent_cache/block_cache_tier_file.cc",
|
||||
"utilities/persistent_cache/block_cache_tier_metadata.cc",
|
||||
"utilities/persistent_cache/persistent_cache_tier.cc",
|
||||
"utilities/persistent_cache/volatile_tier_impl.cc",
|
||||
"utilities/simulator_cache/cache_simulator.cc",
|
||||
"utilities/simulator_cache/sim_cache.cc",
|
||||
"utilities/table_properties_collectors/compact_on_deletion_collector.cc",
|
||||
"utilities/trace/file_trace_reader_writer.cc",
|
||||
"utilities/trace/replayer_impl.cc",
|
||||
"utilities/transactions/lock/lock_manager.cc",
|
||||
"utilities/transactions/lock/point/point_lock_manager.cc",
|
||||
"utilities/transactions/lock/point/point_lock_tracker.cc",
|
||||
"utilities/transactions/lock/range/range_tree/lib/locktree/concurrent_tree.cc",
|
||||
"utilities/transactions/lock/range/range_tree/lib/locktree/keyrange.cc",
|
||||
"utilities/transactions/lock/range/range_tree/lib/locktree/lock_request.cc",
|
||||
"utilities/transactions/lock/range/range_tree/lib/locktree/locktree.cc",
|
||||
"utilities/transactions/lock/range/range_tree/lib/locktree/manager.cc",
|
||||
"utilities/transactions/lock/range/range_tree/lib/locktree/range_buffer.cc",
|
||||
"utilities/transactions/lock/range/range_tree/lib/locktree/treenode.cc",
|
||||
"utilities/transactions/lock/range/range_tree/lib/locktree/txnid_set.cc",
|
||||
"utilities/transactions/lock/range/range_tree/lib/locktree/wfg.cc",
|
||||
"utilities/transactions/lock/range/range_tree/lib/standalone_port.cc",
|
||||
"utilities/transactions/lock/range/range_tree/lib/util/dbt.cc",
|
||||
"utilities/transactions/lock/range/range_tree/lib/util/memarena.cc",
|
||||
"utilities/transactions/lock/range/range_tree/range_tree_lock_manager.cc",
|
||||
"utilities/transactions/lock/range/range_tree/range_tree_lock_tracker.cc",
|
||||
"utilities/transactions/optimistic_transaction.cc",
|
||||
"utilities/transactions/optimistic_transaction_db_impl.cc",
|
||||
"utilities/transactions/pessimistic_transaction.cc",
|
||||
"utilities/transactions/pessimistic_transaction_db.cc",
|
||||
"utilities/transactions/snapshot_checker.cc",
|
||||
"utilities/transactions/transaction_base.cc",
|
||||
"utilities/transactions/transaction_db_mutex_impl.cc",
|
||||
"utilities/transactions/transaction_util.cc",
|
||||
"utilities/transactions/write_prepared_txn.cc",
|
||||
"utilities/transactions/write_prepared_txn_db.cc",
|
||||
"utilities/transactions/write_unprepared_txn.cc",
|
||||
"utilities/transactions/write_unprepared_txn_db.cc",
|
||||
"utilities/ttl/db_ttl_impl.cc",
|
||||
"utilities/wal_filter.cc",
|
||||
"utilities/write_batch_with_index/write_batch_with_index.cc",
|
||||
"utilities/write_batch_with_index/write_batch_with_index_internal.cc",
|
||||
], deps=[
|
||||
"//folly/container:f14_hash",
|
||||
"//folly/experimental/coro:blocking_wait",
|
||||
"//folly/experimental/coro:collect",
|
||||
"//folly/experimental/coro:coroutine",
|
||||
"//folly/experimental/coro:task",
|
||||
"//folly/synchronization:distributed_mutex",
|
||||
], headers=None, link_whole=True, extra_test_libs=False)
|
||||
cpp_library_wrapper(name="rocksdb_whole_archive_lib", srcs=[], deps=[":rocksdb_lib"], headers=None, link_whole=True, extra_test_libs=False)
|
||||
|
||||
cpp_library_wrapper(name="rocksdb_test_lib", srcs=[
|
||||
"db/db_test_util.cc",
|
||||
"db/db_with_timestamp_test_util.cc",
|
||||
"table/mock_table.cc",
|
||||
"test_util/mock_time_env.cc",
|
||||
"test_util/secondary_cache_test_util.cc",
|
||||
"test_util/testharness.cc",
|
||||
"test_util/testutil.cc",
|
||||
"tools/block_cache_analyzer/block_cache_trace_analyzer.cc",
|
||||
|
||||
@@ -15,6 +15,9 @@ At Facebook, we use RocksDB as storage engines in multiple data management servi
|
||||
|
||||
[2] https://code.facebook.com/posts/357056558062811/logdevice-a-distributed-data-store-for-logs/
|
||||
|
||||
## Microsoft
|
||||
The Bing search engine from Microsoft uses RocksDB as the storage engine for its web data platform: https://blogs.bing.com/Engineering-Blog/october-2021/RocksDB-in-Microsoft-Bing
|
||||
|
||||
## LinkedIn
|
||||
Two different use cases at Linkedin are using RocksDB as a storage engine:
|
||||
|
||||
|
||||
@@ -154,16 +154,9 @@ def generate_targets(repo_path, deps_map):
|
||||
# rocksdb_whole_archive_lib
|
||||
TARGETS.add_library(
|
||||
"rocksdb_whole_archive_lib",
|
||||
src_mk["LIB_SOURCES"] +
|
||||
# always add range_tree, it's only excluded on ppc64, which we don't use internally
|
||||
src_mk["RANGE_TREE_SOURCES"] + src_mk["TOOL_LIB_SOURCES"],
|
||||
[],
|
||||
deps=[
|
||||
"//folly/container:f14_hash",
|
||||
"//folly/experimental/coro:blocking_wait",
|
||||
"//folly/experimental/coro:collect",
|
||||
"//folly/experimental/coro:coroutine",
|
||||
"//folly/experimental/coro:task",
|
||||
"//folly/synchronization:distributed_mutex",
|
||||
":rocksdb_lib",
|
||||
],
|
||||
headers=None,
|
||||
extra_external_deps="",
|
||||
|
||||
@@ -63,13 +63,7 @@ if [ -z "$ROCKSDB_NO_FBCODE" -a -d /mnt/gvfs/third-party ]; then
|
||||
if [ "$LIB_MODE" == "shared" ]; then
|
||||
PIC_BUILD=1
|
||||
fi
|
||||
if [ -n "$ROCKSDB_FBCODE_BUILD_WITH_PLATFORM010" ]; then
|
||||
source "$PWD/build_tools/fbcode_config_platform010.sh"
|
||||
elif [ -n "$ROCKSDB_FBCODE_BUILD_WITH_PLATFORM009" ]; then
|
||||
source "$PWD/build_tools/fbcode_config_platform009.sh"
|
||||
else
|
||||
source "$PWD/build_tools/fbcode_config_platform009.sh"
|
||||
fi
|
||||
source "$PWD/build_tools/fbcode_config_platform010.sh"
|
||||
fi
|
||||
|
||||
# Delete existing output, if it exists
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
|
||||
GCC_BASE=/mnt/gvfs/third-party2/gcc/1795efe5f06778c15a92c8f9a2aba5dc496d9d4d/9.x/centos7-native/3bed279
|
||||
CLANG_BASE=/mnt/gvfs/third-party2/llvm-fb/7318eaac22659b6ff2fe43918e4b69fd0772a8a7/9.0.0/platform009/651ee30
|
||||
LIBGCC_BASE=/mnt/gvfs/third-party2/libgcc/4959b39cfbe5965a37c861c4c327fa7c5c759b87/9.x/platform009/9202ce7
|
||||
GLIBC_BASE=/mnt/gvfs/third-party2/glibc/45ce3375cdc77ecb2520bbf8f0ecddd3f98efd7a/2.30/platform009/f259413
|
||||
SNAPPY_BASE=/mnt/gvfs/third-party2/snappy/be4de3205e029101b18aa8103daa696c2bef3b19/1.1.3/platform009/7f3b187
|
||||
ZLIB_BASE=/mnt/gvfs/third-party2/zlib/3c160ac5c67e257501e24c6c1d00ad5e01d73db6/1.2.8/platform009/7f3b187
|
||||
BZIP2_BASE=/mnt/gvfs/third-party2/bzip2/73a237ac5bc0a5f5d67b39b8d253cfebaab88684/1.0.6/platform009/7f3b187
|
||||
LZ4_BASE=/mnt/gvfs/third-party2/lz4/6ca38d3c390be2774d61a300f151464bbd632d62/1.9.1/platform009/7f3b187
|
||||
ZSTD_BASE=/mnt/gvfs/third-party2/zstd/64c58a207d2495e83abc57a500a956df09b79a7c/1.4.x/platform009/ba86d1f
|
||||
GFLAGS_BASE=/mnt/gvfs/third-party2/gflags/824d0a8a5abb5b121afd1b35fc3896407ea50092/2.2.0/platform009/7f3b187
|
||||
JEMALLOC_BASE=/mnt/gvfs/third-party2/jemalloc/b62912d333ef33f9760efa6219dbe3fe6abb3b0e/master/platform009/c305944
|
||||
NUMA_BASE=/mnt/gvfs/third-party2/numa/0af65f71e23a67bf65dc91b11f95caa39325c432/2.0.11/platform009/7f3b187
|
||||
LIBUNWIND_BASE=/mnt/gvfs/third-party2/libunwind/02486dac347645d31dce116f44e1de3177315be2/1.4/platform009/5191652
|
||||
TBB_BASE=/mnt/gvfs/third-party2/tbb/2e0ec671e550bfca347300bf3f789d9c0fff24ad/2018_U5/platform009/7f3b187
|
||||
LIBURING_BASE=/mnt/gvfs/third-party2/liburing/70dbd9cfee63a25611417d09433a86d7711b3990/20200729/platform009/7f3b187
|
||||
KERNEL_HEADERS_BASE=/mnt/gvfs/third-party2/kernel-headers/32b8a2407b634df3f8f948ba373fc4acc6a18296/fb/platform009/da39a3e
|
||||
BINUTILS_BASE=/mnt/gvfs/third-party2/binutils/08634589372fa5f237bfd374e8c644a8364e78c1/2.32/platform009/ba86d1f/
|
||||
VALGRIND_BASE=/mnt/gvfs/third-party2/valgrind/6ae525939ad02e5e676855082fbbc7828dbafeac/3.15.0/platform009/7f3b187
|
||||
LUA_BASE=/mnt/gvfs/third-party2/lua/162efd9561a3d21f6869f4814011e9cf1b3ff4dc/5.3.4/platform009/a6271c4
|
||||
BENCHMARK_BASE=/mnt/gvfs/third-party2/benchmark/30bf49ad6414325e17f3425b0edcb64239427ae3/1.6.1/platform009/7f3b187
|
||||
GLOG_BASE=/mnt/gvfs/third-party2/glog/32d751bd5673375b438158717ab6a57c1cc57e3d/0.3.2_fb/platform009/10a364d
|
||||
@@ -1,170 +0,0 @@
|
||||
#!/bin/sh
|
||||
# Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
|
||||
#
|
||||
# Set environment variables so that we can compile rocksdb using
|
||||
# fbcode settings. It uses the latest g++ and clang compilers and also
|
||||
# uses jemalloc
|
||||
# Environment variables that change the behavior of this script:
|
||||
# PIC_BUILD -- if true, it will only take pic versions of libraries from fbcode. libraries that don't have pic variant will not be included
|
||||
|
||||
|
||||
BASEDIR=`dirname $BASH_SOURCE`
|
||||
source "$BASEDIR/dependencies_platform009.sh"
|
||||
|
||||
CFLAGS=""
|
||||
|
||||
# libgcc
|
||||
LIBGCC_INCLUDE="$LIBGCC_BASE/include/c++/9.3.0 -I $LIBGCC_BASE/include/c++/9.3.0/backward"
|
||||
LIBGCC_LIBS=" -L $LIBGCC_BASE/lib"
|
||||
|
||||
# glibc
|
||||
GLIBC_INCLUDE="$GLIBC_BASE/include"
|
||||
GLIBC_LIBS=" -L $GLIBC_BASE/lib"
|
||||
|
||||
if test -z $PIC_BUILD; then
|
||||
MAYBE_PIC=
|
||||
else
|
||||
MAYBE_PIC=_pic
|
||||
fi
|
||||
|
||||
if ! test $ROCKSDB_DISABLE_SNAPPY; then
|
||||
# snappy
|
||||
SNAPPY_INCLUDE=" -I $SNAPPY_BASE/include/"
|
||||
SNAPPY_LIBS=" $SNAPPY_BASE/lib/libsnappy${MAYBE_PIC}.a"
|
||||
CFLAGS+=" -DSNAPPY"
|
||||
fi
|
||||
|
||||
if ! test $ROCKSDB_DISABLE_ZLIB; then
|
||||
# location of zlib headers and libraries
|
||||
ZLIB_INCLUDE=" -I $ZLIB_BASE/include/"
|
||||
ZLIB_LIBS=" $ZLIB_BASE/lib/libz${MAYBE_PIC}.a"
|
||||
CFLAGS+=" -DZLIB"
|
||||
fi
|
||||
|
||||
if ! test $ROCKSDB_DISABLE_BZIP; then
|
||||
# location of bzip headers and libraries
|
||||
BZIP_INCLUDE=" -I $BZIP2_BASE/include/"
|
||||
BZIP_LIBS=" $BZIP2_BASE/lib/libbz2${MAYBE_PIC}.a"
|
||||
CFLAGS+=" -DBZIP2"
|
||||
fi
|
||||
|
||||
if ! test $ROCKSDB_DISABLE_LZ4; then
|
||||
LZ4_INCLUDE=" -I $LZ4_BASE/include/"
|
||||
LZ4_LIBS=" $LZ4_BASE/lib/liblz4${MAYBE_PIC}.a"
|
||||
CFLAGS+=" -DLZ4"
|
||||
fi
|
||||
|
||||
if ! test $ROCKSDB_DISABLE_ZSTD; then
|
||||
ZSTD_INCLUDE=" -I $ZSTD_BASE/include/"
|
||||
ZSTD_LIBS=" $ZSTD_BASE/lib/libzstd${MAYBE_PIC}.a"
|
||||
CFLAGS+=" -DZSTD"
|
||||
fi
|
||||
|
||||
# location of gflags headers and libraries
|
||||
GFLAGS_INCLUDE=" -I $GFLAGS_BASE/include/"
|
||||
GFLAGS_LIBS=" $GFLAGS_BASE/lib/libgflags${MAYBE_PIC}.a"
|
||||
CFLAGS+=" -DGFLAGS=gflags"
|
||||
|
||||
BENCHMARK_INCLUDE=" -I $BENCHMARK_BASE/include/"
|
||||
BENCHMARK_LIBS=" $BENCHMARK_BASE/lib/libbenchmark${MAYBE_PIC}.a"
|
||||
|
||||
GLOG_INCLUDE=" -I $GLOG_BASE/include/"
|
||||
GLOG_LIBS=" $GLOG_BASE/lib/libglog${MAYBE_PIC}.a"
|
||||
|
||||
# location of jemalloc
|
||||
JEMALLOC_INCLUDE=" -I $JEMALLOC_BASE/include/"
|
||||
JEMALLOC_LIB=" $JEMALLOC_BASE/lib/libjemalloc${MAYBE_PIC}.a"
|
||||
|
||||
# location of numa
|
||||
NUMA_INCLUDE=" -I $NUMA_BASE/include/"
|
||||
NUMA_LIB=" $NUMA_BASE/lib/libnuma${MAYBE_PIC}.a"
|
||||
CFLAGS+=" -DNUMA"
|
||||
|
||||
# location of libunwind
|
||||
LIBUNWIND="$LIBUNWIND_BASE/lib/libunwind${MAYBE_PIC}.a"
|
||||
|
||||
# location of TBB
|
||||
TBB_INCLUDE=" -isystem $TBB_BASE/include/"
|
||||
TBB_LIBS="$TBB_BASE/lib/libtbb${MAYBE_PIC}.a"
|
||||
CFLAGS+=" -DTBB"
|
||||
|
||||
# location of LIBURING
|
||||
LIBURING_INCLUDE=" -isystem $LIBURING_BASE/include/"
|
||||
LIBURING_LIBS="$LIBURING_BASE/lib/liburing${MAYBE_PIC}.a"
|
||||
CFLAGS+=" -DLIBURING"
|
||||
|
||||
test "$USE_SSE" || USE_SSE=1
|
||||
export USE_SSE
|
||||
test "$PORTABLE" || PORTABLE=1
|
||||
export PORTABLE
|
||||
|
||||
BINUTILS="$BINUTILS_BASE/bin"
|
||||
AR="$BINUTILS/ar"
|
||||
AS="$BINUTILS/as"
|
||||
|
||||
DEPS_INCLUDE="$SNAPPY_INCLUDE $ZLIB_INCLUDE $BZIP_INCLUDE $LZ4_INCLUDE $ZSTD_INCLUDE $GFLAGS_INCLUDE $NUMA_INCLUDE $TBB_INCLUDE $LIBURING_INCLUDE $BENCHMARK_INCLUDE $GLOG_INCLUDE"
|
||||
|
||||
STDLIBS="-L $GCC_BASE/lib64"
|
||||
|
||||
CLANG_BIN="$CLANG_BASE/bin"
|
||||
CLANG_LIB="$CLANG_BASE/lib"
|
||||
CLANG_SRC="$CLANG_BASE/../../src"
|
||||
|
||||
CLANG_ANALYZER="$CLANG_BIN/clang++"
|
||||
CLANG_SCAN_BUILD="$CLANG_SRC/llvm/clang/tools/scan-build/bin/scan-build"
|
||||
|
||||
if [ -z "$USE_CLANG" ]; then
|
||||
# gcc
|
||||
CC="$GCC_BASE/bin/gcc"
|
||||
CXX="$GCC_BASE/bin/g++"
|
||||
AR="$GCC_BASE/bin/gcc-ar"
|
||||
|
||||
CFLAGS+=" -B$BINUTILS"
|
||||
CFLAGS+=" -isystem $LIBGCC_INCLUDE"
|
||||
CFLAGS+=" -isystem $GLIBC_INCLUDE"
|
||||
JEMALLOC=1
|
||||
else
|
||||
# clang
|
||||
CLANG_INCLUDE="$CLANG_LIB/clang/stable/include"
|
||||
CC="$CLANG_BIN/clang"
|
||||
CXX="$CLANG_BIN/clang++"
|
||||
AR="$CLANG_BIN/llvm-ar"
|
||||
|
||||
KERNEL_HEADERS_INCLUDE="$KERNEL_HEADERS_BASE/include"
|
||||
|
||||
CFLAGS+=" -B$BINUTILS -nostdinc -nostdlib"
|
||||
CFLAGS+=" -isystem $LIBGCC_BASE/include/c++/9.x "
|
||||
CFLAGS+=" -isystem $LIBGCC_BASE/include/c++/9.x/x86_64-facebook-linux "
|
||||
CFLAGS+=" -isystem $GLIBC_INCLUDE"
|
||||
CFLAGS+=" -isystem $LIBGCC_INCLUDE"
|
||||
CFLAGS+=" -isystem $CLANG_INCLUDE"
|
||||
CFLAGS+=" -isystem $KERNEL_HEADERS_INCLUDE/linux "
|
||||
CFLAGS+=" -isystem $KERNEL_HEADERS_INCLUDE "
|
||||
CFLAGS+=" -Wno-expansion-to-defined "
|
||||
CXXFLAGS="-nostdinc++"
|
||||
fi
|
||||
|
||||
CFLAGS+=" $DEPS_INCLUDE"
|
||||
CFLAGS+=" -DROCKSDB_PLATFORM_POSIX -DROCKSDB_LIB_IO_POSIX -DROCKSDB_FALLOCATE_PRESENT -DROCKSDB_MALLOC_USABLE_SIZE -DROCKSDB_RANGESYNC_PRESENT -DROCKSDB_SCHED_GETCPU_PRESENT -DHAVE_SSE42 -DROCKSDB_IOURING_PRESENT"
|
||||
CXXFLAGS+=" $CFLAGS"
|
||||
|
||||
EXEC_LDFLAGS=" $SNAPPY_LIBS $ZLIB_LIBS $BZIP_LIBS $LZ4_LIBS $ZSTD_LIBS $GFLAGS_LIBS $NUMA_LIB $TBB_LIBS $LIBURING_LIBS $BENCHMARK_LIBS"
|
||||
EXEC_LDFLAGS+=" -Wl,--dynamic-linker,/usr/local/fbcode/platform009/lib/ld.so"
|
||||
EXEC_LDFLAGS+=" $LIBUNWIND"
|
||||
EXEC_LDFLAGS+=" -Wl,-rpath=/usr/local/fbcode/platform009/lib"
|
||||
EXEC_LDFLAGS+=" -Wl,-rpath=$GCC_BASE/lib64"
|
||||
# required by libtbb
|
||||
EXEC_LDFLAGS+=" -ldl"
|
||||
|
||||
PLATFORM_LDFLAGS="$LIBGCC_LIBS $GLIBC_LIBS $STDLIBS -lgcc -lstdc++"
|
||||
PLATFORM_LDFLAGS+=" -B$BINUTILS"
|
||||
|
||||
EXEC_LDFLAGS_SHARED="$SNAPPY_LIBS $ZLIB_LIBS $BZIP_LIBS $LZ4_LIBS $ZSTD_LIBS $GFLAGS_LIBS $TBB_LIBS $LIBURING_LIBS $BENCHMARK_LIBS"
|
||||
|
||||
VALGRIND_VER="$VALGRIND_BASE/bin/"
|
||||
|
||||
# lua not supported because it's on track for deprecation, I think
|
||||
LUA_PATH=
|
||||
LUA_LIB=
|
||||
|
||||
export CC CXX AR AS CFLAGS CXXFLAGS EXEC_LDFLAGS EXEC_LDFLAGS_SHARED VALGRIND_VER JEMALLOC_LIB JEMALLOC_INCLUDE CLANG_ANALYZER CLANG_SCAN_BUILD LUA_PATH LUA_LIB
|
||||
@@ -360,7 +360,7 @@ function send_to_ods {
|
||||
echo >&2 "ERROR: Key $key doesn't have a value."
|
||||
return
|
||||
fi
|
||||
curl --silent "https://www.intern.facebook.com/intern/agent/ods_set.php?entity=rocksdb_build&key=$key&value=$value" \
|
||||
curl --silent "https://www.facebook.com/intern/agent/ods_set.php?entity=rocksdb_build&key=$key&value=$value" \
|
||||
--connect-timeout 60
|
||||
}
|
||||
|
||||
|
||||
@@ -104,46 +104,3 @@ get_lib_base valgrind LATEST platform010
|
||||
get_lib_base lua 5.3.4 platform010
|
||||
|
||||
git diff $OUTPUT
|
||||
|
||||
|
||||
###########################################################
|
||||
# platform009 dependencies #
|
||||
###########################################################
|
||||
|
||||
OUTPUT="$BASEDIR/dependencies_platform009.sh"
|
||||
|
||||
rm -f "$OUTPUT"
|
||||
touch "$OUTPUT"
|
||||
|
||||
echo "Writing dependencies to $OUTPUT"
|
||||
|
||||
# Compilers locations
|
||||
GCC_BASE=`readlink -f $TP2_LATEST/gcc/9.x/centos7-native/*/`
|
||||
CLANG_BASE=`readlink -f $TP2_LATEST/llvm-fb/9.0.0/platform009/*/`
|
||||
|
||||
log_header
|
||||
log_variable GCC_BASE
|
||||
log_variable CLANG_BASE
|
||||
|
||||
# Libraries locations
|
||||
get_lib_base libgcc 9.x platform009
|
||||
get_lib_base glibc 2.30 platform009
|
||||
get_lib_base snappy LATEST platform009
|
||||
get_lib_base zlib LATEST platform009
|
||||
get_lib_base bzip2 LATEST platform009
|
||||
get_lib_base lz4 LATEST platform009
|
||||
get_lib_base zstd LATEST platform009
|
||||
get_lib_base gflags LATEST platform009
|
||||
get_lib_base jemalloc LATEST platform009
|
||||
get_lib_base numa LATEST platform009
|
||||
get_lib_base libunwind LATEST platform009
|
||||
get_lib_base tbb 2018_U5 platform009
|
||||
get_lib_base liburing LATEST platform009
|
||||
get_lib_base benchmark LATEST platform009
|
||||
|
||||
get_lib_base kernel-headers fb platform009
|
||||
get_lib_base binutils LATEST centos7-native
|
||||
get_lib_base valgrind LATEST platform009
|
||||
get_lib_base lua 5.3.4 platform009
|
||||
|
||||
git diff $OUTPUT
|
||||
Vendored
+44
-2
@@ -16,6 +16,8 @@
|
||||
#include "util/string_util.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
const Cache::CacheItemHelper kNoopCacheItemHelper{};
|
||||
|
||||
static std::unordered_map<std::string, OptionTypeInfo>
|
||||
lru_cache_options_type_info = {
|
||||
{"capacity",
|
||||
@@ -87,8 +89,7 @@ Status SecondaryCache::CreateFromString(
|
||||
}
|
||||
return status;
|
||||
} else {
|
||||
return LoadSharedObject<SecondaryCache>(config_options, value, nullptr,
|
||||
result);
|
||||
return LoadSharedObject<SecondaryCache>(config_options, value, result);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,4 +114,45 @@ Status Cache::CreateFromString(const ConfigOptions& config_options,
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
bool Cache::AsyncLookupHandle::IsReady() {
|
||||
return pending_handle == nullptr || pending_handle->IsReady();
|
||||
}
|
||||
|
||||
bool Cache::AsyncLookupHandle::IsPending() { return pending_handle != nullptr; }
|
||||
|
||||
Cache::Handle* Cache::AsyncLookupHandle::Result() {
|
||||
assert(!IsPending());
|
||||
return result_handle;
|
||||
}
|
||||
|
||||
void Cache::StartAsyncLookup(AsyncLookupHandle& async_handle) {
|
||||
async_handle.found_dummy_entry = false; // in case re-used
|
||||
assert(!async_handle.IsPending());
|
||||
async_handle.result_handle =
|
||||
Lookup(async_handle.key, async_handle.helper, async_handle.create_context,
|
||||
async_handle.priority, async_handle.stats);
|
||||
}
|
||||
|
||||
Cache::Handle* Cache::Wait(AsyncLookupHandle& async_handle) {
|
||||
WaitAll(&async_handle, 1);
|
||||
return async_handle.Result();
|
||||
}
|
||||
|
||||
void Cache::WaitAll(AsyncLookupHandle* async_handles, size_t count) {
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
if (async_handles[i].IsPending()) {
|
||||
// If a pending handle gets here, it should be marked at "to be handled
|
||||
// by a caller" by that caller erasing the pending_cache on it.
|
||||
assert(async_handles[i].pending_cache == nullptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void Cache::SetEvictionCallback(EvictionCallback&& fn) {
|
||||
// Overwriting non-empty with non-empty could indicate a bug
|
||||
assert(!eviction_callback_ || !fn);
|
||||
eviction_callback_ = std::move(fn);
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
Vendored
+9
-6
@@ -16,7 +16,7 @@
|
||||
#include "db/db_impl/db_impl.h"
|
||||
#include "monitoring/histogram.h"
|
||||
#include "port/port.h"
|
||||
#include "rocksdb/cache.h"
|
||||
#include "rocksdb/advanced_cache.h"
|
||||
#include "rocksdb/convenience.h"
|
||||
#include "rocksdb/db.h"
|
||||
#include "rocksdb/env.h"
|
||||
@@ -255,12 +255,15 @@ void DeleteFn(Cache::ObjectPtr value, MemoryAllocator* /*alloc*/) {
|
||||
delete[] static_cast<char*>(value);
|
||||
}
|
||||
|
||||
Cache::CacheItemHelper helper1_wos(CacheEntryRole::kDataBlock, DeleteFn);
|
||||
Cache::CacheItemHelper helper1(CacheEntryRole::kDataBlock, DeleteFn, SizeFn,
|
||||
SaveToFn, CreateFn);
|
||||
SaveToFn, CreateFn, &helper1_wos);
|
||||
Cache::CacheItemHelper helper2_wos(CacheEntryRole::kIndexBlock, DeleteFn);
|
||||
Cache::CacheItemHelper helper2(CacheEntryRole::kIndexBlock, DeleteFn, SizeFn,
|
||||
SaveToFn, CreateFn);
|
||||
SaveToFn, CreateFn, &helper2_wos);
|
||||
Cache::CacheItemHelper helper3_wos(CacheEntryRole::kFilterBlock, DeleteFn);
|
||||
Cache::CacheItemHelper helper3(CacheEntryRole::kFilterBlock, DeleteFn, SizeFn,
|
||||
SaveToFn, CreateFn);
|
||||
SaveToFn, CreateFn, &helper3_wos);
|
||||
} // namespace
|
||||
|
||||
class CacheBench {
|
||||
@@ -544,7 +547,7 @@ class CacheBench {
|
||||
}
|
||||
// do lookup
|
||||
handle = cache_->Lookup(key, &helper2, /*context*/ nullptr,
|
||||
Cache::Priority::LOW, true);
|
||||
Cache::Priority::LOW);
|
||||
if (handle) {
|
||||
if (!FLAGS_lean) {
|
||||
// do something with the data
|
||||
@@ -573,7 +576,7 @@ class CacheBench {
|
||||
}
|
||||
// do lookup
|
||||
handle = cache_->Lookup(key, &helper2, /*context*/ nullptr,
|
||||
Cache::Priority::LOW, true);
|
||||
Cache::Priority::LOW);
|
||||
if (handle) {
|
||||
if (!FLAGS_lean) {
|
||||
// do something with the data
|
||||
|
||||
Vendored
+1
-1
@@ -143,7 +143,7 @@ class CacheEntryStatsCollector {
|
||||
}
|
||||
}
|
||||
// If we reach here, shared entry is in cache with handle `h`.
|
||||
assert(cache.get()->GetCacheItemHelper(h) == &cache.kBasicHelper);
|
||||
assert(cache.get()->GetCacheItemHelper(h) == cache.GetBasicHelper());
|
||||
|
||||
// Build an aliasing shared_ptr that keeps `ptr` in cache while there
|
||||
// are references.
|
||||
|
||||
Vendored
+1
-1
@@ -7,7 +7,7 @@
|
||||
|
||||
#include <cassert>
|
||||
|
||||
#include "rocksdb/cache.h"
|
||||
#include "rocksdb/advanced_cache.h"
|
||||
#include "rocksdb/rocksdb_namespace.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
Vendored
+1
-1
@@ -8,7 +8,7 @@
|
||||
#include <algorithm>
|
||||
#include <atomic>
|
||||
|
||||
#include "rocksdb/cache.h"
|
||||
#include "rocksdb/advanced_cache.h"
|
||||
#include "table/unique_id_impl.h"
|
||||
#include "util/hash.h"
|
||||
#include "util/math.h"
|
||||
|
||||
Vendored
+1
-1
@@ -169,7 +169,7 @@ Slice CacheReservationManagerImpl<R>::GetNextCacheKey() {
|
||||
template <CacheEntryRole R>
|
||||
const Cache::CacheItemHelper*
|
||||
CacheReservationManagerImpl<R>::TEST_GetCacheItemHelperForRole() {
|
||||
return &CacheInterface::kHelper;
|
||||
return CacheInterface::GetHelper();
|
||||
}
|
||||
|
||||
template class CacheReservationManagerImpl<
|
||||
|
||||
Vendored
+12
-48
@@ -18,6 +18,7 @@
|
||||
#include "cache/lru_cache.h"
|
||||
#include "cache/typed_cache.h"
|
||||
#include "port/stack_trace.h"
|
||||
#include "test_util/secondary_cache_test_util.h"
|
||||
#include "test_util/testharness.h"
|
||||
#include "util/coding.h"
|
||||
#include "util/string_util.h"
|
||||
@@ -81,13 +82,10 @@ const Cache::CacheItemHelper kEraseOnDeleteHelper2{
|
||||
Cache* cache = static_cast<Cache*>(value);
|
||||
cache->Erase(EncodeKey16Bytes(1234));
|
||||
}};
|
||||
|
||||
const std::string kLRU = "lru";
|
||||
const std::string kHyperClock = "hyper_clock";
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
class CacheTest : public testing::TestWithParam<std::string> {
|
||||
class CacheTest : public testing::Test,
|
||||
public secondary_cache_test_util::WithCacheTypeParam {
|
||||
public:
|
||||
static CacheTest* current_;
|
||||
static std::string type_;
|
||||
@@ -95,8 +93,7 @@ class CacheTest : public testing::TestWithParam<std::string> {
|
||||
static void Deleter(Cache::ObjectPtr v, MemoryAllocator*) {
|
||||
current_->deleted_values_.push_back(DecodeValue(v));
|
||||
}
|
||||
static constexpr Cache::CacheItemHelper kHelper{CacheEntryRole::kMisc,
|
||||
&Deleter};
|
||||
static const Cache::CacheItemHelper kHelper;
|
||||
|
||||
static const int kCacheSize = 1000;
|
||||
static const int kNumShardBits = 4;
|
||||
@@ -108,8 +105,6 @@ class CacheTest : public testing::TestWithParam<std::string> {
|
||||
std::shared_ptr<Cache> cache_;
|
||||
std::shared_ptr<Cache> cache2_;
|
||||
|
||||
size_t estimated_value_size_ = 1;
|
||||
|
||||
CacheTest()
|
||||
: cache_(NewCache(kCacheSize, kNumShardBits, false)),
|
||||
cache2_(NewCache(kCacheSize2, kNumShardBits2, false)) {
|
||||
@@ -119,41 +114,6 @@ class CacheTest : public testing::TestWithParam<std::string> {
|
||||
|
||||
~CacheTest() override {}
|
||||
|
||||
std::shared_ptr<Cache> NewCache(size_t capacity) {
|
||||
auto type = GetParam();
|
||||
if (type == kLRU) {
|
||||
return NewLRUCache(capacity);
|
||||
}
|
||||
if (type == kHyperClock) {
|
||||
return HyperClockCacheOptions(
|
||||
capacity, estimated_value_size_ /*estimated_value_size*/)
|
||||
.MakeSharedCache();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::shared_ptr<Cache> NewCache(
|
||||
size_t capacity, int num_shard_bits, bool strict_capacity_limit,
|
||||
CacheMetadataChargePolicy charge_policy = kDontChargeCacheMetadata) {
|
||||
auto type = GetParam();
|
||||
if (type == kLRU) {
|
||||
LRUCacheOptions co;
|
||||
co.capacity = capacity;
|
||||
co.num_shard_bits = num_shard_bits;
|
||||
co.strict_capacity_limit = strict_capacity_limit;
|
||||
co.high_pri_pool_ratio = 0;
|
||||
co.metadata_charge_policy = charge_policy;
|
||||
return NewLRUCache(co);
|
||||
}
|
||||
if (type == kHyperClock) {
|
||||
return HyperClockCacheOptions(capacity, 1 /*estimated_value_size*/,
|
||||
num_shard_bits, strict_capacity_limit,
|
||||
nullptr /*allocator*/, charge_policy)
|
||||
.MakeSharedCache();
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// These functions encode/decode keys in tests cases that use
|
||||
// int keys.
|
||||
// Currently, HyperClockCache requires keys to be 16B long, whereas
|
||||
@@ -187,8 +147,8 @@ class CacheTest : public testing::TestWithParam<std::string> {
|
||||
|
||||
void Insert(std::shared_ptr<Cache> cache, int key, int value,
|
||||
int charge = 1) {
|
||||
EXPECT_OK(
|
||||
cache->Insert(EncodeKey(key), EncodeValue(value), &kHelper, charge));
|
||||
EXPECT_OK(cache->Insert(EncodeKey(key), EncodeValue(value), &kHelper,
|
||||
charge, /*handle*/ nullptr, Cache::Priority::HIGH));
|
||||
}
|
||||
|
||||
void Erase(std::shared_ptr<Cache> cache, int key) {
|
||||
@@ -212,6 +172,9 @@ class CacheTest : public testing::TestWithParam<std::string> {
|
||||
void Erase2(int key) { Erase(cache2_, key); }
|
||||
};
|
||||
|
||||
const Cache::CacheItemHelper CacheTest::kHelper{CacheEntryRole::kMisc,
|
||||
&CacheTest::Deleter};
|
||||
|
||||
CacheTest* CacheTest::current_;
|
||||
std::string CacheTest::type_;
|
||||
|
||||
@@ -993,8 +956,9 @@ TEST_P(CacheTest, GetChargeAndDeleter) {
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(CacheTestInstance, CacheTest,
|
||||
testing::Values(kLRU, kHyperClock));
|
||||
INSTANTIATE_TEST_CASE_P(CacheTestInstance, LRUCacheTest, testing::Values(kLRU));
|
||||
secondary_cache_test_util::GetTestingCacheTypes());
|
||||
INSTANTIATE_TEST_CASE_P(CacheTestInstance, LRUCacheTest,
|
||||
testing::Values(secondary_cache_test_util::kLRU));
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
|
||||
Vendored
+26
-18
@@ -11,7 +11,7 @@ namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
ChargedCache::ChargedCache(std::shared_ptr<Cache> cache,
|
||||
std::shared_ptr<Cache> block_cache)
|
||||
: cache_(cache),
|
||||
: CacheWrapper(cache),
|
||||
cache_res_mgr_(std::make_shared<ConcurrentCacheReservationManager>(
|
||||
std::make_shared<
|
||||
CacheReservationManagerImpl<CacheEntryRole::kBlobCache>>(
|
||||
@@ -20,13 +20,13 @@ ChargedCache::ChargedCache(std::shared_ptr<Cache> cache,
|
||||
Status ChargedCache::Insert(const Slice& key, ObjectPtr obj,
|
||||
const CacheItemHelper* helper, size_t charge,
|
||||
Handle** handle, Priority priority) {
|
||||
Status s = cache_->Insert(key, obj, helper, charge, handle, priority);
|
||||
Status s = target_->Insert(key, obj, helper, charge, handle, priority);
|
||||
if (s.ok()) {
|
||||
// Insert may cause the cache entry eviction if the cache is full. So we
|
||||
// directly call the reservation manager to update the total memory used
|
||||
// in the cache.
|
||||
assert(cache_res_mgr_);
|
||||
cache_res_mgr_->UpdateCacheReservation(cache_->GetUsage())
|
||||
cache_res_mgr_->UpdateCacheReservation(target_->GetUsage())
|
||||
.PermitUncheckedError();
|
||||
}
|
||||
return s;
|
||||
@@ -35,25 +35,33 @@ Status ChargedCache::Insert(const Slice& key, ObjectPtr obj,
|
||||
Cache::Handle* ChargedCache::Lookup(const Slice& key,
|
||||
const CacheItemHelper* helper,
|
||||
CreateContext* create_context,
|
||||
Priority priority, bool wait,
|
||||
Statistics* stats) {
|
||||
auto handle =
|
||||
cache_->Lookup(key, helper, create_context, priority, wait, stats);
|
||||
Priority priority, Statistics* stats) {
|
||||
auto handle = target_->Lookup(key, helper, create_context, priority, stats);
|
||||
// Lookup may promote the KV pair from the secondary cache to the primary
|
||||
// cache. So we directly call the reservation manager to update the total
|
||||
// memory used in the cache.
|
||||
if (helper && helper->create_cb) {
|
||||
assert(cache_res_mgr_);
|
||||
cache_res_mgr_->UpdateCacheReservation(cache_->GetUsage())
|
||||
cache_res_mgr_->UpdateCacheReservation(target_->GetUsage())
|
||||
.PermitUncheckedError();
|
||||
}
|
||||
return handle;
|
||||
}
|
||||
|
||||
void ChargedCache::WaitAll(AsyncLookupHandle* async_handles, size_t count) {
|
||||
target_->WaitAll(async_handles, count);
|
||||
// In case of any promotions. Although some could finish by return of
|
||||
// StartAsyncLookup, Wait/WaitAll will generally be used, so simpler to
|
||||
// update here.
|
||||
assert(cache_res_mgr_);
|
||||
cache_res_mgr_->UpdateCacheReservation(target_->GetUsage())
|
||||
.PermitUncheckedError();
|
||||
}
|
||||
|
||||
bool ChargedCache::Release(Cache::Handle* handle, bool useful,
|
||||
bool erase_if_last_ref) {
|
||||
size_t memory_used_delta = cache_->GetUsage(handle);
|
||||
bool erased = cache_->Release(handle, useful, erase_if_last_ref);
|
||||
size_t memory_used_delta = target_->GetUsage(handle);
|
||||
bool erased = target_->Release(handle, useful, erase_if_last_ref);
|
||||
if (erased) {
|
||||
assert(cache_res_mgr_);
|
||||
cache_res_mgr_
|
||||
@@ -64,8 +72,8 @@ bool ChargedCache::Release(Cache::Handle* handle, bool useful,
|
||||
}
|
||||
|
||||
bool ChargedCache::Release(Cache::Handle* handle, bool erase_if_last_ref) {
|
||||
size_t memory_used_delta = cache_->GetUsage(handle);
|
||||
bool erased = cache_->Release(handle, erase_if_last_ref);
|
||||
size_t memory_used_delta = target_->GetUsage(handle);
|
||||
bool erased = target_->Release(handle, erase_if_last_ref);
|
||||
if (erased) {
|
||||
assert(cache_res_mgr_);
|
||||
cache_res_mgr_
|
||||
@@ -76,25 +84,25 @@ bool ChargedCache::Release(Cache::Handle* handle, bool erase_if_last_ref) {
|
||||
}
|
||||
|
||||
void ChargedCache::Erase(const Slice& key) {
|
||||
cache_->Erase(key);
|
||||
target_->Erase(key);
|
||||
assert(cache_res_mgr_);
|
||||
cache_res_mgr_->UpdateCacheReservation(cache_->GetUsage())
|
||||
cache_res_mgr_->UpdateCacheReservation(target_->GetUsage())
|
||||
.PermitUncheckedError();
|
||||
}
|
||||
|
||||
void ChargedCache::EraseUnRefEntries() {
|
||||
cache_->EraseUnRefEntries();
|
||||
target_->EraseUnRefEntries();
|
||||
assert(cache_res_mgr_);
|
||||
cache_res_mgr_->UpdateCacheReservation(cache_->GetUsage())
|
||||
cache_res_mgr_->UpdateCacheReservation(target_->GetUsage())
|
||||
.PermitUncheckedError();
|
||||
}
|
||||
|
||||
void ChargedCache::SetCapacity(size_t capacity) {
|
||||
cache_->SetCapacity(capacity);
|
||||
target_->SetCapacity(capacity);
|
||||
// SetCapacity can result in evictions when the cache capacity is decreased,
|
||||
// so we would want to update the cache reservation here as well.
|
||||
assert(cache_res_mgr_);
|
||||
cache_res_mgr_->UpdateCacheReservation(cache_->GetUsage())
|
||||
cache_res_mgr_->UpdateCacheReservation(target_->GetUsage())
|
||||
.PermitUncheckedError();
|
||||
}
|
||||
|
||||
|
||||
Vendored
+6
-63
@@ -8,7 +8,7 @@
|
||||
#include <string>
|
||||
|
||||
#include "port/port.h"
|
||||
#include "rocksdb/cache.h"
|
||||
#include "rocksdb/advanced_cache.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
@@ -17,11 +17,10 @@ class ConcurrentCacheReservationManager;
|
||||
// A cache interface which wraps around another cache and takes care of
|
||||
// reserving space in block cache towards a single global memory limit, and
|
||||
// forwards all the calls to the underlying cache.
|
||||
class ChargedCache : public Cache {
|
||||
class ChargedCache : public CacheWrapper {
|
||||
public:
|
||||
ChargedCache(std::shared_ptr<Cache> cache,
|
||||
std::shared_ptr<Cache> block_cache);
|
||||
~ChargedCache() override = default;
|
||||
|
||||
Status Insert(const Slice& key, ObjectPtr obj, const CacheItemHelper* helper,
|
||||
size_t charge, Handle** handle = nullptr,
|
||||
@@ -29,9 +28,11 @@ class ChargedCache : public Cache {
|
||||
|
||||
Cache::Handle* Lookup(const Slice& key, const CacheItemHelper* helper,
|
||||
CreateContext* create_context,
|
||||
Priority priority = Priority::LOW, bool wait = true,
|
||||
Priority priority = Priority::LOW,
|
||||
Statistics* stats = nullptr) override;
|
||||
|
||||
void WaitAll(AsyncLookupHandle* async_handles, size_t count) override;
|
||||
|
||||
bool Release(Cache::Handle* handle, bool useful,
|
||||
bool erase_if_last_ref = false) override;
|
||||
bool Release(Cache::Handle* handle, bool erase_if_last_ref = false) override;
|
||||
@@ -42,66 +43,9 @@ class ChargedCache : public Cache {
|
||||
static const char* kClassName() { return "ChargedCache"; }
|
||||
const char* Name() const override { return kClassName(); }
|
||||
|
||||
uint64_t NewId() override { return cache_->NewId(); }
|
||||
|
||||
void SetCapacity(size_t capacity) override;
|
||||
|
||||
void SetStrictCapacityLimit(bool strict_capacity_limit) override {
|
||||
cache_->SetStrictCapacityLimit(strict_capacity_limit);
|
||||
}
|
||||
|
||||
bool HasStrictCapacityLimit() const override {
|
||||
return cache_->HasStrictCapacityLimit();
|
||||
}
|
||||
|
||||
ObjectPtr Value(Cache::Handle* handle) override {
|
||||
return cache_->Value(handle);
|
||||
}
|
||||
|
||||
bool IsReady(Cache::Handle* handle) override {
|
||||
return cache_->IsReady(handle);
|
||||
}
|
||||
|
||||
void Wait(Cache::Handle* handle) override { cache_->Wait(handle); }
|
||||
|
||||
void WaitAll(std::vector<Handle*>& handles) override {
|
||||
cache_->WaitAll(handles);
|
||||
}
|
||||
|
||||
bool Ref(Cache::Handle* handle) override { return cache_->Ref(handle); }
|
||||
|
||||
size_t GetCapacity() const override { return cache_->GetCapacity(); }
|
||||
|
||||
size_t GetUsage() const override { return cache_->GetUsage(); }
|
||||
|
||||
size_t GetUsage(Cache::Handle* handle) const override {
|
||||
return cache_->GetUsage(handle);
|
||||
}
|
||||
|
||||
size_t GetPinnedUsage() const override { return cache_->GetPinnedUsage(); }
|
||||
|
||||
size_t GetCharge(Cache::Handle* handle) const override {
|
||||
return cache_->GetCharge(handle);
|
||||
}
|
||||
|
||||
const CacheItemHelper* GetCacheItemHelper(Handle* handle) const override {
|
||||
return cache_->GetCacheItemHelper(handle);
|
||||
}
|
||||
|
||||
void ApplyToAllEntries(
|
||||
const std::function<void(const Slice& key, ObjectPtr value, size_t charge,
|
||||
const CacheItemHelper* helper)>& callback,
|
||||
const Cache::ApplyToAllEntriesOptions& opts) override {
|
||||
cache_->ApplyToAllEntries(callback, opts);
|
||||
}
|
||||
|
||||
std::string GetPrintableOptions() const override {
|
||||
return cache_->GetPrintableOptions();
|
||||
}
|
||||
|
||||
void DisownData() override { return cache_->DisownData(); }
|
||||
|
||||
inline Cache* GetCache() const { return cache_.get(); }
|
||||
inline Cache* GetCache() const { return target_.get(); }
|
||||
|
||||
inline ConcurrentCacheReservationManager* TEST_GetCacheReservationManager()
|
||||
const {
|
||||
@@ -109,7 +53,6 @@ class ChargedCache : public Cache {
|
||||
}
|
||||
|
||||
private:
|
||||
std::shared_ptr<Cache> cache_;
|
||||
std::shared_ptr<ConcurrentCacheReservationManager> cache_res_mgr_;
|
||||
};
|
||||
|
||||
|
||||
Vendored
+120
-45
@@ -9,15 +9,16 @@
|
||||
|
||||
#include "cache/clock_cache.h"
|
||||
|
||||
#include <cassert>
|
||||
#include <functional>
|
||||
#include <numeric>
|
||||
|
||||
#include "cache/cache_key.h"
|
||||
#include "cache/secondary_cache_adapter.h"
|
||||
#include "logging/logging.h"
|
||||
#include "monitoring/perf_context_imp.h"
|
||||
#include "monitoring/statistics.h"
|
||||
#include "port/lang.h"
|
||||
#include "rocksdb/env.h"
|
||||
#include "util/hash.h"
|
||||
#include "util/math.h"
|
||||
#include "util/random.h"
|
||||
@@ -50,13 +51,7 @@ inline uint64_t GetInitialCountdown(Cache::Priority priority) {
|
||||
}
|
||||
}
|
||||
|
||||
inline void FreeDataMarkEmpty(ClockHandle& h, MemoryAllocator* allocator) {
|
||||
// NOTE: in theory there's more room for parallelism if we copy the handle
|
||||
// data and delay actions like this until after marking the entry as empty,
|
||||
// but performance tests only show a regression by copying the few words
|
||||
// of data.
|
||||
h.FreeData(allocator);
|
||||
|
||||
inline void MarkEmpty(ClockHandle& h) {
|
||||
#ifndef NDEBUG
|
||||
// Mark slot as empty, with assertion
|
||||
uint64_t meta = h.meta.exchange(0, std::memory_order_release);
|
||||
@@ -67,6 +62,16 @@ inline void FreeDataMarkEmpty(ClockHandle& h, MemoryAllocator* allocator) {
|
||||
#endif
|
||||
}
|
||||
|
||||
inline void FreeDataMarkEmpty(ClockHandle& h, MemoryAllocator* allocator) {
|
||||
// NOTE: in theory there's more room for parallelism if we copy the handle
|
||||
// data and delay actions like this until after marking the entry as empty,
|
||||
// but performance tests only show a regression by copying the few words
|
||||
// of data.
|
||||
h.FreeData(allocator);
|
||||
|
||||
MarkEmpty(h);
|
||||
}
|
||||
|
||||
inline bool ClockUpdate(ClockHandle& h) {
|
||||
uint64_t meta = h.meta.load(std::memory_order_relaxed);
|
||||
|
||||
@@ -124,14 +129,16 @@ void ClockHandleBasicData::FreeData(MemoryAllocator* allocator) const {
|
||||
HyperClockTable::HyperClockTable(
|
||||
size_t capacity, bool /*strict_capacity_limit*/,
|
||||
CacheMetadataChargePolicy metadata_charge_policy,
|
||||
MemoryAllocator* allocator, const Opts& opts)
|
||||
MemoryAllocator* allocator,
|
||||
const Cache::EvictionCallback* eviction_callback, const Opts& opts)
|
||||
: length_bits_(CalcHashBits(capacity, opts.estimated_value_size,
|
||||
metadata_charge_policy)),
|
||||
length_bits_mask_((size_t{1} << length_bits_) - 1),
|
||||
occupancy_limit_(static_cast<size_t>((uint64_t{1} << length_bits_) *
|
||||
kStrictLoadFactor)),
|
||||
array_(new HandleImpl[size_t{1} << length_bits_]),
|
||||
allocator_(allocator) {
|
||||
allocator_(allocator),
|
||||
eviction_callback_(*eviction_callback) {
|
||||
if (metadata_charge_policy ==
|
||||
CacheMetadataChargePolicy::kFullChargeCacheMetadata) {
|
||||
usage_ += size_t{GetTableSize()} * sizeof(HandleImpl);
|
||||
@@ -364,21 +371,22 @@ inline bool HyperClockTable::ChargeUsageMaybeEvictNonStrict(
|
||||
return true;
|
||||
}
|
||||
|
||||
inline HyperClockTable::HandleImpl* HyperClockTable::DetachedInsert(
|
||||
inline HyperClockTable::HandleImpl* HyperClockTable::StandaloneInsert(
|
||||
const ClockHandleBasicData& proto) {
|
||||
// Heap allocated separate from table
|
||||
HandleImpl* h = new HandleImpl();
|
||||
ClockHandleBasicData* h_alias = h;
|
||||
*h_alias = proto;
|
||||
h->SetDetached();
|
||||
// Single reference (detached entries only created if returning a refed
|
||||
h->SetStandalone();
|
||||
// Single reference (standalone entries only created if returning a refed
|
||||
// Handle back to user)
|
||||
uint64_t meta = uint64_t{ClockHandle::kStateInvisible}
|
||||
<< ClockHandle::kStateShift;
|
||||
meta |= uint64_t{1} << ClockHandle::kAcquireCounterShift;
|
||||
h->meta.store(meta, std::memory_order_release);
|
||||
// Keep track of how much of usage is detached
|
||||
detached_usage_.fetch_add(proto.GetTotalCharge(), std::memory_order_relaxed);
|
||||
// Keep track of how much of usage is standalone
|
||||
standalone_usage_.fetch_add(proto.GetTotalCharge(),
|
||||
std::memory_order_relaxed);
|
||||
return h;
|
||||
}
|
||||
|
||||
@@ -396,7 +404,7 @@ Status HyperClockTable::Insert(const ClockHandleBasicData& proto,
|
||||
|
||||
// Usage/capacity handling is somewhat different depending on
|
||||
// strict_capacity_limit, but mostly pessimistic.
|
||||
bool use_detached_insert = false;
|
||||
bool use_standalone_insert = false;
|
||||
const size_t total_charge = proto.GetTotalCharge();
|
||||
if (strict_capacity_limit) {
|
||||
Status s = ChargeUsageMaybeEvictStrict(total_charge, capacity,
|
||||
@@ -417,9 +425,9 @@ Status HyperClockTable::Insert(const ClockHandleBasicData& proto,
|
||||
proto.FreeData(allocator_);
|
||||
return Status::OK();
|
||||
} else {
|
||||
// Need to track usage of fallback detached insert
|
||||
// Need to track usage of fallback standalone insert
|
||||
usage_.fetch_add(total_charge, std::memory_order_relaxed);
|
||||
use_detached_insert = true;
|
||||
use_standalone_insert = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -429,7 +437,7 @@ Status HyperClockTable::Insert(const ClockHandleBasicData& proto,
|
||||
assert(usage_.load(std::memory_order_relaxed) < SIZE_MAX / 2);
|
||||
};
|
||||
|
||||
if (!use_detached_insert) {
|
||||
if (!use_standalone_insert) {
|
||||
// Attempt a table insert, but abort if we find an existing entry for the
|
||||
// key. If we were to overwrite old entries, we would either
|
||||
// * Have to gain ownership over an existing entry to overwrite it, which
|
||||
@@ -500,8 +508,8 @@ Status HyperClockTable::Insert(const ClockHandleBasicData& proto,
|
||||
std::memory_order_acq_rel);
|
||||
// Correct for possible (but rare) overflow
|
||||
CorrectNearOverflow(old_meta, h->meta);
|
||||
// Insert detached instead (only if return handle needed)
|
||||
use_detached_insert = true;
|
||||
// Insert standalone instead (only if return handle needed)
|
||||
use_standalone_insert = true;
|
||||
return true;
|
||||
} else {
|
||||
// Mismatch. Pretend we never took the reference
|
||||
@@ -539,9 +547,9 @@ Status HyperClockTable::Insert(const ClockHandleBasicData& proto,
|
||||
// That should be infeasible for roughly n >= 256, so if this assertion
|
||||
// fails, that suggests something is going wrong.
|
||||
assert(GetTableSize() < 256);
|
||||
use_detached_insert = true;
|
||||
use_standalone_insert = true;
|
||||
}
|
||||
if (!use_detached_insert) {
|
||||
if (!use_standalone_insert) {
|
||||
// Successfully inserted
|
||||
if (handle) {
|
||||
*handle = e;
|
||||
@@ -551,7 +559,7 @@ Status HyperClockTable::Insert(const ClockHandleBasicData& proto,
|
||||
// Roll back table insertion
|
||||
Rollback(proto.hashed_key, e);
|
||||
revert_occupancy_fn();
|
||||
// Maybe fall back on detached insert
|
||||
// Maybe fall back on standalone insert
|
||||
if (handle == nullptr) {
|
||||
revert_usage_fn();
|
||||
// As if unrefed entry immdiately evicted
|
||||
@@ -560,19 +568,47 @@ Status HyperClockTable::Insert(const ClockHandleBasicData& proto,
|
||||
}
|
||||
}
|
||||
|
||||
// Run detached insert
|
||||
assert(use_detached_insert);
|
||||
// Run standalone insert
|
||||
assert(use_standalone_insert);
|
||||
|
||||
*handle = DetachedInsert(proto);
|
||||
*handle = StandaloneInsert(proto);
|
||||
|
||||
// The OkOverwritten status is used to count "redundant" insertions into
|
||||
// block cache. This implementation doesn't strictly check for redundant
|
||||
// insertions, but we instead are probably interested in how many insertions
|
||||
// didn't go into the table (instead "detached"), which could be redundant
|
||||
// Insert or some other reason (use_detached_insert reasons above).
|
||||
// didn't go into the table (instead "standalone"), which could be redundant
|
||||
// Insert or some other reason (use_standalone_insert reasons above).
|
||||
return Status::OkOverwritten();
|
||||
}
|
||||
|
||||
HyperClockTable::HandleImpl* HyperClockTable::CreateStandalone(
|
||||
ClockHandleBasicData& proto, size_t capacity, bool strict_capacity_limit,
|
||||
bool allow_uncharged) {
|
||||
const size_t total_charge = proto.GetTotalCharge();
|
||||
if (strict_capacity_limit) {
|
||||
Status s = ChargeUsageMaybeEvictStrict(total_charge, capacity,
|
||||
/*need_evict_for_occupancy=*/false);
|
||||
if (!s.ok()) {
|
||||
if (allow_uncharged) {
|
||||
proto.total_charge = 0;
|
||||
} else {
|
||||
return nullptr;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Case strict_capacity_limit == false
|
||||
bool success =
|
||||
ChargeUsageMaybeEvictNonStrict(total_charge, capacity,
|
||||
/*need_evict_for_occupancy=*/false);
|
||||
if (!success) {
|
||||
// Force the issue
|
||||
usage_.fetch_add(total_charge, std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
return StandaloneInsert(proto);
|
||||
}
|
||||
|
||||
HyperClockTable::HandleImpl* HyperClockTable::Lookup(
|
||||
const UniqueId64x2& hashed_key) {
|
||||
size_t probe = 0;
|
||||
@@ -696,11 +732,11 @@ bool HyperClockTable::Release(HandleImpl* h, bool useful,
|
||||
std::memory_order_acquire));
|
||||
// Took ownership
|
||||
size_t total_charge = h->GetTotalCharge();
|
||||
if (UNLIKELY(h->IsDetached())) {
|
||||
if (UNLIKELY(h->IsStandalone())) {
|
||||
h->FreeData(allocator_);
|
||||
// Delete detached handle
|
||||
// Delete standalone handle
|
||||
delete h;
|
||||
detached_usage_.fetch_sub(total_charge, std::memory_order_relaxed);
|
||||
standalone_usage_.fetch_sub(total_charge, std::memory_order_relaxed);
|
||||
usage_.fetch_sub(total_charge, std::memory_order_relaxed);
|
||||
} else {
|
||||
Rollback(h->hashed_key, h);
|
||||
@@ -959,6 +995,9 @@ inline void HyperClockTable::Evict(size_t requested_charge,
|
||||
uint64_t max_clock_pointer =
|
||||
old_clock_pointer + (ClockHandle::kMaxCountdown << length_bits_);
|
||||
|
||||
// For key reconstructed from hash
|
||||
UniqueId64x2 unhashed;
|
||||
|
||||
for (;;) {
|
||||
for (size_t i = 0; i < step_size; i++) {
|
||||
HandleImpl& h = array_[ModTableSize(Lower32of64(old_clock_pointer + i))];
|
||||
@@ -967,7 +1006,17 @@ inline void HyperClockTable::Evict(size_t requested_charge,
|
||||
Rollback(h.hashed_key, &h);
|
||||
*freed_charge += h.GetTotalCharge();
|
||||
*freed_count += 1;
|
||||
FreeDataMarkEmpty(h, allocator_);
|
||||
bool took_ownership = false;
|
||||
if (eviction_callback_) {
|
||||
took_ownership =
|
||||
eviction_callback_(ClockCacheShard<HyperClockTable>::ReverseHash(
|
||||
h.GetHash(), &unhashed),
|
||||
reinterpret_cast<Cache::Handle*>(&h));
|
||||
}
|
||||
if (!took_ownership) {
|
||||
h.FreeData(allocator_);
|
||||
}
|
||||
MarkEmpty(h);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -989,10 +1038,12 @@ template <class Table>
|
||||
ClockCacheShard<Table>::ClockCacheShard(
|
||||
size_t capacity, bool strict_capacity_limit,
|
||||
CacheMetadataChargePolicy metadata_charge_policy,
|
||||
MemoryAllocator* allocator, const typename Table::Opts& opts)
|
||||
MemoryAllocator* allocator,
|
||||
const Cache::EvictionCallback* eviction_callback,
|
||||
const typename Table::Opts& opts)
|
||||
: CacheShardBase(metadata_charge_policy),
|
||||
table_(capacity, strict_capacity_limit, metadata_charge_policy, allocator,
|
||||
opts),
|
||||
eviction_callback, opts),
|
||||
capacity_(capacity),
|
||||
strict_capacity_limit_(strict_capacity_limit) {
|
||||
// Initial charge metadata should not exceed capacity
|
||||
@@ -1092,10 +1143,29 @@ Status ClockCacheShard<Table>::Insert(const Slice& key,
|
||||
proto.value = value;
|
||||
proto.helper = helper;
|
||||
proto.total_charge = charge;
|
||||
Status s = table_.Insert(
|
||||
proto, handle, priority, capacity_.load(std::memory_order_relaxed),
|
||||
strict_capacity_limit_.load(std::memory_order_relaxed));
|
||||
return s;
|
||||
return table_.Insert(proto, handle, priority,
|
||||
capacity_.load(std::memory_order_relaxed),
|
||||
strict_capacity_limit_.load(std::memory_order_relaxed));
|
||||
}
|
||||
|
||||
template <class Table>
|
||||
typename ClockCacheShard<Table>::HandleImpl*
|
||||
ClockCacheShard<Table>::CreateStandalone(const Slice& key,
|
||||
const UniqueId64x2& hashed_key,
|
||||
Cache::ObjectPtr obj,
|
||||
const Cache::CacheItemHelper* helper,
|
||||
size_t charge, bool allow_uncharged) {
|
||||
if (UNLIKELY(key.size() != kCacheKeySize)) {
|
||||
return nullptr;
|
||||
}
|
||||
ClockHandleBasicData proto;
|
||||
proto.hashed_key = hashed_key;
|
||||
proto.value = obj;
|
||||
proto.helper = helper;
|
||||
proto.total_charge = charge;
|
||||
return table_.CreateStandalone(
|
||||
proto, capacity_.load(std::memory_order_relaxed),
|
||||
strict_capacity_limit_.load(std::memory_order_relaxed), allow_uncharged);
|
||||
}
|
||||
|
||||
template <class Table>
|
||||
@@ -1156,8 +1226,8 @@ size_t ClockCacheShard<Table>::GetUsage() const {
|
||||
}
|
||||
|
||||
template <class Table>
|
||||
size_t ClockCacheShard<Table>::GetDetachedUsage() const {
|
||||
return table_.GetDetachedUsage();
|
||||
size_t ClockCacheShard<Table>::GetStandaloneUsage() const {
|
||||
return table_.GetStandaloneUsage();
|
||||
}
|
||||
|
||||
template <class Table>
|
||||
@@ -1191,7 +1261,7 @@ size_t ClockCacheShard<Table>::GetPinnedUsage() const {
|
||||
},
|
||||
0, table_.GetTableSize(), true);
|
||||
|
||||
return table_pinned_usage + table_.GetDetachedUsage();
|
||||
return table_pinned_usage + table_.GetStandaloneUsage();
|
||||
}
|
||||
|
||||
template <class Table>
|
||||
@@ -1225,11 +1295,12 @@ HyperClockCache::HyperClockCache(
|
||||
// get to table entries
|
||||
size_t per_shard = GetPerShardCapacity();
|
||||
MemoryAllocator* alloc = this->memory_allocator();
|
||||
const Cache::EvictionCallback* eviction_callback = &eviction_callback_;
|
||||
InitShards([=](Shard* cs) {
|
||||
HyperClockTable::Opts opts;
|
||||
opts.estimated_value_size = estimated_value_size;
|
||||
new (cs) Shard(per_shard, strict_capacity_limit, metadata_charge_policy,
|
||||
alloc, opts);
|
||||
alloc, eviction_callback, opts);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1259,7 +1330,7 @@ namespace {
|
||||
void AddShardEvaluation(const HyperClockCache::Shard& shard,
|
||||
std::vector<double>& predicted_load_factors,
|
||||
size_t& min_recommendation) {
|
||||
size_t usage = shard.GetUsage() - shard.GetDetachedUsage();
|
||||
size_t usage = shard.GetUsage() - shard.GetStandaloneUsage();
|
||||
size_t capacity = shard.GetCapacity();
|
||||
double usage_ratio = 1.0 * usage / capacity;
|
||||
|
||||
@@ -1399,9 +1470,13 @@ std::shared_ptr<Cache> HyperClockCacheOptions::MakeSharedCache() const {
|
||||
constexpr size_t min_shard_size = 32U * 1024U * 1024U;
|
||||
my_num_shard_bits = GetDefaultCacheShardBits(capacity, min_shard_size);
|
||||
}
|
||||
return std::make_shared<clock_cache::HyperClockCache>(
|
||||
std::shared_ptr<Cache> cache = std::make_shared<clock_cache::HyperClockCache>(
|
||||
capacity, estimated_entry_charge, my_num_shard_bits,
|
||||
strict_capacity_limit, metadata_charge_policy, memory_allocator);
|
||||
if (secondary_cache) {
|
||||
cache = std::make_shared<CacheWithSecondaryAdapter>(cache, secondary_cache);
|
||||
}
|
||||
return cache;
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
Vendored
+34
-29
@@ -145,7 +145,7 @@ class ClockCacheTest;
|
||||
// (erased by user) but can be read by existing references, and ref count
|
||||
// changed by Ref and Release.
|
||||
//
|
||||
// A special case is "detached" entries, which are heap-allocated handles
|
||||
// A special case is "standalone" entries, which are heap-allocated handles
|
||||
// not in the table. They are always Invisible and freed on zero refs.
|
||||
//
|
||||
// State transitions:
|
||||
@@ -200,8 +200,8 @@ class ClockCacheTest;
|
||||
// table occupancy limit has been reached. If strict_capacity_limit=false,
|
||||
// we must never fail Insert, and if a Handle* is provided, we have to return
|
||||
// a usable Cache handle on success. The solution to this (typically rare)
|
||||
// problem is "detached" handles, which are usable by the caller but not
|
||||
// actually available for Lookup in the Cache. Detached handles are allocated
|
||||
// problem is "standalone" handles, which are usable by the caller but not
|
||||
// actually available for Lookup in the Cache. Standalone handles are allocated
|
||||
// independently on the heap and specially marked so that they are freed on
|
||||
// the heap when their last reference is released.
|
||||
//
|
||||
@@ -312,12 +312,6 @@ struct ClockHandleBasicData {
|
||||
UniqueId64x2 hashed_key = kNullUniqueId64x2;
|
||||
size_t total_charge = 0;
|
||||
|
||||
// For total_charge_and_flags
|
||||
// "Detached" means the handle is allocated separately from hash table.
|
||||
static constexpr uint64_t kFlagDetached = uint64_t{1} << 63;
|
||||
// Extract just the total charge
|
||||
static constexpr uint64_t kTotalChargeMask = kFlagDetached - 1;
|
||||
|
||||
inline size_t GetTotalCharge() const { return total_charge; }
|
||||
|
||||
// Calls deleter (if non-null) on cache key and value
|
||||
@@ -398,11 +392,11 @@ class HyperClockTable {
|
||||
// TODO: ideally this would be packed into some other data field, such
|
||||
// as upper bits of total_charge, but that incurs a measurable performance
|
||||
// regression.
|
||||
bool detached = false;
|
||||
bool standalone = false;
|
||||
|
||||
inline bool IsDetached() const { return detached; }
|
||||
inline bool IsStandalone() const { return standalone; }
|
||||
|
||||
inline void SetDetached() { detached = true; }
|
||||
inline void SetStandalone() { standalone = true; }
|
||||
}; // struct HandleImpl
|
||||
|
||||
struct Opts {
|
||||
@@ -411,13 +405,19 @@ class HyperClockTable {
|
||||
|
||||
HyperClockTable(size_t capacity, bool strict_capacity_limit,
|
||||
CacheMetadataChargePolicy metadata_charge_policy,
|
||||
MemoryAllocator* allocator, const Opts& opts);
|
||||
MemoryAllocator* allocator,
|
||||
const Cache::EvictionCallback* eviction_callback,
|
||||
const Opts& opts);
|
||||
~HyperClockTable();
|
||||
|
||||
Status Insert(const ClockHandleBasicData& proto, HandleImpl** handle,
|
||||
Cache::Priority priority, size_t capacity,
|
||||
bool strict_capacity_limit);
|
||||
|
||||
HandleImpl* CreateStandalone(ClockHandleBasicData& proto, size_t capacity,
|
||||
bool strict_capacity_limit,
|
||||
bool allow_uncharged);
|
||||
|
||||
HandleImpl* Lookup(const UniqueId64x2& hashed_key);
|
||||
|
||||
bool Release(HandleImpl* handle, bool useful, bool erase_if_last_ref);
|
||||
@@ -444,8 +444,8 @@ class HyperClockTable {
|
||||
|
||||
size_t GetUsage() const { return usage_.load(std::memory_order_relaxed); }
|
||||
|
||||
size_t GetDetachedUsage() const {
|
||||
return detached_usage_.load(std::memory_order_relaxed);
|
||||
size_t GetStandaloneUsage() const {
|
||||
return standalone_usage_.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
// Acquire/release N references
|
||||
@@ -514,10 +514,10 @@ class HyperClockTable {
|
||||
size_t capacity,
|
||||
bool need_evict_for_occupancy);
|
||||
|
||||
// Creates a "detached" handle for returning from an Insert operation that
|
||||
// Creates a "standalone" handle for returning from an Insert operation that
|
||||
// cannot be completed by actually inserting into the table.
|
||||
// Updates `detached_usage_` but not `usage_` nor `occupancy_`.
|
||||
inline HandleImpl* DetachedInsert(const ClockHandleBasicData& proto);
|
||||
// Updates `standalone_usage_` but not `usage_` nor `occupancy_`.
|
||||
inline HandleImpl* StandaloneInsert(const ClockHandleBasicData& proto);
|
||||
|
||||
MemoryAllocator* GetAllocator() const { return allocator_; }
|
||||
|
||||
@@ -543,6 +543,9 @@ class HyperClockTable {
|
||||
// From Cache, for deleter
|
||||
MemoryAllocator* const allocator_;
|
||||
|
||||
// A reference to Cache::eviction_callback_
|
||||
const Cache::EvictionCallback& eviction_callback_;
|
||||
|
||||
// We partition the following members into different cache lines
|
||||
// to avoid false sharing among Lookup, Release, Erase and Insert
|
||||
// operations in ClockCacheShard.
|
||||
@@ -555,11 +558,11 @@ class HyperClockTable {
|
||||
// Number of elements in the table.
|
||||
std::atomic<size_t> occupancy_{};
|
||||
|
||||
// Memory usage by entries tracked by the cache (including detached)
|
||||
// Memory usage by entries tracked by the cache (including standalone)
|
||||
std::atomic<size_t> usage_{};
|
||||
|
||||
// Part of usage by detached entries (not in table)
|
||||
std::atomic<size_t> detached_usage_{};
|
||||
// Part of usage by standalone entries (not in table)
|
||||
std::atomic<size_t> standalone_usage_{};
|
||||
}; // class HyperClockTable
|
||||
|
||||
// A single shard of sharded cache.
|
||||
@@ -568,7 +571,9 @@ class ALIGN_AS(CACHE_LINE_SIZE) ClockCacheShard final : public CacheShardBase {
|
||||
public:
|
||||
ClockCacheShard(size_t capacity, bool strict_capacity_limit,
|
||||
CacheMetadataChargePolicy metadata_charge_policy,
|
||||
MemoryAllocator* allocator, const typename Table::Opts& opts);
|
||||
MemoryAllocator* allocator,
|
||||
const Cache::EvictionCallback* eviction_callback,
|
||||
const typename Table::Opts& opts);
|
||||
|
||||
// For CacheShard concept
|
||||
using HandleImpl = typename Table::HandleImpl;
|
||||
@@ -609,6 +614,11 @@ class ALIGN_AS(CACHE_LINE_SIZE) ClockCacheShard final : public CacheShardBase {
|
||||
Cache::ObjectPtr value, const Cache::CacheItemHelper* helper,
|
||||
size_t charge, HandleImpl** handle, Cache::Priority priority);
|
||||
|
||||
HandleImpl* CreateStandalone(const Slice& key, const UniqueId64x2& hashed_key,
|
||||
Cache::ObjectPtr obj,
|
||||
const Cache::CacheItemHelper* helper,
|
||||
size_t charge, bool allow_uncharged);
|
||||
|
||||
HandleImpl* Lookup(const Slice& key, const UniqueId64x2& hashed_key);
|
||||
|
||||
bool Release(HandleImpl* handle, bool useful, bool erase_if_last_ref);
|
||||
@@ -623,7 +633,7 @@ class ALIGN_AS(CACHE_LINE_SIZE) ClockCacheShard final : public CacheShardBase {
|
||||
|
||||
size_t GetUsage() const;
|
||||
|
||||
size_t GetDetachedUsage() const;
|
||||
size_t GetStandaloneUsage() const;
|
||||
|
||||
size_t GetPinnedUsage() const;
|
||||
|
||||
@@ -646,15 +656,10 @@ class ALIGN_AS(CACHE_LINE_SIZE) ClockCacheShard final : public CacheShardBase {
|
||||
HandleImpl* Lookup(const Slice& key, const UniqueId64x2& hashed_key,
|
||||
const Cache::CacheItemHelper* /*helper*/,
|
||||
Cache::CreateContext* /*create_context*/,
|
||||
Cache::Priority /*priority*/, bool /*wait*/,
|
||||
Statistics* /*stats*/) {
|
||||
Cache::Priority /*priority*/, Statistics* /*stats*/) {
|
||||
return Lookup(key, hashed_key);
|
||||
}
|
||||
|
||||
bool IsReady(HandleImpl* /*handle*/) { return true; }
|
||||
|
||||
void Wait(HandleImpl* /*handle*/) {}
|
||||
|
||||
// Acquire/release N references
|
||||
void TEST_RefN(HandleImpl* handle, size_t n);
|
||||
void TEST_ReleaseN(HandleImpl* handle, size_t n);
|
||||
|
||||
Vendored
+14
-10
@@ -22,12 +22,13 @@ CompressedSecondaryCache::CompressedSecondaryCache(
|
||||
std::shared_ptr<MemoryAllocator> memory_allocator, bool use_adaptive_mutex,
|
||||
CacheMetadataChargePolicy metadata_charge_policy,
|
||||
CompressionType compression_type, uint32_t compress_format_version,
|
||||
bool enable_custom_split_merge)
|
||||
bool enable_custom_split_merge,
|
||||
const CacheEntryRoleSet& do_not_compress_roles)
|
||||
: cache_options_(capacity, num_shard_bits, strict_capacity_limit,
|
||||
high_pri_pool_ratio, low_pri_pool_ratio, memory_allocator,
|
||||
use_adaptive_mutex, metadata_charge_policy,
|
||||
compression_type, compress_format_version,
|
||||
enable_custom_split_merge) {
|
||||
enable_custom_split_merge, do_not_compress_roles) {
|
||||
cache_ =
|
||||
NewLRUCache(capacity, num_shard_bits, strict_capacity_limit,
|
||||
high_pri_pool_ratio, memory_allocator, use_adaptive_mutex,
|
||||
@@ -39,10 +40,10 @@ CompressedSecondaryCache::~CompressedSecondaryCache() { cache_.reset(); }
|
||||
std::unique_ptr<SecondaryCacheResultHandle> CompressedSecondaryCache::Lookup(
|
||||
const Slice& key, const Cache::CacheItemHelper* helper,
|
||||
Cache::CreateContext* create_context, bool /*wait*/, bool advise_erase,
|
||||
bool& is_in_sec_cache) {
|
||||
bool& kept_in_sec_cache) {
|
||||
assert(helper);
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle;
|
||||
is_in_sec_cache = false;
|
||||
kept_in_sec_cache = false;
|
||||
Cache::Handle* lru_handle = cache_->Lookup(key);
|
||||
if (lru_handle == nullptr) {
|
||||
return nullptr;
|
||||
@@ -71,7 +72,8 @@ std::unique_ptr<SecondaryCacheResultHandle> CompressedSecondaryCache::Lookup(
|
||||
Status s;
|
||||
Cache::ObjectPtr value{nullptr};
|
||||
size_t charge{0};
|
||||
if (cache_options_.compression_type == kNoCompression) {
|
||||
if (cache_options_.compression_type == kNoCompression ||
|
||||
cache_options_.do_not_compress_roles.Contains(helper->role)) {
|
||||
s = helper->create_cb(Slice(ptr->get(), handle_value_charge),
|
||||
create_context, allocator, &value, &charge);
|
||||
} else {
|
||||
@@ -107,7 +109,7 @@ std::unique_ptr<SecondaryCacheResultHandle> CompressedSecondaryCache::Lookup(
|
||||
/*charge=*/0)
|
||||
.PermitUncheckedError();
|
||||
} else {
|
||||
is_in_sec_cache = true;
|
||||
kept_in_sec_cache = true;
|
||||
cache_->Release(lru_handle, /*erase_if_last_ref=*/false);
|
||||
}
|
||||
handle.reset(new CompressedSecondaryCacheResultHandle(value, charge));
|
||||
@@ -143,7 +145,8 @@ Status CompressedSecondaryCache::Insert(const Slice& key,
|
||||
Slice val(ptr.get(), size);
|
||||
|
||||
std::string compressed_val;
|
||||
if (cache_options_.compression_type != kNoCompression) {
|
||||
if (cache_options_.compression_type != kNoCompression &&
|
||||
!cache_options_.do_not_compress_roles.Contains(helper->role)) {
|
||||
PERF_COUNTER_ADD(compressed_sec_cache_uncompressed_bytes, size);
|
||||
CompressionOptions compression_opts;
|
||||
CompressionContext compression_context(cache_options_.compression_type);
|
||||
@@ -314,12 +317,13 @@ std::shared_ptr<SecondaryCache> NewCompressedSecondaryCache(
|
||||
std::shared_ptr<MemoryAllocator> memory_allocator, bool use_adaptive_mutex,
|
||||
CacheMetadataChargePolicy metadata_charge_policy,
|
||||
CompressionType compression_type, uint32_t compress_format_version,
|
||||
bool enable_custom_split_merge) {
|
||||
bool enable_custom_split_merge,
|
||||
const CacheEntryRoleSet& do_not_compress_roles) {
|
||||
return std::make_shared<CompressedSecondaryCache>(
|
||||
capacity, num_shard_bits, strict_capacity_limit, high_pri_pool_ratio,
|
||||
low_pri_pool_ratio, memory_allocator, use_adaptive_mutex,
|
||||
metadata_charge_policy, compression_type, compress_format_version,
|
||||
enable_custom_split_merge);
|
||||
enable_custom_split_merge, do_not_compress_roles);
|
||||
}
|
||||
|
||||
std::shared_ptr<SecondaryCache> NewCompressedSecondaryCache(
|
||||
@@ -331,7 +335,7 @@ std::shared_ptr<SecondaryCache> NewCompressedSecondaryCache(
|
||||
opts.high_pri_pool_ratio, opts.low_pri_pool_ratio, opts.memory_allocator,
|
||||
opts.use_adaptive_mutex, opts.metadata_charge_policy,
|
||||
opts.compression_type, opts.compress_format_version,
|
||||
opts.enable_custom_split_merge);
|
||||
opts.enable_custom_split_merge, opts.do_not_compress_roles);
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
Vendored
+5
-3
@@ -78,7 +78,9 @@ class CompressedSecondaryCache : public SecondaryCache {
|
||||
kDefaultCacheMetadataChargePolicy,
|
||||
CompressionType compression_type = CompressionType::kLZ4Compression,
|
||||
uint32_t compress_format_version = 2,
|
||||
bool enable_custom_split_merge = false);
|
||||
bool enable_custom_split_merge = false,
|
||||
const CacheEntryRoleSet& do_not_compress_roles = {
|
||||
CacheEntryRole::kFilterBlock});
|
||||
~CompressedSecondaryCache() override;
|
||||
|
||||
const char* Name() const override { return "CompressedSecondaryCache"; }
|
||||
@@ -89,7 +91,7 @@ class CompressedSecondaryCache : public SecondaryCache {
|
||||
std::unique_ptr<SecondaryCacheResultHandle> Lookup(
|
||||
const Slice& key, const Cache::CacheItemHelper* helper,
|
||||
Cache::CreateContext* create_context, bool /*wait*/, bool advise_erase,
|
||||
bool& is_in_sec_cache) override;
|
||||
bool& kept_in_sec_cache) override;
|
||||
|
||||
bool SupportForceErase() const override { return true; }
|
||||
|
||||
@@ -104,7 +106,7 @@ class CompressedSecondaryCache : public SecondaryCache {
|
||||
std::string GetPrintableOptions() const override;
|
||||
|
||||
private:
|
||||
friend class CompressedSecondaryCacheTest;
|
||||
friend class CompressedSecondaryCacheTestBase;
|
||||
static constexpr std::array<uint16_t, 8> malloc_bin_sizes_{
|
||||
128, 256, 512, 1024, 2048, 4096, 8192, 16384};
|
||||
|
||||
|
||||
+216
-187
@@ -5,93 +5,43 @@
|
||||
|
||||
#include "cache/compressed_secondary_cache.h"
|
||||
|
||||
#include <array>
|
||||
#include <iterator>
|
||||
#include <memory>
|
||||
#include <tuple>
|
||||
|
||||
#include "memory/jemalloc_nodump_allocator.h"
|
||||
#include "rocksdb/convenience.h"
|
||||
#include "test_util/secondary_cache_test_util.h"
|
||||
#include "test_util/testharness.h"
|
||||
#include "test_util/testutil.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
class CompressedSecondaryCacheTest : public testing::Test,
|
||||
public Cache::CreateContext {
|
||||
using secondary_cache_test_util::GetTestingCacheTypes;
|
||||
using secondary_cache_test_util::WithCacheType;
|
||||
|
||||
// 16 bytes for HCC compatibility
|
||||
const std::string key0 = "____ ____key0";
|
||||
const std::string key1 = "____ ____key1";
|
||||
const std::string key2 = "____ ____key2";
|
||||
const std::string key3 = "____ ____key3";
|
||||
|
||||
class CompressedSecondaryCacheTestBase : public testing::Test,
|
||||
public WithCacheType {
|
||||
public:
|
||||
CompressedSecondaryCacheTest() : fail_create_(false) {}
|
||||
~CompressedSecondaryCacheTest() override = default;
|
||||
CompressedSecondaryCacheTestBase() {}
|
||||
~CompressedSecondaryCacheTestBase() override = default;
|
||||
|
||||
protected:
|
||||
class TestItem {
|
||||
public:
|
||||
TestItem(const char* buf, size_t size) : buf_(new char[size]), size_(size) {
|
||||
memcpy(buf_.get(), buf, size);
|
||||
}
|
||||
~TestItem() = default;
|
||||
|
||||
char* Buf() { return buf_.get(); }
|
||||
[[nodiscard]] size_t Size() const { return size_; }
|
||||
|
||||
private:
|
||||
std::unique_ptr<char[]> buf_;
|
||||
size_t size_;
|
||||
};
|
||||
|
||||
static size_t SizeCallback(Cache::ObjectPtr obj) {
|
||||
return static_cast<TestItem*>(obj)->Size();
|
||||
}
|
||||
|
||||
static Status SaveToCallback(Cache::ObjectPtr from_obj, size_t from_offset,
|
||||
size_t length, char* out) {
|
||||
auto item = static_cast<TestItem*>(from_obj);
|
||||
const char* buf = item->Buf();
|
||||
EXPECT_EQ(length, item->Size());
|
||||
EXPECT_EQ(from_offset, 0);
|
||||
memcpy(out, buf, length);
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
static void DeletionCallback(Cache::ObjectPtr obj,
|
||||
MemoryAllocator* /*alloc*/) {
|
||||
delete static_cast<TestItem*>(obj);
|
||||
obj = nullptr;
|
||||
}
|
||||
|
||||
static Status SaveToCallbackFail(Cache::ObjectPtr /*obj*/, size_t /*offset*/,
|
||||
size_t /*size*/, char* /*out*/) {
|
||||
return Status::NotSupported();
|
||||
}
|
||||
|
||||
static Status CreateCallback(const Slice& data, Cache::CreateContext* context,
|
||||
MemoryAllocator* /*allocator*/,
|
||||
Cache::ObjectPtr* out_obj, size_t* out_charge) {
|
||||
auto t = static_cast<CompressedSecondaryCacheTest*>(context);
|
||||
if (t->fail_create_) {
|
||||
return Status::NotSupported();
|
||||
}
|
||||
*out_obj = new TestItem(data.data(), data.size());
|
||||
*out_charge = data.size();
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
static constexpr Cache::CacheItemHelper kHelper{
|
||||
CacheEntryRole::kMisc, &DeletionCallback, &SizeCallback, &SaveToCallback,
|
||||
&CreateCallback};
|
||||
|
||||
static constexpr Cache::CacheItemHelper kHelperFail{
|
||||
CacheEntryRole::kMisc, &DeletionCallback, &SizeCallback,
|
||||
&SaveToCallbackFail, &CreateCallback};
|
||||
|
||||
void SetFailCreate(bool fail) { fail_create_ = fail; }
|
||||
|
||||
void BasicTestHelper(std::shared_ptr<SecondaryCache> sec_cache,
|
||||
bool sec_cache_is_compressed) {
|
||||
get_perf_context()->Reset();
|
||||
bool is_in_sec_cache{true};
|
||||
bool kept_in_sec_cache{true};
|
||||
// Lookup an non-existent key.
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle0 = sec_cache->Lookup(
|
||||
"k0", &kHelper, this, true, /*advise_erase=*/true, is_in_sec_cache);
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle0 =
|
||||
sec_cache->Lookup(key0, GetHelper(), this, true, /*advise_erase=*/true,
|
||||
kept_in_sec_cache);
|
||||
ASSERT_EQ(handle0, nullptr);
|
||||
|
||||
Random rnd(301);
|
||||
@@ -99,23 +49,25 @@ class CompressedSecondaryCacheTest : public testing::Test,
|
||||
std::string str1(rnd.RandomString(1000));
|
||||
TestItem item1(str1.data(), str1.length());
|
||||
// A dummy handle is inserted if the item is inserted for the first time.
|
||||
ASSERT_OK(sec_cache->Insert("k1", &item1, &kHelper));
|
||||
ASSERT_OK(sec_cache->Insert(key1, &item1, GetHelper()));
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_insert_dummy_count, 1);
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_uncompressed_bytes, 0);
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_compressed_bytes, 0);
|
||||
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle1_1 = sec_cache->Lookup(
|
||||
"k1", &kHelper, this, true, /*advise_erase=*/false, is_in_sec_cache);
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle1_1 =
|
||||
sec_cache->Lookup(key1, GetHelper(), this, true, /*advise_erase=*/false,
|
||||
kept_in_sec_cache);
|
||||
ASSERT_EQ(handle1_1, nullptr);
|
||||
|
||||
// Insert and Lookup the item k1 for the second time and advise erasing it.
|
||||
ASSERT_OK(sec_cache->Insert("k1", &item1, &kHelper));
|
||||
ASSERT_OK(sec_cache->Insert(key1, &item1, GetHelper()));
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_insert_real_count, 1);
|
||||
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle1_2 = sec_cache->Lookup(
|
||||
"k1", &kHelper, this, true, /*advise_erase=*/true, is_in_sec_cache);
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle1_2 =
|
||||
sec_cache->Lookup(key1, GetHelper(), this, true, /*advise_erase=*/true,
|
||||
kept_in_sec_cache);
|
||||
ASSERT_NE(handle1_2, nullptr);
|
||||
ASSERT_FALSE(is_in_sec_cache);
|
||||
ASSERT_FALSE(kept_in_sec_cache);
|
||||
if (sec_cache_is_compressed) {
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_uncompressed_bytes,
|
||||
1000);
|
||||
@@ -132,20 +84,22 @@ class CompressedSecondaryCacheTest : public testing::Test,
|
||||
ASSERT_EQ(memcmp(val1->Buf(), item1.Buf(), item1.Size()), 0);
|
||||
|
||||
// Lookup the item k1 again.
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle1_3 = sec_cache->Lookup(
|
||||
"k1", &kHelper, this, true, /*advise_erase=*/true, is_in_sec_cache);
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle1_3 =
|
||||
sec_cache->Lookup(key1, GetHelper(), this, true, /*advise_erase=*/true,
|
||||
kept_in_sec_cache);
|
||||
ASSERT_EQ(handle1_3, nullptr);
|
||||
|
||||
// Insert and Lookup the item k2.
|
||||
std::string str2(rnd.RandomString(1000));
|
||||
TestItem item2(str2.data(), str2.length());
|
||||
ASSERT_OK(sec_cache->Insert("k2", &item2, &kHelper));
|
||||
ASSERT_OK(sec_cache->Insert(key2, &item2, GetHelper()));
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_insert_dummy_count, 2);
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle2_1 = sec_cache->Lookup(
|
||||
"k2", &kHelper, this, true, /*advise_erase=*/false, is_in_sec_cache);
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle2_1 =
|
||||
sec_cache->Lookup(key2, GetHelper(), this, true, /*advise_erase=*/false,
|
||||
kept_in_sec_cache);
|
||||
ASSERT_EQ(handle2_1, nullptr);
|
||||
|
||||
ASSERT_OK(sec_cache->Insert("k2", &item2, &kHelper));
|
||||
ASSERT_OK(sec_cache->Insert(key2, &item2, GetHelper()));
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_insert_real_count, 2);
|
||||
if (sec_cache_is_compressed) {
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_uncompressed_bytes,
|
||||
@@ -156,8 +110,9 @@ class CompressedSecondaryCacheTest : public testing::Test,
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_uncompressed_bytes, 0);
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_compressed_bytes, 0);
|
||||
}
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle2_2 = sec_cache->Lookup(
|
||||
"k2", &kHelper, this, true, /*advise_erase=*/false, is_in_sec_cache);
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle2_2 =
|
||||
sec_cache->Lookup(key2, GetHelper(), this, true, /*advise_erase=*/false,
|
||||
kept_in_sec_cache);
|
||||
ASSERT_NE(handle2_2, nullptr);
|
||||
std::unique_ptr<TestItem> val2 =
|
||||
std::unique_ptr<TestItem>(static_cast<TestItem*>(handle2_2->Value()));
|
||||
@@ -226,24 +181,26 @@ class CompressedSecondaryCacheTest : public testing::Test,
|
||||
std::string str1(rnd.RandomString(1000));
|
||||
TestItem item1(str1.data(), str1.length());
|
||||
// Insert a dummy handle.
|
||||
ASSERT_OK(sec_cache->Insert("k1", &item1, &kHelper));
|
||||
ASSERT_OK(sec_cache->Insert(key1, &item1, GetHelper()));
|
||||
// Insert k1.
|
||||
ASSERT_OK(sec_cache->Insert("k1", &item1, &kHelper));
|
||||
ASSERT_OK(sec_cache->Insert(key1, &item1, GetHelper()));
|
||||
|
||||
// Insert and Lookup the second item.
|
||||
std::string str2(rnd.RandomString(200));
|
||||
TestItem item2(str2.data(), str2.length());
|
||||
// Insert a dummy handle, k1 is not evicted.
|
||||
ASSERT_OK(sec_cache->Insert("k2", &item2, &kHelper));
|
||||
bool is_in_sec_cache{false};
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle1 = sec_cache->Lookup(
|
||||
"k1", &kHelper, this, true, /*advise_erase=*/false, is_in_sec_cache);
|
||||
ASSERT_OK(sec_cache->Insert(key2, &item2, GetHelper()));
|
||||
bool kept_in_sec_cache{false};
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle1 =
|
||||
sec_cache->Lookup(key1, GetHelper(), this, true, /*advise_erase=*/false,
|
||||
kept_in_sec_cache);
|
||||
ASSERT_EQ(handle1, nullptr);
|
||||
|
||||
// Insert k2 and k1 is evicted.
|
||||
ASSERT_OK(sec_cache->Insert("k2", &item2, &kHelper));
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle2 = sec_cache->Lookup(
|
||||
"k2", &kHelper, this, true, /*advise_erase=*/false, is_in_sec_cache);
|
||||
ASSERT_OK(sec_cache->Insert(key2, &item2, GetHelper()));
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle2 =
|
||||
sec_cache->Lookup(key2, GetHelper(), this, true, /*advise_erase=*/false,
|
||||
kept_in_sec_cache);
|
||||
ASSERT_NE(handle2, nullptr);
|
||||
std::unique_ptr<TestItem> val2 =
|
||||
std::unique_ptr<TestItem>(static_cast<TestItem*>(handle2->Value()));
|
||||
@@ -251,24 +208,26 @@ class CompressedSecondaryCacheTest : public testing::Test,
|
||||
ASSERT_EQ(memcmp(val2->Buf(), item2.Buf(), item2.Size()), 0);
|
||||
|
||||
// Insert k1 again and a dummy handle is inserted.
|
||||
ASSERT_OK(sec_cache->Insert("k1", &item1, &kHelper));
|
||||
ASSERT_OK(sec_cache->Insert(key1, &item1, GetHelper()));
|
||||
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle1_1 = sec_cache->Lookup(
|
||||
"k1", &kHelper, this, true, /*advise_erase=*/false, is_in_sec_cache);
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle1_1 =
|
||||
sec_cache->Lookup(key1, GetHelper(), this, true, /*advise_erase=*/false,
|
||||
kept_in_sec_cache);
|
||||
ASSERT_EQ(handle1_1, nullptr);
|
||||
|
||||
// Create Fails.
|
||||
SetFailCreate(true);
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle2_1 = sec_cache->Lookup(
|
||||
"k2", &kHelper, this, true, /*advise_erase=*/true, is_in_sec_cache);
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle2_1 =
|
||||
sec_cache->Lookup(key2, GetHelper(), this, true, /*advise_erase=*/true,
|
||||
kept_in_sec_cache);
|
||||
ASSERT_EQ(handle2_1, nullptr);
|
||||
|
||||
// Save Fails.
|
||||
std::string str3 = rnd.RandomString(10);
|
||||
TestItem item3(str3.data(), str3.length());
|
||||
// The Status is OK because a dummy handle is inserted.
|
||||
ASSERT_OK(sec_cache->Insert("k3", &item3, &kHelperFail));
|
||||
ASSERT_NOK(sec_cache->Insert("k3", &item3, &kHelperFail));
|
||||
ASSERT_OK(sec_cache->Insert(key3, &item3, GetHelperFail()));
|
||||
ASSERT_NOK(sec_cache->Insert(key3, &item3, GetHelperFail()));
|
||||
|
||||
sec_cache.reset();
|
||||
}
|
||||
@@ -292,26 +251,22 @@ class CompressedSecondaryCacheTest : public testing::Test,
|
||||
secondary_cache_opts.enable_custom_split_merge = enable_custom_split_merge;
|
||||
std::shared_ptr<SecondaryCache> secondary_cache =
|
||||
NewCompressedSecondaryCache(secondary_cache_opts);
|
||||
LRUCacheOptions lru_cache_opts(
|
||||
std::shared_ptr<Cache> cache = NewCache(
|
||||
/*_capacity =*/1300, /*_num_shard_bits =*/0,
|
||||
/*_strict_capacity_limit =*/false, /*_high_pri_pool_ratio =*/0.5,
|
||||
/*_memory_allocator =*/nullptr, kDefaultToAdaptiveMutex,
|
||||
kDefaultCacheMetadataChargePolicy, /*_low_pri_pool_ratio =*/0.0);
|
||||
lru_cache_opts.secondary_cache = secondary_cache;
|
||||
std::shared_ptr<Cache> cache = NewLRUCache(lru_cache_opts);
|
||||
/*_strict_capacity_limit =*/true, secondary_cache);
|
||||
std::shared_ptr<Statistics> stats = CreateDBStatistics();
|
||||
|
||||
get_perf_context()->Reset();
|
||||
Random rnd(301);
|
||||
std::string str1 = rnd.RandomString(1001);
|
||||
auto item1_1 = new TestItem(str1.data(), str1.length());
|
||||
ASSERT_OK(cache->Insert("k1", item1_1, &kHelper, str1.length()));
|
||||
ASSERT_OK(cache->Insert(key1, item1_1, GetHelper(), str1.length()));
|
||||
|
||||
std::string str2 = rnd.RandomString(1012);
|
||||
auto item2_1 = new TestItem(str2.data(), str2.length());
|
||||
// After this Insert, primary cache contains k2 and secondary cache contains
|
||||
// k1's dummy item.
|
||||
ASSERT_OK(cache->Insert("k2", item2_1, &kHelper, str2.length()));
|
||||
ASSERT_OK(cache->Insert(key2, item2_1, GetHelper(), str2.length()));
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_insert_dummy_count, 1);
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_uncompressed_bytes, 0);
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_compressed_bytes, 0);
|
||||
@@ -320,19 +275,19 @@ class CompressedSecondaryCacheTest : public testing::Test,
|
||||
auto item3_1 = new TestItem(str3.data(), str3.length());
|
||||
// After this Insert, primary cache contains k3 and secondary cache contains
|
||||
// k1's dummy item and k2's dummy item.
|
||||
ASSERT_OK(cache->Insert("k3", item3_1, &kHelper, str3.length()));
|
||||
ASSERT_OK(cache->Insert(key3, item3_1, GetHelper(), str3.length()));
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_insert_dummy_count, 2);
|
||||
|
||||
// After this Insert, primary cache contains k1 and secondary cache contains
|
||||
// k1's dummy item, k2's dummy item, and k3's dummy item.
|
||||
auto item1_2 = new TestItem(str1.data(), str1.length());
|
||||
ASSERT_OK(cache->Insert("k1", item1_2, &kHelper, str1.length()));
|
||||
ASSERT_OK(cache->Insert(key1, item1_2, GetHelper(), str1.length()));
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_insert_dummy_count, 3);
|
||||
|
||||
// After this Insert, primary cache contains k2 and secondary cache contains
|
||||
// k1's item, k2's dummy item, and k3's dummy item.
|
||||
auto item2_2 = new TestItem(str2.data(), str2.length());
|
||||
ASSERT_OK(cache->Insert("k2", item2_2, &kHelper, str2.length()));
|
||||
ASSERT_OK(cache->Insert(key2, item2_2, GetHelper(), str2.length()));
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_insert_real_count, 1);
|
||||
if (sec_cache_is_compressed) {
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_uncompressed_bytes,
|
||||
@@ -347,7 +302,7 @@ class CompressedSecondaryCacheTest : public testing::Test,
|
||||
// After this Insert, primary cache contains k3 and secondary cache contains
|
||||
// k1's item and k2's item.
|
||||
auto item3_2 = new TestItem(str3.data(), str3.length());
|
||||
ASSERT_OK(cache->Insert("k3", item3_2, &kHelper, str3.length()));
|
||||
ASSERT_OK(cache->Insert(key3, item3_2, GetHelper(), str3.length()));
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_insert_real_count, 2);
|
||||
if (sec_cache_is_compressed) {
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_uncompressed_bytes,
|
||||
@@ -360,7 +315,7 @@ class CompressedSecondaryCacheTest : public testing::Test,
|
||||
}
|
||||
|
||||
Cache::Handle* handle;
|
||||
handle = cache->Lookup("k3", &kHelper, this, Cache::Priority::LOW, true,
|
||||
handle = cache->Lookup(key3, GetHelper(), this, Cache::Priority::LOW,
|
||||
stats.get());
|
||||
ASSERT_NE(handle, nullptr);
|
||||
auto val3 = static_cast<TestItem*>(cache->Value(handle));
|
||||
@@ -369,13 +324,13 @@ class CompressedSecondaryCacheTest : public testing::Test,
|
||||
cache->Release(handle);
|
||||
|
||||
// Lookup an non-existent key.
|
||||
handle = cache->Lookup("k0", &kHelper, this, Cache::Priority::LOW, true,
|
||||
handle = cache->Lookup(key0, GetHelper(), this, Cache::Priority::LOW,
|
||||
stats.get());
|
||||
ASSERT_EQ(handle, nullptr);
|
||||
|
||||
// This Lookup should just insert a dummy handle in the primary cache
|
||||
// and the k1 is still in the secondary cache.
|
||||
handle = cache->Lookup("k1", &kHelper, this, Cache::Priority::LOW, true,
|
||||
handle = cache->Lookup(key1, GetHelper(), this, Cache::Priority::LOW,
|
||||
stats.get());
|
||||
ASSERT_NE(handle, nullptr);
|
||||
ASSERT_EQ(get_perf_context()->block_cache_standalone_handle_count, 1);
|
||||
@@ -387,7 +342,7 @@ class CompressedSecondaryCacheTest : public testing::Test,
|
||||
// This Lookup should erase k1 from the secondary cache and insert
|
||||
// it into primary cache; then k3 is demoted.
|
||||
// k2 and k3 are in secondary cache.
|
||||
handle = cache->Lookup("k1", &kHelper, this, Cache::Priority::LOW, true,
|
||||
handle = cache->Lookup(key1, GetHelper(), this, Cache::Priority::LOW,
|
||||
stats.get());
|
||||
ASSERT_NE(handle, nullptr);
|
||||
ASSERT_EQ(get_perf_context()->block_cache_standalone_handle_count, 1);
|
||||
@@ -395,7 +350,7 @@ class CompressedSecondaryCacheTest : public testing::Test,
|
||||
cache->Release(handle);
|
||||
|
||||
// k2 is still in secondary cache.
|
||||
handle = cache->Lookup("k2", &kHelper, this, Cache::Priority::LOW, true,
|
||||
handle = cache->Lookup(key2, GetHelper(), this, Cache::Priority::LOW,
|
||||
stats.get());
|
||||
ASSERT_NE(handle, nullptr);
|
||||
ASSERT_EQ(get_perf_context()->block_cache_standalone_handle_count, 2);
|
||||
@@ -403,7 +358,7 @@ class CompressedSecondaryCacheTest : public testing::Test,
|
||||
|
||||
// Testing SetCapacity().
|
||||
ASSERT_OK(secondary_cache->SetCapacity(0));
|
||||
handle = cache->Lookup("k3", &kHelper, this, Cache::Priority::LOW, true,
|
||||
handle = cache->Lookup(key3, GetHelper(), this, Cache::Priority::LOW,
|
||||
stats.get());
|
||||
ASSERT_EQ(handle, nullptr);
|
||||
|
||||
@@ -413,30 +368,30 @@ class CompressedSecondaryCacheTest : public testing::Test,
|
||||
ASSERT_EQ(capacity, 7000);
|
||||
auto item1_3 = new TestItem(str1.data(), str1.length());
|
||||
// After this Insert, primary cache contains k1.
|
||||
ASSERT_OK(cache->Insert("k1", item1_3, &kHelper, str2.length()));
|
||||
ASSERT_OK(cache->Insert(key1, item1_3, GetHelper(), str2.length()));
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_insert_dummy_count, 3);
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_insert_real_count, 4);
|
||||
|
||||
auto item2_3 = new TestItem(str2.data(), str2.length());
|
||||
// After this Insert, primary cache contains k2 and secondary cache contains
|
||||
// k1's dummy item.
|
||||
ASSERT_OK(cache->Insert("k2", item2_3, &kHelper, str1.length()));
|
||||
ASSERT_OK(cache->Insert(key2, item2_3, GetHelper(), str1.length()));
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_insert_dummy_count, 4);
|
||||
|
||||
auto item1_4 = new TestItem(str1.data(), str1.length());
|
||||
// After this Insert, primary cache contains k1 and secondary cache contains
|
||||
// k1's dummy item and k2's dummy item.
|
||||
ASSERT_OK(cache->Insert("k1", item1_4, &kHelper, str2.length()));
|
||||
ASSERT_OK(cache->Insert(key1, item1_4, GetHelper(), str2.length()));
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_insert_dummy_count, 5);
|
||||
|
||||
auto item2_4 = new TestItem(str2.data(), str2.length());
|
||||
// After this Insert, primary cache contains k2 and secondary cache contains
|
||||
// k1's real item and k2's dummy item.
|
||||
ASSERT_OK(cache->Insert("k2", item2_4, &kHelper, str2.length()));
|
||||
ASSERT_OK(cache->Insert(key2, item2_4, GetHelper(), str2.length()));
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_insert_real_count, 5);
|
||||
// This Lookup should just insert a dummy handle in the primary cache
|
||||
// and the k1 is still in the secondary cache.
|
||||
handle = cache->Lookup("k1", &kHelper, this, Cache::Priority::LOW, true,
|
||||
handle = cache->Lookup(key1, GetHelper(), this, Cache::Priority::LOW,
|
||||
stats.get());
|
||||
|
||||
ASSERT_NE(handle, nullptr);
|
||||
@@ -464,26 +419,31 @@ class CompressedSecondaryCacheTest : public testing::Test,
|
||||
std::shared_ptr<SecondaryCache> secondary_cache =
|
||||
NewCompressedSecondaryCache(secondary_cache_opts);
|
||||
|
||||
LRUCacheOptions opts(
|
||||
std::shared_ptr<Cache> cache = NewCache(
|
||||
/*_capacity=*/1300, /*_num_shard_bits=*/0,
|
||||
/*_strict_capacity_limit=*/false, /*_high_pri_pool_ratio=*/0.5,
|
||||
/*_memory_allocator=*/nullptr, kDefaultToAdaptiveMutex,
|
||||
kDefaultCacheMetadataChargePolicy, /*_low_pri_pool_ratio=*/0.0);
|
||||
opts.secondary_cache = secondary_cache;
|
||||
std::shared_ptr<Cache> cache = NewLRUCache(opts);
|
||||
/*_strict_capacity_limit=*/false, secondary_cache);
|
||||
|
||||
Random rnd(301);
|
||||
std::string str1 = rnd.RandomString(1001);
|
||||
auto item1 = std::make_unique<TestItem>(str1.data(), str1.length());
|
||||
ASSERT_OK(cache->Insert("k1", item1.get(), &kHelper, str1.length()));
|
||||
ASSERT_OK(cache->Insert(key1, item1.get(), GetHelper(), str1.length()));
|
||||
item1.release(); // Appease clang-analyze "potential memory leak"
|
||||
|
||||
Cache::Handle* handle;
|
||||
handle = cache->Lookup("k2", nullptr, this, Cache::Priority::LOW, true);
|
||||
handle = cache->Lookup(key2, nullptr, this, Cache::Priority::LOW);
|
||||
ASSERT_EQ(handle, nullptr);
|
||||
handle = cache->Lookup("k2", &kHelper, this, Cache::Priority::LOW, false);
|
||||
handle = cache->Lookup(key2, GetHelper(), this, Cache::Priority::LOW);
|
||||
ASSERT_EQ(handle, nullptr);
|
||||
|
||||
Cache::AsyncLookupHandle ah;
|
||||
ah.key = key2;
|
||||
ah.helper = GetHelper();
|
||||
ah.create_context = this;
|
||||
ah.priority = Cache::Priority::LOW;
|
||||
cache->StartAsyncLookup(ah);
|
||||
cache->Wait(ah);
|
||||
ASSERT_EQ(ah.Result(), nullptr);
|
||||
|
||||
cache.reset();
|
||||
secondary_cache.reset();
|
||||
}
|
||||
@@ -506,36 +466,29 @@ class CompressedSecondaryCacheTest : public testing::Test,
|
||||
std::shared_ptr<SecondaryCache> secondary_cache =
|
||||
NewCompressedSecondaryCache(secondary_cache_opts);
|
||||
|
||||
LRUCacheOptions opts(
|
||||
std::shared_ptr<Cache> cache = NewCache(
|
||||
/*_capacity=*/1300, /*_num_shard_bits=*/0,
|
||||
/*_strict_capacity_limit=*/false, /*_high_pri_pool_ratio=*/0.5,
|
||||
/*_memory_allocator=*/nullptr, kDefaultToAdaptiveMutex,
|
||||
kDefaultCacheMetadataChargePolicy, /*_low_pri_pool_ratio=*/0.0);
|
||||
opts.secondary_cache = secondary_cache;
|
||||
std::shared_ptr<Cache> cache = NewLRUCache(opts);
|
||||
/*_strict_capacity_limit=*/true, secondary_cache);
|
||||
|
||||
Random rnd(301);
|
||||
std::string str1 = rnd.RandomString(1001);
|
||||
auto item1 = new TestItem(str1.data(), str1.length());
|
||||
ASSERT_OK(cache->Insert("k1", item1, &kHelperFail, str1.length()));
|
||||
ASSERT_OK(cache->Insert(key1, item1, GetHelperFail(), str1.length()));
|
||||
|
||||
std::string str2 = rnd.RandomString(1002);
|
||||
auto item2 = new TestItem(str2.data(), str2.length());
|
||||
// k1 should be demoted to the secondary cache.
|
||||
ASSERT_OK(cache->Insert("k2", item2, &kHelperFail, str2.length()));
|
||||
ASSERT_OK(cache->Insert(key2, item2, GetHelperFail(), str2.length()));
|
||||
|
||||
Cache::Handle* handle;
|
||||
handle =
|
||||
cache->Lookup("k2", &kHelperFail, this, Cache::Priority::LOW, true);
|
||||
handle = cache->Lookup(key2, GetHelperFail(), this, Cache::Priority::LOW);
|
||||
ASSERT_NE(handle, nullptr);
|
||||
cache->Release(handle);
|
||||
// This lookup should fail, since k1 demotion would have failed.
|
||||
handle =
|
||||
cache->Lookup("k1", &kHelperFail, this, Cache::Priority::LOW, true);
|
||||
handle = cache->Lookup(key1, GetHelperFail(), this, Cache::Priority::LOW);
|
||||
ASSERT_EQ(handle, nullptr);
|
||||
// Since k1 was not promoted, k2 should still be in cache.
|
||||
handle =
|
||||
cache->Lookup("k2", &kHelperFail, this, Cache::Priority::LOW, true);
|
||||
handle = cache->Lookup(key2, GetHelperFail(), this, Cache::Priority::LOW);
|
||||
ASSERT_NE(handle, nullptr);
|
||||
cache->Release(handle);
|
||||
|
||||
@@ -561,34 +514,30 @@ class CompressedSecondaryCacheTest : public testing::Test,
|
||||
std::shared_ptr<SecondaryCache> secondary_cache =
|
||||
NewCompressedSecondaryCache(secondary_cache_opts);
|
||||
|
||||
LRUCacheOptions opts(
|
||||
std::shared_ptr<Cache> cache = NewCache(
|
||||
/*_capacity=*/1300, /*_num_shard_bits=*/0,
|
||||
/*_strict_capacity_limit=*/false, /*_high_pri_pool_ratio=*/0.5,
|
||||
/*_memory_allocator=*/nullptr, kDefaultToAdaptiveMutex,
|
||||
kDefaultCacheMetadataChargePolicy, /*_low_pri_pool_ratio=*/0.0);
|
||||
opts.secondary_cache = secondary_cache;
|
||||
std::shared_ptr<Cache> cache = NewLRUCache(opts);
|
||||
/*_strict_capacity_limit=*/true, secondary_cache);
|
||||
|
||||
Random rnd(301);
|
||||
std::string str1 = rnd.RandomString(1001);
|
||||
auto item1 = new TestItem(str1.data(), str1.length());
|
||||
ASSERT_OK(cache->Insert("k1", item1, &kHelper, str1.length()));
|
||||
ASSERT_OK(cache->Insert(key1, item1, GetHelper(), str1.length()));
|
||||
|
||||
std::string str2 = rnd.RandomString(1002);
|
||||
auto item2 = new TestItem(str2.data(), str2.length());
|
||||
// k1 should be demoted to the secondary cache.
|
||||
ASSERT_OK(cache->Insert("k2", item2, &kHelper, str2.length()));
|
||||
ASSERT_OK(cache->Insert(key2, item2, GetHelper(), str2.length()));
|
||||
|
||||
Cache::Handle* handle;
|
||||
SetFailCreate(true);
|
||||
handle = cache->Lookup("k2", &kHelper, this, Cache::Priority::LOW, true);
|
||||
handle = cache->Lookup(key2, GetHelper(), this, Cache::Priority::LOW);
|
||||
ASSERT_NE(handle, nullptr);
|
||||
cache->Release(handle);
|
||||
// This lookup should fail, since k1 creation would have failed
|
||||
handle = cache->Lookup("k1", &kHelper, this, Cache::Priority::LOW, true);
|
||||
handle = cache->Lookup(key1, GetHelper(), this, Cache::Priority::LOW);
|
||||
ASSERT_EQ(handle, nullptr);
|
||||
// Since k1 didn't get promoted, k2 should still be in cache
|
||||
handle = cache->Lookup("k2", &kHelper, this, Cache::Priority::LOW, true);
|
||||
handle = cache->Lookup(key2, GetHelper(), this, Cache::Priority::LOW);
|
||||
ASSERT_NE(handle, nullptr);
|
||||
cache->Release(handle);
|
||||
|
||||
@@ -614,38 +563,34 @@ class CompressedSecondaryCacheTest : public testing::Test,
|
||||
std::shared_ptr<SecondaryCache> secondary_cache =
|
||||
NewCompressedSecondaryCache(secondary_cache_opts);
|
||||
|
||||
LRUCacheOptions opts(
|
||||
std::shared_ptr<Cache> cache = NewCache(
|
||||
/*_capacity=*/1300, /*_num_shard_bits=*/0,
|
||||
/*_strict_capacity_limit=*/false, /*_high_pri_pool_ratio=*/0.5,
|
||||
/*_memory_allocator=*/nullptr, kDefaultToAdaptiveMutex,
|
||||
kDefaultCacheMetadataChargePolicy, /*_low_pri_pool_ratio=*/0.0);
|
||||
opts.secondary_cache = secondary_cache;
|
||||
std::shared_ptr<Cache> cache = NewLRUCache(opts);
|
||||
/*_strict_capacity_limit=*/false, secondary_cache);
|
||||
|
||||
Random rnd(301);
|
||||
std::string str1 = rnd.RandomString(1001);
|
||||
auto item1_1 = new TestItem(str1.data(), str1.length());
|
||||
ASSERT_OK(cache->Insert("k1", item1_1, &kHelper, str1.length()));
|
||||
ASSERT_OK(cache->Insert(key1, item1_1, GetHelper(), str1.length()));
|
||||
|
||||
std::string str2 = rnd.RandomString(1002);
|
||||
std::string str2_clone{str2};
|
||||
auto item2 = new TestItem(str2.data(), str2.length());
|
||||
// After this Insert, primary cache contains k2 and secondary cache contains
|
||||
// k1's dummy item.
|
||||
ASSERT_OK(cache->Insert("k2", item2, &kHelper, str2.length()));
|
||||
ASSERT_OK(cache->Insert(key2, item2, GetHelper(), str2.length()));
|
||||
|
||||
// After this Insert, primary cache contains k1 and secondary cache contains
|
||||
// k1's dummy item and k2's dummy item.
|
||||
auto item1_2 = new TestItem(str1.data(), str1.length());
|
||||
ASSERT_OK(cache->Insert("k1", item1_2, &kHelper, str1.length()));
|
||||
ASSERT_OK(cache->Insert(key1, item1_2, GetHelper(), str1.length()));
|
||||
|
||||
auto item2_2 = new TestItem(str2.data(), str2.length());
|
||||
// After this Insert, primary cache contains k2 and secondary cache contains
|
||||
// k1's item and k2's dummy item.
|
||||
ASSERT_OK(cache->Insert("k2", item2_2, &kHelper, str2.length()));
|
||||
ASSERT_OK(cache->Insert(key2, item2_2, GetHelper(), str2.length()));
|
||||
|
||||
Cache::Handle* handle2;
|
||||
handle2 = cache->Lookup("k2", &kHelper, this, Cache::Priority::LOW, true);
|
||||
handle2 = cache->Lookup(key2, GetHelper(), this, Cache::Priority::LOW);
|
||||
ASSERT_NE(handle2, nullptr);
|
||||
cache->Release(handle2);
|
||||
|
||||
@@ -653,12 +598,12 @@ class CompressedSecondaryCacheTest : public testing::Test,
|
||||
// strict_capacity_limit is true, but the lookup should still succeed.
|
||||
// A k1's dummy item is inserted into primary cache.
|
||||
Cache::Handle* handle1;
|
||||
handle1 = cache->Lookup("k1", &kHelper, this, Cache::Priority::LOW, true);
|
||||
handle1 = cache->Lookup(key1, GetHelper(), this, Cache::Priority::LOW);
|
||||
ASSERT_NE(handle1, nullptr);
|
||||
cache->Release(handle1);
|
||||
|
||||
// Since k1 didn't get inserted, k2 should still be in cache
|
||||
handle2 = cache->Lookup("k2", &kHelper, this, Cache::Priority::LOW, true);
|
||||
handle2 = cache->Lookup(key2, GetHelper(), this, Cache::Priority::LOW);
|
||||
ASSERT_NE(handle2, nullptr);
|
||||
cache->Release(handle2);
|
||||
|
||||
@@ -782,19 +727,27 @@ class CompressedSecondaryCacheTest : public testing::Test,
|
||||
|
||||
sec_cache->GetHelper(true)->del_cb(chunks_head, /*alloc*/ nullptr);
|
||||
}
|
||||
|
||||
private:
|
||||
bool fail_create_;
|
||||
};
|
||||
|
||||
class CompressedSecondaryCacheTest
|
||||
: public CompressedSecondaryCacheTestBase,
|
||||
public testing::WithParamInterface<std::string> {
|
||||
const std::string& Type() override { return GetParam(); }
|
||||
};
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(CompressedSecondaryCacheTest,
|
||||
CompressedSecondaryCacheTest, GetTestingCacheTypes());
|
||||
|
||||
class CompressedSecCacheTestWithCompressAndAllocatorParam
|
||||
: public CompressedSecondaryCacheTest,
|
||||
public ::testing::WithParamInterface<std::tuple<bool, bool>> {
|
||||
: public CompressedSecondaryCacheTestBase,
|
||||
public ::testing::WithParamInterface<
|
||||
std::tuple<bool, bool, std::string>> {
|
||||
public:
|
||||
CompressedSecCacheTestWithCompressAndAllocatorParam() {
|
||||
sec_cache_is_compressed_ = std::get<0>(GetParam());
|
||||
use_jemalloc_ = std::get<1>(GetParam());
|
||||
}
|
||||
const std::string& Type() override { return std::get<2>(GetParam()); }
|
||||
bool sec_cache_is_compressed_;
|
||||
bool use_jemalloc_;
|
||||
};
|
||||
@@ -805,19 +758,20 @@ TEST_P(CompressedSecCacheTestWithCompressAndAllocatorParam, BasicTes) {
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(CompressedSecCacheTests,
|
||||
CompressedSecCacheTestWithCompressAndAllocatorParam,
|
||||
::testing::Combine(testing::Bool(), testing::Bool()));
|
||||
::testing::Combine(testing::Bool(), testing::Bool(),
|
||||
GetTestingCacheTypes()));
|
||||
|
||||
class CompressedSecondaryCacheTestWithCompressionParam
|
||||
: public CompressedSecondaryCacheTest,
|
||||
public ::testing::WithParamInterface<bool> {
|
||||
: public CompressedSecondaryCacheTestBase,
|
||||
public ::testing::WithParamInterface<std::tuple<bool, std::string>> {
|
||||
public:
|
||||
CompressedSecondaryCacheTestWithCompressionParam() {
|
||||
sec_cache_is_compressed_ = GetParam();
|
||||
sec_cache_is_compressed_ = std::get<0>(GetParam());
|
||||
}
|
||||
const std::string& Type() override { return std::get<1>(GetParam()); }
|
||||
bool sec_cache_is_compressed_;
|
||||
};
|
||||
|
||||
|
||||
TEST_P(CompressedSecondaryCacheTestWithCompressionParam, BasicTestFromString) {
|
||||
std::shared_ptr<SecondaryCache> sec_cache{nullptr};
|
||||
std::string sec_cache_uri;
|
||||
@@ -906,18 +860,92 @@ TEST_P(CompressedSecondaryCacheTestWithCompressionParam,
|
||||
IntegrationFullCapacityTest(sec_cache_is_compressed_);
|
||||
}
|
||||
|
||||
TEST_P(CompressedSecondaryCacheTestWithCompressionParam, EntryRoles) {
|
||||
CompressedSecondaryCacheOptions opts;
|
||||
opts.capacity = 2048;
|
||||
opts.num_shard_bits = 0;
|
||||
|
||||
if (sec_cache_is_compressed_) {
|
||||
if (!LZ4_Supported()) {
|
||||
ROCKSDB_GTEST_SKIP("This test requires LZ4 support.");
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
opts.compression_type = CompressionType::kNoCompression;
|
||||
}
|
||||
|
||||
// Select a random subset to include, for fast test
|
||||
Random& r = *Random::GetTLSInstance();
|
||||
CacheEntryRoleSet do_not_compress;
|
||||
for (uint32_t i = 0; i < kNumCacheEntryRoles; ++i) {
|
||||
// A few included on average, but decent chance of zero
|
||||
if (r.OneIn(5)) {
|
||||
do_not_compress.Add(static_cast<CacheEntryRole>(i));
|
||||
}
|
||||
}
|
||||
opts.do_not_compress_roles = do_not_compress;
|
||||
|
||||
std::shared_ptr<SecondaryCache> sec_cache = NewCompressedSecondaryCache(opts);
|
||||
|
||||
// Fixed seed to ensure consistent compressibility (doesn't compress)
|
||||
std::string junk(Random(301).RandomString(1000));
|
||||
|
||||
for (uint32_t i = 0; i < kNumCacheEntryRoles; ++i) {
|
||||
CacheEntryRole role = static_cast<CacheEntryRole>(i);
|
||||
|
||||
// Uniquify `junk`
|
||||
junk[0] = static_cast<char>(i);
|
||||
TestItem item{junk.data(), junk.length()};
|
||||
Slice ith_key = Slice(junk.data(), 16);
|
||||
|
||||
get_perf_context()->Reset();
|
||||
ASSERT_OK(sec_cache->Insert(ith_key, &item, GetHelper(role)));
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_insert_dummy_count, 1U);
|
||||
|
||||
ASSERT_OK(sec_cache->Insert(ith_key, &item, GetHelper(role)));
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_insert_real_count, 1U);
|
||||
|
||||
bool kept_in_sec_cache{true};
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle =
|
||||
sec_cache->Lookup(ith_key, GetHelper(role), this, true,
|
||||
/*advise_erase=*/true, kept_in_sec_cache);
|
||||
ASSERT_NE(handle, nullptr);
|
||||
|
||||
// Lookup returns the right data
|
||||
std::unique_ptr<TestItem> val =
|
||||
std::unique_ptr<TestItem>(static_cast<TestItem*>(handle->Value()));
|
||||
ASSERT_NE(val, nullptr);
|
||||
ASSERT_EQ(memcmp(val->Buf(), item.Buf(), item.Size()), 0);
|
||||
|
||||
bool compressed =
|
||||
sec_cache_is_compressed_ && !do_not_compress.Contains(role);
|
||||
if (compressed) {
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_uncompressed_bytes,
|
||||
1000);
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_compressed_bytes,
|
||||
1007);
|
||||
} else {
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_uncompressed_bytes, 0);
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_compressed_bytes, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(CompressedSecCacheTests,
|
||||
CompressedSecondaryCacheTestWithCompressionParam,
|
||||
testing::Bool());
|
||||
testing::Combine(testing::Bool(),
|
||||
GetTestingCacheTypes()));
|
||||
|
||||
class CompressedSecCacheTestWithCompressAndSplitParam
|
||||
: public CompressedSecondaryCacheTest,
|
||||
public ::testing::WithParamInterface<std::tuple<bool, bool>> {
|
||||
: public CompressedSecondaryCacheTestBase,
|
||||
public ::testing::WithParamInterface<
|
||||
std::tuple<bool, bool, std::string>> {
|
||||
public:
|
||||
CompressedSecCacheTestWithCompressAndSplitParam() {
|
||||
sec_cache_is_compressed_ = std::get<0>(GetParam());
|
||||
enable_custom_split_merge_ = std::get<1>(GetParam());
|
||||
}
|
||||
const std::string& Type() override { return std::get<2>(GetParam()); }
|
||||
bool sec_cache_is_compressed_;
|
||||
bool enable_custom_split_merge_;
|
||||
};
|
||||
@@ -928,17 +956,18 @@ TEST_P(CompressedSecCacheTestWithCompressAndSplitParam, BasicIntegrationTest) {
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(CompressedSecCacheTests,
|
||||
CompressedSecCacheTestWithCompressAndSplitParam,
|
||||
::testing::Combine(testing::Bool(), testing::Bool()));
|
||||
::testing::Combine(testing::Bool(), testing::Bool(),
|
||||
GetTestingCacheTypes()));
|
||||
|
||||
TEST_F(CompressedSecondaryCacheTest, SplitValueIntoChunksTest) {
|
||||
TEST_P(CompressedSecondaryCacheTest, SplitValueIntoChunksTest) {
|
||||
SplitValueIntoChunksTest();
|
||||
}
|
||||
|
||||
TEST_F(CompressedSecondaryCacheTest, MergeChunksIntoValueTest) {
|
||||
TEST_P(CompressedSecondaryCacheTest, MergeChunksIntoValueTest) {
|
||||
MergeChunksIntoValueTest();
|
||||
}
|
||||
|
||||
TEST_F(CompressedSecondaryCacheTest, SplictValueAndMergeChunksTest) {
|
||||
TEST_P(CompressedSecondaryCacheTest, SplictValueAndMergeChunksTest) {
|
||||
SplictValueAndMergeChunksTest();
|
||||
}
|
||||
|
||||
|
||||
Vendored
+118
-284
@@ -14,6 +14,7 @@
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
#include "cache/secondary_cache_adapter.h"
|
||||
#include "monitoring/perf_context_imp.h"
|
||||
#include "monitoring/statistics.h"
|
||||
#include "port/lang.h"
|
||||
@@ -22,14 +23,6 @@
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
namespace lru_cache {
|
||||
|
||||
namespace {
|
||||
// A distinct pointer value for marking "dummy" cache entries
|
||||
struct DummyValue {
|
||||
char val[12] = "kDummyValue";
|
||||
};
|
||||
DummyValue kDummyValue{};
|
||||
} // namespace
|
||||
|
||||
LRUHandleTable::LRUHandleTable(int max_upper_hash_bits,
|
||||
MemoryAllocator* allocator)
|
||||
: length_bits_(/* historical starting size*/ 4),
|
||||
@@ -103,7 +96,7 @@ void LRUHandleTable::Resize() {
|
||||
std::unique_ptr<LRUHandle* []> new_list {
|
||||
new LRUHandle* [size_t{1} << new_length_bits] {}
|
||||
};
|
||||
uint32_t count = 0;
|
||||
[[maybe_unused]] uint32_t count = 0;
|
||||
for (uint32_t i = 0; i < old_length; i++) {
|
||||
LRUHandle* h = list_[i];
|
||||
while (h != nullptr) {
|
||||
@@ -127,7 +120,7 @@ LRUCacheShard::LRUCacheShard(size_t capacity, bool strict_capacity_limit,
|
||||
CacheMetadataChargePolicy metadata_charge_policy,
|
||||
int max_upper_hash_bits,
|
||||
MemoryAllocator* allocator,
|
||||
SecondaryCache* secondary_cache)
|
||||
const Cache::EvictionCallback* eviction_callback)
|
||||
: CacheShardBase(metadata_charge_policy),
|
||||
capacity_(0),
|
||||
high_pri_pool_usage_(0),
|
||||
@@ -141,7 +134,7 @@ LRUCacheShard::LRUCacheShard(size_t capacity, bool strict_capacity_limit,
|
||||
usage_(0),
|
||||
lru_usage_(0),
|
||||
mutex_(use_adaptive_mutex),
|
||||
secondary_cache_(secondary_cache) {
|
||||
eviction_callback_(*eviction_callback) {
|
||||
// Make empty circular linked list.
|
||||
lru_.next = &lru_;
|
||||
lru_.prev = &lru_;
|
||||
@@ -341,16 +334,19 @@ void LRUCacheShard::EvictFromLRU(size_t charge,
|
||||
}
|
||||
}
|
||||
|
||||
void LRUCacheShard::TryInsertIntoSecondaryCache(
|
||||
autovector<LRUHandle*> evicted_handles) {
|
||||
for (auto entry : evicted_handles) {
|
||||
if (secondary_cache_ && entry->IsSecondaryCacheCompatible() &&
|
||||
!entry->IsInSecondaryCache()) {
|
||||
secondary_cache_->Insert(entry->key(), entry->value, entry->helper)
|
||||
.PermitUncheckedError();
|
||||
void LRUCacheShard::NotifyEvicted(
|
||||
const autovector<LRUHandle*>& evicted_handles) {
|
||||
MemoryAllocator* alloc = table_.GetAllocator();
|
||||
for (LRUHandle* entry : evicted_handles) {
|
||||
if (eviction_callback_ &&
|
||||
eviction_callback_(entry->key(),
|
||||
reinterpret_cast<Cache::Handle*>(entry))) {
|
||||
// Callback took ownership of obj; just free handle
|
||||
free(entry);
|
||||
} else {
|
||||
// Free the entries here outside of mutex for performance reasons.
|
||||
entry->Free(alloc);
|
||||
}
|
||||
// Free the entries here outside of mutex for performance reasons.
|
||||
entry->Free(table_.GetAllocator());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -364,7 +360,7 @@ void LRUCacheShard::SetCapacity(size_t capacity) {
|
||||
EvictFromLRU(0, &last_reference_list);
|
||||
}
|
||||
|
||||
TryInsertIntoSecondaryCache(last_reference_list);
|
||||
NotifyEvicted(last_reference_list);
|
||||
}
|
||||
|
||||
void LRUCacheShard::SetStrictCapacityLimit(bool strict_capacity_limit) {
|
||||
@@ -372,8 +368,7 @@ void LRUCacheShard::SetStrictCapacityLimit(bool strict_capacity_limit) {
|
||||
strict_capacity_limit_ = strict_capacity_limit;
|
||||
}
|
||||
|
||||
Status LRUCacheShard::InsertItem(LRUHandle* e, LRUHandle** handle,
|
||||
bool free_handle_on_fail) {
|
||||
Status LRUCacheShard::InsertItem(LRUHandle* e, LRUHandle** handle) {
|
||||
Status s = Status::OK();
|
||||
autovector<LRUHandle*> last_reference_list;
|
||||
|
||||
@@ -392,10 +387,9 @@ Status LRUCacheShard::InsertItem(LRUHandle* e, LRUHandle** handle,
|
||||
// into cache and get evicted immediately.
|
||||
last_reference_list.push_back(e);
|
||||
} else {
|
||||
if (free_handle_on_fail) {
|
||||
free(e);
|
||||
*handle = nullptr;
|
||||
}
|
||||
free(e);
|
||||
e = nullptr;
|
||||
*handle = nullptr;
|
||||
s = Status::MemoryLimit("Insert failed due to LRU cache being full.");
|
||||
}
|
||||
} else {
|
||||
@@ -427,185 +421,27 @@ Status LRUCacheShard::InsertItem(LRUHandle* e, LRUHandle** handle,
|
||||
}
|
||||
}
|
||||
|
||||
TryInsertIntoSecondaryCache(last_reference_list);
|
||||
NotifyEvicted(last_reference_list);
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
void LRUCacheShard::Promote(LRUHandle* e) {
|
||||
SecondaryCacheResultHandle* secondary_handle = e->sec_handle;
|
||||
|
||||
assert(secondary_handle->IsReady());
|
||||
// e is not thread-shared here; OK to modify "immutable" fields as well as
|
||||
// "mutable" (normally requiring mutex)
|
||||
e->SetIsPending(false);
|
||||
e->value = secondary_handle->Value();
|
||||
assert(e->total_charge == 0);
|
||||
size_t value_size = secondary_handle->Size();
|
||||
delete secondary_handle;
|
||||
|
||||
if (e->value) {
|
||||
e->CalcTotalCharge(value_size, metadata_charge_policy_);
|
||||
Status s;
|
||||
if (e->IsStandalone()) {
|
||||
assert(secondary_cache_ && secondary_cache_->SupportForceErase());
|
||||
|
||||
// Insert a dummy handle and return a standalone handle to caller.
|
||||
// Charge the standalone handle.
|
||||
autovector<LRUHandle*> last_reference_list;
|
||||
bool free_standalone_handle{false};
|
||||
{
|
||||
DMutexLock l(mutex_);
|
||||
|
||||
// Free the space following strict LRU policy until enough space
|
||||
// is freed or the lru list is empty.
|
||||
EvictFromLRU(e->total_charge, &last_reference_list);
|
||||
|
||||
if ((usage_ + e->total_charge) > capacity_ && strict_capacity_limit_) {
|
||||
free_standalone_handle = true;
|
||||
} else {
|
||||
usage_ += e->total_charge;
|
||||
}
|
||||
}
|
||||
|
||||
TryInsertIntoSecondaryCache(last_reference_list);
|
||||
if (free_standalone_handle) {
|
||||
e->Unref();
|
||||
e->Free(table_.GetAllocator());
|
||||
e = nullptr;
|
||||
} else {
|
||||
PERF_COUNTER_ADD(block_cache_standalone_handle_count, 1);
|
||||
}
|
||||
|
||||
// Insert a dummy handle into the primary cache. This dummy handle is
|
||||
// not IsSecondaryCacheCompatible().
|
||||
// FIXME? This should not overwrite an existing non-dummy entry in the
|
||||
// rare case that one exists
|
||||
Cache::Priority priority =
|
||||
e->IsHighPri() ? Cache::Priority::HIGH : Cache::Priority::LOW;
|
||||
s = Insert(e->key(), e->hash, &kDummyValue, &kNoopCacheItemHelper,
|
||||
/*charge=*/0,
|
||||
/*handle=*/nullptr, priority);
|
||||
} else {
|
||||
e->SetInCache(true);
|
||||
LRUHandle* handle = e;
|
||||
// This InsertItem() could fail if the cache is over capacity and
|
||||
// strict_capacity_limit_ is true. In such a case, we don't want
|
||||
// InsertItem() to free the handle, since the item is already in memory
|
||||
// and the caller will most likely just read it from disk if we erase it
|
||||
// here.
|
||||
s = InsertItem(e, &handle, /*free_handle_on_fail=*/false);
|
||||
if (s.ok()) {
|
||||
PERF_COUNTER_ADD(block_cache_real_handle_count, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (!s.ok()) {
|
||||
// Item is in memory, but not accounted against the cache capacity.
|
||||
// When the handle is released, the item should get deleted.
|
||||
assert(!e->InCache());
|
||||
}
|
||||
} else {
|
||||
// Secondary cache lookup failed. The caller will take care of detecting
|
||||
// this and eventually releasing e.
|
||||
assert(!e->value);
|
||||
assert(!e->InCache());
|
||||
}
|
||||
}
|
||||
|
||||
LRUHandle* LRUCacheShard::Lookup(const Slice& key, uint32_t hash,
|
||||
const Cache::CacheItemHelper* helper,
|
||||
Cache::CreateContext* create_context,
|
||||
Cache::Priority priority, bool wait,
|
||||
Statistics* stats) {
|
||||
LRUHandle* e = nullptr;
|
||||
bool found_dummy_entry{false};
|
||||
{
|
||||
DMutexLock l(mutex_);
|
||||
e = table_.Lookup(key, hash);
|
||||
if (e != nullptr) {
|
||||
assert(e->InCache());
|
||||
if (e->value == &kDummyValue) {
|
||||
// For a dummy handle, if it was retrieved from secondary cache,
|
||||
// it may still exist in secondary cache.
|
||||
// If the handle exists in secondary cache, the value should be
|
||||
// erased from sec cache and be inserted into primary cache.
|
||||
found_dummy_entry = true;
|
||||
// Let the dummy entry be overwritten
|
||||
e = nullptr;
|
||||
} else {
|
||||
if (!e->HasRefs()) {
|
||||
// The entry is in LRU since it's in hash and has no external
|
||||
// references.
|
||||
LRU_Remove(e);
|
||||
}
|
||||
e->Ref();
|
||||
e->SetHit();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If handle table lookup failed or the handle is a dummy one, allocate
|
||||
// a handle outside the mutex if we re going to lookup in the secondary cache.
|
||||
//
|
||||
// When a block is firstly Lookup from CompressedSecondaryCache, we just
|
||||
// insert a dummy block into the primary cache (charging the actual size of
|
||||
// the block) and don't erase the block from CompressedSecondaryCache. A
|
||||
// standalone handle is returned to the caller. Only if the block is hit
|
||||
// again, we erase it from CompressedSecondaryCache and add it into the
|
||||
// primary cache.
|
||||
if (!e && secondary_cache_ && helper && helper->create_cb) {
|
||||
bool is_in_sec_cache{false};
|
||||
std::unique_ptr<SecondaryCacheResultHandle> secondary_handle =
|
||||
secondary_cache_->Lookup(key, helper, create_context, wait,
|
||||
found_dummy_entry, is_in_sec_cache);
|
||||
if (secondary_handle != nullptr) {
|
||||
e = static_cast<LRUHandle*>(malloc(sizeof(LRUHandle) - 1 + key.size()));
|
||||
|
||||
e->m_flags = 0;
|
||||
e->im_flags = 0;
|
||||
e->helper = helper;
|
||||
e->key_length = key.size();
|
||||
e->hash = hash;
|
||||
e->refs = 0;
|
||||
e->next = e->prev = nullptr;
|
||||
e->SetPriority(priority);
|
||||
memcpy(e->key_data, key.data(), key.size());
|
||||
e->value = nullptr;
|
||||
e->sec_handle = secondary_handle.release();
|
||||
e->total_charge = 0;
|
||||
e->Ref();
|
||||
e->SetIsInSecondaryCache(is_in_sec_cache);
|
||||
e->SetIsStandalone(secondary_cache_->SupportForceErase() &&
|
||||
!found_dummy_entry);
|
||||
|
||||
if (wait) {
|
||||
Promote(e);
|
||||
if (e) {
|
||||
if (!e->value) {
|
||||
// The secondary cache returned a handle, but the lookup failed.
|
||||
e->Unref();
|
||||
e->Free(table_.GetAllocator());
|
||||
e = nullptr;
|
||||
} else {
|
||||
PERF_COUNTER_ADD(secondary_cache_hit_count, 1);
|
||||
RecordTick(stats, SECONDARY_CACHE_HITS);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If wait is false, we always return a handle and let the caller
|
||||
// release the handle after checking for success or failure.
|
||||
e->SetIsPending(true);
|
||||
// This may be slightly inaccurate, if the lookup eventually fails.
|
||||
// But the probability is very low.
|
||||
PERF_COUNTER_ADD(secondary_cache_hit_count, 1);
|
||||
RecordTick(stats, SECONDARY_CACHE_HITS);
|
||||
}
|
||||
} else {
|
||||
// Caller will most likely overwrite the dummy entry with an Insert
|
||||
// after this Lookup fails
|
||||
assert(e == nullptr);
|
||||
const Cache::CacheItemHelper* /*helper*/,
|
||||
Cache::CreateContext* /*create_context*/,
|
||||
Cache::Priority /*priority*/,
|
||||
Statistics* /*stats*/) {
|
||||
DMutexLock l(mutex_);
|
||||
LRUHandle* e = table_.Lookup(key, hash);
|
||||
if (e != nullptr) {
|
||||
assert(e->InCache());
|
||||
if (!e->HasRefs()) {
|
||||
// The entry is in LRU since it's in hash and has no external
|
||||
// references.
|
||||
LRU_Remove(e);
|
||||
}
|
||||
e->Ref();
|
||||
e->SetHit();
|
||||
}
|
||||
return e;
|
||||
}
|
||||
@@ -614,8 +450,6 @@ bool LRUCacheShard::Ref(LRUHandle* e) {
|
||||
DMutexLock l(mutex_);
|
||||
// To create another reference - entry must be already externally referenced.
|
||||
assert(e->HasRefs());
|
||||
// Pending handles are not for sharing
|
||||
assert(!e->IsPending());
|
||||
e->Ref();
|
||||
return true;
|
||||
}
|
||||
@@ -639,14 +473,13 @@ bool LRUCacheShard::Release(LRUHandle* e, bool /*useful*/,
|
||||
if (e == nullptr) {
|
||||
return false;
|
||||
}
|
||||
bool last_reference = false;
|
||||
// Must Wait or WaitAll first on pending handles. Otherwise, would leak
|
||||
// a secondary cache handle.
|
||||
assert(!e->IsPending());
|
||||
bool must_free;
|
||||
bool was_in_cache;
|
||||
{
|
||||
DMutexLock l(mutex_);
|
||||
last_reference = e->Unref();
|
||||
if (last_reference && e->InCache()) {
|
||||
must_free = e->Unref();
|
||||
was_in_cache = e->InCache();
|
||||
if (must_free && was_in_cache) {
|
||||
// The item is still in cache, and nobody else holds a reference to it.
|
||||
if (usage_ > capacity_ || erase_if_last_ref) {
|
||||
// The LRU list must be empty since the cache is full.
|
||||
@@ -657,29 +490,38 @@ bool LRUCacheShard::Release(LRUHandle* e, bool /*useful*/,
|
||||
} else {
|
||||
// Put the item back on the LRU list, and don't free it.
|
||||
LRU_Insert(e);
|
||||
last_reference = false;
|
||||
must_free = false;
|
||||
}
|
||||
}
|
||||
// If it was the last reference, then decrement the cache usage.
|
||||
if (last_reference) {
|
||||
// If about to be freed, then decrement the cache usage.
|
||||
if (must_free) {
|
||||
assert(usage_ >= e->total_charge);
|
||||
usage_ -= e->total_charge;
|
||||
}
|
||||
}
|
||||
|
||||
// Free the entry here outside of mutex for performance reasons.
|
||||
if (last_reference) {
|
||||
e->Free(table_.GetAllocator());
|
||||
if (must_free) {
|
||||
// Only call eviction callback if we're sure no one requested erasure
|
||||
// FIXME: disabled because of test churn
|
||||
if (false && was_in_cache && !erase_if_last_ref && eviction_callback_ &&
|
||||
eviction_callback_(e->key(), reinterpret_cast<Cache::Handle*>(e))) {
|
||||
// Callback took ownership of obj; just free handle
|
||||
free(e);
|
||||
} else {
|
||||
e->Free(table_.GetAllocator());
|
||||
}
|
||||
}
|
||||
return last_reference;
|
||||
return must_free;
|
||||
}
|
||||
|
||||
Status LRUCacheShard::Insert(const Slice& key, uint32_t hash,
|
||||
Cache::ObjectPtr value,
|
||||
const Cache::CacheItemHelper* helper,
|
||||
size_t charge, LRUHandle** handle,
|
||||
Cache::Priority priority) {
|
||||
LRUHandle* LRUCacheShard::CreateHandle(const Slice& key, uint32_t hash,
|
||||
Cache::ObjectPtr value,
|
||||
const Cache::CacheItemHelper* helper,
|
||||
size_t charge) {
|
||||
assert(helper);
|
||||
// value == nullptr is reserved for indicating failure in SecondaryCache
|
||||
assert(!(helper->IsSecondaryCacheCompatible() && value == nullptr));
|
||||
|
||||
// Allocate the memory here outside of the mutex.
|
||||
// If the cache is full, we'll have to release it.
|
||||
@@ -695,16 +537,53 @@ Status LRUCacheShard::Insert(const Slice& key, uint32_t hash,
|
||||
e->hash = hash;
|
||||
e->refs = 0;
|
||||
e->next = e->prev = nullptr;
|
||||
e->SetInCache(true);
|
||||
e->SetPriority(priority);
|
||||
memcpy(e->key_data, key.data(), key.size());
|
||||
e->CalcTotalCharge(charge, metadata_charge_policy_);
|
||||
|
||||
// value == nullptr is reserved for indicating failure for when secondary
|
||||
// cache compatible
|
||||
assert(!(e->IsSecondaryCacheCompatible() && value == nullptr));
|
||||
return e;
|
||||
}
|
||||
|
||||
return InsertItem(e, handle, /* free_handle_on_fail */ true);
|
||||
Status LRUCacheShard::Insert(const Slice& key, uint32_t hash,
|
||||
Cache::ObjectPtr value,
|
||||
const Cache::CacheItemHelper* helper,
|
||||
size_t charge, LRUHandle** handle,
|
||||
Cache::Priority priority) {
|
||||
LRUHandle* e = CreateHandle(key, hash, value, helper, charge);
|
||||
e->SetPriority(priority);
|
||||
e->SetInCache(true);
|
||||
return InsertItem(e, handle);
|
||||
}
|
||||
|
||||
LRUHandle* LRUCacheShard::CreateStandalone(const Slice& key, uint32_t hash,
|
||||
Cache::ObjectPtr value,
|
||||
const Cache::CacheItemHelper* helper,
|
||||
size_t charge,
|
||||
bool allow_uncharged) {
|
||||
LRUHandle* e = CreateHandle(key, hash, value, helper, charge);
|
||||
e->SetIsStandalone(true);
|
||||
e->Ref();
|
||||
|
||||
autovector<LRUHandle*> last_reference_list;
|
||||
|
||||
{
|
||||
DMutexLock l(mutex_);
|
||||
|
||||
EvictFromLRU(e->total_charge, &last_reference_list);
|
||||
|
||||
if (strict_capacity_limit_ && (usage_ + e->total_charge) > capacity_) {
|
||||
if (allow_uncharged) {
|
||||
e->total_charge = 0;
|
||||
} else {
|
||||
free(e);
|
||||
e = nullptr;
|
||||
}
|
||||
} else {
|
||||
usage_ += e->total_charge;
|
||||
}
|
||||
}
|
||||
|
||||
NotifyEvicted(last_reference_list);
|
||||
return e;
|
||||
}
|
||||
|
||||
void LRUCacheShard::Erase(const Slice& key, uint32_t hash) {
|
||||
@@ -733,16 +612,6 @@ void LRUCacheShard::Erase(const Slice& key, uint32_t hash) {
|
||||
}
|
||||
}
|
||||
|
||||
bool LRUCacheShard::IsReady(LRUHandle* e) {
|
||||
bool ready = true;
|
||||
if (e->IsPending()) {
|
||||
assert(secondary_cache_);
|
||||
assert(e->sec_handle);
|
||||
ready = e->sec_handle->IsReady();
|
||||
}
|
||||
return ready;
|
||||
}
|
||||
|
||||
size_t LRUCacheShard::GetUsage() const {
|
||||
DMutexLock l(mutex_);
|
||||
return usage_;
|
||||
@@ -782,26 +651,23 @@ LRUCache::LRUCache(size_t capacity, int num_shard_bits,
|
||||
double low_pri_pool_ratio,
|
||||
std::shared_ptr<MemoryAllocator> allocator,
|
||||
bool use_adaptive_mutex,
|
||||
CacheMetadataChargePolicy metadata_charge_policy,
|
||||
std::shared_ptr<SecondaryCache> _secondary_cache)
|
||||
CacheMetadataChargePolicy metadata_charge_policy)
|
||||
: ShardedCache(capacity, num_shard_bits, strict_capacity_limit,
|
||||
std::move(allocator)),
|
||||
secondary_cache_(std::move(_secondary_cache)) {
|
||||
std::move(allocator)) {
|
||||
size_t per_shard = GetPerShardCapacity();
|
||||
SecondaryCache* secondary_cache = secondary_cache_.get();
|
||||
MemoryAllocator* alloc = memory_allocator();
|
||||
const EvictionCallback* eviction_callback = &eviction_callback_;
|
||||
InitShards([=](LRUCacheShard* cs) {
|
||||
new (cs) LRUCacheShard(
|
||||
per_shard, strict_capacity_limit, high_pri_pool_ratio,
|
||||
low_pri_pool_ratio, use_adaptive_mutex, metadata_charge_policy,
|
||||
/* max_upper_hash_bits */ 32 - num_shard_bits, alloc, secondary_cache);
|
||||
new (cs) LRUCacheShard(per_shard, strict_capacity_limit,
|
||||
high_pri_pool_ratio, low_pri_pool_ratio,
|
||||
use_adaptive_mutex, metadata_charge_policy,
|
||||
/* max_upper_hash_bits */ 32 - num_shard_bits, alloc,
|
||||
eviction_callback);
|
||||
});
|
||||
}
|
||||
|
||||
Cache::ObjectPtr LRUCache::Value(Handle* handle) {
|
||||
auto h = reinterpret_cast<const LRUHandle*>(handle);
|
||||
assert(!h->IsPending() || h->value == nullptr);
|
||||
assert(h->value != &kDummyValue);
|
||||
return h->value;
|
||||
}
|
||||
|
||||
@@ -824,42 +690,6 @@ double LRUCache::GetHighPriPoolRatio() {
|
||||
return GetShard(0).GetHighPriPoolRatio();
|
||||
}
|
||||
|
||||
void LRUCache::WaitAll(std::vector<Handle*>& handles) {
|
||||
if (secondary_cache_) {
|
||||
std::vector<SecondaryCacheResultHandle*> sec_handles;
|
||||
sec_handles.reserve(handles.size());
|
||||
for (Handle* handle : handles) {
|
||||
if (!handle) {
|
||||
continue;
|
||||
}
|
||||
LRUHandle* lru_handle = reinterpret_cast<LRUHandle*>(handle);
|
||||
if (!lru_handle->IsPending()) {
|
||||
continue;
|
||||
}
|
||||
sec_handles.emplace_back(lru_handle->sec_handle);
|
||||
}
|
||||
secondary_cache_->WaitAll(sec_handles);
|
||||
for (Handle* handle : handles) {
|
||||
if (!handle) {
|
||||
continue;
|
||||
}
|
||||
LRUHandle* lru_handle = reinterpret_cast<LRUHandle*>(handle);
|
||||
if (!lru_handle->IsPending()) {
|
||||
continue;
|
||||
}
|
||||
GetShard(lru_handle->hash).Promote(lru_handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LRUCache::AppendPrintableOptions(std::string& str) const {
|
||||
ShardedCache::AppendPrintableOptions(str); // options from shard
|
||||
if (secondary_cache_) {
|
||||
str.append(" secondary_cache:\n");
|
||||
str.append(secondary_cache_->GetPrintableOptions());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace lru_cache
|
||||
|
||||
std::shared_ptr<Cache> NewLRUCache(
|
||||
@@ -887,10 +717,14 @@ std::shared_ptr<Cache> NewLRUCache(
|
||||
if (num_shard_bits < 0) {
|
||||
num_shard_bits = GetDefaultCacheShardBits(capacity);
|
||||
}
|
||||
return std::make_shared<LRUCache>(
|
||||
std::shared_ptr<Cache> cache = std::make_shared<LRUCache>(
|
||||
capacity, num_shard_bits, strict_capacity_limit, high_pri_pool_ratio,
|
||||
low_pri_pool_ratio, std::move(memory_allocator), use_adaptive_mutex,
|
||||
metadata_charge_policy, secondary_cache);
|
||||
metadata_charge_policy);
|
||||
if (secondary_cache) {
|
||||
cache = std::make_shared<CacheWithSecondaryAdapter>(cache, secondary_cache);
|
||||
}
|
||||
return cache;
|
||||
}
|
||||
|
||||
std::shared_ptr<Cache> NewLRUCache(const LRUCacheOptions& cache_opts) {
|
||||
|
||||
Vendored
+23
-71
@@ -16,7 +16,6 @@
|
||||
#include "port/likely.h"
|
||||
#include "port/malloc.h"
|
||||
#include "port/port.h"
|
||||
#include "rocksdb/secondary_cache.h"
|
||||
#include "util/autovector.h"
|
||||
#include "util/distributed_mutex.h"
|
||||
|
||||
@@ -51,12 +50,7 @@ namespace lru_cache {
|
||||
struct LRUHandle {
|
||||
Cache::ObjectPtr value;
|
||||
const Cache::CacheItemHelper* helper;
|
||||
// An entry is not added to the LRUHandleTable until the secondary cache
|
||||
// lookup is complete, so its safe to have this union.
|
||||
union {
|
||||
LRUHandle* next_hash;
|
||||
SecondaryCacheResultHandle* sec_handle;
|
||||
};
|
||||
LRUHandle* next_hash;
|
||||
LRUHandle* next;
|
||||
LRUHandle* prev;
|
||||
size_t total_charge; // TODO(opt): Only allow uint32_t?
|
||||
@@ -89,12 +83,8 @@ struct LRUHandle {
|
||||
IM_IS_HIGH_PRI = (1 << 0),
|
||||
// Whether this entry is low priority entry.
|
||||
IM_IS_LOW_PRI = (1 << 1),
|
||||
// Is the handle still being read from a lower tier.
|
||||
IM_IS_PENDING = (1 << 2),
|
||||
// Whether this handle is still in a lower tier
|
||||
IM_IS_IN_SECONDARY_CACHE = (1 << 3),
|
||||
// Marks result handles that should not be inserted into cache
|
||||
IM_IS_STANDALONE = (1 << 4),
|
||||
IM_IS_STANDALONE = (1 << 2),
|
||||
};
|
||||
|
||||
// Beginning of the key (MUST BE THE LAST FIELD IN THIS STRUCT!)
|
||||
@@ -124,11 +114,6 @@ struct LRUHandle {
|
||||
bool IsLowPri() const { return im_flags & IM_IS_LOW_PRI; }
|
||||
bool InLowPriPool() const { return m_flags & M_IN_LOW_PRI_POOL; }
|
||||
bool HasHit() const { return m_flags & M_HAS_HIT; }
|
||||
bool IsSecondaryCacheCompatible() const { return helper->size_cb != nullptr; }
|
||||
bool IsPending() const { return im_flags & IM_IS_PENDING; }
|
||||
bool IsInSecondaryCache() const {
|
||||
return im_flags & IM_IS_IN_SECONDARY_CACHE;
|
||||
}
|
||||
bool IsStandalone() const { return im_flags & IM_IS_STANDALONE; }
|
||||
|
||||
void SetInCache(bool in_cache) {
|
||||
@@ -170,22 +155,6 @@ struct LRUHandle {
|
||||
|
||||
void SetHit() { m_flags |= M_HAS_HIT; }
|
||||
|
||||
void SetIsPending(bool pending) {
|
||||
if (pending) {
|
||||
im_flags |= IM_IS_PENDING;
|
||||
} else {
|
||||
im_flags &= ~IM_IS_PENDING;
|
||||
}
|
||||
}
|
||||
|
||||
void SetIsInSecondaryCache(bool is_in_secondary_cache) {
|
||||
if (is_in_secondary_cache) {
|
||||
im_flags |= IM_IS_IN_SECONDARY_CACHE;
|
||||
} else {
|
||||
im_flags &= ~IM_IS_IN_SECONDARY_CACHE;
|
||||
}
|
||||
}
|
||||
|
||||
void SetIsStandalone(bool is_standalone) {
|
||||
if (is_standalone) {
|
||||
im_flags |= IM_IS_STANDALONE;
|
||||
@@ -196,14 +165,6 @@ struct LRUHandle {
|
||||
|
||||
void Free(MemoryAllocator* allocator) {
|
||||
assert(refs == 0);
|
||||
|
||||
if (UNLIKELY(IsPending())) {
|
||||
assert(sec_handle != nullptr);
|
||||
SecondaryCacheResultHandle* tmp_sec_handle = sec_handle;
|
||||
tmp_sec_handle->Wait();
|
||||
value = tmp_sec_handle->Value();
|
||||
delete tmp_sec_handle;
|
||||
}
|
||||
assert(helper);
|
||||
if (helper->del_cb) {
|
||||
helper->del_cb(value, allocator);
|
||||
@@ -303,12 +264,14 @@ class LRUHandleTable {
|
||||
// A single shard of sharded cache.
|
||||
class ALIGN_AS(CACHE_LINE_SIZE) LRUCacheShard final : public CacheShardBase {
|
||||
public:
|
||||
// NOTE: the eviction_callback ptr is saved, as is it assumed to be kept
|
||||
// alive in Cache.
|
||||
LRUCacheShard(size_t capacity, bool strict_capacity_limit,
|
||||
double high_pri_pool_ratio, double low_pri_pool_ratio,
|
||||
bool use_adaptive_mutex,
|
||||
CacheMetadataChargePolicy metadata_charge_policy,
|
||||
int max_upper_hash_bits, MemoryAllocator* allocator,
|
||||
SecondaryCache* secondary_cache);
|
||||
const Cache::EvictionCallback* eviction_callback);
|
||||
|
||||
public: // Type definitions expected as parameter to ShardedCache
|
||||
using HandleImpl = LRUHandle;
|
||||
@@ -339,14 +302,17 @@ class ALIGN_AS(CACHE_LINE_SIZE) LRUCacheShard final : public CacheShardBase {
|
||||
const Cache::CacheItemHelper* helper, size_t charge,
|
||||
LRUHandle** handle, Cache::Priority priority);
|
||||
|
||||
LRUHandle* CreateStandalone(const Slice& key, uint32_t hash,
|
||||
Cache::ObjectPtr obj,
|
||||
const Cache::CacheItemHelper* helper,
|
||||
size_t charge, bool allow_uncharged);
|
||||
|
||||
LRUHandle* Lookup(const Slice& key, uint32_t hash,
|
||||
const Cache::CacheItemHelper* helper,
|
||||
Cache::CreateContext* create_context,
|
||||
Cache::Priority priority, bool wait, Statistics* stats);
|
||||
Cache::Priority priority, Statistics* stats);
|
||||
|
||||
bool Release(LRUHandle* handle, bool useful, bool erase_if_last_ref);
|
||||
bool IsReady(LRUHandle* /*handle*/);
|
||||
void Wait(LRUHandle* /*handle*/) {}
|
||||
bool Ref(LRUHandle* handle);
|
||||
void Erase(const Slice& key, uint32_t hash);
|
||||
|
||||
@@ -386,20 +352,10 @@ class ALIGN_AS(CACHE_LINE_SIZE) LRUCacheShard final : public CacheShardBase {
|
||||
private:
|
||||
friend class LRUCache;
|
||||
// Insert an item into the hash table and, if handle is null, insert into
|
||||
// the LRU list. Older items are evicted as necessary. If the cache is full
|
||||
// and free_handle_on_fail is true, the item is deleted and handle is set to
|
||||
// nullptr.
|
||||
Status InsertItem(LRUHandle* item, LRUHandle** handle,
|
||||
bool free_handle_on_fail);
|
||||
// Promote an item looked up from the secondary cache to the LRU cache.
|
||||
// The item may be still in the secondary cache.
|
||||
// It is only inserted into the hash table and not the LRU list, and only
|
||||
// if the cache is not at full capacity, as is the case during Insert. The
|
||||
// caller should hold a reference on the LRUHandle. When the caller releases
|
||||
// the last reference, the item is added to the LRU list.
|
||||
// The item is promoted to the high pri or low pri pool as specified by the
|
||||
// caller in Lookup.
|
||||
void Promote(LRUHandle* e);
|
||||
// the LRU list. Older items are evicted as necessary. Frees `item` on
|
||||
// non-OK status.
|
||||
Status InsertItem(LRUHandle* item, LRUHandle** handle);
|
||||
|
||||
void LRU_Remove(LRUHandle* e);
|
||||
void LRU_Insert(LRUHandle* e);
|
||||
|
||||
@@ -413,8 +369,11 @@ class ALIGN_AS(CACHE_LINE_SIZE) LRUCacheShard final : public CacheShardBase {
|
||||
// holding the mutex_.
|
||||
void EvictFromLRU(size_t charge, autovector<LRUHandle*>* deleted);
|
||||
|
||||
// Try to insert the evicted handles into the secondary cache.
|
||||
void TryInsertIntoSecondaryCache(autovector<LRUHandle*> evicted_handles);
|
||||
void NotifyEvicted(const autovector<LRUHandle*>& evicted_handles);
|
||||
|
||||
LRUHandle* CreateHandle(const Slice& key, uint32_t hash,
|
||||
Cache::ObjectPtr value,
|
||||
const Cache::CacheItemHelper* helper, size_t charge);
|
||||
|
||||
// Initialized before use.
|
||||
size_t capacity_;
|
||||
@@ -477,8 +436,8 @@ class ALIGN_AS(CACHE_LINE_SIZE) LRUCacheShard final : public CacheShardBase {
|
||||
// don't mind mutex_ invoking the non-const actions.
|
||||
mutable DMutex mutex_;
|
||||
|
||||
// Owned by LRUCache
|
||||
SecondaryCache* secondary_cache_;
|
||||
// A reference to Cache::eviction_callback_
|
||||
const Cache::EvictionCallback& eviction_callback_;
|
||||
};
|
||||
|
||||
class LRUCache
|
||||
@@ -492,23 +451,16 @@ class LRUCache
|
||||
std::shared_ptr<MemoryAllocator> memory_allocator = nullptr,
|
||||
bool use_adaptive_mutex = kDefaultToAdaptiveMutex,
|
||||
CacheMetadataChargePolicy metadata_charge_policy =
|
||||
kDontChargeCacheMetadata,
|
||||
std::shared_ptr<SecondaryCache> secondary_cache = nullptr);
|
||||
kDontChargeCacheMetadata);
|
||||
const char* Name() const override { return "LRUCache"; }
|
||||
ObjectPtr Value(Handle* handle) override;
|
||||
size_t GetCharge(Handle* handle) const override;
|
||||
const CacheItemHelper* GetCacheItemHelper(Handle* handle) const override;
|
||||
void WaitAll(std::vector<Handle*>& handles) override;
|
||||
|
||||
// Retrieves number of elements in LRU, for unit test purpose only.
|
||||
size_t TEST_GetLRUSize();
|
||||
// Retrieves high pri pool ratio.
|
||||
double GetHighPriPoolRatio();
|
||||
|
||||
void AppendPrintableOptions(std::string& str) const override;
|
||||
|
||||
private:
|
||||
std::shared_ptr<SecondaryCache> secondary_cache_;
|
||||
};
|
||||
|
||||
} // namespace lru_cache
|
||||
|
||||
Vendored
+348
-389
File diff suppressed because it is too large.
Load diff
Vendored
+5
-2
@@ -32,8 +32,11 @@ Status FailCreate(const Slice&, Cache::CreateContext*, MemoryAllocator*,
|
||||
} // namespace
|
||||
|
||||
Status SecondaryCache::InsertSaved(const Slice& key, const Slice& saved) {
|
||||
static Cache::CacheItemHelper helper{CacheEntryRole::kMisc, &NoopDelete,
|
||||
&SliceSize, &SliceSaveTo, &FailCreate};
|
||||
static Cache::CacheItemHelper helper_no_secondary{CacheEntryRole::kMisc,
|
||||
&NoopDelete};
|
||||
static Cache::CacheItemHelper helper{
|
||||
CacheEntryRole::kMisc, &NoopDelete, &SliceSize,
|
||||
&SliceSaveTo, &FailCreate, &helper_no_secondary};
|
||||
// NOTE: depends on Insert() being synchronous, not keeping pointer `&saved`
|
||||
return Insert(key, const_cast<Slice*>(&saved), &helper);
|
||||
}
|
||||
|
||||
Vendored
+296
@@ -0,0 +1,296 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#include "cache/secondary_cache_adapter.h"
|
||||
|
||||
#include "monitoring/perf_context_imp.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
namespace {
|
||||
// A distinct pointer value for marking "dummy" cache entries
|
||||
struct Dummy {
|
||||
char val[7] = "kDummy";
|
||||
};
|
||||
const Dummy kDummy{};
|
||||
Cache::ObjectPtr const kDummyObj = const_cast<Dummy*>(&kDummy);
|
||||
} // namespace
|
||||
|
||||
CacheWithSecondaryAdapter::CacheWithSecondaryAdapter(
|
||||
std::shared_ptr<Cache> target,
|
||||
std::shared_ptr<SecondaryCache> secondary_cache)
|
||||
: CacheWrapper(std::move(target)),
|
||||
secondary_cache_(std::move(secondary_cache)) {
|
||||
target_->SetEvictionCallback([this](const Slice& key, Handle* handle) {
|
||||
return EvictionHandler(key, handle);
|
||||
});
|
||||
}
|
||||
|
||||
CacheWithSecondaryAdapter::~CacheWithSecondaryAdapter() {
|
||||
// `*this` will be destroyed before `*target_`, so we have to prevent
|
||||
// use after free
|
||||
target_->SetEvictionCallback({});
|
||||
}
|
||||
|
||||
bool CacheWithSecondaryAdapter::EvictionHandler(const Slice& key,
|
||||
Handle* handle) {
|
||||
auto helper = GetCacheItemHelper(handle);
|
||||
if (helper->IsSecondaryCacheCompatible()) {
|
||||
auto obj = target_->Value(handle);
|
||||
// Ignore dummy entry
|
||||
if (obj != kDummyObj) {
|
||||
// Spill into secondary cache.
|
||||
secondary_cache_->Insert(key, obj, helper).PermitUncheckedError();
|
||||
}
|
||||
}
|
||||
// Never takes ownership of obj
|
||||
return false;
|
||||
}
|
||||
|
||||
bool CacheWithSecondaryAdapter::ProcessDummyResult(Cache::Handle** handle,
|
||||
bool erase) {
|
||||
if (*handle && target_->Value(*handle) == kDummyObj) {
|
||||
target_->Release(*handle, erase);
|
||||
*handle = nullptr;
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void CacheWithSecondaryAdapter::CleanupCacheObject(
|
||||
ObjectPtr obj, const CacheItemHelper* helper) {
|
||||
if (helper->del_cb) {
|
||||
helper->del_cb(obj, memory_allocator());
|
||||
}
|
||||
}
|
||||
|
||||
Cache::Handle* CacheWithSecondaryAdapter::Promote(
|
||||
std::unique_ptr<SecondaryCacheResultHandle>&& secondary_handle,
|
||||
const Slice& key, const CacheItemHelper* helper, Priority priority,
|
||||
Statistics* stats, bool found_dummy_entry, bool kept_in_sec_cache) {
|
||||
assert(secondary_handle->IsReady());
|
||||
|
||||
ObjectPtr obj = secondary_handle->Value();
|
||||
if (!obj) {
|
||||
// Nothing found.
|
||||
return nullptr;
|
||||
}
|
||||
// Found something.
|
||||
switch (helper->role) {
|
||||
case CacheEntryRole::kFilterBlock:
|
||||
RecordTick(stats, SECONDARY_CACHE_FILTER_HITS);
|
||||
break;
|
||||
case CacheEntryRole::kIndexBlock:
|
||||
RecordTick(stats, SECONDARY_CACHE_INDEX_HITS);
|
||||
break;
|
||||
case CacheEntryRole::kDataBlock:
|
||||
RecordTick(stats, SECONDARY_CACHE_DATA_HITS);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
PERF_COUNTER_ADD(secondary_cache_hit_count, 1);
|
||||
RecordTick(stats, SECONDARY_CACHE_HITS);
|
||||
|
||||
// FIXME? using charge=Size() is suspicious but inherited from older
|
||||
// implementation
|
||||
size_t charge = secondary_handle->Size();
|
||||
Handle* result = nullptr;
|
||||
// Insert into primary cache, possibly as a standalone+dummy entries.
|
||||
if (secondary_cache_->SupportForceErase() && !found_dummy_entry) {
|
||||
// Create standalone and insert dummy
|
||||
// Allow standalone to be created even if cache is full, to avoid
|
||||
// reading the entry from storage.
|
||||
result =
|
||||
CreateStandalone(key, obj, helper, charge, /*allow_uncharged*/ true);
|
||||
assert(result);
|
||||
PERF_COUNTER_ADD(block_cache_standalone_handle_count, 1);
|
||||
|
||||
// Insert dummy to record recent use
|
||||
// TODO: try to avoid case where inserting this dummy could overwrite a
|
||||
// regular entry
|
||||
Status s = Insert(key, kDummyObj, &kNoopCacheItemHelper, /*charge=*/0,
|
||||
/*handle=*/nullptr, priority);
|
||||
s.PermitUncheckedError();
|
||||
// Nothing to do or clean up on dummy insertion failure
|
||||
} else {
|
||||
// Insert regular entry into primary cache.
|
||||
// Don't allow it to spill into secondary cache again if it was kept there.
|
||||
Status s = Insert(
|
||||
key, obj, kept_in_sec_cache ? helper->without_secondary_compat : helper,
|
||||
charge, &result, priority);
|
||||
if (s.ok()) {
|
||||
assert(result);
|
||||
PERF_COUNTER_ADD(block_cache_real_handle_count, 1);
|
||||
} else {
|
||||
// Create standalone result instead, even if cache is full, to avoid
|
||||
// reading the entry from storage.
|
||||
result =
|
||||
CreateStandalone(key, obj, helper, charge, /*allow_uncharged*/ true);
|
||||
assert(result);
|
||||
PERF_COUNTER_ADD(block_cache_standalone_handle_count, 1);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Cache::Handle* CacheWithSecondaryAdapter::Lookup(const Slice& key,
|
||||
const CacheItemHelper* helper,
|
||||
CreateContext* create_context,
|
||||
Priority priority,
|
||||
Statistics* stats) {
|
||||
// NOTE: we could just StartAsyncLookup() and Wait(), but this should be a bit
|
||||
// more efficient
|
||||
Handle* result =
|
||||
target_->Lookup(key, helper, create_context, priority, stats);
|
||||
bool secondary_compatible = helper && helper->IsSecondaryCacheCompatible();
|
||||
bool found_dummy_entry =
|
||||
ProcessDummyResult(&result, /*erase=*/secondary_compatible);
|
||||
if (!result && secondary_compatible) {
|
||||
// Try our secondary cache
|
||||
bool kept_in_sec_cache = false;
|
||||
std::unique_ptr<SecondaryCacheResultHandle> secondary_handle =
|
||||
secondary_cache_->Lookup(key, helper, create_context, /*wait*/ true,
|
||||
found_dummy_entry, /*out*/ kept_in_sec_cache);
|
||||
if (secondary_handle) {
|
||||
result = Promote(std::move(secondary_handle), key, helper, priority,
|
||||
stats, found_dummy_entry, kept_in_sec_cache);
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
Cache::ObjectPtr CacheWithSecondaryAdapter::Value(Handle* handle) {
|
||||
ObjectPtr v = target_->Value(handle);
|
||||
// TODO with stacked secondaries: might fail in EvictionHandler
|
||||
assert(v != kDummyObj);
|
||||
return v;
|
||||
}
|
||||
|
||||
void CacheWithSecondaryAdapter::StartAsyncLookupOnMySecondary(
|
||||
AsyncLookupHandle& async_handle) {
|
||||
assert(!async_handle.IsPending());
|
||||
assert(async_handle.result_handle == nullptr);
|
||||
|
||||
std::unique_ptr<SecondaryCacheResultHandle> secondary_handle =
|
||||
secondary_cache_->Lookup(async_handle.key, async_handle.helper,
|
||||
async_handle.create_context, /*wait*/ false,
|
||||
async_handle.found_dummy_entry,
|
||||
/*out*/ async_handle.kept_in_sec_cache);
|
||||
if (secondary_handle) {
|
||||
// TODO with stacked secondaries: Check & process if already ready?
|
||||
async_handle.pending_handle = secondary_handle.release();
|
||||
async_handle.pending_cache = secondary_cache_.get();
|
||||
}
|
||||
}
|
||||
|
||||
void CacheWithSecondaryAdapter::StartAsyncLookup(
|
||||
AsyncLookupHandle& async_handle) {
|
||||
target_->StartAsyncLookup(async_handle);
|
||||
if (!async_handle.IsPending()) {
|
||||
bool secondary_compatible =
|
||||
async_handle.helper &&
|
||||
async_handle.helper->IsSecondaryCacheCompatible();
|
||||
async_handle.found_dummy_entry |= ProcessDummyResult(
|
||||
&async_handle.result_handle, /*erase=*/secondary_compatible);
|
||||
|
||||
if (async_handle.Result() == nullptr && secondary_compatible) {
|
||||
// Not found and not pending on another secondary cache
|
||||
StartAsyncLookupOnMySecondary(async_handle);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CacheWithSecondaryAdapter::WaitAll(AsyncLookupHandle* async_handles,
|
||||
size_t count) {
|
||||
if (count == 0) {
|
||||
// Nothing to do
|
||||
return;
|
||||
}
|
||||
// Requests that are pending on *my* secondary cache, at the start of this
|
||||
// function
|
||||
std::vector<AsyncLookupHandle*> my_pending;
|
||||
// Requests that are pending on an "inner" secondary cache (managed somewhere
|
||||
// under target_), as of the start of this function
|
||||
std::vector<AsyncLookupHandle*> inner_pending;
|
||||
|
||||
// Initial accounting of pending handles, excluding those already handled
|
||||
// by "outer" secondary caches. (See cur->pending_cache = nullptr.)
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
AsyncLookupHandle* cur = async_handles + i;
|
||||
if (cur->pending_cache) {
|
||||
assert(cur->IsPending());
|
||||
assert(cur->helper);
|
||||
assert(cur->helper->IsSecondaryCacheCompatible());
|
||||
if (cur->pending_cache == secondary_cache_.get()) {
|
||||
my_pending.push_back(cur);
|
||||
// Mark as "to be handled by this caller"
|
||||
cur->pending_cache = nullptr;
|
||||
} else {
|
||||
// Remember as potentially needing a lookup in my secondary
|
||||
inner_pending.push_back(cur);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait on inner-most cache lookups first
|
||||
// TODO with stacked secondaries: because we are not using proper
|
||||
// async/await constructs here yet, there is a false synchronization point
|
||||
// here where all the results at one level are needed before initiating
|
||||
// any lookups at the next level. Probably not a big deal, but worth noting.
|
||||
if (!inner_pending.empty()) {
|
||||
target_->WaitAll(async_handles, count);
|
||||
}
|
||||
|
||||
// For those that failed to find something, convert to lookup in my
|
||||
// secondary cache.
|
||||
for (AsyncLookupHandle* cur : inner_pending) {
|
||||
if (cur->Result() == nullptr) {
|
||||
// Not found, try my secondary
|
||||
StartAsyncLookupOnMySecondary(*cur);
|
||||
if (cur->IsPending()) {
|
||||
assert(cur->pending_cache == secondary_cache_.get());
|
||||
my_pending.push_back(cur);
|
||||
// Mark as "to be handled by this caller"
|
||||
cur->pending_cache = nullptr;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait on all lookups on my secondary cache
|
||||
{
|
||||
std::vector<SecondaryCacheResultHandle*> my_secondary_handles;
|
||||
for (AsyncLookupHandle* cur : my_pending) {
|
||||
my_secondary_handles.push_back(cur->pending_handle);
|
||||
}
|
||||
secondary_cache_->WaitAll(my_secondary_handles);
|
||||
}
|
||||
|
||||
// Process results
|
||||
for (AsyncLookupHandle* cur : my_pending) {
|
||||
std::unique_ptr<SecondaryCacheResultHandle> secondary_handle(
|
||||
cur->pending_handle);
|
||||
cur->pending_handle = nullptr;
|
||||
cur->result_handle = Promote(
|
||||
std::move(secondary_handle), cur->key, cur->helper, cur->priority,
|
||||
cur->stats, cur->found_dummy_entry, cur->kept_in_sec_cache);
|
||||
assert(cur->pending_cache == nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
std::string CacheWithSecondaryAdapter::GetPrintableOptions() const {
|
||||
std::string str = target_->GetPrintableOptions();
|
||||
str.append(" secondary_cache:\n");
|
||||
str.append(secondary_cache_->GetPrintableOptions());
|
||||
return str;
|
||||
}
|
||||
|
||||
const char* CacheWithSecondaryAdapter::Name() const {
|
||||
// To the user, at least for now, configure the underlying cache with
|
||||
// a secondary cache. So we pretend to be that cache
|
||||
return target_->Name();
|
||||
}
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
Vendored
+52
@@ -0,0 +1,52 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "rocksdb/secondary_cache.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
class CacheWithSecondaryAdapter : public CacheWrapper {
|
||||
public:
|
||||
explicit CacheWithSecondaryAdapter(
|
||||
std::shared_ptr<Cache> target,
|
||||
std::shared_ptr<SecondaryCache> secondary_cache);
|
||||
|
||||
~CacheWithSecondaryAdapter() override;
|
||||
|
||||
Handle* Lookup(const Slice& key, const CacheItemHelper* helper,
|
||||
CreateContext* create_context,
|
||||
Priority priority = Priority::LOW,
|
||||
Statistics* stats = nullptr) override;
|
||||
|
||||
ObjectPtr Value(Handle* handle) override;
|
||||
|
||||
void StartAsyncLookup(AsyncLookupHandle& async_handle) override;
|
||||
|
||||
void WaitAll(AsyncLookupHandle* async_handles, size_t count) override;
|
||||
|
||||
std::string GetPrintableOptions() const override;
|
||||
|
||||
const char* Name() const override;
|
||||
|
||||
private:
|
||||
bool EvictionHandler(const Slice& key, Handle* handle);
|
||||
|
||||
void StartAsyncLookupOnMySecondary(AsyncLookupHandle& async_handle);
|
||||
|
||||
Handle* Promote(
|
||||
std::unique_ptr<SecondaryCacheResultHandle>&& secondary_handle,
|
||||
const Slice& key, const CacheItemHelper* helper, Priority priority,
|
||||
Statistics* stats, bool found_dummy_entry, bool kept_in_sec_cache);
|
||||
|
||||
bool ProcessDummyResult(Cache::Handle** handle, bool erase);
|
||||
|
||||
void CleanupCacheObject(ObjectPtr obj, const CacheItemHelper* helper);
|
||||
|
||||
std::shared_ptr<SecondaryCache> secondary_cache_;
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
Vendored
+23
-20
@@ -15,7 +15,7 @@
|
||||
|
||||
#include "port/lang.h"
|
||||
#include "port/port.h"
|
||||
#include "rocksdb/cache.h"
|
||||
#include "rocksdb/advanced_cache.h"
|
||||
#include "util/hash.h"
|
||||
#include "util/mutexlock.h"
|
||||
|
||||
@@ -51,15 +51,17 @@ class CacheShardBase {
|
||||
};
|
||||
Status Insert(const Slice& key, HashCref hash, Cache::ObjectPtr value,
|
||||
const Cache::CacheItemHelper* helper, size_t charge,
|
||||
HandleImpl** handle, Cache::Priority priority) = 0;
|
||||
HandleImpl** handle, Cache::Priority priority,
|
||||
bool standalone) = 0;
|
||||
Handle* CreateStandalone(const Slice& key, HashCref hash, ObjectPtr obj,
|
||||
const CacheItemHelper* helper,
|
||||
size_t charge, bool allow_uncharged) = 0;
|
||||
HandleImpl* Lookup(const Slice& key, HashCref hash,
|
||||
const Cache::CacheItemHelper* helper,
|
||||
Cache::CreateContext* create_context,
|
||||
Cache::Priority priority, bool wait,
|
||||
Cache::Priority priority,
|
||||
Statistics* stats) = 0;
|
||||
bool Release(HandleImpl* handle, bool useful, bool erase_if_last_ref) = 0;
|
||||
bool IsReady(HandleImpl* handle) = 0;
|
||||
void Wait(HandleImpl* handle) = 0;
|
||||
bool Ref(HandleImpl* handle) = 0;
|
||||
void Erase(const Slice& key, HashCref hash) = 0;
|
||||
void SetCapacity(size_t capacity) = 0;
|
||||
@@ -169,24 +171,33 @@ class ShardedCache : public ShardedCacheBase {
|
||||
[s_c_l](CacheShard* cs) { cs->SetStrictCapacityLimit(s_c_l); });
|
||||
}
|
||||
|
||||
Status Insert(const Slice& key, ObjectPtr value,
|
||||
const CacheItemHelper* helper, size_t charge,
|
||||
Handle** handle = nullptr,
|
||||
Status Insert(const Slice& key, ObjectPtr obj, const CacheItemHelper* helper,
|
||||
size_t charge, Handle** handle = nullptr,
|
||||
Priority priority = Priority::LOW) override {
|
||||
assert(helper);
|
||||
HashVal hash = CacheShard::ComputeHash(key);
|
||||
auto h_out = reinterpret_cast<HandleImpl**>(handle);
|
||||
return GetShard(hash).Insert(key, hash, value, helper, charge, h_out,
|
||||
return GetShard(hash).Insert(key, hash, obj, helper, charge, h_out,
|
||||
priority);
|
||||
}
|
||||
|
||||
Handle* CreateStandalone(const Slice& key, ObjectPtr obj,
|
||||
const CacheItemHelper* helper, size_t charge,
|
||||
bool allow_uncharged) override {
|
||||
assert(helper);
|
||||
HashVal hash = CacheShard::ComputeHash(key);
|
||||
HandleImpl* result = GetShard(hash).CreateStandalone(
|
||||
key, hash, obj, helper, charge, allow_uncharged);
|
||||
return reinterpret_cast<Handle*>(result);
|
||||
}
|
||||
|
||||
Handle* Lookup(const Slice& key, const CacheItemHelper* helper = nullptr,
|
||||
CreateContext* create_context = nullptr,
|
||||
Priority priority = Priority::LOW, bool wait = true,
|
||||
Priority priority = Priority::LOW,
|
||||
Statistics* stats = nullptr) override {
|
||||
HashVal hash = CacheShard::ComputeHash(key);
|
||||
HandleImpl* result = GetShard(hash).Lookup(
|
||||
key, hash, helper, create_context, priority, wait, stats);
|
||||
HandleImpl* result = GetShard(hash).Lookup(key, hash, helper,
|
||||
create_context, priority, stats);
|
||||
return reinterpret_cast<Handle*>(result);
|
||||
}
|
||||
|
||||
@@ -200,14 +211,6 @@ class ShardedCache : public ShardedCacheBase {
|
||||
auto h = reinterpret_cast<HandleImpl*>(handle);
|
||||
return GetShard(h->GetHash()).Release(h, useful, erase_if_last_ref);
|
||||
}
|
||||
bool IsReady(Handle* handle) override {
|
||||
auto h = reinterpret_cast<HandleImpl*>(handle);
|
||||
return GetShard(h->GetHash()).IsReady(h);
|
||||
}
|
||||
void Wait(Handle* handle) override {
|
||||
auto h = reinterpret_cast<HandleImpl*>(handle);
|
||||
GetShard(h->GetHash()).Wait(h);
|
||||
}
|
||||
bool Ref(Handle* handle) override {
|
||||
auto h = reinterpret_cast<HandleImpl*>(handle);
|
||||
return GetShard(h->GetHash()).Ref(h);
|
||||
|
||||
Vendored
+57
-21
@@ -29,8 +29,8 @@
|
||||
#include <type_traits>
|
||||
|
||||
#include "cache/cache_helpers.h"
|
||||
#include "rocksdb/advanced_cache.h"
|
||||
#include "rocksdb/advanced_options.h"
|
||||
#include "rocksdb/cache.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
@@ -83,11 +83,14 @@ class PlaceholderCacheInterface : public BaseCacheInterface<CachePtr> {
|
||||
using BaseCacheInterface<CachePtr>::BaseCacheInterface;
|
||||
|
||||
inline Status Insert(const Slice& key, size_t charge, Handle** handle) {
|
||||
return this->cache_->Insert(key, /*value=*/nullptr, &kHelper, charge,
|
||||
return this->cache_->Insert(key, /*value=*/nullptr, GetHelper(), charge,
|
||||
handle);
|
||||
}
|
||||
|
||||
static constexpr Cache::CacheItemHelper kHelper{kRole};
|
||||
static const Cache::CacheItemHelper* GetHelper() {
|
||||
static const Cache::CacheItemHelper kHelper{kRole};
|
||||
return &kHelper;
|
||||
}
|
||||
};
|
||||
|
||||
template <CacheEntryRole kRole>
|
||||
@@ -128,8 +131,11 @@ class BasicTypedCacheHelperFns {
|
||||
template <class TValue, CacheEntryRole kRole>
|
||||
class BasicTypedCacheHelper : public BasicTypedCacheHelperFns<TValue> {
|
||||
public:
|
||||
static constexpr Cache::CacheItemHelper kBasicHelper{
|
||||
kRole, &BasicTypedCacheHelper::Delete};
|
||||
static const Cache::CacheItemHelper* GetBasicHelper() {
|
||||
static const Cache::CacheItemHelper kHelper{kRole,
|
||||
&BasicTypedCacheHelper::Delete};
|
||||
return &kHelper;
|
||||
}
|
||||
};
|
||||
|
||||
// BasicTypedCacheInterface - Used for primary cache storage of objects of
|
||||
@@ -144,9 +150,14 @@ class BasicTypedCacheInterface : public BaseCacheInterface<CachePtr>,
|
||||
CACHE_TYPE_DEFS();
|
||||
using typename BasicTypedCacheHelperFns<TValue>::TValuePtr;
|
||||
struct TypedHandle : public Handle {};
|
||||
using BasicTypedCacheHelper<TValue, kRole>::kBasicHelper;
|
||||
using BasicTypedCacheHelper<TValue, kRole>::GetBasicHelper;
|
||||
// ctor
|
||||
using BaseCacheInterface<CachePtr>::BaseCacheInterface;
|
||||
struct TypedAsyncLookupHandle : public Cache::AsyncLookupHandle {
|
||||
TypedHandle* Result() {
|
||||
return reinterpret_cast<TypedHandle*>(Cache::AsyncLookupHandle::Result());
|
||||
}
|
||||
};
|
||||
|
||||
inline Status Insert(const Slice& key, TValuePtr value, size_t charge,
|
||||
TypedHandle** handle = nullptr,
|
||||
@@ -154,7 +165,7 @@ class BasicTypedCacheInterface : public BaseCacheInterface<CachePtr>,
|
||||
auto untyped_handle = reinterpret_cast<Handle**>(handle);
|
||||
return this->cache_->Insert(
|
||||
key, BasicTypedCacheHelperFns<TValue>::UpCastValue(value),
|
||||
&kBasicHelper, charge, untyped_handle, priority);
|
||||
GetBasicHelper(), charge, untyped_handle, priority);
|
||||
}
|
||||
|
||||
inline TypedHandle* Lookup(const Slice& key, Statistics* stats = nullptr) {
|
||||
@@ -162,6 +173,11 @@ class BasicTypedCacheInterface : public BaseCacheInterface<CachePtr>,
|
||||
this->cache_->BasicLookup(key, stats));
|
||||
}
|
||||
|
||||
inline void StartAsyncLookup(TypedAsyncLookupHandle& async_handle) {
|
||||
assert(async_handle.helper == nullptr);
|
||||
this->cache_->StartAsyncLookup(async_handle);
|
||||
}
|
||||
|
||||
inline CacheHandleGuard<TValue> Guard(TypedHandle* handle) {
|
||||
if (handle) {
|
||||
return CacheHandleGuard<TValue>(&*this->cache_, handle);
|
||||
@@ -239,9 +255,16 @@ template <class TValue, class TCreateContext, CacheEntryRole kRole>
|
||||
class FullTypedCacheHelper
|
||||
: public FullTypedCacheHelperFns<TValue, TCreateContext> {
|
||||
public:
|
||||
static constexpr Cache::CacheItemHelper kFullHelper{
|
||||
kRole, &FullTypedCacheHelper::Delete, &FullTypedCacheHelper::Size,
|
||||
&FullTypedCacheHelper::SaveTo, &FullTypedCacheHelper::Create};
|
||||
static const Cache::CacheItemHelper* GetFullHelper() {
|
||||
static const Cache::CacheItemHelper kHelper{
|
||||
kRole,
|
||||
&FullTypedCacheHelper::Delete,
|
||||
&FullTypedCacheHelper::Size,
|
||||
&FullTypedCacheHelper::SaveTo,
|
||||
&FullTypedCacheHelper::Create,
|
||||
BasicTypedCacheHelper<TValue, kRole>::GetBasicHelper()};
|
||||
return &kHelper;
|
||||
}
|
||||
};
|
||||
|
||||
// FullTypedCacheHelper - Used for secondary cache compatible storage of
|
||||
@@ -262,9 +285,11 @@ class FullTypedCacheInterface
|
||||
public:
|
||||
CACHE_TYPE_DEFS();
|
||||
using typename BasicTypedCacheInterface<TValue, kRole, CachePtr>::TypedHandle;
|
||||
using typename BasicTypedCacheInterface<TValue, kRole,
|
||||
CachePtr>::TypedAsyncLookupHandle;
|
||||
using typename BasicTypedCacheHelperFns<TValue>::TValuePtr;
|
||||
using BasicTypedCacheHelper<TValue, kRole>::kBasicHelper;
|
||||
using FullTypedCacheHelper<TValue, TCreateContext, kRole>::kFullHelper;
|
||||
using BasicTypedCacheHelper<TValue, kRole>::GetBasicHelper;
|
||||
using FullTypedCacheHelper<TValue, TCreateContext, kRole>::GetFullHelper;
|
||||
using BasicTypedCacheHelperFns<TValue>::UpCastValue;
|
||||
using BasicTypedCacheHelperFns<TValue>::DownCastValue;
|
||||
// ctor
|
||||
@@ -279,8 +304,8 @@ class FullTypedCacheInterface
|
||||
CacheTier lowest_used_cache_tier = CacheTier::kNonVolatileBlockTier) {
|
||||
auto untyped_handle = reinterpret_cast<Handle**>(handle);
|
||||
auto helper = lowest_used_cache_tier == CacheTier::kNonVolatileBlockTier
|
||||
? &kFullHelper
|
||||
: &kBasicHelper;
|
||||
? GetFullHelper()
|
||||
: GetBasicHelper();
|
||||
return this->cache_->Insert(key, UpCastValue(value), helper, charge,
|
||||
untyped_handle, priority);
|
||||
}
|
||||
@@ -294,9 +319,9 @@ class FullTypedCacheInterface
|
||||
size_t* out_charge = nullptr) {
|
||||
ObjectPtr value;
|
||||
size_t charge;
|
||||
Status st = kFullHelper.create_cb(data, create_context,
|
||||
this->cache_->memory_allocator(), &value,
|
||||
&charge);
|
||||
Status st = GetFullHelper()->create_cb(data, create_context,
|
||||
this->cache_->memory_allocator(),
|
||||
&value, &charge);
|
||||
if (out_charge) {
|
||||
*out_charge = charge;
|
||||
}
|
||||
@@ -304,7 +329,7 @@ class FullTypedCacheInterface
|
||||
st = InsertFull(key, DownCastValue(value), charge, nullptr /*handle*/,
|
||||
priority, lowest_used_cache_tier);
|
||||
} else {
|
||||
kFullHelper.del_cb(value, this->cache_->memory_allocator());
|
||||
GetFullHelper()->del_cb(value, this->cache_->memory_allocator());
|
||||
}
|
||||
return st;
|
||||
}
|
||||
@@ -313,17 +338,28 @@ class FullTypedCacheInterface
|
||||
// (Basic Lookup() also inherited.)
|
||||
inline TypedHandle* LookupFull(
|
||||
const Slice& key, TCreateContext* create_context = nullptr,
|
||||
Priority priority = Priority::LOW, bool wait = true,
|
||||
Statistics* stats = nullptr,
|
||||
Priority priority = Priority::LOW, Statistics* stats = nullptr,
|
||||
CacheTier lowest_used_cache_tier = CacheTier::kNonVolatileBlockTier) {
|
||||
if (lowest_used_cache_tier == CacheTier::kNonVolatileBlockTier) {
|
||||
return reinterpret_cast<TypedHandle*>(this->cache_->Lookup(
|
||||
key, &kFullHelper, create_context, priority, wait, stats));
|
||||
key, GetFullHelper(), create_context, priority, stats));
|
||||
} else {
|
||||
return BasicTypedCacheInterface<TValue, kRole, CachePtr>::Lookup(key,
|
||||
stats);
|
||||
}
|
||||
}
|
||||
|
||||
inline void StartAsyncLookupFull(
|
||||
TypedAsyncLookupHandle& async_handle,
|
||||
CacheTier lowest_used_cache_tier = CacheTier::kNonVolatileBlockTier) {
|
||||
if (lowest_used_cache_tier == CacheTier::kNonVolatileBlockTier) {
|
||||
async_handle.helper = GetFullHelper();
|
||||
this->cache_->StartAsyncLookup(async_handle);
|
||||
} else {
|
||||
BasicTypedCacheInterface<TValue, kRole, CachePtr>::StartAsyncLookup(
|
||||
async_handle);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// FullTypedSharedCacheInterface - Like FullTypedCacheInterface but with a
|
||||
|
||||
@@ -12,7 +12,7 @@ fi
|
||||
ROOT=".."
|
||||
# Fetch right version of gcov
|
||||
if [ -d /mnt/gvfs/third-party -a -z "$CXX" ]; then
|
||||
source $ROOT/build_tools/fbcode_config_platform009.sh
|
||||
source $ROOT/build_tools/fbcode_config_platform010.sh
|
||||
GCOV=$GCC_BASE/bin/gcov
|
||||
else
|
||||
GCOV=$(which gcov)
|
||||
|
||||
@@ -47,6 +47,9 @@ void ArenaWrappedDBIter::Init(
|
||||
read_options_ = read_options;
|
||||
allow_refresh_ = allow_refresh;
|
||||
memtable_range_tombstone_iter_ = nullptr;
|
||||
if (!env->GetFileSystem()->use_async_io()) {
|
||||
read_options_.async_io = false;
|
||||
}
|
||||
}
|
||||
|
||||
Status ArenaWrappedDBIter::Refresh() {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
#include <memory>
|
||||
|
||||
#include "memory/memory_allocator.h"
|
||||
#include "rocksdb/cache.h"
|
||||
#include "rocksdb/advanced_cache.h"
|
||||
#include "rocksdb/rocksdb_namespace.h"
|
||||
#include "rocksdb/slice.h"
|
||||
#include "rocksdb/status.h"
|
||||
|
||||
@@ -123,6 +123,10 @@ class BlobCountingIterator : public InternalIterator {
|
||||
return iter_->GetProperty(prop_name, prop);
|
||||
}
|
||||
|
||||
bool IsDeleteRangeSentinelKey() const override {
|
||||
return iter_->IsDeleteRangeSentinelKey();
|
||||
}
|
||||
|
||||
private:
|
||||
void UpdateAndCountBlobIfNeeded() {
|
||||
assert(!iter_->Valid() || iter_->status().ok());
|
||||
|
||||
@@ -104,9 +104,9 @@ Status BlobSource::PutBlobIntoCache(
|
||||
}
|
||||
|
||||
BlobSource::TypedHandle* BlobSource::GetEntryFromCache(const Slice& key) const {
|
||||
return blob_cache_.LookupFull(
|
||||
key, nullptr /* context */, Cache::Priority::BOTTOM,
|
||||
true /* wait_for_cache */, statistics_, lowest_used_cache_tier_);
|
||||
return blob_cache_.LookupFull(key, nullptr /* context */,
|
||||
Cache::Priority::BOTTOM, statistics_,
|
||||
lowest_used_cache_tier_);
|
||||
}
|
||||
|
||||
void BlobSource::PinCachedBlob(CacheHandleGuard<BlobContents>* cached_blob,
|
||||
|
||||
+19
-25
@@ -1214,12 +1214,12 @@ TEST_F(BlobSecondaryCacheTest, GetBlobsFromSecondaryCache) {
|
||||
ASSERT_EQ(handle0, nullptr);
|
||||
|
||||
// key0's item should be in the secondary cache.
|
||||
bool is_in_sec_cache = false;
|
||||
bool kept_in_sec_cache = false;
|
||||
auto sec_handle0 = secondary_cache->Lookup(
|
||||
key0, &BlobSource::SharedCacheInterface::kFullHelper,
|
||||
key0, BlobSource::SharedCacheInterface::GetFullHelper(),
|
||||
/*context*/ nullptr, true,
|
||||
/*advise_erase=*/true, is_in_sec_cache);
|
||||
ASSERT_FALSE(is_in_sec_cache);
|
||||
/*advise_erase=*/true, kept_in_sec_cache);
|
||||
ASSERT_FALSE(kept_in_sec_cache);
|
||||
ASSERT_NE(sec_handle0, nullptr);
|
||||
ASSERT_TRUE(sec_handle0->IsReady());
|
||||
auto value = static_cast<BlobContents*>(sec_handle0->Value());
|
||||
@@ -1242,12 +1242,12 @@ TEST_F(BlobSecondaryCacheTest, GetBlobsFromSecondaryCache) {
|
||||
ASSERT_NE(handle1, nullptr);
|
||||
blob_cache->Release(handle1);
|
||||
|
||||
bool is_in_sec_cache = false;
|
||||
bool kept_in_sec_cache = false;
|
||||
auto sec_handle1 = secondary_cache->Lookup(
|
||||
key1, &BlobSource::SharedCacheInterface::kFullHelper,
|
||||
key1, BlobSource::SharedCacheInterface::GetFullHelper(),
|
||||
/*context*/ nullptr, true,
|
||||
/*advise_erase=*/true, is_in_sec_cache);
|
||||
ASSERT_FALSE(is_in_sec_cache);
|
||||
/*advise_erase=*/true, kept_in_sec_cache);
|
||||
ASSERT_FALSE(kept_in_sec_cache);
|
||||
ASSERT_EQ(sec_handle1, nullptr);
|
||||
|
||||
ASSERT_TRUE(blob_source.TEST_BlobInCache(file_number, file_size,
|
||||
@@ -1372,7 +1372,7 @@ class BlobSourceCacheReservationTest : public DBTestBase {
|
||||
|
||||
static constexpr std::size_t kSizeDummyEntry = CacheReservationManagerImpl<
|
||||
CacheEntryRole::kBlobCache>::GetDummyEntrySize();
|
||||
static constexpr std::size_t kCacheCapacity = 1 * kSizeDummyEntry;
|
||||
static constexpr std::size_t kCacheCapacity = 2 * kSizeDummyEntry;
|
||||
static constexpr int kNumShardBits = 0; // 2^0 shard
|
||||
|
||||
static constexpr uint32_t kColumnFamilyId = 1;
|
||||
@@ -1505,11 +1505,10 @@ TEST_F(BlobSourceCacheReservationTest, SimpleCacheReservation) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(BlobSourceCacheReservationTest, IncreaseCacheReservationOnFullCache) {
|
||||
TEST_F(BlobSourceCacheReservationTest, IncreaseCacheReservation) {
|
||||
options_.cf_paths.emplace_back(
|
||||
test::PerThreadDBPath(
|
||||
env_,
|
||||
"BlobSourceCacheReservationTest_IncreaseCacheReservationOnFullCache"),
|
||||
env_, "BlobSourceCacheReservationTest_IncreaseCacheReservation"),
|
||||
0);
|
||||
|
||||
GenerateKeysAndBlobs();
|
||||
@@ -1517,7 +1516,7 @@ TEST_F(BlobSourceCacheReservationTest, IncreaseCacheReservationOnFullCache) {
|
||||
DestroyAndReopen(options_);
|
||||
|
||||
ImmutableOptions immutable_options(options_);
|
||||
constexpr size_t blob_size = kSizeDummyEntry / (kNumBlobs / 2);
|
||||
constexpr size_t blob_size = 24 << 10; // 24KB
|
||||
for (size_t i = 0; i < kNumBlobs; ++i) {
|
||||
blob_file_size_ -= blobs_[i].size(); // old blob size
|
||||
blob_strs_[i].resize(blob_size, '@');
|
||||
@@ -1575,11 +1574,6 @@ TEST_F(BlobSourceCacheReservationTest, IncreaseCacheReservationOnFullCache) {
|
||||
|
||||
std::vector<PinnableSlice> values(keys_.size());
|
||||
|
||||
// Since we resized each blob to be kSizeDummyEntry / (num_blobs / 2), we
|
||||
// can't fit all the blobs in the cache at the same time, which means we
|
||||
// should observe cache evictions once we reach the cache's capacity.
|
||||
// Due to the overhead of the cache and the BlobContents objects, as well as
|
||||
// jemalloc bin sizes, this happens after inserting seven blobs.
|
||||
uint64_t blob_bytes = 0;
|
||||
for (size_t i = 0; i < kNumBlobs; ++i) {
|
||||
ASSERT_OK(blob_source.GetBlob(
|
||||
@@ -1590,15 +1584,15 @@ TEST_F(BlobSourceCacheReservationTest, IncreaseCacheReservationOnFullCache) {
|
||||
// Release cache handle
|
||||
values[i].Reset();
|
||||
|
||||
if (i < kNumBlobs / 2 - 1) {
|
||||
size_t charge = 0;
|
||||
ASSERT_TRUE(blob_source.TEST_BlobInCache(
|
||||
kBlobFileNumber, blob_file_size_, blob_offsets[i], &charge));
|
||||
size_t charge = 0;
|
||||
ASSERT_TRUE(blob_source.TEST_BlobInCache(kBlobFileNumber, blob_file_size_,
|
||||
blob_offsets[i], &charge));
|
||||
|
||||
blob_bytes += charge;
|
||||
}
|
||||
blob_bytes += charge;
|
||||
|
||||
ASSERT_EQ(cache_res_mgr->GetTotalReservedCacheSize(), kSizeDummyEntry);
|
||||
ASSERT_EQ(cache_res_mgr->GetTotalReservedCacheSize(),
|
||||
(blob_bytes <= kSizeDummyEntry) ? kSizeDummyEntry
|
||||
: (2 * kSizeDummyEntry));
|
||||
ASSERT_EQ(cache_res_mgr->GetTotalMemoryUsed(), blob_bytes);
|
||||
ASSERT_EQ(cache_res_mgr->GetTotalMemoryUsed(),
|
||||
options_.blob_cache->GetUsage());
|
||||
|
||||
@@ -1782,16 +1782,49 @@ TEST_F(DBBlobBasicTest, GetEntityBlob) {
|
||||
constexpr char key[] = "key";
|
||||
constexpr char blob_value[] = "blob_value";
|
||||
|
||||
constexpr char other_key[] = "other_key";
|
||||
constexpr char other_blob_value[] = "other_blob_value";
|
||||
|
||||
ASSERT_OK(Put(key, blob_value));
|
||||
ASSERT_OK(Put(other_key, other_blob_value));
|
||||
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
PinnableWideColumns result;
|
||||
ASSERT_OK(
|
||||
db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(), key, &result));
|
||||
|
||||
WideColumns expected_columns{{kDefaultWideColumnName, blob_value}};
|
||||
ASSERT_EQ(result.columns(), expected_columns);
|
||||
WideColumns other_expected_columns{
|
||||
{kDefaultWideColumnName, other_blob_value}};
|
||||
|
||||
{
|
||||
PinnableWideColumns result;
|
||||
ASSERT_OK(db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(), key,
|
||||
&result));
|
||||
ASSERT_EQ(result.columns(), expected_columns);
|
||||
}
|
||||
|
||||
{
|
||||
PinnableWideColumns result;
|
||||
ASSERT_OK(db_->GetEntity(ReadOptions(), db_->DefaultColumnFamily(),
|
||||
other_key, &result));
|
||||
|
||||
ASSERT_EQ(result.columns(), other_expected_columns);
|
||||
}
|
||||
|
||||
{
|
||||
constexpr size_t num_keys = 2;
|
||||
|
||||
std::array<Slice, num_keys> keys{{key, other_key}};
|
||||
std::array<PinnableWideColumns, num_keys> results;
|
||||
std::array<Status, num_keys> statuses;
|
||||
|
||||
db_->MultiGetEntity(ReadOptions(), db_->DefaultColumnFamily(), num_keys,
|
||||
&keys[0], &results[0], &statuses[0]);
|
||||
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ(results[0].columns(), expected_columns);
|
||||
|
||||
ASSERT_OK(statuses[1]);
|
||||
ASSERT_EQ(results[1].columns(), other_expected_columns);
|
||||
}
|
||||
}
|
||||
|
||||
class DBBlobWithTimestampTest : public DBBasicTestWithTimestampBase {
|
||||
@@ -1978,6 +2011,222 @@ TEST_F(DBBlobWithTimestampTest, MultiGetMergeBlobWithPut) {
|
||||
ASSERT_EQ(values[2], "v2_0");
|
||||
}
|
||||
|
||||
TEST_F(DBBlobWithTimestampTest, IterateBlobs) {
|
||||
Options options = GetDefaultOptions();
|
||||
options.enable_blob_files = true;
|
||||
options.create_if_missing = true;
|
||||
const size_t kTimestampSize = Timestamp(0, 0).size();
|
||||
TestComparator test_cmp(kTimestampSize);
|
||||
options.comparator = &test_cmp;
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
int num_blobs = 5;
|
||||
std::vector<std::string> keys;
|
||||
std::vector<std::string> blobs;
|
||||
|
||||
WriteOptions write_opts;
|
||||
std::vector<std::string> write_timestamps = {Timestamp(1, 0),
|
||||
Timestamp(2, 0)};
|
||||
|
||||
// For each key in ["key0", ... "keyi", ...], write two versions:
|
||||
// Timestamp(1, 0), "blobi0"
|
||||
// Timestamp(2, 0), "blobi1"
|
||||
for (int i = 0; i < num_blobs; i++) {
|
||||
keys.push_back("key" + std::to_string(i));
|
||||
blobs.push_back("blob" + std::to_string(i));
|
||||
for (size_t j = 0; j < write_timestamps.size(); j++) {
|
||||
ASSERT_OK(db_->Put(write_opts, keys[i], write_timestamps[j],
|
||||
blobs[i] + std::to_string(j)));
|
||||
}
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
ReadOptions read_options;
|
||||
std::vector<std::string> read_timestamps = {Timestamp(0, 0), Timestamp(3, 0)};
|
||||
Slice ts_upper_bound(read_timestamps[1]);
|
||||
read_options.timestamp = &ts_upper_bound;
|
||||
|
||||
auto check_iter_entry =
|
||||
[](const Iterator* iter, const std::string& expected_key,
|
||||
const std::string& expected_ts, const std::string& expected_value,
|
||||
bool key_is_internal = true) {
|
||||
ASSERT_OK(iter->status());
|
||||
if (key_is_internal) {
|
||||
std::string expected_ukey_and_ts;
|
||||
expected_ukey_and_ts.assign(expected_key.data(), expected_key.size());
|
||||
expected_ukey_and_ts.append(expected_ts.data(), expected_ts.size());
|
||||
|
||||
ParsedInternalKey parsed_ikey;
|
||||
ASSERT_OK(ParseInternalKey(iter->key(), &parsed_ikey,
|
||||
true /* log_err_key */));
|
||||
ASSERT_EQ(parsed_ikey.user_key, expected_ukey_and_ts);
|
||||
} else {
|
||||
ASSERT_EQ(iter->key(), expected_key);
|
||||
}
|
||||
ASSERT_EQ(iter->timestamp(), expected_ts);
|
||||
ASSERT_EQ(iter->value(), expected_value);
|
||||
};
|
||||
|
||||
// Forward iterating one version of each key, get in this order:
|
||||
// [("key0", Timestamp(2, 0), "blob01"),
|
||||
// ("key1", Timestamp(2, 0), "blob11")...]
|
||||
{
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
ASSERT_OK(iter->status());
|
||||
|
||||
iter->SeekToFirst();
|
||||
for (int i = 0; i < num_blobs; i++) {
|
||||
check_iter_entry(iter.get(), keys[i], write_timestamps[1],
|
||||
blobs[i] + std::to_string(1), /*key_is_internal*/ false);
|
||||
iter->Next();
|
||||
}
|
||||
}
|
||||
|
||||
// Forward iteration, then reverse to backward.
|
||||
{
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
ASSERT_OK(iter->status());
|
||||
|
||||
iter->SeekToFirst();
|
||||
for (int i = 0; i < num_blobs * 2 - 1; i++) {
|
||||
if (i < num_blobs) {
|
||||
check_iter_entry(iter.get(), keys[i], write_timestamps[1],
|
||||
blobs[i] + std::to_string(1),
|
||||
/*key_is_internal*/ false);
|
||||
if (i != num_blobs - 1) {
|
||||
iter->Next();
|
||||
}
|
||||
} else {
|
||||
if (i != num_blobs) {
|
||||
check_iter_entry(iter.get(), keys[num_blobs * 2 - 1 - i],
|
||||
write_timestamps[1],
|
||||
blobs[num_blobs * 2 - 1 - i] + std::to_string(1),
|
||||
/*key_is_internal*/ false);
|
||||
}
|
||||
iter->Prev();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Backward iterating one versions of each key, get in this order:
|
||||
// [("key4", Timestamp(2, 0), "blob41"),
|
||||
// ("key3", Timestamp(2, 0), "blob31")...]
|
||||
{
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
ASSERT_OK(iter->status());
|
||||
|
||||
iter->SeekToLast();
|
||||
for (int i = 0; i < num_blobs; i++) {
|
||||
check_iter_entry(iter.get(), keys[num_blobs - 1 - i], write_timestamps[1],
|
||||
blobs[num_blobs - 1 - i] + std::to_string(1),
|
||||
/*key_is_internal*/ false);
|
||||
iter->Prev();
|
||||
}
|
||||
}
|
||||
|
||||
// Backward iteration, then reverse to forward.
|
||||
{
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
ASSERT_OK(iter->status());
|
||||
|
||||
iter->SeekToLast();
|
||||
for (int i = 0; i < num_blobs * 2 - 1; i++) {
|
||||
if (i < num_blobs) {
|
||||
check_iter_entry(iter.get(), keys[num_blobs - 1 - i],
|
||||
write_timestamps[1],
|
||||
blobs[num_blobs - 1 - i] + std::to_string(1),
|
||||
/*key_is_internal*/ false);
|
||||
if (i != num_blobs - 1) {
|
||||
iter->Prev();
|
||||
}
|
||||
} else {
|
||||
if (i != num_blobs) {
|
||||
check_iter_entry(iter.get(), keys[i - num_blobs], write_timestamps[1],
|
||||
blobs[i - num_blobs] + std::to_string(1),
|
||||
/*key_is_internal*/ false);
|
||||
}
|
||||
iter->Next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Slice ts_lower_bound(read_timestamps[0]);
|
||||
read_options.iter_start_ts = &ts_lower_bound;
|
||||
// Forward iterating multiple versions of the same key, get in this order:
|
||||
// [("key0", Timestamp(2, 0), "blob01"),
|
||||
// ("key0", Timestamp(1, 0), "blob00"),
|
||||
// ("key1", Timestamp(2, 0), "blob11")...]
|
||||
{
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
ASSERT_OK(iter->status());
|
||||
|
||||
iter->SeekToFirst();
|
||||
for (int i = 0; i < num_blobs; i++) {
|
||||
for (size_t j = write_timestamps.size(); j > 0; --j) {
|
||||
check_iter_entry(iter.get(), keys[i], write_timestamps[j - 1],
|
||||
blobs[i] + std::to_string(j - 1));
|
||||
iter->Next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Backward iterating multiple versions of the same key, get in this order:
|
||||
// [("key4", Timestamp(1, 0), "blob00"),
|
||||
// ("key4", Timestamp(2, 0), "blob01"),
|
||||
// ("key3", Timestamp(1, 0), "blob10")...]
|
||||
{
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
ASSERT_OK(iter->status());
|
||||
|
||||
iter->SeekToLast();
|
||||
for (int i = num_blobs; i > 0; i--) {
|
||||
for (size_t j = 0; j < write_timestamps.size(); j++) {
|
||||
check_iter_entry(iter.get(), keys[i - 1], write_timestamps[j],
|
||||
blobs[i - 1] + std::to_string(j));
|
||||
iter->Prev();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int upper_bound_idx = num_blobs - 2;
|
||||
int lower_bound_idx = 1;
|
||||
Slice upper_bound_slice(keys[upper_bound_idx]);
|
||||
Slice lower_bound_slice(keys[lower_bound_idx]);
|
||||
read_options.iterate_upper_bound = &upper_bound_slice;
|
||||
read_options.iterate_lower_bound = &lower_bound_slice;
|
||||
|
||||
// Forward iteration with upper and lower bound.
|
||||
{
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
ASSERT_OK(iter->status());
|
||||
|
||||
iter->SeekToFirst();
|
||||
for (int i = lower_bound_idx; i < upper_bound_idx; i++) {
|
||||
for (size_t j = write_timestamps.size(); j > 0; --j) {
|
||||
check_iter_entry(iter.get(), keys[i], write_timestamps[j - 1],
|
||||
blobs[i] + std::to_string(j - 1));
|
||||
iter->Next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Backward iteration with upper and lower bound.
|
||||
{
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
ASSERT_OK(iter->status());
|
||||
|
||||
iter->SeekToLast();
|
||||
for (int i = upper_bound_idx; i > lower_bound_idx; i--) {
|
||||
for (size_t j = 0; j < write_timestamps.size(); j++) {
|
||||
check_iter_entry(iter.get(), keys[i - 1], write_timestamps[j],
|
||||
blobs[i - 1] + std::to_string(j));
|
||||
iter->Prev();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
// 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 "rocksdb/c.h"
|
||||
|
||||
#include <cstdlib>
|
||||
@@ -16,7 +15,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "port/port.h"
|
||||
#include "rocksdb/cache.h"
|
||||
#include "rocksdb/advanced_cache.h"
|
||||
#include "rocksdb/compaction_filter.h"
|
||||
#include "rocksdb/comparator.h"
|
||||
#include "rocksdb/convenience.h"
|
||||
@@ -78,6 +77,7 @@ using ROCKSDB_NAMESPACE::EnvOptions;
|
||||
using ROCKSDB_NAMESPACE::FileLock;
|
||||
using ROCKSDB_NAMESPACE::FilterPolicy;
|
||||
using ROCKSDB_NAMESPACE::FlushOptions;
|
||||
using ROCKSDB_NAMESPACE::HyperClockCacheOptions;
|
||||
using ROCKSDB_NAMESPACE::InfoLogLevel;
|
||||
using ROCKSDB_NAMESPACE::IngestExternalFileOptions;
|
||||
using ROCKSDB_NAMESPACE::Iterator;
|
||||
@@ -208,6 +208,9 @@ struct rocksdb_logger_t {
|
||||
struct rocksdb_lru_cache_options_t {
|
||||
LRUCacheOptions rep;
|
||||
};
|
||||
struct rocksdb_hyper_clock_cache_options_t {
|
||||
HyperClockCacheOptions rep;
|
||||
};
|
||||
struct rocksdb_memory_allocator_t {
|
||||
std::shared_ptr<MemoryAllocator> rep;
|
||||
};
|
||||
@@ -1805,6 +1808,17 @@ void rocksdb_flush_cf(rocksdb_t* db, const rocksdb_flushoptions_t* options,
|
||||
SaveError(errptr, db->rep->Flush(options->rep, column_family->rep));
|
||||
}
|
||||
|
||||
void rocksdb_flush_cfs(rocksdb_t* db, const rocksdb_flushoptions_t* options,
|
||||
rocksdb_column_family_handle_t** column_families,
|
||||
int num_column_families, char** errptr) {
|
||||
std::vector<ColumnFamilyHandle*> column_family_handles;
|
||||
for (int i = 0; i < num_column_families; i++) {
|
||||
column_family_handles.push_back(column_families[i]->rep);
|
||||
}
|
||||
|
||||
SaveError(errptr, db->rep->Flush(options->rep, column_family_handles));
|
||||
}
|
||||
|
||||
void rocksdb_flush_wal(rocksdb_t* db, unsigned char sync, char** errptr) {
|
||||
SaveError(errptr, db->rep->FlushWAL(sync));
|
||||
}
|
||||
@@ -4671,6 +4685,53 @@ rocksdb_cache_t* rocksdb_cache_create_lru_opts(
|
||||
return c;
|
||||
}
|
||||
|
||||
rocksdb_hyper_clock_cache_options_t* rocksdb_hyper_clock_cache_options_create(
|
||||
size_t capacity, size_t estimated_entry_charge) {
|
||||
return new rocksdb_hyper_clock_cache_options_t{
|
||||
HyperClockCacheOptions(capacity, estimated_entry_charge)};
|
||||
}
|
||||
|
||||
void rocksdb_hyper_clock_cache_options_destroy(
|
||||
rocksdb_hyper_clock_cache_options_t* opt) {
|
||||
delete opt;
|
||||
}
|
||||
|
||||
void rocksdb_hyper_clock_cache_options_set_capacity(
|
||||
rocksdb_hyper_clock_cache_options_t* opts, size_t capacity) {
|
||||
opts->rep.capacity = capacity;
|
||||
}
|
||||
|
||||
void rocksdb_hyper_clock_cache_options_set_estimated_entry_charge(
|
||||
rocksdb_hyper_clock_cache_options_t* opts, size_t estimated_entry_charge) {
|
||||
opts->rep.estimated_entry_charge = estimated_entry_charge;
|
||||
}
|
||||
|
||||
void rocksdb_hyper_clock_cache_options_set_num_shard_bits(
|
||||
rocksdb_hyper_clock_cache_options_t* opts, int num_shard_bits) {
|
||||
opts->rep.num_shard_bits = num_shard_bits;
|
||||
}
|
||||
|
||||
void rocksdb_hyper_clock_cache_options_set_memory_allocator(
|
||||
rocksdb_hyper_clock_cache_options_t* opts,
|
||||
rocksdb_memory_allocator_t* memory_allocator) {
|
||||
opts->rep.memory_allocator = memory_allocator->rep;
|
||||
}
|
||||
|
||||
rocksdb_cache_t* rocksdb_cache_create_hyper_clock(
|
||||
size_t capacity, size_t estimated_entry_charge) {
|
||||
HyperClockCacheOptions opts(capacity, estimated_entry_charge);
|
||||
rocksdb_cache_t* c = new rocksdb_cache_t;
|
||||
c->rep = opts.MakeSharedCache();
|
||||
return c;
|
||||
}
|
||||
|
||||
rocksdb_cache_t* rocksdb_cache_create_hyper_clock_opts(
|
||||
rocksdb_hyper_clock_cache_options_t* opts) {
|
||||
rocksdb_cache_t* c = new rocksdb_cache_t;
|
||||
c->rep = opts->rep.MakeSharedCache();
|
||||
return c;
|
||||
}
|
||||
|
||||
void rocksdb_cache_destroy(rocksdb_cache_t* cache) { delete cache; }
|
||||
|
||||
void rocksdb_cache_disown_data(rocksdb_cache_t* cache) {
|
||||
@@ -4911,6 +4972,12 @@ void rocksdb_ingestexternalfileoptions_set_ingest_behind(
|
||||
opt->rep.ingest_behind = ingest_behind;
|
||||
}
|
||||
|
||||
void rocksdb_ingestexternalfileoptions_set_fail_if_not_bottommost_level(
|
||||
rocksdb_ingestexternalfileoptions_t* opt,
|
||||
unsigned char fail_if_not_bottommost_level) {
|
||||
opt->rep.fail_if_not_bottommost_level = fail_if_not_bottommost_level;
|
||||
}
|
||||
|
||||
void rocksdb_ingestexternalfileoptions_destroy(
|
||||
rocksdb_ingestexternalfileoptions_t* opt) {
|
||||
delete opt;
|
||||
@@ -5072,6 +5139,16 @@ rocksdb_fifo_compaction_options_t* rocksdb_fifo_compaction_options_create() {
|
||||
return result;
|
||||
}
|
||||
|
||||
void rocksdb_fifo_compaction_options_set_allow_compaction(
|
||||
rocksdb_fifo_compaction_options_t* fifo_opts, unsigned char allow_compaction) {
|
||||
fifo_opts->rep.allow_compaction = allow_compaction;
|
||||
}
|
||||
|
||||
unsigned char rocksdb_fifo_compaction_options_get_allow_compaction(
|
||||
rocksdb_fifo_compaction_options_t* fifo_opts) {
|
||||
return fifo_opts->rep.allow_compaction;
|
||||
}
|
||||
|
||||
void rocksdb_fifo_compaction_options_set_max_table_files_size(
|
||||
rocksdb_fifo_compaction_options_t* fifo_opts, uint64_t size) {
|
||||
fifo_opts->rep.max_table_files_size = size;
|
||||
@@ -6109,6 +6186,18 @@ void rocksdb_transactiondb_flush_cf(
|
||||
SaveError(errptr, txn_db->rep->Flush(options->rep, column_family->rep));
|
||||
}
|
||||
|
||||
void rocksdb_transactiondb_flush_cfs(
|
||||
rocksdb_transactiondb_t* txn_db, const rocksdb_flushoptions_t* options,
|
||||
rocksdb_column_family_handle_t** column_families, int num_column_families,
|
||||
char** errptr) {
|
||||
std::vector<ColumnFamilyHandle*> column_family_handles;
|
||||
for (int i = 0; i < num_column_families; i++) {
|
||||
column_family_handles.push_back(column_families[i]->rep);
|
||||
}
|
||||
|
||||
SaveError(errptr, txn_db->rep->Flush(options->rep, column_family_handles));
|
||||
}
|
||||
|
||||
rocksdb_checkpoint_t* rocksdb_transactiondb_checkpoint_object_create(
|
||||
rocksdb_transactiondb_t* txn_db, char** errptr) {
|
||||
Checkpoint* checkpoint;
|
||||
@@ -6397,4 +6486,3 @@ void rocksdb_enable_manual_compaction(rocksdb_t* db) {
|
||||
}
|
||||
|
||||
} // end extern "C"
|
||||
|
||||
+8
-7
@@ -869,7 +869,7 @@ int GetL0ThresholdSpeedupCompaction(int level0_file_num_compaction_trigger,
|
||||
}
|
||||
} // anonymous namespace
|
||||
|
||||
std::pair<WriteStallCondition, ColumnFamilyData::WriteStallCause>
|
||||
std::pair<WriteStallCondition, WriteStallCause>
|
||||
ColumnFamilyData::GetWriteStallConditionAndCause(
|
||||
int num_unflushed_memtables, int num_l0_files,
|
||||
uint64_t num_compaction_needed_bytes,
|
||||
@@ -942,7 +942,8 @@ WriteStallCondition ColumnFamilyData::RecalculateWriteStallConditions(
|
||||
internal_stats_->AddCFStats(InternalStats::L0_FILE_COUNT_LIMIT_STOPS, 1);
|
||||
if (compaction_picker_->IsLevel0CompactionInProgress()) {
|
||||
internal_stats_->AddCFStats(
|
||||
InternalStats::LOCKED_L0_FILE_COUNT_LIMIT_STOPS, 1);
|
||||
InternalStats::L0_FILE_COUNT_LIMIT_STOPS_WITH_ONGOING_COMPACTION,
|
||||
1);
|
||||
}
|
||||
ROCKS_LOG_WARN(ioptions_.logger,
|
||||
"[%s] Stopping writes because we have %d level-0 files",
|
||||
@@ -963,7 +964,7 @@ WriteStallCondition ColumnFamilyData::RecalculateWriteStallConditions(
|
||||
SetupDelay(write_controller, compaction_needed_bytes,
|
||||
prev_compaction_needed_bytes_, was_stopped,
|
||||
mutable_cf_options.disable_auto_compactions);
|
||||
internal_stats_->AddCFStats(InternalStats::MEMTABLE_LIMIT_SLOWDOWNS, 1);
|
||||
internal_stats_->AddCFStats(InternalStats::MEMTABLE_LIMIT_DELAYS, 1);
|
||||
ROCKS_LOG_WARN(
|
||||
ioptions_.logger,
|
||||
"[%s] Stalling writes because we have %d immutable memtables "
|
||||
@@ -981,11 +982,11 @@ WriteStallCondition ColumnFamilyData::RecalculateWriteStallConditions(
|
||||
SetupDelay(write_controller, compaction_needed_bytes,
|
||||
prev_compaction_needed_bytes_, was_stopped || near_stop,
|
||||
mutable_cf_options.disable_auto_compactions);
|
||||
internal_stats_->AddCFStats(InternalStats::L0_FILE_COUNT_LIMIT_SLOWDOWNS,
|
||||
1);
|
||||
internal_stats_->AddCFStats(InternalStats::L0_FILE_COUNT_LIMIT_DELAYS, 1);
|
||||
if (compaction_picker_->IsLevel0CompactionInProgress()) {
|
||||
internal_stats_->AddCFStats(
|
||||
InternalStats::LOCKED_L0_FILE_COUNT_LIMIT_SLOWDOWNS, 1);
|
||||
InternalStats::L0_FILE_COUNT_LIMIT_DELAYS_WITH_ONGOING_COMPACTION,
|
||||
1);
|
||||
}
|
||||
ROCKS_LOG_WARN(ioptions_.logger,
|
||||
"[%s] Stalling writes because we have %d level-0 files "
|
||||
@@ -1011,7 +1012,7 @@ WriteStallCondition ColumnFamilyData::RecalculateWriteStallConditions(
|
||||
prev_compaction_needed_bytes_, was_stopped || near_stop,
|
||||
mutable_cf_options.disable_auto_compactions);
|
||||
internal_stats_->AddCFStats(
|
||||
InternalStats::PENDING_COMPACTION_BYTES_LIMIT_SLOWDOWNS, 1);
|
||||
InternalStats::PENDING_COMPACTION_BYTES_LIMIT_DELAYS, 1);
|
||||
ROCKS_LOG_WARN(
|
||||
ioptions_.logger,
|
||||
"[%s] Stalling writes because of estimated pending compaction "
|
||||
|
||||
@@ -462,12 +462,6 @@ class ColumnFamilyData {
|
||||
bool queued_for_flush() { return queued_for_flush_; }
|
||||
bool queued_for_compaction() { return queued_for_compaction_; }
|
||||
|
||||
enum class WriteStallCause {
|
||||
kNone,
|
||||
kMemtableLimit,
|
||||
kL0FileCountLimit,
|
||||
kPendingCompactionBytes,
|
||||
};
|
||||
static std::pair<WriteStallCondition, WriteStallCause>
|
||||
GetWriteStallConditionAndCause(
|
||||
int num_unflushed_memtables, int num_l0_files,
|
||||
|
||||
@@ -188,6 +188,11 @@ class ClippingIterator : public InternalIterator {
|
||||
return iter_->GetProperty(prop_name, prop);
|
||||
}
|
||||
|
||||
bool IsDeleteRangeSentinelKey() const override {
|
||||
assert(valid_);
|
||||
return iter_->IsDeleteRangeSentinelKey();
|
||||
}
|
||||
|
||||
private:
|
||||
void UpdateValid() {
|
||||
assert(!iter_->Valid() || iter_->status().ok());
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include "db/blob/blob_index.h"
|
||||
#include "db/blob/prefetch_buffer_collection.h"
|
||||
#include "db/snapshot_checker.h"
|
||||
#include "db/wide/wide_column_serialization.h"
|
||||
#include "logging/logging.h"
|
||||
#include "port/likely.h"
|
||||
#include "rocksdb/listener.h"
|
||||
@@ -225,8 +226,8 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO: support compaction filter for wide-column entities
|
||||
if (ikey_.type != kTypeValue && ikey_.type != kTypeBlobIndex) {
|
||||
if (ikey_.type != kTypeValue && ikey_.type != kTypeBlobIndex &&
|
||||
ikey_.type != kTypeWideColumnEntity) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -234,7 +235,9 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
|
||||
CompactionFilter::Decision::kUndetermined;
|
||||
CompactionFilter::ValueType value_type =
|
||||
ikey_.type == kTypeValue ? CompactionFilter::ValueType::kValue
|
||||
: CompactionFilter::ValueType::kBlobIndex;
|
||||
: ikey_.type == kTypeBlobIndex
|
||||
? CompactionFilter::ValueType::kBlobIndex
|
||||
: CompactionFilter::ValueType::kWideColumnEntity;
|
||||
|
||||
// Hack: pass internal key to BlobIndexCompactionFilter since it needs
|
||||
// to get sequence number.
|
||||
@@ -248,6 +251,8 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
|
||||
compaction_filter_value_.clear();
|
||||
compaction_filter_skip_until_.Clear();
|
||||
|
||||
std::vector<std::pair<std::string, std::string>> new_columns;
|
||||
|
||||
{
|
||||
StopWatchNano timer(clock_, report_detailed_time_);
|
||||
|
||||
@@ -303,10 +308,36 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
|
||||
value_type = CompactionFilter::ValueType::kValue;
|
||||
}
|
||||
}
|
||||
|
||||
if (decision == CompactionFilter::Decision::kUndetermined) {
|
||||
decision = compaction_filter_->FilterV2(
|
||||
level_, filter_key, value_type,
|
||||
blob_value_.empty() ? value_ : blob_value_, &compaction_filter_value_,
|
||||
const Slice* existing_val = nullptr;
|
||||
const WideColumns* existing_col = nullptr;
|
||||
|
||||
WideColumns existing_columns;
|
||||
|
||||
if (ikey_.type != kTypeWideColumnEntity) {
|
||||
if (!blob_value_.empty()) {
|
||||
existing_val = &blob_value_;
|
||||
} else {
|
||||
existing_val = &value_;
|
||||
}
|
||||
} else {
|
||||
Slice value_copy = value_;
|
||||
const Status s =
|
||||
WideColumnSerialization::Deserialize(value_copy, existing_columns);
|
||||
|
||||
if (!s.ok()) {
|
||||
status_ = s;
|
||||
validity_info_.Invalidate();
|
||||
return false;
|
||||
}
|
||||
|
||||
existing_col = &existing_columns;
|
||||
}
|
||||
|
||||
decision = compaction_filter_->FilterV3(
|
||||
level_, filter_key, value_type, existing_val, existing_col,
|
||||
&compaction_filter_value_, &new_columns,
|
||||
compaction_filter_skip_until_.rep());
|
||||
}
|
||||
|
||||
@@ -315,9 +346,10 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
|
||||
}
|
||||
|
||||
if (decision == CompactionFilter::Decision::kUndetermined) {
|
||||
// Should not reach here, since FilterV2 should never return kUndetermined.
|
||||
status_ =
|
||||
Status::NotSupported("FilterV2() should never return kUndetermined");
|
||||
// Should not reach here, since FilterV2/FilterV3 should never return
|
||||
// kUndetermined.
|
||||
status_ = Status::NotSupported(
|
||||
"FilterV2/FilterV3 should never return kUndetermined");
|
||||
validity_info_.Invalidate();
|
||||
return false;
|
||||
}
|
||||
@@ -326,7 +358,7 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
|
||||
cmp_->Compare(*compaction_filter_skip_until_.rep(), ikey_.user_key) <=
|
||||
0) {
|
||||
// Can't skip to a key smaller than the current one.
|
||||
// Keep the key as per FilterV2 documentation.
|
||||
// Keep the key as per FilterV2/FilterV3 documentation.
|
||||
decision = CompactionFilter::Decision::kKeep;
|
||||
}
|
||||
|
||||
@@ -388,6 +420,35 @@ bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
|
||||
status_ = Status::IOError("Failed to access blob during compaction filter");
|
||||
validity_info_.Invalidate();
|
||||
return false;
|
||||
} else if (decision == CompactionFilter::Decision::kChangeWideColumnEntity) {
|
||||
WideColumns sorted_columns;
|
||||
|
||||
sorted_columns.reserve(new_columns.size());
|
||||
for (const auto& column : new_columns) {
|
||||
sorted_columns.emplace_back(column.first, column.second);
|
||||
}
|
||||
|
||||
std::sort(sorted_columns.begin(), sorted_columns.end(),
|
||||
[](const WideColumn& lhs, const WideColumn& rhs) {
|
||||
return lhs.name().compare(rhs.name()) < 0;
|
||||
});
|
||||
|
||||
{
|
||||
const Status s = WideColumnSerialization::Serialize(
|
||||
sorted_columns, compaction_filter_value_);
|
||||
if (!s.ok()) {
|
||||
status_ = s;
|
||||
validity_info_.Invalidate();
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (ikey_.type != kTypeWideColumnEntity) {
|
||||
ikey_.type = kTypeWideColumnEntity;
|
||||
current_key_.UpdateInternalKey(ikey_.sequence, kTypeWideColumnEntity);
|
||||
}
|
||||
|
||||
value_ = compaction_filter_value_;
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -403,6 +464,7 @@ void CompactionIterator::NextFromInput() {
|
||||
value_ = input_.value();
|
||||
blob_value_.Reset();
|
||||
iter_stats_.num_input_records++;
|
||||
is_range_del_ = input_.IsDeleteRangeSentinelKey();
|
||||
|
||||
Status pik_status = ParseInternalKey(key_, &ikey_, allow_data_in_errors_);
|
||||
if (!pik_status.ok()) {
|
||||
@@ -422,7 +484,10 @@ void CompactionIterator::NextFromInput() {
|
||||
break;
|
||||
}
|
||||
TEST_SYNC_POINT_CALLBACK("CompactionIterator:ProcessKV", &ikey_);
|
||||
|
||||
if (is_range_del_) {
|
||||
validity_info_.SetValid(kRangeDeletion);
|
||||
break;
|
||||
}
|
||||
// Update input statistics
|
||||
if (ikey_.type == kTypeDeletion || ikey_.type == kTypeSingleDeletion ||
|
||||
ikey_.type == kTypeDeletionWithTimestamp) {
|
||||
@@ -644,6 +709,14 @@ void CompactionIterator::NextFromInput() {
|
||||
|
||||
ParsedInternalKey next_ikey;
|
||||
AdvanceInputIter();
|
||||
while (input_.Valid() && input_.IsDeleteRangeSentinelKey() &&
|
||||
ParseInternalKey(input_.key(), &next_ikey, allow_data_in_errors_)
|
||||
.ok() &&
|
||||
cmp_->EqualWithoutTimestamp(ikey_.user_key, next_ikey.user_key)) {
|
||||
// skip range tombstone start keys with the same user key
|
||||
// since they are not "real" point keys.
|
||||
AdvanceInputIter();
|
||||
}
|
||||
|
||||
// Check whether the next key exists, is not corrupt, and is the same key
|
||||
// as the single delete.
|
||||
@@ -651,6 +724,7 @@ void CompactionIterator::NextFromInput() {
|
||||
ParseInternalKey(input_.key(), &next_ikey, allow_data_in_errors_)
|
||||
.ok() &&
|
||||
cmp_->EqualWithoutTimestamp(ikey_.user_key, next_ikey.user_key)) {
|
||||
assert(!input_.IsDeleteRangeSentinelKey());
|
||||
#ifndef NDEBUG
|
||||
const Compaction* c =
|
||||
compaction_ ? compaction_->real_compaction() : nullptr;
|
||||
@@ -875,12 +949,14 @@ void CompactionIterator::NextFromInput() {
|
||||
// Note that a deletion marker of type kTypeDeletionWithTimestamp will be
|
||||
// considered to have a different user key unless the timestamp is older
|
||||
// than *full_history_ts_low_.
|
||||
//
|
||||
// Range tombstone start keys are skipped as they are not "real" keys.
|
||||
while (!IsPausingManualCompaction() && !IsShuttingDown() &&
|
||||
input_.Valid() &&
|
||||
(ParseInternalKey(input_.key(), &next_ikey, allow_data_in_errors_)
|
||||
.ok()) &&
|
||||
cmp_->EqualWithoutTimestamp(ikey_.user_key, next_ikey.user_key) &&
|
||||
(prev_snapshot == 0 ||
|
||||
(prev_snapshot == 0 || input_.IsDeleteRangeSentinelKey() ||
|
||||
DefinitelyNotInSnapshot(next_ikey.sequence, prev_snapshot))) {
|
||||
AdvanceInputIter();
|
||||
}
|
||||
@@ -1174,10 +1250,12 @@ void CompactionIterator::DecideOutputLevel() {
|
||||
|
||||
void CompactionIterator::PrepareOutput() {
|
||||
if (Valid()) {
|
||||
if (ikey_.type == kTypeValue) {
|
||||
ExtractLargeValueIfNeeded();
|
||||
} else if (ikey_.type == kTypeBlobIndex) {
|
||||
GarbageCollectBlobIfNeeded();
|
||||
if (LIKELY(!is_range_del_)) {
|
||||
if (ikey_.type == kTypeValue) {
|
||||
ExtractLargeValueIfNeeded();
|
||||
} else if (ikey_.type == kTypeBlobIndex) {
|
||||
GarbageCollectBlobIfNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
if (compaction_ != nullptr && compaction_->SupportsPerKeyPlacement()) {
|
||||
@@ -1200,7 +1278,7 @@ void CompactionIterator::PrepareOutput() {
|
||||
DefinitelyInSnapshot(ikey_.sequence, earliest_snapshot_) &&
|
||||
ikey_.type != kTypeMerge && current_key_committed_ &&
|
||||
!output_to_penultimate_level_ &&
|
||||
ikey_.sequence < preserve_time_min_seqno_) {
|
||||
ikey_.sequence < preserve_time_min_seqno_ && !is_range_del_) {
|
||||
if (ikey_.type == kTypeDeletion ||
|
||||
(ikey_.type == kTypeSingleDeletion && timestamp_size_ == 0)) {
|
||||
ROCKS_LOG_FATAL(
|
||||
|
||||
@@ -63,6 +63,10 @@ class SequenceIterWrapper : public InternalIterator {
|
||||
void SeekToLast() override { assert(false); }
|
||||
|
||||
uint64_t num_itered() const { return num_itered_; }
|
||||
bool IsDeleteRangeSentinelKey() const override {
|
||||
assert(Valid());
|
||||
return inner_iter_->IsDeleteRangeSentinelKey();
|
||||
}
|
||||
|
||||
private:
|
||||
InternalKeyComparator icmp_;
|
||||
@@ -242,7 +246,12 @@ class CompactionIterator {
|
||||
const Status& status() const { return status_; }
|
||||
const ParsedInternalKey& ikey() const { return ikey_; }
|
||||
inline bool Valid() const { return validity_info_.IsValid(); }
|
||||
const Slice& user_key() const { return current_user_key_; }
|
||||
const Slice& user_key() const {
|
||||
if (UNLIKELY(is_range_del_)) {
|
||||
return ikey_.user_key;
|
||||
}
|
||||
return current_user_key_;
|
||||
}
|
||||
const CompactionIterationStats& iter_stats() const { return iter_stats_; }
|
||||
uint64_t num_input_entry_scanned() const { return input_.num_itered(); }
|
||||
// If the current key should be placed on penultimate level, only valid if
|
||||
@@ -252,6 +261,8 @@ class CompactionIterator {
|
||||
}
|
||||
Status InputStatus() const { return input_.status(); }
|
||||
|
||||
bool IsDeleteRangeSentinelKey() const { return is_range_del_; }
|
||||
|
||||
private:
|
||||
// Processes the input stream to find the next output
|
||||
void NextFromInput();
|
||||
@@ -385,6 +396,7 @@ class CompactionIterator {
|
||||
kKeepSD = 8,
|
||||
kKeepDel = 9,
|
||||
kNewUserKey = 10,
|
||||
kRangeDeletion = 11,
|
||||
};
|
||||
|
||||
struct ValidityInfo {
|
||||
@@ -493,6 +505,10 @@ class CompactionIterator {
|
||||
// This is a best-effort facility, so memory_order_relaxed is sufficient.
|
||||
return manual_compaction_canceled_.load(std::memory_order_relaxed);
|
||||
}
|
||||
|
||||
// Stores whether the current compaction iterator output
|
||||
// is a range tombstone start key.
|
||||
bool is_range_del_{false};
|
||||
};
|
||||
|
||||
inline bool CompactionIterator::DefinitelyInSnapshot(SequenceNumber seq,
|
||||
|
||||
@@ -1118,6 +1118,8 @@ void CompactionJob::ProcessKeyValueCompaction(SubcompactionState* sub_compact) {
|
||||
IterKey end_ikey;
|
||||
Slice start_slice;
|
||||
Slice end_slice;
|
||||
Slice start_user_key{};
|
||||
Slice end_user_key{};
|
||||
|
||||
static constexpr char kMaxTs[] =
|
||||
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff";
|
||||
@@ -1140,6 +1142,7 @@ void CompactionJob::ProcessKeyValueCompaction(SubcompactionState* sub_compact) {
|
||||
&ts_slice);
|
||||
}
|
||||
start_slice = start_ikey.GetInternalKey();
|
||||
start_user_key = start_ikey.GetUserKey();
|
||||
}
|
||||
if (end.has_value()) {
|
||||
end_ikey.SetInternalKey(end.value(), kMaxSequenceNumber, kValueTypeForSeek);
|
||||
@@ -1148,6 +1151,7 @@ void CompactionJob::ProcessKeyValueCompaction(SubcompactionState* sub_compact) {
|
||||
&ts_slice);
|
||||
}
|
||||
end_slice = end_ikey.GetInternalKey();
|
||||
end_user_key = end_ikey.GetUserKey();
|
||||
}
|
||||
|
||||
std::unique_ptr<InternalIterator> clip;
|
||||
@@ -1263,11 +1267,15 @@ void CompactionJob::ProcessKeyValueCompaction(SubcompactionState* sub_compact) {
|
||||
[this, sub_compact](CompactionOutputs& outputs) {
|
||||
return this->OpenCompactionOutputFile(sub_compact, outputs);
|
||||
};
|
||||
|
||||
const CompactionFileCloseFunc close_file_func =
|
||||
[this, sub_compact](CompactionOutputs& outputs, const Status& status,
|
||||
const Slice& next_table_min_key) {
|
||||
return this->FinishCompactionOutputFile(status, sub_compact, outputs,
|
||||
next_table_min_key);
|
||||
[this, sub_compact, start_user_key, end_user_key](
|
||||
CompactionOutputs& outputs, const Status& status,
|
||||
const Slice& next_table_min_key) {
|
||||
return this->FinishCompactionOutputFile(
|
||||
status, sub_compact, outputs, next_table_min_key,
|
||||
sub_compact->start.has_value() ? &start_user_key : nullptr,
|
||||
sub_compact->end.has_value() ? &end_user_key : nullptr);
|
||||
};
|
||||
|
||||
Status status;
|
||||
@@ -1278,7 +1286,6 @@ void CompactionJob::ProcessKeyValueCompaction(SubcompactionState* sub_compact) {
|
||||
while (status.ok() && !cfd->IsDropped() && c_iter->Valid()) {
|
||||
// Invariant: c_iter.status() is guaranteed to be OK if c_iter->Valid()
|
||||
// returns true.
|
||||
|
||||
assert(!end.has_value() || cfd->user_comparator()->Compare(
|
||||
c_iter->user_key(), end.value()) < 0);
|
||||
|
||||
@@ -1458,7 +1465,8 @@ void CompactionJob::RecordDroppedKeys(
|
||||
|
||||
Status CompactionJob::FinishCompactionOutputFile(
|
||||
const Status& input_status, SubcompactionState* sub_compact,
|
||||
CompactionOutputs& outputs, const Slice& next_table_min_key) {
|
||||
CompactionOutputs& outputs, const Slice& next_table_min_key,
|
||||
const Slice* comp_start_user_key, const Slice* comp_end_user_key) {
|
||||
AutoThreadOperationStageUpdater stage_updater(
|
||||
ThreadStatus::STAGE_COMPACTION_SYNC_FILE);
|
||||
assert(sub_compact != nullptr);
|
||||
@@ -1488,12 +1496,10 @@ Status CompactionJob::FinishCompactionOutputFile(
|
||||
// output_to_penultimate_level compaction here, as it's only used to decide
|
||||
// if range dels could be dropped.
|
||||
if (outputs.HasRangeDel()) {
|
||||
s = outputs.AddRangeDels(
|
||||
sub_compact->start.has_value() ? &(sub_compact->start.value())
|
||||
: nullptr,
|
||||
sub_compact->end.has_value() ? &(sub_compact->end.value()) : nullptr,
|
||||
range_del_out_stats, bottommost_level_, cfd->internal_comparator(),
|
||||
earliest_snapshot, next_table_min_key, full_history_ts_low_);
|
||||
s = outputs.AddRangeDels(comp_start_user_key, comp_end_user_key,
|
||||
range_del_out_stats, bottommost_level_,
|
||||
cfd->internal_comparator(), earliest_snapshot,
|
||||
next_table_min_key, full_history_ts_low_);
|
||||
}
|
||||
RecordDroppedKeys(range_del_out_stats, &sub_compact->compaction_job_stats);
|
||||
TEST_SYNC_POINT("CompactionJob::FinishCompactionOutputFile1");
|
||||
|
||||
@@ -256,7 +256,9 @@ class CompactionJob {
|
||||
Status FinishCompactionOutputFile(const Status& input_status,
|
||||
SubcompactionState* sub_compact,
|
||||
CompactionOutputs& outputs,
|
||||
const Slice& next_table_min_key);
|
||||
const Slice& next_table_min_key,
|
||||
const Slice* comp_start_user_key,
|
||||
const Slice* comp_end_user_key);
|
||||
Status InstallCompactionResults(const MutableCFOptions& mutable_cf_options);
|
||||
Status OpenCompactionOutputFile(SubcompactionState* sub_compact,
|
||||
CompactionOutputs& outputs);
|
||||
|
||||
+246
-303
@@ -226,6 +226,15 @@ uint64_t CompactionOutputs::GetCurrentKeyGrandparentOverlappedBytes(
|
||||
bool CompactionOutputs::ShouldStopBefore(const CompactionIterator& c_iter) {
|
||||
assert(c_iter.Valid());
|
||||
const Slice& internal_key = c_iter.key();
|
||||
#ifndef NDEBUG
|
||||
bool should_stop = false;
|
||||
std::pair<bool*, const Slice> p{&should_stop, internal_key};
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"CompactionOutputs::ShouldStopBefore::manual_decision", (void*)&p);
|
||||
if (should_stop) {
|
||||
return true;
|
||||
}
|
||||
#endif // NDEBUG
|
||||
const uint64_t previous_overlapped_bytes = grandparent_overlapped_bytes_;
|
||||
const InternalKeyComparator* icmp =
|
||||
&compaction_->column_family_data()->internal_comparator();
|
||||
@@ -347,8 +356,14 @@ Status CompactionOutputs::AddToOutput(
|
||||
const CompactionFileOpenFunc& open_file_func,
|
||||
const CompactionFileCloseFunc& close_file_func) {
|
||||
Status s;
|
||||
bool is_range_del = c_iter.IsDeleteRangeSentinelKey();
|
||||
if (is_range_del && compaction_->bottommost_level()) {
|
||||
// We don't consider range tombstone for bottommost level since:
|
||||
// 1. there is no grandparent and hence no overlap to consider
|
||||
// 2. range tombstone may be dropped at bottommost level.
|
||||
return s;
|
||||
}
|
||||
const Slice& key = c_iter.key();
|
||||
|
||||
if (ShouldStopBefore(c_iter) && HasBuilder()) {
|
||||
s = close_file_func(*this, c_iter.InputStatus(), key);
|
||||
if (!s.ok()) {
|
||||
@@ -358,6 +373,13 @@ Status CompactionOutputs::AddToOutput(
|
||||
grandparent_boundary_switched_num_ = 0;
|
||||
grandparent_overlapped_bytes_ =
|
||||
GetCurrentKeyGrandparentOverlappedBytes(key);
|
||||
if (UNLIKELY(is_range_del)) {
|
||||
// lower bound for this new output file, this is needed as the lower bound
|
||||
// does not come from the smallest point key in this case.
|
||||
range_tombstone_lower_bound_.DecodeFrom(key);
|
||||
} else {
|
||||
range_tombstone_lower_bound_.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Open output file if necessary
|
||||
@@ -368,6 +390,17 @@ Status CompactionOutputs::AddToOutput(
|
||||
}
|
||||
}
|
||||
|
||||
// c_iter may emit range deletion keys, so update `last_key_for_partitioner_`
|
||||
// here before returning below when `is_range_del` is true
|
||||
if (partitioner_) {
|
||||
last_key_for_partitioner_.assign(c_iter.user_key().data_,
|
||||
c_iter.user_key().size_);
|
||||
}
|
||||
|
||||
if (UNLIKELY(is_range_del)) {
|
||||
return s;
|
||||
}
|
||||
|
||||
assert(builder_ != nullptr);
|
||||
const Slice& value = c_iter.value();
|
||||
s = current_output().validator.Add(key, value);
|
||||
@@ -391,28 +424,33 @@ Status CompactionOutputs::AddToOutput(
|
||||
s = current_output().meta.UpdateBoundaries(key, value, ikey.sequence,
|
||||
ikey.type);
|
||||
|
||||
if (partitioner_) {
|
||||
last_key_for_partitioner_.assign(c_iter.user_key().data_,
|
||||
c_iter.user_key().size_);
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
namespace {
|
||||
void SetMaxSeqAndTs(InternalKey& internal_key, const Slice& user_key,
|
||||
const size_t ts_sz) {
|
||||
if (ts_sz) {
|
||||
static constexpr char kTsMax[] = "\xff\xff\xff\xff\xff\xff\xff\xff\xff";
|
||||
if (ts_sz <= strlen(kTsMax)) {
|
||||
internal_key = InternalKey(user_key, kMaxSequenceNumber,
|
||||
kTypeRangeDeletion, Slice(kTsMax, ts_sz));
|
||||
} else {
|
||||
internal_key =
|
||||
InternalKey(user_key, kMaxSequenceNumber, kTypeRangeDeletion,
|
||||
std::string(ts_sz, '\xff'));
|
||||
}
|
||||
} else {
|
||||
internal_key.Set(user_key, kMaxSequenceNumber, kTypeRangeDeletion);
|
||||
}
|
||||
}
|
||||
} // namespace
|
||||
|
||||
Status CompactionOutputs::AddRangeDels(
|
||||
const Slice* comp_start_user_key, const Slice* comp_end_user_key,
|
||||
CompactionIterationStats& range_del_out_stats, bool bottommost_level,
|
||||
const InternalKeyComparator& icmp, SequenceNumber earliest_snapshot,
|
||||
const Slice& next_table_min_key, const std::string& full_history_ts_low) {
|
||||
assert(HasRangeDel());
|
||||
FileMetaData& meta = current_output().meta;
|
||||
const Comparator* ucmp = icmp.user_comparator();
|
||||
|
||||
Slice lower_bound_guard, upper_bound_guard;
|
||||
std::string smallest_user_key;
|
||||
const Slice *lower_bound, *upper_bound;
|
||||
bool lower_bound_from_sub_compact = false;
|
||||
|
||||
// The following example does not happen since
|
||||
// CompactionOutput::ShouldStopBefore() always return false for the first
|
||||
// point key. But we should consider removing this dependency. Suppose for the
|
||||
@@ -424,98 +462,134 @@ Status CompactionOutputs::AddRangeDels(
|
||||
// Then meta.smallest will be set to comp_start_user_key@seqno
|
||||
// and meta.largest will be set to comp_start_user_key@kMaxSequenceNumber
|
||||
// which violates the assumption that meta.smallest should be <= meta.largest.
|
||||
assert(HasRangeDel());
|
||||
FileMetaData& meta = current_output().meta;
|
||||
const Comparator* ucmp = icmp.user_comparator();
|
||||
InternalKey lower_bound_buf, upper_bound_buf;
|
||||
Slice lower_bound_guard, upper_bound_guard;
|
||||
std::string smallest_user_key;
|
||||
const Slice *lower_bound, *upper_bound;
|
||||
|
||||
// We first determine the internal key lower_bound and upper_bound for
|
||||
// this output file. All and only range tombstones that overlap with
|
||||
// [lower_bound, upper_bound] should be added to this file. File
|
||||
// boundaries (meta.smallest/largest) should be updated accordingly when
|
||||
// extended by range tombstones.
|
||||
size_t output_size = outputs_.size();
|
||||
if (output_size == 1) {
|
||||
// For the first output table, include range tombstones before the min
|
||||
// key but after the subcompaction boundary.
|
||||
lower_bound = comp_start_user_key;
|
||||
lower_bound_from_sub_compact = true;
|
||||
} else if (meta.smallest.size() > 0) {
|
||||
// This is the first file in the subcompaction.
|
||||
//
|
||||
// When outputting a range tombstone that spans a subcompaction boundary,
|
||||
// the files on either side of that boundary need to include that
|
||||
// boundary's user key. Otherwise, the spanning range tombstone would lose
|
||||
// coverage.
|
||||
//
|
||||
// To achieve this while preventing files from overlapping in internal key
|
||||
// (an LSM invariant violation), we allow the earlier file to include the
|
||||
// boundary user key up to `kMaxSequenceNumber,kTypeRangeDeletion`. The
|
||||
// later file can begin at the boundary user key at the newest key version
|
||||
// it contains. At this point that version number is unknown since we have
|
||||
// not processed the range tombstones yet, so permit any version. Same story
|
||||
// applies to timestamp, and a non-nullptr `comp_start_user_key` should have
|
||||
// `kMaxTs` here, which similarly permits any timestamp.
|
||||
if (comp_start_user_key) {
|
||||
lower_bound_buf.Set(*comp_start_user_key, kMaxSequenceNumber,
|
||||
kTypeRangeDeletion);
|
||||
lower_bound_guard = lower_bound_buf.Encode();
|
||||
lower_bound = &lower_bound_guard;
|
||||
} else {
|
||||
lower_bound = nullptr;
|
||||
}
|
||||
} else {
|
||||
// For subsequent output tables, only include range tombstones from min
|
||||
// key onwards since the previous file was extended to contain range
|
||||
// tombstones falling before min key.
|
||||
smallest_user_key = meta.smallest.user_key().ToString(false /*hex*/);
|
||||
lower_bound_guard = Slice(smallest_user_key);
|
||||
lower_bound = &lower_bound_guard;
|
||||
} else {
|
||||
lower_bound = nullptr;
|
||||
}
|
||||
if (!next_table_min_key.empty()) {
|
||||
// This may be the last file in the subcompaction in some cases, so we
|
||||
// need to compare the end key of subcompaction with the next file start
|
||||
// key. When the end key is chosen by the subcompaction, we know that
|
||||
// it must be the biggest key in output file. Therefore, it is safe to
|
||||
// use the smaller key as the upper bound of the output file, to ensure
|
||||
// that there is no overlapping between different output files.
|
||||
upper_bound_guard = ExtractUserKey(next_table_min_key);
|
||||
if (comp_end_user_key != nullptr &&
|
||||
ucmp->CompareWithoutTimestamp(upper_bound_guard, *comp_end_user_key) >=
|
||||
0) {
|
||||
upper_bound = comp_end_user_key;
|
||||
if (range_tombstone_lower_bound_.size() > 0) {
|
||||
assert(meta.smallest.size() == 0 ||
|
||||
icmp.Compare(range_tombstone_lower_bound_, meta.smallest) < 0);
|
||||
lower_bound_guard = range_tombstone_lower_bound_.Encode();
|
||||
} else {
|
||||
upper_bound = &upper_bound_guard;
|
||||
assert(meta.smallest.size() > 0);
|
||||
lower_bound_guard = meta.smallest.Encode();
|
||||
}
|
||||
} else {
|
||||
// This is the last file in the subcompaction, so extend until the
|
||||
// subcompaction ends.
|
||||
upper_bound = comp_end_user_key;
|
||||
}
|
||||
bool has_overlapping_endpoints;
|
||||
if (upper_bound != nullptr && meta.largest.size() > 0) {
|
||||
has_overlapping_endpoints = ucmp->CompareWithoutTimestamp(
|
||||
meta.largest.user_key(), *upper_bound) == 0;
|
||||
} else {
|
||||
has_overlapping_endpoints = false;
|
||||
lower_bound = &lower_bound_guard;
|
||||
}
|
||||
|
||||
const size_t ts_sz = ucmp->timestamp_size();
|
||||
if (next_table_min_key.empty()) {
|
||||
// Last file of the subcompaction.
|
||||
if (comp_end_user_key) {
|
||||
upper_bound_buf.Set(*comp_end_user_key, kMaxSequenceNumber,
|
||||
kTypeRangeDeletion);
|
||||
upper_bound_guard = upper_bound_buf.Encode();
|
||||
upper_bound = &upper_bound_guard;
|
||||
} else {
|
||||
upper_bound = nullptr;
|
||||
}
|
||||
} else {
|
||||
// There is another file coming whose coverage will begin at
|
||||
// `next_table_min_key`. The current file needs to extend range tombstone
|
||||
// coverage through its own keys (through `meta.largest`) and through user
|
||||
// keys preceding `next_table_min_key`'s user key.
|
||||
ParsedInternalKey next_table_min_key_parsed;
|
||||
ParseInternalKey(next_table_min_key, &next_table_min_key_parsed,
|
||||
false /* log_err_key */)
|
||||
.PermitUncheckedError();
|
||||
assert(next_table_min_key_parsed.sequence < kMaxSequenceNumber);
|
||||
assert(meta.largest.size() == 0 ||
|
||||
icmp.Compare(meta.largest.Encode(), next_table_min_key) < 0);
|
||||
assert(!lower_bound || icmp.Compare(*lower_bound, next_table_min_key) <= 0);
|
||||
if (meta.largest.size() > 0 &&
|
||||
ucmp->EqualWithoutTimestamp(meta.largest.user_key(),
|
||||
next_table_min_key_parsed.user_key)) {
|
||||
// Caution: this assumes meta.largest.Encode() lives longer than
|
||||
// upper_bound, which is only true if meta.largest is never updated.
|
||||
// This just happens to be the case here since meta.largest serves
|
||||
// as the upper_bound.
|
||||
upper_bound_guard = meta.largest.Encode();
|
||||
} else {
|
||||
SetMaxSeqAndTs(upper_bound_buf, next_table_min_key_parsed.user_key,
|
||||
ts_sz);
|
||||
upper_bound_guard = upper_bound_buf.Encode();
|
||||
}
|
||||
upper_bound = &upper_bound_guard;
|
||||
}
|
||||
if (lower_bound && upper_bound &&
|
||||
icmp.Compare(*lower_bound, *upper_bound) > 0) {
|
||||
assert(meta.smallest.size() == 0 &&
|
||||
ucmp->EqualWithoutTimestamp(ExtractUserKey(*lower_bound),
|
||||
ExtractUserKey(*upper_bound)));
|
||||
// This can only happen when lower_bound have the same user key as
|
||||
// next_table_min_key and that there is no point key in the current
|
||||
// compaction output file.
|
||||
return Status::OK();
|
||||
}
|
||||
// The end key of the subcompaction must be bigger or equal to the upper
|
||||
// bound. If the end of subcompaction is null or the upper bound is null,
|
||||
// it means that this file is the last file in the compaction. So there
|
||||
// will be no overlapping between this file and others.
|
||||
assert(comp_end_user_key == nullptr || upper_bound == nullptr ||
|
||||
ucmp->CompareWithoutTimestamp(*upper_bound, *comp_end_user_key) <= 0);
|
||||
auto it = range_del_agg_->NewIterator(lower_bound, upper_bound,
|
||||
has_overlapping_endpoints);
|
||||
// Position the range tombstone output iterator. There may be tombstone
|
||||
// fragments that are entirely out of range, so make sure that we do not
|
||||
// include those.
|
||||
if (lower_bound != nullptr) {
|
||||
it->Seek(*lower_bound);
|
||||
} else {
|
||||
it->SeekToFirst();
|
||||
}
|
||||
ucmp->CompareWithoutTimestamp(ExtractUserKey(*upper_bound),
|
||||
*comp_end_user_key) <= 0);
|
||||
auto it = range_del_agg_->NewIterator(lower_bound, upper_bound);
|
||||
Slice last_tombstone_start_user_key{};
|
||||
for (; it->Valid(); it->Next()) {
|
||||
bool reached_lower_bound = false;
|
||||
for (it->SeekToFirst(); it->Valid(); it->Next()) {
|
||||
auto tombstone = it->Tombstone();
|
||||
if (upper_bound != nullptr) {
|
||||
int cmp =
|
||||
ucmp->CompareWithoutTimestamp(*upper_bound, tombstone.start_key_);
|
||||
// Tombstones starting after upper_bound only need to be included in
|
||||
// the next table.
|
||||
// If the current SST ends before upper_bound, i.e.,
|
||||
// `has_overlapping_endpoints == false`, we can also skip over range
|
||||
// tombstones that start exactly at upper_bound. Such range
|
||||
// tombstones will be included in the next file and are not relevant
|
||||
// to the point keys or endpoints of the current file.
|
||||
// If the current SST ends at the same user key at upper_bound,
|
||||
// i.e., `has_overlapping_endpoints == true`, AND the tombstone has
|
||||
// the same start key as upper_bound, i.e., cmp == 0, then
|
||||
// the tombstone is relevant only if the tombstone's sequence number
|
||||
// is no larger than this file's largest key's sequence number. This
|
||||
// is because the upper bound to truncate this file's range tombstone
|
||||
// will be meta.largest in this case, and any tombstone that starts after
|
||||
// it will not be relevant.
|
||||
if (cmp < 0) {
|
||||
break;
|
||||
} else if (cmp == 0) {
|
||||
if (!has_overlapping_endpoints ||
|
||||
tombstone.seq_ < GetInternalKeySeqno(meta.largest.Encode())) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
auto kv = tombstone.Serialize();
|
||||
InternalKey tombstone_end = tombstone.SerializeEndKey();
|
||||
// TODO: the underlying iterator should support clamping the bounds.
|
||||
// tombstone_end.Encode is of form user_key@kMaxSeqno
|
||||
// if it is equal to lower_bound, there is no need to include
|
||||
// such range tombstone.
|
||||
if (!reached_lower_bound && lower_bound &&
|
||||
icmp.Compare(tombstone_end.Encode(), *lower_bound) <= 0) {
|
||||
continue;
|
||||
}
|
||||
assert(!lower_bound ||
|
||||
icmp.Compare(*lower_bound, tombstone_end.Encode()) <= 0);
|
||||
reached_lower_bound = true;
|
||||
|
||||
const size_t ts_sz = ucmp->timestamp_size();
|
||||
// Garbage collection for range tombstones.
|
||||
// If user-defined timestamp is enabled, range tombstones are dropped if
|
||||
// they are at bottommost_level, below full_history_ts_low and not visible
|
||||
@@ -534,83 +608,93 @@ Status CompactionOutputs::AddRangeDels(
|
||||
continue;
|
||||
}
|
||||
|
||||
auto kv = tombstone.Serialize();
|
||||
assert(lower_bound == nullptr ||
|
||||
ucmp->CompareWithoutTimestamp(*lower_bound, kv.second) < 0);
|
||||
ucmp->CompareWithoutTimestamp(ExtractUserKey(*lower_bound),
|
||||
kv.second) < 0);
|
||||
InternalKey tombstone_start = kv.first;
|
||||
if (lower_bound &&
|
||||
ucmp->CompareWithoutTimestamp(tombstone_start.user_key(),
|
||||
ExtractUserKey(*lower_bound)) < 0) {
|
||||
// This just updates the non-timestamp portion of `tombstone_start`'s user
|
||||
// key. Ideally there would be a simpler API usage
|
||||
ParsedInternalKey tombstone_start_parsed;
|
||||
ParseInternalKey(tombstone_start.Encode(), &tombstone_start_parsed,
|
||||
false /* log_err_key */)
|
||||
.PermitUncheckedError();
|
||||
// timestamp should be from where sequence number is from, which is from
|
||||
// tombstone in this case
|
||||
std::string ts =
|
||||
tombstone_start_parsed.GetTimestamp(ucmp->timestamp_size())
|
||||
.ToString();
|
||||
tombstone_start_parsed.user_key = ExtractUserKey(*lower_bound);
|
||||
tombstone_start.SetFrom(tombstone_start_parsed, ts);
|
||||
}
|
||||
if (upper_bound != nullptr &&
|
||||
icmp.Compare(*upper_bound, tombstone_start.Encode()) < 0) {
|
||||
break;
|
||||
}
|
||||
// Here we show that *only* range tombstones that overlap with
|
||||
// [lower_bound, upper_bound] are added to the current file, and
|
||||
// sanity checking invariants that should hold:
|
||||
// - [tombstone_start, tombstone_end] overlaps with [lower_bound,
|
||||
// upper_bound]
|
||||
// - meta.smallest <= meta.largest
|
||||
// Corresponding assertions are made, the proof is broken is any of them
|
||||
// fails.
|
||||
// TODO: show that *all* range tombstones that overlap with
|
||||
// [lower_bound, upper_bound] are added.
|
||||
// TODO: some invariant about boundaries are correctly updated.
|
||||
//
|
||||
// Note that `tombstone_start` is updated in the if condition above, we use
|
||||
// tombstone_start to refer to its initial value, i.e.,
|
||||
// it->Tombstone().first, and use tombstone_start* to refer to its value
|
||||
// after the update.
|
||||
//
|
||||
// To show [lower_bound, upper_bound] overlaps with [tombstone_start,
|
||||
// tombstone_end]:
|
||||
// lower_bound <= upper_bound from the if condition right after all
|
||||
// bounds are initialized. We assume each tombstone fragment has
|
||||
// start_key.user_key < end_key.user_key, so
|
||||
// tombstone_start < tombstone_end by
|
||||
// FragmentedTombstoneIterator::Tombstone(). So these two ranges are both
|
||||
// non-emtpy. The flag `reached_lower_bound` and the if logic before it
|
||||
// ensures lower_bound <= tombstone_end. tombstone_start is only updated
|
||||
// if it has a smaller user_key than lower_bound user_key, so
|
||||
// tombstone_start <= tombstone_start*. The above if condition implies
|
||||
// tombstone_start* <= upper_bound. So we have
|
||||
// tombstone_start <= upper_bound and lower_bound <= tombstone_end
|
||||
// and the two ranges overlap.
|
||||
//
|
||||
// To show meta.smallest <= meta.largest:
|
||||
// From the implementation of UpdateBoundariesForRange(), it suffices to
|
||||
// prove that when it is first called in this function, its parameters
|
||||
// satisfy `start <= end`, where start = max(tombstone_start*, lower_bound)
|
||||
// and end = min(tombstone_end, upper_bound). From the above proof we have
|
||||
// lower_bound <= tombstone_end and lower_bound <= upper_bound. We only need
|
||||
// to show that tombstone_start* <= min(tombstone_end, upper_bound).
|
||||
// Note that tombstone_start*.user_key = max(tombstone_start.user_key,
|
||||
// lower_bound.user_key). Assuming tombstone_end always has
|
||||
// kMaxSequenceNumber and lower_bound.seqno < kMaxSequenceNumber.
|
||||
// Since lower_bound <= tombstone_end and lower_bound.seqno <
|
||||
// tombstone_end.seqno (in absolute number order, not internal key order),
|
||||
// lower_bound.user_key < tombstone_end.user_key.
|
||||
// Since lower_bound.user_key < tombstone_end.user_key and
|
||||
// tombstone_start.user_key < tombstone_end.user_key, tombstone_start* <
|
||||
// tombstone_end. Since tombstone_start* <= upper_bound from the above proof
|
||||
// and tombstone_start* < tombstone_end, tombstone_start* <=
|
||||
// min(tombstone_end, upper_bound), so the two ranges overlap.
|
||||
|
||||
// Range tombstone is not supported by output validator yet.
|
||||
builder_->Add(kv.first.Encode(), kv.second);
|
||||
InternalKey tombstone_start = std::move(kv.first);
|
||||
InternalKey smallest_candidate{tombstone_start};
|
||||
if (lower_bound != nullptr &&
|
||||
ucmp->CompareWithoutTimestamp(smallest_candidate.user_key(),
|
||||
*lower_bound) <= 0) {
|
||||
// Pretend the smallest key has the same user key as lower_bound
|
||||
// (the max key in the previous table or subcompaction) in order for
|
||||
// files to appear key-space partitioned.
|
||||
if (lower_bound_from_sub_compact) {
|
||||
// When lower_bound is chosen by a subcompaction
|
||||
// (lower_bound_from_sub_compact), we know that subcompactions over
|
||||
// smaller keys cannot contain any keys at lower_bound. We also know
|
||||
// that smaller subcompactions exist, because otherwise the
|
||||
// subcompaction woud be unbounded on the left. As a result, we know
|
||||
// that no other files on the output level will contain actual keys at
|
||||
// lower_bound (an output file may have a largest key of
|
||||
// lower_bound@kMaxSequenceNumber, but this only indicates a large range
|
||||
// tombstone was truncated). Therefore, it is safe to use the
|
||||
// tombstone's sequence number, to ensure that keys at lower_bound at
|
||||
// lower levels are covered by truncated tombstones.
|
||||
if (ts_sz) {
|
||||
assert(tombstone.ts_.size() == ts_sz);
|
||||
smallest_candidate = InternalKey(*lower_bound, tombstone.seq_,
|
||||
kTypeRangeDeletion, tombstone.ts_);
|
||||
} else {
|
||||
smallest_candidate =
|
||||
InternalKey(*lower_bound, tombstone.seq_, kTypeRangeDeletion);
|
||||
}
|
||||
} else {
|
||||
// If lower_bound was chosen by the smallest data key in the file,
|
||||
// choose lowest seqnum so this file's smallest internal key comes
|
||||
// after the previous file's largest. The fake seqnum is OK because
|
||||
// the read path's file-picking code only considers user key.
|
||||
smallest_candidate = InternalKey(*lower_bound, 0, kTypeRangeDeletion);
|
||||
}
|
||||
if (lower_bound &&
|
||||
icmp.Compare(tombstone_start.Encode(), *lower_bound) < 0) {
|
||||
tombstone_start.DecodeFrom(*lower_bound);
|
||||
}
|
||||
InternalKey tombstone_end = tombstone.SerializeEndKey();
|
||||
InternalKey largest_candidate{tombstone_end};
|
||||
if (upper_bound != nullptr &&
|
||||
ucmp->CompareWithoutTimestamp(*upper_bound,
|
||||
largest_candidate.user_key()) <= 0) {
|
||||
// Pretend the largest key has the same user key as upper_bound (the
|
||||
// min key in the following table or subcompaction) in order for files
|
||||
// to appear key-space partitioned.
|
||||
//
|
||||
// Choose highest seqnum so this file's largest internal key comes
|
||||
// before the next file's/subcompaction's smallest. The fake seqnum is
|
||||
// OK because the read path's file-picking code only considers the
|
||||
// user key portion.
|
||||
//
|
||||
// Note Seek() also creates InternalKey with (user_key,
|
||||
// kMaxSequenceNumber), but with kTypeDeletion (0x7) instead of
|
||||
// kTypeRangeDeletion (0xF), so the range tombstone comes before the
|
||||
// Seek() key in InternalKey's ordering. So Seek() will look in the
|
||||
// next file for the user key
|
||||
if (ts_sz) {
|
||||
static constexpr char kTsMax[] = "\xff\xff\xff\xff\xff\xff\xff\xff\xff";
|
||||
if (ts_sz <= strlen(kTsMax)) {
|
||||
largest_candidate =
|
||||
InternalKey(*upper_bound, kMaxSequenceNumber, kTypeRangeDeletion,
|
||||
Slice(kTsMax, ts_sz));
|
||||
} else {
|
||||
largest_candidate =
|
||||
InternalKey(*upper_bound, kMaxSequenceNumber, kTypeRangeDeletion,
|
||||
std::string(ts_sz, '\xff'));
|
||||
}
|
||||
} else {
|
||||
largest_candidate =
|
||||
InternalKey(*upper_bound, kMaxSequenceNumber, kTypeRangeDeletion);
|
||||
}
|
||||
if (upper_bound && icmp.Compare(*upper_bound, tombstone_end.Encode()) < 0) {
|
||||
tombstone_end.DecodeFrom(*upper_bound);
|
||||
}
|
||||
meta.UpdateBoundariesForRange(smallest_candidate, largest_candidate,
|
||||
assert(icmp.Compare(tombstone_start, tombstone_end) <= 0);
|
||||
meta.UpdateBoundariesForRange(tombstone_start, tombstone_end,
|
||||
tombstone.seq_, icmp);
|
||||
if (!bottommost_level) {
|
||||
bool start_user_key_changed =
|
||||
@@ -618,17 +702,8 @@ Status CompactionOutputs::AddRangeDels(
|
||||
ucmp->CompareWithoutTimestamp(last_tombstone_start_user_key,
|
||||
it->start_key()) < 0;
|
||||
last_tombstone_start_user_key = it->start_key();
|
||||
// Range tombstones are truncated at file boundaries
|
||||
if (icmp.Compare(tombstone_start, meta.smallest) < 0) {
|
||||
tombstone_start = meta.smallest;
|
||||
}
|
||||
if (icmp.Compare(tombstone_end, meta.largest) > 0) {
|
||||
tombstone_end = meta.largest;
|
||||
}
|
||||
// this assertion validates invariant (2) in the comment below.
|
||||
assert(icmp.Compare(tombstone_start, tombstone_end) <= 0);
|
||||
if (start_user_key_changed) {
|
||||
// if tombstone_start >= tombstone_end, then either no key range is
|
||||
// If tombstone_start >= tombstone_end, then either no key range is
|
||||
// covered, or that they have the same user key. If they have the same
|
||||
// user key, then the internal key range should only be within this
|
||||
// level, and no keys from older levels is covered.
|
||||
@@ -646,138 +721,6 @@ Status CompactionOutputs::AddRangeDels(
|
||||
}
|
||||
}
|
||||
}
|
||||
// TODO: show invariants that ensure all necessary range tombstones are
|
||||
// added
|
||||
// and that file boundaries ensure no coverage is lost.
|
||||
// Each range tombstone with internal key range [tombstone_start,
|
||||
// tombstone_end] is being added to the current compaction output file here.
|
||||
// The range tombstone is going to be truncated at range [meta.smallest,
|
||||
// meta.largest] during reading/scanning. We should maintain invariants
|
||||
// (1) meta.smallest <= meta.largest and,
|
||||
// (2) [tombstone_start, tombstone_end] and [meta.smallest, meta.largest]
|
||||
// overlaps, as there is no point adding range tombstone with a range
|
||||
// outside the file's range.
|
||||
// Since `tombstone_end` is always some user_key@kMaxSeqno, it is okay to
|
||||
// use either open or closed range. Using closed range here to make
|
||||
// reasoning easier, and it is more consistent with an ongoing work that
|
||||
// tries to simplify this method.
|
||||
//
|
||||
// There are two cases:
|
||||
// Case 1. Output file has no point key:
|
||||
// First we show this case only happens when the entire compaction output
|
||||
// is range tombstone only. This is true if CompactionIterator does not
|
||||
// emit any point key. Suppose CompactionIterator emits some point key.
|
||||
// Based on the assumption that CompactionOutputs::ShouldStopBefore()
|
||||
// always return false for the first point key, the first compaction
|
||||
// output file always contains a point key. Each new compaction output
|
||||
// file is created if there is a point key for which ShouldStopBefore()
|
||||
// returns true, and the point key would be added to the new compaction
|
||||
// output file. So each new compaction file always contains a point key.
|
||||
// So Case 1 only happens when CompactionIterator does not emit any
|
||||
// point key.
|
||||
//
|
||||
// To show (1) meta.smallest <= meta.largest:
|
||||
// Since the compaction output is range tombstone only, `lower_bound` and
|
||||
// `upper_bound` are either null or comp_start/end_user_key respectively.
|
||||
// According to how UpdateBoundariesForRange() is implemented, it blindly
|
||||
// updates meta.smallest and meta.largest to smallest_candidate and
|
||||
// largest_candidate the first time it is called. Subsequently, it
|
||||
// compares input parameter with meta.smallest and meta.largest and only
|
||||
// updates them when input is smaller/larger. So we only need to show
|
||||
// smallest_candidate <= largest_candidate the first time
|
||||
// UpdateBoundariesForRange() is called. Here we show something stronger
|
||||
// that smallest_candidate.user_key < largest_candidate.user_key always
|
||||
// hold for Case 1.
|
||||
// We assume comp_start_user_key < comp_end_user_key, if provided. We
|
||||
// assume that tombstone_start < tombstone_end. This assumption is based
|
||||
// on that each fragment in FragmentedTombstoneList has
|
||||
// start_key < end_key (user_key) and that
|
||||
// FragmentedTombstoneIterator::Tombstone() returns the pair
|
||||
// (start_key@tombstone_seqno with op_type kTypeRangeDeletion, end_key).
|
||||
// The logic in this loop sets smallest_candidate to
|
||||
// max(tombstone_start.user_key, comp_start_user_key)@tombstone.seq_ with
|
||||
// op_type kTypeRangeDeletion, largest_candidate to
|
||||
// min(tombstone_end.user_key, comp_end_user_key)@kMaxSequenceNumber with
|
||||
// op_type kTypeRangeDeletion. When a bound is null, there is no
|
||||
// truncation on that end. To show that smallest_candidate.user_key <
|
||||
// largest_candidate.user_key, it suffices to show
|
||||
// tombstone_start.user_key < comp_end_user_key (if not null) AND
|
||||
// comp_start_user_key (if not null) < tombstone_end.user_key.
|
||||
// Since the file has no point key, `has_overlapping_endpoints` is false.
|
||||
// In the first sanity check of this for-loop, we compare
|
||||
// tombstone_start.user_key against upper_bound = comp_end_user_key,
|
||||
// and only proceed if tombstone_start.user_key < comp_end_user_key.
|
||||
// We assume FragmentedTombstoneIterator::Seek(k) lands
|
||||
// on a tombstone with end_key > k. So the call it->Seek(*lower_bound)
|
||||
// above implies compact_start_user_key < tombstone_end.user_key.
|
||||
//
|
||||
// To show (2) [tombstone_start, tombstone_end] and [meta.smallest,
|
||||
// meta.largest] overlaps (after the call to UpdateBoundariesForRange()):
|
||||
// In the proof for (1) we have shown that
|
||||
// smallest_candidate <= largest_candidate. Since tombstone_start <=
|
||||
// smallest_candidate <= largest_candidate <= tombstone_end, for (2) to
|
||||
// hold, it suffices to show that [smallest_candidate, largest_candidate]
|
||||
// overlaps with [meta.smallest, meta.largest]. too.
|
||||
// Given meta.smallest <= meta.largest shown above, we need to show
|
||||
// that it is impossible to have largest_candidate < meta.smallest or
|
||||
// meta.largest < smallest_candidate. If the above
|
||||
// meta.UpdateBoundariesForRange(smallest_candidate, largest_candidate)
|
||||
// updates meta.largest or meta.smallest, then the two ranges overlap.
|
||||
// So we assume meta.UpdateBoundariesForRange(smallest_candidate,
|
||||
// largest_candidate) did not update meta.smallest nor meta.largest, which
|
||||
// means meta.smallest < smallest_candidate and largest_candidate <
|
||||
// meta.largest.
|
||||
//
|
||||
// Case 2. Output file has >= 1 point key. This means meta.smallest and
|
||||
// meta.largest are not empty when AddRangeDels() is called.
|
||||
// To show (1) meta.smallest <= meta.largest:
|
||||
// Assume meta.smallest <= meta.largest when AddRangeDels() is called,
|
||||
// this follow from how UpdateBoundariesForRange() is implemented where it
|
||||
// takes min or max to update meta.smallest or meta.largest.
|
||||
//
|
||||
// To show (2) [tombstone_start, tombstone_end] and [meta.smallest,
|
||||
// meta.largest] overlaps (after the call to UpdateBoundariesForRange()):
|
||||
// When smallest_candidate <= largest_candidate, the proof in Case 1
|
||||
// applies, so we only need to show (2) holds when smallest_candidate >
|
||||
// largest_candidate. When both bounds are either null or from
|
||||
// subcompaction boundary, the proof in Case 1 applies, so we only need to
|
||||
// show (2) holds when at least one bound is from a point key (either
|
||||
// meta.smallest for lower bound or next_table_min_key for upper bound).
|
||||
//
|
||||
// Suppose lower bound is meta.smallest.user_key. The call
|
||||
// it->Seek(*lower_bound) implies tombstone_end.user_key >
|
||||
// meta.smallest.user_key. We have smallest_candidate.user_key =
|
||||
// max(tombstone_start.user_key, meta.smallest.user_key). For
|
||||
// smallest_candidate to be > largest_candidate, we need
|
||||
// largest_candidate.user_key = upper_bound = smallest_candidate.user_key,
|
||||
// where tombstone_end is truncated to largest_candidate.
|
||||
// Subcase 1:
|
||||
// Suppose largest_candidate.user_key = comp_end_user_key (there is no
|
||||
// next point key). Subcompaction ensures any point key from this
|
||||
// subcompaction has a user_key < comp_end_user_key, so 1)
|
||||
// meta.smallest.user_key < comp_end_user_key, 2)
|
||||
// `has_overlapping_endpoints` is false, and the first if condition in
|
||||
// this for-loop ensures tombstone_start.user_key < comp_end_user_key. So
|
||||
// smallest_candidate.user_key < largest_candidate.user_key. This case
|
||||
// cannot happen when smallest > largest_candidate.
|
||||
// Subcase 2:
|
||||
// Suppose largest_candidate.user_key = next_table_min_key.user_key.
|
||||
// The first if condition in this for-loop together with
|
||||
// smallest_candidate.user_key = next_table_min_key.user_key =
|
||||
// upper_bound implies `has_overlapping_endpoints` is true (so meta
|
||||
// largest.user_key = upper_bound) and
|
||||
// tombstone.seq_ < meta.largest.seqno. So
|
||||
// tombstone_start < meta.largest < tombstone_end.
|
||||
//
|
||||
// Suppose lower bound is comp_start_user_key and upper_bound is
|
||||
// next_table_min_key. The call it->Seek(*lower_bound) implies we have
|
||||
// tombstone_end_key.user_key > comp_start_user_key. So
|
||||
// tombstone_end_key.user_key > smallest_candidate.user_key. For
|
||||
// smallest_candidate to be > largest_candidate, we need
|
||||
// tombstone_start.user_key = largest_candidate.user_key = upper_bound =
|
||||
// next_table_min_key.user_key. This means `has_overlapping_endpoints` is
|
||||
// true (so meta.largest.user_key = upper_bound) and tombstone.seq_ <
|
||||
// meta.largest.seqno. So tombstone_start < meta.largest < tombstone_end.
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
@@ -167,9 +167,15 @@ class CompactionOutputs {
|
||||
current_output_file_size_ = 0;
|
||||
}
|
||||
|
||||
// Add range-dels from the aggregator to the current output file
|
||||
// Add range deletions from the range_del_agg_ to the current output file.
|
||||
// Input parameters, `range_tombstone_lower_bound_` and current output's
|
||||
// metadata determine the bounds on range deletions to add. Updates output
|
||||
// file metadata boundary if extended by range tombstones.
|
||||
//
|
||||
// @param comp_start_user_key and comp_end_user_key include timestamp if
|
||||
// user-defined timestamp is enabled.
|
||||
// user-defined timestamp is enabled. Their timestamp should be max timestamp.
|
||||
// @param next_table_min_key internal key lower bound for the next compaction
|
||||
// output.
|
||||
// @param full_history_ts_low used for range tombstone garbage collection.
|
||||
Status AddRangeDels(const Slice* comp_start_user_key,
|
||||
const Slice* comp_end_user_key,
|
||||
@@ -314,6 +320,7 @@ class CompactionOutputs {
|
||||
std::unique_ptr<SstPartitioner> partitioner_;
|
||||
|
||||
// A flag determines if this subcompaction has been split by the cursor
|
||||
// for RoundRobin compaction
|
||||
bool is_split_ = false;
|
||||
|
||||
// We also maintain the output split key for each subcompaction to avoid
|
||||
@@ -345,6 +352,10 @@ class CompactionOutputs {
|
||||
// for the current output file, how many file boundaries has it crossed,
|
||||
// basically number of files overlapped * 2
|
||||
size_t grandparent_boundary_switched_num_ = 0;
|
||||
|
||||
// The smallest key of the current output file, this is set when current
|
||||
// output file's smallest key is a range tombstone start key.
|
||||
InternalKey range_tombstone_lower_bound_;
|
||||
};
|
||||
|
||||
// helper struct to concatenate the last level and penultimate level outputs
|
||||
|
||||
@@ -78,7 +78,6 @@ Status SstPartitionerFactory::CreateFromString(
|
||||
std::call_once(once, [&]() {
|
||||
RegisterSstPartitionerFactories(*(ObjectLibrary::Default().get()), "");
|
||||
});
|
||||
return LoadSharedObject<SstPartitionerFactory>(options, value, nullptr,
|
||||
result);
|
||||
return LoadSharedObject<SstPartitionerFactory>(options, value, result);
|
||||
}
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -84,6 +84,11 @@ class SubcompactionState {
|
||||
// Assign range dels aggregator, for each range_del, it can only be assigned
|
||||
// to one output level, for per_key_placement, it's going to be the
|
||||
// penultimate level.
|
||||
// TODO: This does not work for per_key_placement + user-defined timestamp +
|
||||
// DeleteRange() combo. If user-defined timestamp is enabled,
|
||||
// it is possible for a range tombstone to belong to bottommost level (
|
||||
// seqno < earliest snapshot) without being dropped (garbage collection
|
||||
// for user-defined timestamp).
|
||||
void AssignRangeDelAggregator(
|
||||
std::unique_ptr<CompactionRangeDelAggregator>&& range_del_agg) {
|
||||
if (compaction->SupportsPerKeyPlacement()) {
|
||||
|
||||
+150
-4
@@ -30,6 +30,9 @@
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
static bool enable_io_uring = true;
|
||||
extern "C" bool RocksDbIOUringEnable() { return enable_io_uring; }
|
||||
|
||||
class DBBasicTest : public DBTestBase {
|
||||
public:
|
||||
DBBasicTest() : DBTestBase("db_basic_test", /*env_do_fsync=*/false) {}
|
||||
@@ -2173,6 +2176,7 @@ class DBMultiGetAsyncIOTest : public DBBasicTest,
|
||||
options_.disable_auto_compactions = true;
|
||||
options_.statistics = statistics_;
|
||||
options_.table_factory.reset(NewBlockBasedTableFactory(bbto));
|
||||
options_.env = Env::Default();
|
||||
Reopen(options_);
|
||||
int num_keys = 0;
|
||||
|
||||
@@ -2239,6 +2243,20 @@ class DBMultiGetAsyncIOTest : public DBBasicTest,
|
||||
const std::shared_ptr<Statistics>& statistics() { return statistics_; }
|
||||
|
||||
protected:
|
||||
void PrepareDBForTest() {
|
||||
#ifdef ROCKSDB_IOURING_PRESENT
|
||||
Reopen(options_);
|
||||
#else // ROCKSDB_IOURING_PRESENT
|
||||
// Warm up the block cache so we don't need to use the IO uring
|
||||
Iterator* iter = dbfull()->NewIterator(ReadOptions());
|
||||
for (iter->SeekToFirst(); iter->Valid() && iter->status().ok();
|
||||
iter->Next())
|
||||
;
|
||||
EXPECT_OK(iter->status());
|
||||
delete iter;
|
||||
#endif // ROCKSDB_IOURING_PRESENT
|
||||
}
|
||||
|
||||
void ReopenDB() { Reopen(options_); }
|
||||
|
||||
private:
|
||||
@@ -2253,6 +2271,8 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL0) {
|
||||
std::vector<PinnableSlice> values(key_strs.size());
|
||||
std::vector<Status> statuses(key_strs.size());
|
||||
|
||||
PrepareDBForTest();
|
||||
|
||||
ReadOptions ro;
|
||||
ro.async_io = true;
|
||||
ro.optimize_multiget_for_io = GetParam();
|
||||
@@ -2271,6 +2291,7 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL0) {
|
||||
statistics()->histogramData(MULTIGET_IO_BATCH_SIZE, &multiget_io_batch_size);
|
||||
|
||||
// With async IO, lookups will happen in parallel for each key
|
||||
#ifdef ROCKSDB_IOURING_PRESENT
|
||||
if (GetParam()) {
|
||||
ASSERT_EQ(multiget_io_batch_size.count, 1);
|
||||
ASSERT_EQ(multiget_io_batch_size.max, 3);
|
||||
@@ -2280,6 +2301,9 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL0) {
|
||||
// L0 file
|
||||
ASSERT_EQ(multiget_io_batch_size.count, 3);
|
||||
}
|
||||
#else // ROCKSDB_IOURING_PRESENT
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 0);
|
||||
#endif // ROCKSDB_IOURING_PRESENT
|
||||
}
|
||||
|
||||
TEST_P(DBMultiGetAsyncIOTest, GetFromL1) {
|
||||
@@ -2297,6 +2321,8 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL1) {
|
||||
values.resize(keys.size());
|
||||
statuses.resize(keys.size());
|
||||
|
||||
PrepareDBForTest();
|
||||
|
||||
ReadOptions ro;
|
||||
ro.async_io = true;
|
||||
ro.optimize_multiget_for_io = GetParam();
|
||||
@@ -2314,12 +2340,17 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL1) {
|
||||
|
||||
statistics()->histogramData(MULTIGET_IO_BATCH_SIZE, &multiget_io_batch_size);
|
||||
|
||||
#ifdef ROCKSDB_IOURING_PRESENT
|
||||
// A batch of 3 async IOs is expected, one for each overlapping file in L1
|
||||
ASSERT_EQ(multiget_io_batch_size.count, 1);
|
||||
ASSERT_EQ(multiget_io_batch_size.max, 3);
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 3);
|
||||
#else // ROCKSDB_IOURING_PRESENT
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 0);
|
||||
#endif // ROCKSDB_IOURING_PRESENT
|
||||
}
|
||||
|
||||
#ifdef ROCKSDB_IOURING_PRESENT
|
||||
TEST_P(DBMultiGetAsyncIOTest, GetFromL1Error) {
|
||||
std::vector<std::string> key_strs;
|
||||
std::vector<Slice> keys;
|
||||
@@ -2335,9 +2366,9 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL1Error) {
|
||||
values.resize(keys.size());
|
||||
statuses.resize(keys.size());
|
||||
|
||||
int count = 0;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"TableCache::GetTableReader:BeforeOpenFile", [&](void* status) {
|
||||
static int count = 0;
|
||||
count++;
|
||||
// Fail the last table reader open, which is the 6th SST file
|
||||
// since 3 overlapping L0 files + 3 L1 files containing the keys
|
||||
@@ -2360,7 +2391,7 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL1Error) {
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
ReopenDB();
|
||||
PrepareDBForTest();
|
||||
|
||||
ReadOptions ro;
|
||||
ro.async_io = true;
|
||||
@@ -2382,6 +2413,7 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL1Error) {
|
||||
ASSERT_EQ(multiget_io_batch_size.max, 2);
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 2);
|
||||
}
|
||||
#endif // ROCKSDB_IOURING_PRESENT
|
||||
|
||||
TEST_P(DBMultiGetAsyncIOTest, LastKeyInFile) {
|
||||
std::vector<std::string> key_strs;
|
||||
@@ -2399,6 +2431,8 @@ TEST_P(DBMultiGetAsyncIOTest, LastKeyInFile) {
|
||||
values.resize(keys.size());
|
||||
statuses.resize(keys.size());
|
||||
|
||||
PrepareDBForTest();
|
||||
|
||||
ReadOptions ro;
|
||||
ro.async_io = true;
|
||||
ro.optimize_multiget_for_io = GetParam();
|
||||
@@ -2412,6 +2446,7 @@ TEST_P(DBMultiGetAsyncIOTest, LastKeyInFile) {
|
||||
ASSERT_EQ(values[1], "val_l1_" + std::to_string(54));
|
||||
ASSERT_EQ(values[2], "val_l1_" + std::to_string(102));
|
||||
|
||||
#ifdef ROCKSDB_IOURING_PRESENT
|
||||
HistogramData multiget_io_batch_size;
|
||||
|
||||
statistics()->histogramData(MULTIGET_IO_BATCH_SIZE, &multiget_io_batch_size);
|
||||
@@ -2422,6 +2457,7 @@ TEST_P(DBMultiGetAsyncIOTest, LastKeyInFile) {
|
||||
// will lookup 2 files in parallel and issue 2 async reads
|
||||
ASSERT_EQ(multiget_io_batch_size.count, 2);
|
||||
ASSERT_EQ(multiget_io_batch_size.max, 2);
|
||||
#endif // ROCKSDB_IOURING_PRESENT
|
||||
}
|
||||
|
||||
TEST_P(DBMultiGetAsyncIOTest, GetFromL1AndL2) {
|
||||
@@ -2440,6 +2476,8 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL1AndL2) {
|
||||
values.resize(keys.size());
|
||||
statuses.resize(keys.size());
|
||||
|
||||
PrepareDBForTest();
|
||||
|
||||
ReadOptions ro;
|
||||
ro.async_io = true;
|
||||
ro.optimize_multiget_for_io = GetParam();
|
||||
@@ -2453,6 +2491,7 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL1AndL2) {
|
||||
ASSERT_EQ(values[1], "val_l2_" + std::to_string(56));
|
||||
ASSERT_EQ(values[2], "val_l1_" + std::to_string(102));
|
||||
|
||||
#ifdef ROCKSDB_IOURING_PRESENT
|
||||
HistogramData multiget_io_batch_size;
|
||||
|
||||
statistics()->histogramData(MULTIGET_IO_BATCH_SIZE, &multiget_io_batch_size);
|
||||
@@ -2462,6 +2501,7 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL1AndL2) {
|
||||
// Otherwise, the L2 lookup will happen after L1.
|
||||
ASSERT_EQ(multiget_io_batch_size.count, GetParam() ? 1 : 2);
|
||||
ASSERT_EQ(multiget_io_batch_size.max, GetParam() ? 3 : 2);
|
||||
#endif // ROCKSDB_IOURING_PRESENT
|
||||
}
|
||||
|
||||
TEST_P(DBMultiGetAsyncIOTest, GetFromL2WithRangeOverlapL0L1) {
|
||||
@@ -2478,6 +2518,8 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL2WithRangeOverlapL0L1) {
|
||||
values.resize(keys.size());
|
||||
statuses.resize(keys.size());
|
||||
|
||||
PrepareDBForTest();
|
||||
|
||||
ReadOptions ro;
|
||||
ro.async_io = true;
|
||||
ro.optimize_multiget_for_io = GetParam();
|
||||
@@ -2489,10 +2531,15 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL2WithRangeOverlapL0L1) {
|
||||
ASSERT_EQ(values[0], "val_l2_" + std::to_string(19));
|
||||
ASSERT_EQ(values[1], "val_l2_" + std::to_string(26));
|
||||
|
||||
#ifdef ROCKSDB_IOURING_PRESENT
|
||||
// Bloom filters in L0/L1 will avoid the coroutine calls in those levels
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 2);
|
||||
#else // ROCKSDB_IOURING_PRESENT
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 0);
|
||||
#endif // ROCKSDB_IOURING_PRESENT
|
||||
}
|
||||
|
||||
#ifdef ROCKSDB_IOURING_PRESENT
|
||||
TEST_P(DBMultiGetAsyncIOTest, GetFromL2WithRangeDelInL1) {
|
||||
std::vector<std::string> key_strs;
|
||||
std::vector<Slice> keys;
|
||||
@@ -2507,6 +2554,8 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL2WithRangeDelInL1) {
|
||||
values.resize(keys.size());
|
||||
statuses.resize(keys.size());
|
||||
|
||||
PrepareDBForTest();
|
||||
|
||||
ReadOptions ro;
|
||||
ro.async_io = true;
|
||||
ro.optimize_multiget_for_io = GetParam();
|
||||
@@ -2536,6 +2585,8 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL1AndL2WithRangeDelInL1) {
|
||||
values.resize(keys.size());
|
||||
statuses.resize(keys.size());
|
||||
|
||||
PrepareDBForTest();
|
||||
|
||||
ReadOptions ro;
|
||||
ro.async_io = true;
|
||||
ro.optimize_multiget_for_io = GetParam();
|
||||
@@ -2550,6 +2601,44 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL1AndL2WithRangeDelInL1) {
|
||||
// Bloom filters in L0/L1 will avoid the coroutine calls in those levels
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 3);
|
||||
}
|
||||
#endif // ROCKSDB_IOURING_PRESENT
|
||||
|
||||
TEST_P(DBMultiGetAsyncIOTest, GetNoIOUring) {
|
||||
std::vector<std::string> key_strs;
|
||||
std::vector<Slice> keys;
|
||||
std::vector<PinnableSlice> values;
|
||||
std::vector<Status> statuses;
|
||||
|
||||
key_strs.push_back(Key(33));
|
||||
key_strs.push_back(Key(54));
|
||||
key_strs.push_back(Key(102));
|
||||
keys.push_back(key_strs[0]);
|
||||
keys.push_back(key_strs[1]);
|
||||
keys.push_back(key_strs[2]);
|
||||
values.resize(keys.size());
|
||||
statuses.resize(keys.size());
|
||||
|
||||
enable_io_uring = false;
|
||||
ReopenDB();
|
||||
|
||||
ReadOptions ro;
|
||||
ro.async_io = true;
|
||||
ro.optimize_multiget_for_io = GetParam();
|
||||
dbfull()->MultiGet(ro, dbfull()->DefaultColumnFamily(), keys.size(),
|
||||
keys.data(), values.data(), statuses.data());
|
||||
ASSERT_EQ(values.size(), 3);
|
||||
ASSERT_EQ(statuses[0], Status::OK());
|
||||
ASSERT_EQ(statuses[1], Status::OK());
|
||||
ASSERT_EQ(statuses[2], Status::OK());
|
||||
|
||||
HistogramData async_read_bytes;
|
||||
|
||||
statistics()->histogramData(ASYNC_READ_BYTES, &async_read_bytes);
|
||||
|
||||
// A batch of 3 async IOs is expected, one for each overlapping file in L1
|
||||
ASSERT_EQ(async_read_bytes.count, 0);
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 0);
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(DBMultiGetAsyncIOTest, DBMultiGetAsyncIOTest,
|
||||
testing::Bool());
|
||||
@@ -3540,11 +3629,11 @@ class DBBasicTestMultiGet : public DBTestBase {
|
||||
|
||||
Handle* Lookup(const Slice& key, const CacheItemHelper* helper,
|
||||
CreateContext* create_context,
|
||||
Priority priority = Priority::LOW, bool wait = true,
|
||||
Priority priority = Priority::LOW,
|
||||
Statistics* stats = nullptr) override {
|
||||
num_lookups_++;
|
||||
Handle* handle =
|
||||
target_->Lookup(key, helper, create_context, priority, wait, stats);
|
||||
target_->Lookup(key, helper, create_context, priority, stats);
|
||||
if (handle != nullptr) {
|
||||
num_found_++;
|
||||
}
|
||||
@@ -4405,6 +4494,63 @@ TEST_F(DBBasicTest, VerifyFileChecksums) {
|
||||
ASSERT_TRUE(db_->VerifyFileChecksums(ReadOptions()).IsInvalidArgument());
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, VerifyFileChecksumsReadahead) {
|
||||
Options options = GetDefaultOptions();
|
||||
options.create_if_missing = true;
|
||||
options.env = env_;
|
||||
options.file_checksum_gen_factory = GetFileChecksumGenCrc32cFactory();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
Random rnd(301);
|
||||
int alignment = 256 * 1024;
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
ASSERT_OK(Put("key" + std::to_string(i), rnd.RandomString(alignment)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
std::vector<std::string> filenames;
|
||||
int sst_cnt = 0;
|
||||
std::string sst_name;
|
||||
uint64_t sst_size;
|
||||
uint64_t number;
|
||||
FileType type;
|
||||
ASSERT_OK(env_->GetChildren(dbname_, &filenames));
|
||||
for (auto name : filenames) {
|
||||
if (ParseFileName(name, &number, &type)) {
|
||||
if (type == kTableFile) {
|
||||
sst_cnt++;
|
||||
sst_name = name;
|
||||
}
|
||||
}
|
||||
}
|
||||
ASSERT_EQ(sst_cnt, 1);
|
||||
ASSERT_OK(env_->GetFileSize(dbname_ + '/' + sst_name, &sst_size));
|
||||
|
||||
bool last_read = false;
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"GenerateOneFileChecksum::Chunk:0", [&](void* /*arg*/) {
|
||||
if (env_->random_read_bytes_counter_.load() == sst_size) {
|
||||
EXPECT_FALSE(last_read);
|
||||
last_read = true;
|
||||
} else {
|
||||
ASSERT_EQ(env_->random_read_bytes_counter_.load() & (alignment - 1),
|
||||
0);
|
||||
}
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
env_->count_random_reads_ = true;
|
||||
env_->random_read_bytes_counter_ = 0;
|
||||
env_->random_read_counter_.Reset();
|
||||
|
||||
ReadOptions ro;
|
||||
ro.readahead_size = alignment;
|
||||
ASSERT_OK(db_->VerifyFileChecksums(ro));
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ASSERT_TRUE(last_read);
|
||||
ASSERT_EQ(env_->random_read_counter_.Read(),
|
||||
(sst_size + alignment - 1) / (alignment));
|
||||
}
|
||||
|
||||
// TODO: re-enable after we provide finer-grained control for WAL tracking to
|
||||
// meet the needs of different use cases, durability levels and recovery modes.
|
||||
TEST_F(DBBasicTest, DISABLED_ManualWalSync) {
|
||||
|
||||
@@ -287,9 +287,11 @@ class PersistentCacheFromCache : public PersistentCache {
|
||||
};
|
||||
|
||||
class ReadOnlyCacheWrapper : public CacheWrapper {
|
||||
public:
|
||||
using CacheWrapper::CacheWrapper;
|
||||
|
||||
using Cache::Insert;
|
||||
const char* Name() const override { return "ReadOnlyCacheWrapper"; }
|
||||
|
||||
Status Insert(const Slice& /*key*/, Cache::ObjectPtr /*value*/,
|
||||
const CacheItemHelper* /*helper*/, size_t /*charge*/,
|
||||
Handle** /*handle*/, Priority /*priority*/) override {
|
||||
@@ -711,10 +713,11 @@ class LookupLiarCache : public CacheWrapper {
|
||||
explicit LookupLiarCache(std::shared_ptr<Cache> target)
|
||||
: CacheWrapper(std::move(target)) {}
|
||||
|
||||
using Cache::Lookup;
|
||||
const char* Name() const override { return "LookupLiarCache"; }
|
||||
|
||||
Handle* Lookup(const Slice& key, const CacheItemHelper* helper = nullptr,
|
||||
CreateContext* create_context = nullptr,
|
||||
Priority priority = Priority::LOW, bool wait = true,
|
||||
Priority priority = Priority::LOW,
|
||||
Statistics* stats = nullptr) override {
|
||||
if (nth_lookup_not_found_ == 1) {
|
||||
nth_lookup_not_found_ = 0;
|
||||
@@ -723,8 +726,7 @@ class LookupLiarCache : public CacheWrapper {
|
||||
if (nth_lookup_not_found_ > 1) {
|
||||
--nth_lookup_not_found_;
|
||||
}
|
||||
return CacheWrapper::Lookup(key, helper, create_context, priority, wait,
|
||||
stats);
|
||||
return CacheWrapper::Lookup(key, helper, create_context, priority, stats);
|
||||
}
|
||||
|
||||
// 1 == next lookup, 2 == after next, etc.
|
||||
|
||||
@@ -34,11 +34,8 @@ Status DBImpl::FlushForGetLiveFiles() {
|
||||
// flush all dirty data to disk.
|
||||
Status status;
|
||||
if (immutable_db_options_.atomic_flush) {
|
||||
autovector<ColumnFamilyData*> cfds;
|
||||
SelectColumnFamiliesForAtomicFlush(&cfds);
|
||||
mutex_.Unlock();
|
||||
status =
|
||||
AtomicFlushMemTables(cfds, FlushOptions(), FlushReason::kGetLiveFiles);
|
||||
status = AtomicFlushMemTables(FlushOptions(), FlushReason::kGetLiveFiles);
|
||||
if (status.IsColumnFamilyDropped()) {
|
||||
status = Status::OK();
|
||||
}
|
||||
@@ -437,4 +434,3 @@ Status DBImpl::GetLiveFilesStorageInfo(
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
+91
-1
@@ -740,7 +740,97 @@ class TestFlushListener : public EventListener {
|
||||
DBFlushTest* test_;
|
||||
};
|
||||
|
||||
// RocksDB lite does not support GetLiveFiles()
|
||||
TEST_F(
|
||||
DBFlushTest,
|
||||
FixUnrecoverableWriteDuringAtomicFlushWaitUntilFlushWouldNotStallWrites) {
|
||||
Options options = CurrentOptions();
|
||||
options.atomic_flush = true;
|
||||
|
||||
// To simulate a real-life crash where we can't flush during db's shutdown
|
||||
options.avoid_flush_during_shutdown = true;
|
||||
|
||||
// Set 3 low thresholds (while `disable_auto_compactions=false`) here so flush
|
||||
// adding one more L0 file during `GetLiveFiles()` will have to wait till such
|
||||
// flush will not stall writes
|
||||
options.level0_stop_writes_trigger = 2;
|
||||
options.level0_slowdown_writes_trigger = 2;
|
||||
// Disable level-0 compaction triggered by number of files to avoid
|
||||
// stalling check being skipped (resulting in the flush mentioned above didn't
|
||||
// wait)
|
||||
options.level0_file_num_compaction_trigger = -1;
|
||||
|
||||
CreateAndReopenWithCF({"cf1"}, options);
|
||||
|
||||
// Manually pause compaction thread to ensure enough L0 files as
|
||||
// `disable_auto_compactions=false`is needed, in order to meet the 3 low
|
||||
// thresholds above
|
||||
std::unique_ptr<test::SleepingBackgroundTask> sleeping_task_;
|
||||
sleeping_task_.reset(new test::SleepingBackgroundTask());
|
||||
env_->SetBackgroundThreads(1, Env::LOW);
|
||||
env_->Schedule(&test::SleepingBackgroundTask::DoSleepTask,
|
||||
sleeping_task_.get(), Env::Priority::LOW);
|
||||
sleeping_task_->WaitUntilSleeping();
|
||||
|
||||
// Create some initial file to help meet the 3 low thresholds above
|
||||
ASSERT_OK(Put(1, "dontcare", "dontcare"));
|
||||
ASSERT_OK(Flush(1));
|
||||
|
||||
// Insert some initial data so we have something to atomic-flush later
|
||||
// triggered by `GetLiveFiles()`
|
||||
WriteOptions write_opts;
|
||||
write_opts.disableWAL = true;
|
||||
ASSERT_OK(Put(1, "k1", "v1", write_opts));
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency({{
|
||||
"DBImpl::WaitUntilFlushWouldNotStallWrites:StallWait",
|
||||
"DBFlushTest::"
|
||||
"UnrecoverableWriteInAtomicFlushWaitUntilFlushWouldNotStallWrites::Write",
|
||||
}});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
// Write to db when atomic flush releases the lock to wait on write stall
|
||||
// condition to be gone in `WaitUntilFlushWouldNotStallWrites()`
|
||||
port::Thread write_thread([&] {
|
||||
TEST_SYNC_POINT(
|
||||
"DBFlushTest::"
|
||||
"UnrecoverableWriteInAtomicFlushWaitUntilFlushWouldNotStallWrites::"
|
||||
"Write");
|
||||
// Before the fix, the empty default CF would've been prematurely excluded
|
||||
// from this atomic flush. The following two writes together make default CF
|
||||
// later contain data that should've been included in the atomic flush.
|
||||
ASSERT_OK(Put(0, "k2", "v2", write_opts));
|
||||
// The following write increases the max seqno of this atomic flush to be 3,
|
||||
// which is greater than the seqno of default CF's data. This then violates
|
||||
// the invariant that all entries of seqno less than the max seqno
|
||||
// of this atomic flush should've been flushed by the time of this atomic
|
||||
// flush finishes.
|
||||
ASSERT_OK(Put(1, "k3", "v3", write_opts));
|
||||
|
||||
// Resume compaction threads and reduce L0 files so `GetLiveFiles()` can
|
||||
// resume from the wait
|
||||
sleeping_task_->WakeUp();
|
||||
sleeping_task_->WaitUntilDone();
|
||||
MoveFilesToLevel(1, 1);
|
||||
});
|
||||
|
||||
// Trigger an atomic flush by `GetLiveFiles()`
|
||||
std::vector<std::string> files;
|
||||
uint64_t manifest_file_size;
|
||||
ASSERT_OK(db_->GetLiveFiles(files, &manifest_file_size, /*flush*/ true));
|
||||
|
||||
write_thread.join();
|
||||
|
||||
ReopenWithColumnFamilies({"default", "cf1"}, options);
|
||||
|
||||
ASSERT_EQ(Get(1, "k3"), "v3");
|
||||
// Prior to the fix, `Get()` will return `NotFound as "k2" entry in default CF
|
||||
// can't be recovered from a crash right after the atomic flush finishes,
|
||||
// resulting in a "recovery hole" as "k3" can be recovered. It's due to the
|
||||
// invariant violation described above.
|
||||
ASSERT_EQ(Get(0, "k2"), "v2");
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
}
|
||||
|
||||
TEST_F(DBFlushTest, FixFlushReasonRaceFromConcurrentFlushes) {
|
||||
Options options = CurrentOptions();
|
||||
options.atomic_flush = true;
|
||||
|
||||
+110
-21
@@ -387,10 +387,8 @@ Status DBImpl::ResumeImpl(DBRecoverContext context) {
|
||||
// We allow flush to stall write since we are trying to resume from error.
|
||||
flush_opts.allow_write_stall = true;
|
||||
if (immutable_db_options_.atomic_flush) {
|
||||
autovector<ColumnFamilyData*> cfds;
|
||||
SelectColumnFamiliesForAtomicFlush(&cfds);
|
||||
mutex_.Unlock();
|
||||
s = AtomicFlushMemTables(cfds, flush_opts, context.flush_reason);
|
||||
s = AtomicFlushMemTables(flush_opts, context.flush_reason);
|
||||
mutex_.Lock();
|
||||
} else {
|
||||
for (auto cfd : versions_->GetRefedColumnFamilySet()) {
|
||||
@@ -507,11 +505,8 @@ void DBImpl::CancelAllBackgroundWork(bool wait) {
|
||||
has_unpersisted_data_.load(std::memory_order_relaxed) &&
|
||||
!mutable_db_options_.avoid_flush_during_shutdown) {
|
||||
if (immutable_db_options_.atomic_flush) {
|
||||
autovector<ColumnFamilyData*> cfds;
|
||||
SelectColumnFamiliesForAtomicFlush(&cfds);
|
||||
mutex_.Unlock();
|
||||
Status s =
|
||||
AtomicFlushMemTables(cfds, FlushOptions(), FlushReason::kShutDown);
|
||||
Status s = AtomicFlushMemTables(FlushOptions(), FlushReason::kShutDown);
|
||||
s.PermitUncheckedError(); //**TODO: What to do on error?
|
||||
mutex_.Lock();
|
||||
} else {
|
||||
@@ -1539,6 +1534,14 @@ Status DBImpl::LockWAL() {
|
||||
assert(lock_wal_write_token_);
|
||||
++lock_wal_count_;
|
||||
} else {
|
||||
// NOTE: this will "unnecessarily" wait for other non-LockWAL() write
|
||||
// stalls to clear before LockWAL returns, however fixing that would
|
||||
// not be simple because if we notice the primary queue is already
|
||||
// stalled, that stall might clear while we release DB mutex in
|
||||
// EnterUnbatched() for the nonmem queue. And if we work around that in
|
||||
// the naive way, we could deadlock by locking the two queues in different
|
||||
// orders.
|
||||
|
||||
WriteThread::Writer w;
|
||||
write_thread_.EnterUnbatched(&w, &mutex_);
|
||||
WriteThread::Writer nonmem_w;
|
||||
@@ -1571,6 +1574,8 @@ Status DBImpl::LockWAL() {
|
||||
|
||||
Status DBImpl::UnlockWAL() {
|
||||
bool signal = false;
|
||||
uint64_t maybe_stall_begun_count = 0;
|
||||
uint64_t nonmem_maybe_stall_begun_count = 0;
|
||||
{
|
||||
InstrumentedMutexLock lock(&mutex_);
|
||||
if (lock_wal_count_ == 0) {
|
||||
@@ -1580,12 +1585,28 @@ Status DBImpl::UnlockWAL() {
|
||||
if (lock_wal_count_ == 0) {
|
||||
lock_wal_write_token_.reset();
|
||||
signal = true;
|
||||
// For the last UnlockWAL, we don't want to return from UnlockWAL()
|
||||
// until the thread(s) that called BeginWriteStall() have had a chance to
|
||||
// call EndWriteStall(), so that no_slowdown writes after UnlockWAL() are
|
||||
// guaranteed to succeed if there's no other source of stall.
|
||||
maybe_stall_begun_count = write_thread_.GetBegunCountOfOutstandingStall();
|
||||
if (two_write_queues_) {
|
||||
nonmem_maybe_stall_begun_count =
|
||||
nonmem_write_thread_.GetBegunCountOfOutstandingStall();
|
||||
}
|
||||
}
|
||||
}
|
||||
if (signal) {
|
||||
// SignalAll outside of mutex for efficiency
|
||||
bg_cv_.SignalAll();
|
||||
}
|
||||
// Ensure stalls have cleared
|
||||
if (maybe_stall_begun_count) {
|
||||
write_thread_.WaitForStallEndedCount(maybe_stall_begun_count);
|
||||
}
|
||||
if (nonmem_maybe_stall_begun_count) {
|
||||
nonmem_write_thread_.WaitForStallEndedCount(nonmem_maybe_stall_begun_count);
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
@@ -2579,14 +2600,25 @@ void DBImpl::MultiGet(const ReadOptions& read_options, const size_t num_keys,
|
||||
ColumnFamilyHandle** column_families, const Slice* keys,
|
||||
PinnableSlice* values, Status* statuses,
|
||||
const bool sorted_input) {
|
||||
return MultiGet(read_options, num_keys, column_families, keys, values,
|
||||
/*timestamps=*/nullptr, statuses, sorted_input);
|
||||
MultiGet(read_options, num_keys, column_families, keys, values,
|
||||
/* timestamps */ nullptr, statuses, sorted_input);
|
||||
}
|
||||
|
||||
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) {
|
||||
MultiGetCommon(read_options, num_keys, column_families, keys, values,
|
||||
/* columns */ nullptr, timestamps, statuses, sorted_input);
|
||||
}
|
||||
|
||||
void DBImpl::MultiGetCommon(const ReadOptions& read_options,
|
||||
const size_t num_keys,
|
||||
ColumnFamilyHandle** column_families,
|
||||
const Slice* keys, PinnableSlice* values,
|
||||
PinnableWideColumns* columns,
|
||||
std::string* timestamps, Status* statuses,
|
||||
const bool sorted_input) {
|
||||
if (num_keys == 0) {
|
||||
return;
|
||||
}
|
||||
@@ -2632,8 +2664,20 @@ void DBImpl::MultiGet(const ReadOptions& read_options, const size_t num_keys,
|
||||
autovector<KeyContext*, MultiGetContext::MAX_BATCH_SIZE> sorted_keys;
|
||||
sorted_keys.resize(num_keys);
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
values[i].Reset();
|
||||
key_context.emplace_back(column_families[i], keys[i], &values[i],
|
||||
PinnableSlice* val = nullptr;
|
||||
PinnableWideColumns* col = nullptr;
|
||||
|
||||
if (values) {
|
||||
val = &values[i];
|
||||
val->Reset();
|
||||
} else {
|
||||
assert(columns);
|
||||
|
||||
col = &columns[i];
|
||||
col->Reset();
|
||||
}
|
||||
|
||||
key_context.emplace_back(column_families[i], keys[i], val, col,
|
||||
timestamps ? ×tamps[i] : nullptr,
|
||||
&statuses[i]);
|
||||
}
|
||||
@@ -2757,8 +2801,8 @@ void DBImpl::MultiGet(const ReadOptions& read_options,
|
||||
ColumnFamilyHandle* column_family, const size_t num_keys,
|
||||
const Slice* keys, PinnableSlice* values,
|
||||
Status* statuses, const bool sorted_input) {
|
||||
return MultiGet(read_options, column_family, num_keys, keys, values,
|
||||
/*timestamp=*/nullptr, statuses, sorted_input);
|
||||
MultiGet(read_options, column_family, num_keys, keys, values,
|
||||
/* timestamps */ nullptr, statuses, sorted_input);
|
||||
}
|
||||
|
||||
void DBImpl::MultiGet(const ReadOptions& read_options,
|
||||
@@ -2766,6 +2810,16 @@ void DBImpl::MultiGet(const ReadOptions& read_options,
|
||||
const Slice* keys, PinnableSlice* values,
|
||||
std::string* timestamps, Status* statuses,
|
||||
const bool sorted_input) {
|
||||
MultiGetCommon(read_options, column_family, num_keys, keys, values,
|
||||
/* columns */ nullptr, timestamps, statuses, sorted_input);
|
||||
}
|
||||
|
||||
void DBImpl::MultiGetCommon(const ReadOptions& read_options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
const size_t num_keys, const Slice* keys,
|
||||
PinnableSlice* values, PinnableWideColumns* columns,
|
||||
std::string* timestamps, Status* statuses,
|
||||
bool sorted_input) {
|
||||
if (tracer_) {
|
||||
// TODO: This mutex should be removed later, to improve performance when
|
||||
// tracing is enabled.
|
||||
@@ -2779,8 +2833,20 @@ void DBImpl::MultiGet(const ReadOptions& read_options,
|
||||
autovector<KeyContext*, MultiGetContext::MAX_BATCH_SIZE> sorted_keys;
|
||||
sorted_keys.resize(num_keys);
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
values[i].Reset();
|
||||
key_context.emplace_back(column_family, keys[i], &values[i],
|
||||
PinnableSlice* val = nullptr;
|
||||
PinnableWideColumns* col = nullptr;
|
||||
|
||||
if (values) {
|
||||
val = &values[i];
|
||||
val->Reset();
|
||||
} else {
|
||||
assert(columns);
|
||||
|
||||
col = &columns[i];
|
||||
col->Reset();
|
||||
}
|
||||
|
||||
key_context.emplace_back(column_family, keys[i], val, col,
|
||||
timestamps ? ×tamps[i] : nullptr,
|
||||
&statuses[i]);
|
||||
}
|
||||
@@ -2942,8 +3008,17 @@ Status DBImpl::MultiGetImpl(
|
||||
uint64_t bytes_read = 0;
|
||||
for (size_t i = start_key; i < start_key + num_keys - keys_left; ++i) {
|
||||
KeyContext* key = (*sorted_keys)[i];
|
||||
assert(key);
|
||||
assert(key->s);
|
||||
|
||||
if (key->s->ok()) {
|
||||
bytes_read += key->value->size();
|
||||
if (key->value) {
|
||||
bytes_read += key->value->size();
|
||||
} else {
|
||||
assert(key->columns);
|
||||
bytes_read += key->columns->serialized_size();
|
||||
}
|
||||
|
||||
num_found++;
|
||||
}
|
||||
}
|
||||
@@ -2967,6 +3042,22 @@ Status DBImpl::MultiGetImpl(
|
||||
return s;
|
||||
}
|
||||
|
||||
void DBImpl::MultiGetEntity(const ReadOptions& options, size_t num_keys,
|
||||
ColumnFamilyHandle** column_families,
|
||||
const Slice* keys, PinnableWideColumns* results,
|
||||
Status* statuses, bool sorted_input) {
|
||||
MultiGetCommon(options, num_keys, column_families, keys, /* values */ nullptr,
|
||||
results, /* timestamps */ nullptr, statuses, sorted_input);
|
||||
}
|
||||
|
||||
void DBImpl::MultiGetEntity(const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family, size_t num_keys,
|
||||
const Slice* keys, PinnableWideColumns* results,
|
||||
Status* statuses, bool sorted_input) {
|
||||
MultiGetCommon(options, column_family, num_keys, keys, /* values */ nullptr,
|
||||
results, /* timestamps */ nullptr, statuses, sorted_input);
|
||||
}
|
||||
|
||||
Status DBImpl::CreateColumnFamily(const ColumnFamilyOptions& cf_options,
|
||||
const std::string& column_family,
|
||||
ColumnFamilyHandle** handle) {
|
||||
@@ -5254,12 +5345,10 @@ Status DBImpl::IngestExternalFiles(
|
||||
FlushOptions flush_opts;
|
||||
flush_opts.allow_write_stall = true;
|
||||
if (immutable_db_options_.atomic_flush) {
|
||||
autovector<ColumnFamilyData*> cfds_to_flush;
|
||||
SelectColumnFamiliesForAtomicFlush(&cfds_to_flush);
|
||||
mutex_.Unlock();
|
||||
status = AtomicFlushMemTables(cfds_to_flush, flush_opts,
|
||||
FlushReason::kExternalFileIngestion,
|
||||
true /* entered_write_thread */);
|
||||
status = AtomicFlushMemTables(
|
||||
flush_opts, FlushReason::kExternalFileIngestion,
|
||||
{} /* provided_candidate_cfds */, true /* entered_write_thread */);
|
||||
mutex_.Lock();
|
||||
} else {
|
||||
for (size_t i = 0; i != num_cfs; ++i) {
|
||||
|
||||
+58
-25
@@ -277,33 +277,40 @@ class DBImpl : public DB {
|
||||
// The values and statuses parameters are arrays with number of elements
|
||||
// equal to keys.size(). This allows the storage for those to be alloacted
|
||||
// by the caller on the stack for small batches
|
||||
virtual void MultiGet(const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
const size_t num_keys, const Slice* keys,
|
||||
PinnableSlice* values, Status* statuses,
|
||||
const bool sorted_input = false) override;
|
||||
virtual void MultiGet(const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
const size_t num_keys, const Slice* keys,
|
||||
PinnableSlice* values, std::string* timestamps,
|
||||
Status* statuses,
|
||||
const bool sorted_input = false) override;
|
||||
void MultiGet(const ReadOptions& options, ColumnFamilyHandle* column_family,
|
||||
const size_t num_keys, const Slice* keys, PinnableSlice* values,
|
||||
Status* statuses, const bool sorted_input = false) override;
|
||||
void MultiGet(const ReadOptions& options, ColumnFamilyHandle* column_family,
|
||||
const size_t num_keys, const Slice* keys, PinnableSlice* values,
|
||||
std::string* timestamps, Status* statuses,
|
||||
const bool sorted_input = false) override;
|
||||
|
||||
virtual void MultiGet(const ReadOptions& options, const size_t num_keys,
|
||||
ColumnFamilyHandle** column_families, const Slice* keys,
|
||||
PinnableSlice* values, Status* statuses,
|
||||
const bool sorted_input = false) override;
|
||||
virtual void MultiGet(const ReadOptions& options, const size_t num_keys,
|
||||
ColumnFamilyHandle** column_families, const Slice* keys,
|
||||
PinnableSlice* values, std::string* timestamps,
|
||||
Status* statuses,
|
||||
const bool sorted_input = false) override;
|
||||
void MultiGet(const ReadOptions& options, const size_t num_keys,
|
||||
ColumnFamilyHandle** column_families, const Slice* keys,
|
||||
PinnableSlice* values, Status* statuses,
|
||||
const bool sorted_input = false) override;
|
||||
void MultiGet(const ReadOptions& options, const size_t num_keys,
|
||||
ColumnFamilyHandle** column_families, const Slice* keys,
|
||||
PinnableSlice* values, std::string* timestamps,
|
||||
Status* statuses, const bool sorted_input = false) override;
|
||||
|
||||
virtual void MultiGetWithCallback(
|
||||
void MultiGetWithCallback(
|
||||
const ReadOptions& options, ColumnFamilyHandle* column_family,
|
||||
ReadCallback* callback,
|
||||
autovector<KeyContext*, MultiGetContext::MAX_BATCH_SIZE>* sorted_keys);
|
||||
|
||||
using DB::MultiGetEntity;
|
||||
|
||||
void MultiGetEntity(const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family, size_t num_keys,
|
||||
const Slice* keys, PinnableWideColumns* results,
|
||||
Status* statuses, bool sorted_input) override;
|
||||
|
||||
void MultiGetEntity(const ReadOptions& options, size_t num_keys,
|
||||
ColumnFamilyHandle** column_families, const Slice* keys,
|
||||
PinnableWideColumns* results, Status* statuses,
|
||||
bool sorted_input) override;
|
||||
|
||||
virtual Status CreateColumnFamily(const ColumnFamilyOptions& cf_options,
|
||||
const std::string& column_family,
|
||||
ColumnFamilyHandle** handle) override;
|
||||
@@ -1074,8 +1081,9 @@ class DBImpl : public DB {
|
||||
// is because in certain cases, we can flush column families, wait for the
|
||||
// flush to complete, but delete the column family handle before the wait
|
||||
// finishes. For example in CompactRange.
|
||||
Status TEST_AtomicFlushMemTables(const autovector<ColumnFamilyData*>& cfds,
|
||||
const FlushOptions& flush_opts);
|
||||
Status TEST_AtomicFlushMemTables(
|
||||
const autovector<ColumnFamilyData*>& provided_candidate_cfds,
|
||||
const FlushOptions& flush_opts);
|
||||
|
||||
// Wait for background threads to complete scheduled work.
|
||||
Status TEST_WaitForBackgroundWork();
|
||||
@@ -1117,6 +1125,8 @@ class DBImpl : public DB {
|
||||
|
||||
void TEST_UnlockMutex();
|
||||
|
||||
void TEST_SignalAllBgCv();
|
||||
|
||||
// REQUIRES: mutex locked
|
||||
void* TEST_BeginWrite();
|
||||
|
||||
@@ -1877,16 +1887,27 @@ class DBImpl : public DB {
|
||||
|
||||
Status SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context);
|
||||
|
||||
void SelectColumnFamiliesForAtomicFlush(autovector<ColumnFamilyData*>* cfds);
|
||||
// Select and output column families qualified for atomic flush in
|
||||
// `selected_cfds`. If `provided_candidate_cfds` is non-empty, it will be used
|
||||
// as candidate CFs to select qualified ones from. Otherwise, all column
|
||||
// families are used as candidate to select from.
|
||||
//
|
||||
// REQUIRES: mutex held
|
||||
void SelectColumnFamiliesForAtomicFlush(
|
||||
autovector<ColumnFamilyData*>* selected_cfds,
|
||||
const autovector<ColumnFamilyData*>& provided_candidate_cfds = {});
|
||||
|
||||
// Force current memtable contents to be flushed.
|
||||
Status FlushMemTable(ColumnFamilyData* cfd, const FlushOptions& options,
|
||||
FlushReason flush_reason,
|
||||
bool entered_write_thread = false);
|
||||
|
||||
// Atomic-flush memtables from quanlified CFs among `provided_candidate_cfds`
|
||||
// (if non-empty) or amomg all column families and atomically record the
|
||||
// result to the MANIFEST.
|
||||
Status AtomicFlushMemTables(
|
||||
const autovector<ColumnFamilyData*>& column_family_datas,
|
||||
const FlushOptions& options, FlushReason flush_reason,
|
||||
const autovector<ColumnFamilyData*>& provided_candidate_cfds = {},
|
||||
bool entered_write_thread = false);
|
||||
|
||||
// Wait until flushing this column family won't stall writes
|
||||
@@ -2189,6 +2210,18 @@ class DBImpl : public DB {
|
||||
const size_t num_keys, bool sorted,
|
||||
autovector<KeyContext*, MultiGetContext::MAX_BATCH_SIZE>* key_ptrs);
|
||||
|
||||
void MultiGetCommon(const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family, const size_t num_keys,
|
||||
const Slice* keys, PinnableSlice* values,
|
||||
PinnableWideColumns* columns, std::string* timestamps,
|
||||
Status* statuses, bool sorted_input);
|
||||
|
||||
void MultiGetCommon(const ReadOptions& options, const size_t num_keys,
|
||||
ColumnFamilyHandle** column_families, const Slice* keys,
|
||||
PinnableSlice* values, PinnableWideColumns* columns,
|
||||
std::string* timestamps, Status* statuses,
|
||||
bool sorted_input);
|
||||
|
||||
// A structure to hold the information required to process MultiGet of keys
|
||||
// belonging to one column family. For a multi column family MultiGet, there
|
||||
// will be a container of these objects.
|
||||
|
||||
@@ -414,7 +414,7 @@ Status DBImpl::AtomicFlushMemTablesToOutputFiles(
|
||||
assert(cfd->imm()->NumNotFlushed() != 0);
|
||||
assert(cfd->imm()->IsFlushPending());
|
||||
}
|
||||
for (const auto bg_flush_arg : bg_flush_args) {
|
||||
for (const auto& bg_flush_arg : bg_flush_args) {
|
||||
assert(bg_flush_arg.flush_reason_ == bg_flush_args[0].flush_reason_);
|
||||
}
|
||||
#endif /* !NDEBUG */
|
||||
@@ -1031,15 +1031,9 @@ Status DBImpl::CompactRangeInternal(const CompactRangeOptions& options,
|
||||
FlushOptions fo;
|
||||
fo.allow_write_stall = options.allow_write_stall;
|
||||
if (immutable_db_options_.atomic_flush) {
|
||||
autovector<ColumnFamilyData*> cfds;
|
||||
mutex_.Lock();
|
||||
SelectColumnFamiliesForAtomicFlush(&cfds);
|
||||
mutex_.Unlock();
|
||||
s = AtomicFlushMemTables(cfds, fo, FlushReason::kManualCompaction,
|
||||
false /* entered_write_thread */);
|
||||
s = AtomicFlushMemTables(fo, FlushReason::kManualCompaction);
|
||||
} else {
|
||||
s = FlushMemTable(cfd, fo, FlushReason::kManualCompaction,
|
||||
false /* entered_write_thread */);
|
||||
s = FlushMemTable(cfd, fo, FlushReason::kManualCompaction);
|
||||
}
|
||||
if (!s.ok()) {
|
||||
LogFlush(immutable_db_options_.info_log);
|
||||
@@ -1800,8 +1794,8 @@ Status DBImpl::Flush(const FlushOptions& flush_options,
|
||||
cfh->GetName().c_str());
|
||||
Status s;
|
||||
if (immutable_db_options_.atomic_flush) {
|
||||
s = AtomicFlushMemTables({cfh->cfd()}, flush_options,
|
||||
FlushReason::kManualFlush);
|
||||
s = AtomicFlushMemTables(flush_options, FlushReason::kManualFlush,
|
||||
{cfh->cfd()});
|
||||
} else {
|
||||
s = FlushMemTable(cfh->cfd(), flush_options, FlushReason::kManualFlush);
|
||||
}
|
||||
@@ -1839,7 +1833,7 @@ Status DBImpl::Flush(const FlushOptions& flush_options,
|
||||
auto cfh = static_cast<ColumnFamilyHandleImpl*>(elem);
|
||||
cfds.emplace_back(cfh->cfd());
|
||||
});
|
||||
s = AtomicFlushMemTables(cfds, flush_options, FlushReason::kManualFlush);
|
||||
s = AtomicFlushMemTables(flush_options, FlushReason::kManualFlush, cfds);
|
||||
ROCKS_LOG_INFO(immutable_db_options_.info_log,
|
||||
"Manual atomic flush finished, status: %s\n"
|
||||
"=====Column families:=====",
|
||||
@@ -2223,11 +2217,9 @@ Status DBImpl::FlushMemTable(ColumnFamilyData* cfd,
|
||||
return s;
|
||||
}
|
||||
|
||||
// Flush all elements in 'column_family_datas'
|
||||
// and atomically record the result to the MANIFEST.
|
||||
Status DBImpl::AtomicFlushMemTables(
|
||||
const autovector<ColumnFamilyData*>& column_family_datas,
|
||||
const FlushOptions& flush_options, FlushReason flush_reason,
|
||||
const autovector<ColumnFamilyData*>& provided_candidate_cfds,
|
||||
bool entered_write_thread) {
|
||||
assert(immutable_db_options_.atomic_flush);
|
||||
if (!flush_options.wait && write_controller_.IsStopped()) {
|
||||
@@ -2237,18 +2229,48 @@ Status DBImpl::AtomicFlushMemTables(
|
||||
return Status::TryAgain(oss.str());
|
||||
}
|
||||
Status s;
|
||||
autovector<ColumnFamilyData*> candidate_cfds;
|
||||
if (provided_candidate_cfds.empty()) {
|
||||
// Generate candidate cfds if not provided
|
||||
{
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
for (ColumnFamilyData* cfd : *versions_->GetColumnFamilySet()) {
|
||||
if (!cfd->IsDropped() && cfd->initialized()) {
|
||||
cfd->Ref();
|
||||
candidate_cfds.push_back(cfd);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
candidate_cfds = provided_candidate_cfds;
|
||||
}
|
||||
|
||||
if (!flush_options.allow_write_stall) {
|
||||
int num_cfs_to_flush = 0;
|
||||
for (auto cfd : column_family_datas) {
|
||||
for (auto cfd : candidate_cfds) {
|
||||
bool flush_needed = true;
|
||||
s = WaitUntilFlushWouldNotStallWrites(cfd, &flush_needed);
|
||||
if (!s.ok()) {
|
||||
// Unref the newly generated candidate cfds (when not provided) in
|
||||
// `candidate_cfds`
|
||||
if (provided_candidate_cfds.empty()) {
|
||||
for (auto candidate_cfd : candidate_cfds) {
|
||||
candidate_cfd->UnrefAndTryDelete();
|
||||
}
|
||||
}
|
||||
return s;
|
||||
} else if (flush_needed) {
|
||||
++num_cfs_to_flush;
|
||||
}
|
||||
}
|
||||
if (0 == num_cfs_to_flush) {
|
||||
// Unref the newly generated candidate cfds (when not provided) in
|
||||
// `candidate_cfds`
|
||||
if (provided_candidate_cfds.empty()) {
|
||||
for (auto candidate_cfd : candidate_cfds) {
|
||||
candidate_cfd->UnrefAndTryDelete();
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
}
|
||||
@@ -2269,15 +2291,16 @@ Status DBImpl::AtomicFlushMemTables(
|
||||
}
|
||||
WaitForPendingWrites();
|
||||
|
||||
for (auto cfd : column_family_datas) {
|
||||
if (cfd->IsDropped()) {
|
||||
continue;
|
||||
}
|
||||
if (cfd->imm()->NumNotFlushed() != 0 || !cfd->mem()->IsEmpty() ||
|
||||
!cached_recoverable_state_empty_.load()) {
|
||||
cfds.emplace_back(cfd);
|
||||
SelectColumnFamiliesForAtomicFlush(&cfds, candidate_cfds);
|
||||
|
||||
// Unref the newly generated candidate cfds (when not provided) in
|
||||
// `candidate_cfds`
|
||||
if (provided_candidate_cfds.empty()) {
|
||||
for (auto candidate_cfd : candidate_cfds) {
|
||||
candidate_cfd->UnrefAndTryDelete();
|
||||
}
|
||||
}
|
||||
|
||||
for (auto cfd : cfds) {
|
||||
if ((cfd->mem()->IsEmpty() && cached_recoverable_state_empty_.load()) ||
|
||||
flush_reason == FlushReason::kErrorRecoveryRetryFlush) {
|
||||
@@ -2908,7 +2931,7 @@ Status DBImpl::BackgroundFlush(bool* made_progress, JobContext* job_context,
|
||||
// All the CFD/bg_flush_arg in the FlushReq must have the same flush reason, so
|
||||
// just grab the first one
|
||||
#ifndef NDEBUG
|
||||
for (const auto bg_flush_arg : bg_flush_args) {
|
||||
for (const auto& bg_flush_arg : bg_flush_args) {
|
||||
assert(bg_flush_arg.flush_reason_ == bg_flush_args[0].flush_reason_);
|
||||
}
|
||||
#endif /* !NDEBUG */
|
||||
|
||||
@@ -155,8 +155,10 @@ Status DBImpl::TEST_FlushMemTable(ColumnFamilyData* cfd,
|
||||
}
|
||||
|
||||
Status DBImpl::TEST_AtomicFlushMemTables(
|
||||
const autovector<ColumnFamilyData*>& cfds, const FlushOptions& flush_opts) {
|
||||
return AtomicFlushMemTables(cfds, flush_opts, FlushReason::kTest);
|
||||
const autovector<ColumnFamilyData*>& provided_candidate_cfds,
|
||||
const FlushOptions& flush_opts) {
|
||||
return AtomicFlushMemTables(flush_opts, FlushReason::kTest,
|
||||
provided_candidate_cfds);
|
||||
}
|
||||
|
||||
Status DBImpl::TEST_WaitForBackgroundWork() {
|
||||
@@ -198,6 +200,8 @@ void DBImpl::TEST_LockMutex() { mutex_.Lock(); }
|
||||
|
||||
void DBImpl::TEST_UnlockMutex() { mutex_.Unlock(); }
|
||||
|
||||
void DBImpl::TEST_SignalAllBgCv() { bg_cv_.SignalAll(); }
|
||||
|
||||
void* DBImpl::TEST_BeginWrite() {
|
||||
auto w = new WriteThread::Writer();
|
||||
write_thread_.EnterUnbatched(w, &mutex_);
|
||||
|
||||
@@ -361,8 +361,6 @@ void DBImpl::DeleteObsoleteFileImpl(int job_id, const std::string& fname,
|
||||
}
|
||||
TEST_SYNC_POINT_CALLBACK("DBImpl::DeleteObsoleteFileImpl:AfterDeletion",
|
||||
&file_deletion_status);
|
||||
TEST_SYNC_POINT_CALLBACK("DBImpl::DeleteObsoleteFileImpl:AfterDeletion2",
|
||||
const_cast<std::string*>(&fname));
|
||||
if (file_deletion_status.ok()) {
|
||||
ROCKS_LOG_DEBUG(immutable_db_options_.info_log,
|
||||
"[JOB %d] Delete %s type=%d #%" PRIu64 " -- %s\n", job_id,
|
||||
|
||||
@@ -216,7 +216,7 @@ Status ValidateOptionsByTable(
|
||||
const DBOptions& db_opts,
|
||||
const std::vector<ColumnFamilyDescriptor>& column_families) {
|
||||
Status s;
|
||||
for (auto cf : column_families) {
|
||||
for (auto& cf : column_families) {
|
||||
s = ValidateOptions(db_opts, cf.options);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
|
||||
@@ -1211,6 +1211,9 @@ Status DBImpl::PreprocessWrite(const WriteOptions& write_options,
|
||||
// exceeded at this point so no new write (including current one) will go
|
||||
// through until memory usage is decreased.
|
||||
if (UNLIKELY(status.ok() && write_buffer_manager_->ShouldStall())) {
|
||||
default_cf_internal_stats_->AddDBStats(
|
||||
InternalStats::kIntStatsWriteBufferManagerLimitStopsCounts, 1,
|
||||
true /* concurrent */);
|
||||
if (write_options.no_slowdown) {
|
||||
status = Status::Incomplete("Write stall");
|
||||
} else {
|
||||
@@ -1543,14 +1546,40 @@ Status DBImpl::WriteRecoverableState() {
|
||||
}
|
||||
|
||||
void DBImpl::SelectColumnFamiliesForAtomicFlush(
|
||||
autovector<ColumnFamilyData*>* cfds) {
|
||||
for (ColumnFamilyData* cfd : *versions_->GetColumnFamilySet()) {
|
||||
autovector<ColumnFamilyData*>* selected_cfds,
|
||||
const autovector<ColumnFamilyData*>& provided_candidate_cfds) {
|
||||
mutex_.AssertHeld();
|
||||
assert(selected_cfds);
|
||||
|
||||
autovector<ColumnFamilyData*> candidate_cfds;
|
||||
|
||||
// Generate candidate cfds if not provided
|
||||
if (provided_candidate_cfds.empty()) {
|
||||
for (ColumnFamilyData* cfd : *versions_->GetColumnFamilySet()) {
|
||||
if (!cfd->IsDropped() && cfd->initialized()) {
|
||||
cfd->Ref();
|
||||
candidate_cfds.push_back(cfd);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
candidate_cfds = provided_candidate_cfds;
|
||||
}
|
||||
|
||||
for (ColumnFamilyData* cfd : candidate_cfds) {
|
||||
if (cfd->IsDropped()) {
|
||||
continue;
|
||||
}
|
||||
if (cfd->imm()->NumNotFlushed() != 0 || !cfd->mem()->IsEmpty() ||
|
||||
!cached_recoverable_state_empty_.load()) {
|
||||
cfds->push_back(cfd);
|
||||
selected_cfds->push_back(cfd);
|
||||
}
|
||||
}
|
||||
|
||||
// Unref the newly generated candidate cfds (when not provided) in
|
||||
// `candidate_cfds`
|
||||
if (provided_candidate_cfds.empty()) {
|
||||
for (auto candidate_cfd : candidate_cfds) {
|
||||
candidate_cfd->UnrefAndTryDelete();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1833,8 +1862,13 @@ Status DBImpl::DelayWrite(uint64_t num_bytes, WriteThread& write_thread,
|
||||
// Notify write_thread about the stall so it can setup a barrier and
|
||||
// fail any pending writers with no_slowdown
|
||||
write_thread.BeginWriteStall();
|
||||
TEST_SYNC_POINT("DBImpl::DelayWrite:Wait");
|
||||
if (&write_thread == &write_thread_) {
|
||||
TEST_SYNC_POINT("DBImpl::DelayWrite:Wait");
|
||||
} else {
|
||||
TEST_SYNC_POINT("DBImpl::DelayWrite:NonmemWait");
|
||||
}
|
||||
bg_cv_.Wait();
|
||||
TEST_SYNC_POINT_CALLBACK("DBImpl::DelayWrite:AfterWait", &mutex_);
|
||||
write_thread.EndWriteStall();
|
||||
}
|
||||
}
|
||||
|
||||
+28
-29
@@ -341,11 +341,6 @@ bool DBIter::FindNextUserEntryInternal(bool skipping_saved_key,
|
||||
} else {
|
||||
assert(!skipping_saved_key ||
|
||||
CompareKeyForSkip(ikey_.user_key, saved_key_.GetUserKey()) > 0);
|
||||
if (!iter_.PrepareValue()) {
|
||||
assert(!iter_.status().ok());
|
||||
valid_ = false;
|
||||
return false;
|
||||
}
|
||||
num_skipped = 0;
|
||||
reseek_done = false;
|
||||
switch (ikey_.type) {
|
||||
@@ -369,6 +364,11 @@ bool DBIter::FindNextUserEntryInternal(bool skipping_saved_key,
|
||||
case kTypeValue:
|
||||
case kTypeBlobIndex:
|
||||
case kTypeWideColumnEntity:
|
||||
if (!iter_.PrepareValue()) {
|
||||
assert(!iter_.status().ok());
|
||||
valid_ = false;
|
||||
return false;
|
||||
}
|
||||
if (timestamp_lb_) {
|
||||
saved_key_.SetInternalKey(ikey_);
|
||||
} else {
|
||||
@@ -397,6 +397,11 @@ bool DBIter::FindNextUserEntryInternal(bool skipping_saved_key,
|
||||
return true;
|
||||
break;
|
||||
case kTypeMerge:
|
||||
if (!iter_.PrepareValue()) {
|
||||
assert(!iter_.status().ok());
|
||||
valid_ = false;
|
||||
return false;
|
||||
}
|
||||
saved_key_.SetUserKey(
|
||||
ikey_.user_key,
|
||||
!pin_thru_lifetime_ || !iter_.iter()->IsKeyPinned() /* copy */);
|
||||
@@ -516,6 +521,8 @@ bool DBIter::MergeValuesNewToOld() {
|
||||
// Start the merge process by pushing the first operand
|
||||
merge_context_.PushOperand(
|
||||
iter_.value(), iter_.iter()->IsValuePinned() /* operand_pinned */);
|
||||
PERF_COUNTER_ADD(internal_merge_count, 1);
|
||||
|
||||
TEST_SYNC_POINT("DBIter::MergeValuesNewToOld:PushedFirstOperand");
|
||||
|
||||
ParsedInternalKey ikey;
|
||||
@@ -872,9 +879,14 @@ bool DBIter::FindValueForCurrentKey() {
|
||||
if (timestamp_lb_ != nullptr) {
|
||||
// Only needed when timestamp_lb_ is not null
|
||||
[[maybe_unused]] const bool ret = ParseKey(&ikey_);
|
||||
saved_ikey_.assign(iter_.key().data(), iter_.key().size());
|
||||
// Since the preceding ParseKey(&ikey) succeeds, so must this.
|
||||
assert(ret);
|
||||
saved_key_.SetInternalKey(ikey);
|
||||
} else if (user_comparator_.Compare(ikey.user_key,
|
||||
saved_key_.GetUserKey()) < 0) {
|
||||
saved_key_.SetUserKey(
|
||||
ikey.user_key,
|
||||
!pin_thru_lifetime_ || !iter_.iter()->IsKeyPinned() /* copy */);
|
||||
}
|
||||
|
||||
valid_entry_seen = true;
|
||||
@@ -959,7 +971,6 @@ bool DBIter::FindValueForCurrentKey() {
|
||||
if (timestamp_lb_ == nullptr) {
|
||||
valid_ = false;
|
||||
} else {
|
||||
saved_key_.SetInternalKey(saved_ikey_);
|
||||
valid_ = true;
|
||||
}
|
||||
return true;
|
||||
@@ -1005,10 +1016,6 @@ bool DBIter::FindValueForCurrentKey() {
|
||||
}
|
||||
break;
|
||||
case kTypeValue:
|
||||
if (timestamp_lb_ != nullptr) {
|
||||
saved_key_.SetInternalKey(saved_ikey_);
|
||||
}
|
||||
|
||||
SetValueAndColumnsFromPlain(pinned_value_);
|
||||
|
||||
break;
|
||||
@@ -1154,6 +1161,8 @@ bool DBIter::FindValueForCurrentKeyUsingSeek() {
|
||||
merge_context_.Clear();
|
||||
merge_context_.PushOperand(
|
||||
iter_.value(), iter_.iter()->IsValuePinned() /* operand_pinned */);
|
||||
PERF_COUNTER_ADD(internal_merge_count, 1);
|
||||
|
||||
while (true) {
|
||||
iter_.Next();
|
||||
|
||||
@@ -1428,9 +1437,8 @@ void DBIter::SetSavedKeyToSeekForPrevTarget(const Slice& target) {
|
||||
if (timestamp_size_ > 0) {
|
||||
const std::string kTsMax(timestamp_size_, '\xff');
|
||||
Slice ts = kTsMax;
|
||||
saved_key_.UpdateInternalKey(
|
||||
kMaxSequenceNumber, kValueTypeForSeekForPrev,
|
||||
timestamp_lb_ != nullptr ? timestamp_lb_ : &ts);
|
||||
saved_key_.UpdateInternalKey(kMaxSequenceNumber, kValueTypeForSeekForPrev,
|
||||
&ts);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1632,24 +1640,15 @@ void DBIter::SeekToLast() {
|
||||
if (iterate_upper_bound_ != nullptr) {
|
||||
// Seek to last key strictly less than ReadOptions.iterate_upper_bound.
|
||||
SeekForPrev(*iterate_upper_bound_);
|
||||
const bool is_ikey = (timestamp_size_ > 0 && timestamp_lb_ != nullptr);
|
||||
#ifndef NDEBUG
|
||||
Slice k = Valid() ? key() : Slice();
|
||||
if (is_ikey && Valid()) {
|
||||
if (Valid() && timestamp_size_ > 0 && timestamp_lb_) {
|
||||
k.remove_suffix(kNumInternalBytes + timestamp_size_);
|
||||
}
|
||||
while (Valid() && 0 == user_comparator_.CompareWithoutTimestamp(
|
||||
*iterate_upper_bound_, /*a_has_ts=*/false, k,
|
||||
/*b_has_ts=*/false)) {
|
||||
ReleaseTempPinnedData();
|
||||
ResetBlobValue();
|
||||
ResetValueAndColumns();
|
||||
PrevInternal(nullptr);
|
||||
|
||||
k = key();
|
||||
if (is_ikey) {
|
||||
k.remove_suffix(kNumInternalBytes + timestamp_size_);
|
||||
}
|
||||
}
|
||||
assert(!Valid() || user_comparator_.CompareWithoutTimestamp(
|
||||
k, /*a_has_ts=*/false, *iterate_upper_bound_,
|
||||
/*b_has_ts=*/false) < 0);
|
||||
#endif
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -394,9 +394,6 @@ class DBIter final : public Iterator {
|
||||
const Slice* const timestamp_lb_;
|
||||
const size_t timestamp_size_;
|
||||
std::string saved_timestamp_;
|
||||
|
||||
// Used only if timestamp_lb_ is not nullptr.
|
||||
std::string saved_ikey_;
|
||||
};
|
||||
|
||||
// Return a new iterator that converts internal keys (yielded by
|
||||
|
||||
@@ -231,7 +231,9 @@ TEST_F(DBMergeOperatorTest, MergeOperatorFailsWithMustMerge) {
|
||||
{
|
||||
std::string value;
|
||||
ASSERT_OK(db_->Get(ReadOptions(), "k0", &value));
|
||||
ASSERT_TRUE(db_->Get(ReadOptions(), "k1", &value).IsCorruption());
|
||||
Status s = db_->Get(ReadOptions(), "k1", &value);
|
||||
ASSERT_TRUE(s.IsCorruption());
|
||||
ASSERT_EQ(Status::SubCode::kMergeOperatorFailed, s.subcode());
|
||||
ASSERT_OK(db_->Get(ReadOptions(), "k2", &value));
|
||||
}
|
||||
|
||||
@@ -243,6 +245,8 @@ TEST_F(DBMergeOperatorTest, MergeOperatorFailsWithMustMerge) {
|
||||
ASSERT_EQ("k0", iter->key());
|
||||
iter->Next();
|
||||
ASSERT_TRUE(iter->status().IsCorruption());
|
||||
ASSERT_EQ(Status::SubCode::kMergeOperatorFailed,
|
||||
iter->status().subcode());
|
||||
|
||||
iter->SeekToLast();
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
#include <string>
|
||||
|
||||
#include "db/db_test_util.h"
|
||||
#include "db/write_stall_stats.h"
|
||||
#include "options/cf_options.h"
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/listener.h"
|
||||
@@ -2106,6 +2107,181 @@ TEST_F(DBPropertiesTest, GetMapPropertyBlockCacheEntryStats) {
|
||||
ASSERT_EQ(3 * kNumCacheEntryRoles + 4, values.size());
|
||||
}
|
||||
|
||||
TEST_F(DBPropertiesTest, WriteStallStatsSanityCheck) {
|
||||
for (uint32_t i = 0; i < static_cast<uint32_t>(WriteStallCause::kNone); ++i) {
|
||||
WriteStallCause cause = static_cast<WriteStallCause>(i);
|
||||
const std::string& str = WriteStallCauseToHyphenString(cause);
|
||||
ASSERT_TRUE(!str.empty())
|
||||
<< "Please ensure mapping from `WriteStallCause` to "
|
||||
"`WriteStallCauseToHyphenString` is complete";
|
||||
if (cause == WriteStallCause::kCFScopeWriteStallCauseEnumMax ||
|
||||
cause == WriteStallCause::kDBScopeWriteStallCauseEnumMax) {
|
||||
ASSERT_EQ(str, InvalidWriteStallHyphenString())
|
||||
<< "Please ensure order in `WriteStallCauseToHyphenString` is "
|
||||
"consistent with `WriteStallCause`";
|
||||
}
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < static_cast<uint32_t>(WriteStallCondition::kNormal);
|
||||
++i) {
|
||||
WriteStallCondition condition = static_cast<WriteStallCondition>(i);
|
||||
const std::string& str = WriteStallConditionToHyphenString(condition);
|
||||
ASSERT_TRUE(!str.empty())
|
||||
<< "Please ensure mapping from `WriteStallCondition` to "
|
||||
"`WriteStallConditionToHyphenString` is complete";
|
||||
}
|
||||
|
||||
for (uint32_t i = 0; i < static_cast<uint32_t>(WriteStallCause::kNone); ++i) {
|
||||
for (uint32_t j = 0;
|
||||
j < static_cast<uint32_t>(WriteStallCondition::kNormal); ++j) {
|
||||
WriteStallCause cause = static_cast<WriteStallCause>(i);
|
||||
WriteStallCondition condition = static_cast<WriteStallCondition>(j);
|
||||
|
||||
if (isCFScopeWriteStallCause(cause)) {
|
||||
ASSERT_TRUE(InternalCFStat(cause, condition) !=
|
||||
InternalStats::INTERNAL_CF_STATS_ENUM_MAX)
|
||||
<< "Please ensure the combination of WriteStallCause(" +
|
||||
std::to_string(static_cast<uint32_t>(cause)) +
|
||||
") + WriteStallCondition(" +
|
||||
std::to_string(static_cast<uint32_t>(condition)) +
|
||||
") is correctly mapped to a valid `InternalStats` or bypass "
|
||||
"its check in this test";
|
||||
} else if (isDBScopeWriteStallCause(cause)) {
|
||||
InternalStats::InternalDBStatsType internal_db_stat =
|
||||
InternalDBStat(cause, condition);
|
||||
if (internal_db_stat == InternalStats::kIntStatsNumMax) {
|
||||
ASSERT_TRUE(cause == WriteStallCause::kWriteBufferManagerLimit &&
|
||||
condition == WriteStallCondition::kDelayed)
|
||||
<< "Please ensure the combination of WriteStallCause(" +
|
||||
std::to_string(static_cast<uint32_t>(cause)) +
|
||||
") + WriteStallCondition(" +
|
||||
std::to_string(static_cast<uint32_t>(condition)) +
|
||||
") is correctly mapped to a valid `InternalStats` or "
|
||||
"bypass its check in this test";
|
||||
}
|
||||
} else if (cause != WriteStallCause::kCFScopeWriteStallCauseEnumMax &&
|
||||
cause != WriteStallCause::kDBScopeWriteStallCauseEnumMax) {
|
||||
ASSERT_TRUE(false) << "Please ensure the WriteStallCause(" +
|
||||
std::to_string(static_cast<uint32_t>(cause)) +
|
||||
") is either CF-scope or DB-scope write "
|
||||
"stall cause in enum `WriteStallCause`";
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
TEST_F(DBPropertiesTest, GetMapPropertyWriteStallStats) {
|
||||
Options options = CurrentOptions();
|
||||
CreateAndReopenWithCF({"heavy_write_cf"}, options);
|
||||
|
||||
for (auto test_cause : {WriteStallCause::kWriteBufferManagerLimit,
|
||||
WriteStallCause::kMemtableLimit}) {
|
||||
if (test_cause == WriteStallCause::kWriteBufferManagerLimit) {
|
||||
options.write_buffer_manager.reset(
|
||||
new WriteBufferManager(100000, nullptr, true));
|
||||
} else if (test_cause == WriteStallCause::kMemtableLimit) {
|
||||
options.max_write_buffer_number = 2;
|
||||
options.disable_auto_compactions = true;
|
||||
}
|
||||
ReopenWithColumnFamilies({"default", "heavy_write_cf"}, options);
|
||||
|
||||
// Assert initial write stall stats are all 0
|
||||
std::map<std::string, std::string> db_values;
|
||||
ASSERT_TRUE(dbfull()->GetMapProperty(DB::Properties::kDBWriteStallStats,
|
||||
&db_values));
|
||||
ASSERT_EQ(std::stoi(db_values[WriteStallStatsMapKeys::CauseConditionCount(
|
||||
WriteStallCause::kWriteBufferManagerLimit,
|
||||
WriteStallCondition::kStopped)]),
|
||||
0);
|
||||
|
||||
for (int cf = 0; cf <= 1; ++cf) {
|
||||
std::map<std::string, std::string> cf_values;
|
||||
ASSERT_TRUE(dbfull()->GetMapProperty(
|
||||
handles_[cf], DB::Properties::kCFWriteStallStats, &cf_values));
|
||||
ASSERT_EQ(std::stoi(cf_values[WriteStallStatsMapKeys::TotalStops()]), 0);
|
||||
ASSERT_EQ(std::stoi(cf_values[WriteStallStatsMapKeys::TotalDelays()]), 0);
|
||||
}
|
||||
|
||||
// Pause flush thread to help coerce write stall
|
||||
std::unique_ptr<test::SleepingBackgroundTask> sleeping_task(
|
||||
new test::SleepingBackgroundTask());
|
||||
env_->SetBackgroundThreads(1, Env::HIGH);
|
||||
env_->Schedule(&test::SleepingBackgroundTask::DoSleepTask,
|
||||
sleeping_task.get(), Env::Priority::HIGH);
|
||||
sleeping_task->WaitUntilSleeping();
|
||||
|
||||
// Coerce write stall
|
||||
if (test_cause == WriteStallCause::kWriteBufferManagerLimit) {
|
||||
ASSERT_OK(dbfull()->Put(
|
||||
WriteOptions(), handles_[1], Key(1),
|
||||
DummyString(options.write_buffer_manager->buffer_size())));
|
||||
|
||||
WriteOptions wo;
|
||||
wo.no_slowdown = true;
|
||||
Status s = dbfull()->Put(
|
||||
wo, handles_[1], Key(2),
|
||||
DummyString(options.write_buffer_manager->buffer_size()));
|
||||
ASSERT_TRUE(s.IsIncomplete());
|
||||
ASSERT_TRUE(s.ToString().find("Write stall") != std::string::npos);
|
||||
} else if (test_cause == WriteStallCause::kMemtableLimit) {
|
||||
FlushOptions fo;
|
||||
fo.allow_write_stall = true;
|
||||
fo.wait = false;
|
||||
|
||||
ASSERT_OK(
|
||||
dbfull()->Put(WriteOptions(), handles_[1], Key(1), DummyString(1)));
|
||||
ASSERT_OK(dbfull()->Flush(fo, handles_[1]));
|
||||
|
||||
ASSERT_OK(
|
||||
dbfull()->Put(WriteOptions(), handles_[1], Key(2), DummyString(1)));
|
||||
ASSERT_OK(dbfull()->Flush(fo, handles_[1]));
|
||||
}
|
||||
|
||||
if (test_cause == WriteStallCause::kWriteBufferManagerLimit) {
|
||||
db_values.clear();
|
||||
EXPECT_TRUE(dbfull()->GetMapProperty(DB::Properties::kDBWriteStallStats,
|
||||
&db_values));
|
||||
EXPECT_EQ(std::stoi(db_values[WriteStallStatsMapKeys::CauseConditionCount(
|
||||
WriteStallCause::kWriteBufferManagerLimit,
|
||||
WriteStallCondition::kStopped)]),
|
||||
1);
|
||||
// `WriteStallCause::kWriteBufferManagerLimit` should not result in any
|
||||
// CF-scope write stall stats changes
|
||||
for (int cf = 0; cf <= 1; ++cf) {
|
||||
std::map<std::string, std::string> cf_values;
|
||||
EXPECT_TRUE(dbfull()->GetMapProperty(
|
||||
handles_[cf], DB::Properties::kCFWriteStallStats, &cf_values));
|
||||
EXPECT_EQ(std::stoi(cf_values[WriteStallStatsMapKeys::TotalStops()]),
|
||||
0);
|
||||
EXPECT_EQ(std::stoi(cf_values[WriteStallStatsMapKeys::TotalDelays()]),
|
||||
0);
|
||||
}
|
||||
} else if (test_cause == WriteStallCause::kMemtableLimit) {
|
||||
for (int cf = 0; cf <= 1; ++cf) {
|
||||
std::map<std::string, std::string> cf_values;
|
||||
EXPECT_TRUE(dbfull()->GetMapProperty(
|
||||
handles_[cf], DB::Properties::kCFWriteStallStats, &cf_values));
|
||||
EXPECT_EQ(std::stoi(cf_values[WriteStallStatsMapKeys::TotalStops()]),
|
||||
cf == 1 ? 1 : 0);
|
||||
EXPECT_EQ(
|
||||
std::stoi(cf_values[WriteStallStatsMapKeys::CauseConditionCount(
|
||||
WriteStallCause::kMemtableLimit,
|
||||
WriteStallCondition::kStopped)]),
|
||||
cf == 1 ? 1 : 0);
|
||||
EXPECT_EQ(std::stoi(cf_values[WriteStallStatsMapKeys::TotalDelays()]),
|
||||
0);
|
||||
EXPECT_EQ(
|
||||
std::stoi(cf_values[WriteStallStatsMapKeys::CauseConditionCount(
|
||||
WriteStallCause::kMemtableLimit,
|
||||
WriteStallCondition::kDelayed)]),
|
||||
0);
|
||||
}
|
||||
}
|
||||
|
||||
sleeping_task->WakeUp();
|
||||
sleeping_task->WaitUntilDone();
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
std::string PopMetaIndexKey(InternalIterator* meta_iter) {
|
||||
Status s = meta_iter->status();
|
||||
|
||||
+391
-2
@@ -1661,6 +1661,217 @@ TEST_F(DBRangeDelTest, RangeTombstoneWrittenToMinimalSsts) {
|
||||
ASSERT_EQ(1, num_range_deletions);
|
||||
}
|
||||
|
||||
TEST_F(DBRangeDelTest, LevelCompactOutputCutAtRangeTombstoneForTtlFiles) {
|
||||
Options options = CurrentOptions();
|
||||
options.compression = kNoCompression;
|
||||
options.compaction_pri = kMinOverlappingRatio;
|
||||
options.disable_auto_compactions = true;
|
||||
options.ttl = 24 * 60 * 60; // 24 hours
|
||||
options.target_file_size_base = 8 << 10;
|
||||
env_->SetMockSleep();
|
||||
options.env = env_;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
Random rnd(301);
|
||||
// Fill some data so that future compactions are not bottommost level
|
||||
// compaction, and hence they would try cut around files for ttl
|
||||
for (int i = 5; i < 10; ++i) {
|
||||
ASSERT_OK(Put(Key(i), rnd.RandomString(1 << 10)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(3);
|
||||
ASSERT_EQ("0,0,0,1", FilesPerLevel());
|
||||
|
||||
for (int i = 5; i < 10; ++i) {
|
||||
ASSERT_OK(Put(Key(i), rnd.RandomString(1 << 10)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(1);
|
||||
ASSERT_EQ("0,1,0,1", FilesPerLevel());
|
||||
|
||||
env_->MockSleepForSeconds(20 * 60 * 60);
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(),
|
||||
Key(11), Key(12)));
|
||||
ASSERT_OK(Put(Key(0), rnd.RandomString(1 << 10)));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_EQ("1,1,0,1", FilesPerLevel());
|
||||
// L0 file is new, L1 and L3 file are old and qualified for TTL
|
||||
env_->MockSleepForSeconds(10 * 60 * 60);
|
||||
MoveFilesToLevel(1);
|
||||
// L1 output should be cut into 3 files:
|
||||
// File 0: Key(0)
|
||||
// File 1: (qualified for TTL): Key(5) - Key(10)
|
||||
// File 1: DeleteRange [11, 12)
|
||||
ASSERT_EQ("0,3,0,1", FilesPerLevel());
|
||||
}
|
||||
|
||||
// Test SST partitioner cut after every single key
|
||||
class SingleKeySstPartitioner : public SstPartitioner {
|
||||
public:
|
||||
const char* Name() const override { return "SingleKeySstPartitioner"; }
|
||||
|
||||
PartitionerResult ShouldPartition(
|
||||
const PartitionerRequest& /*request*/) override {
|
||||
return kRequired;
|
||||
}
|
||||
|
||||
bool CanDoTrivialMove(const Slice& /*smallest_user_key*/,
|
||||
const Slice& /*largest_user_key*/) override {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
class SingleKeySstPartitionerFactory : public SstPartitionerFactory {
|
||||
public:
|
||||
static const char* kClassName() { return "SingleKeySstPartitionerFactory"; }
|
||||
const char* Name() const override { return kClassName(); }
|
||||
|
||||
std::unique_ptr<SstPartitioner> CreatePartitioner(
|
||||
const SstPartitioner::Context& /* context */) const override {
|
||||
return std::unique_ptr<SstPartitioner>(new SingleKeySstPartitioner());
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(DBRangeDelTest, CompactionEmitRangeTombstoneToSSTPartitioner) {
|
||||
Options options = CurrentOptions();
|
||||
auto factory = std::make_shared<SingleKeySstPartitionerFactory>();
|
||||
options.sst_partitioner_factory = factory;
|
||||
options.disable_auto_compactions = true;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
Random rnd(301);
|
||||
// range deletion keys are not processed when compacting to bottommost level,
|
||||
// so creating a file at older level to make the next compaction not
|
||||
// bottommost level
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(4), rnd.RandomString(10)));
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(5);
|
||||
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(1), rnd.RandomString(10)));
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), Key(2),
|
||||
Key(5)));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(0));
|
||||
MoveFilesToLevel(1);
|
||||
// SSTPartitioner decides to cut when range tombstone start key is passed to
|
||||
// it. Note that the range tombstone [2, 5) itself span multiple keys, but we
|
||||
// are not able to partition within its range yet.
|
||||
ASSERT_EQ(2, NumTableFilesAtLevel(1));
|
||||
}
|
||||
|
||||
TEST_F(DBRangeDelTest, OversizeCompactionGapBetweenPointKeyAndTombstone) {
|
||||
// L2 has 2 files
|
||||
// L2_0: 0, 1, 2, 3, 4
|
||||
// L2_1: 5, 6, 7
|
||||
// L0 has 1 file
|
||||
// L0: 0, [5, 6), 8
|
||||
// max_compaction_bytes is less than the size of L2_0 and L2_1.
|
||||
// When compacting L0 into L1, it should split into 3 files:
|
||||
// compaction output should cut before key 5 and key 8 to
|
||||
// limit future compaction size.
|
||||
const int kNumPerFile = 4, kNumFiles = 2;
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
options.target_file_size_base = 9 * 1024;
|
||||
options.max_compaction_bytes = 9 * 1024;
|
||||
DestroyAndReopen(options);
|
||||
Random rnd(301);
|
||||
for (int i = 0; i < kNumFiles; ++i) {
|
||||
std::vector<std::string> values;
|
||||
for (int j = 0; j < kNumPerFile; j++) {
|
||||
values.push_back(rnd.RandomString(3 << 10));
|
||||
ASSERT_OK(Put(Key(i * kNumPerFile + j), values[j]));
|
||||
}
|
||||
}
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(0));
|
||||
MoveFilesToLevel(2);
|
||||
ASSERT_EQ(2, NumTableFilesAtLevel(2));
|
||||
ASSERT_OK(Put(Key(0), rnd.RandomString(1 << 10)));
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), Key(5),
|
||||
Key(6)));
|
||||
ASSERT_OK(Put(Key(8), rnd.RandomString(1 << 10)));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(0));
|
||||
|
||||
ASSERT_OK(dbfull()->TEST_CompactRange(0, nullptr, nullptr, nullptr,
|
||||
true /* disallow_trivial_move */));
|
||||
ASSERT_EQ(3, NumTableFilesAtLevel(1));
|
||||
}
|
||||
|
||||
TEST_F(DBRangeDelTest, OversizeCompactionGapBetweenTombstone) {
|
||||
// L2 has two files
|
||||
// L2_0: 0, 1, 2, 3, 4. L2_1: 5, 6, 7
|
||||
// L0 has two range tombstones [0, 1), [7, 8).
|
||||
// max_compaction_bytes is less than the size of L2_0.
|
||||
// When compacting L0 into L1, the two range tombstones should be
|
||||
// split into two files.
|
||||
const int kNumPerFile = 4, kNumFiles = 2;
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
options.target_file_size_base = 9 * 1024;
|
||||
options.max_compaction_bytes = 9 * 1024;
|
||||
DestroyAndReopen(options);
|
||||
Random rnd(301);
|
||||
for (int i = 0; i < kNumFiles; ++i) {
|
||||
std::vector<std::string> values;
|
||||
// Write 12K (4 values, each 3K)
|
||||
for (int j = 0; j < kNumPerFile; j++) {
|
||||
values.push_back(rnd.RandomString(3 << 10));
|
||||
ASSERT_OK(Put(Key(i * kNumPerFile + j), values[j]));
|
||||
}
|
||||
}
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(0));
|
||||
MoveFilesToLevel(2);
|
||||
ASSERT_EQ(2, NumTableFilesAtLevel(2));
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), Key(0),
|
||||
Key(1)));
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), Key(7),
|
||||
Key(8)));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(0));
|
||||
|
||||
ASSERT_OK(dbfull()->TEST_CompactRange(0, nullptr, nullptr, nullptr,
|
||||
true /* disallow_trivial_move */));
|
||||
// This is L0 -> L1 compaction
|
||||
// The two range tombstones are broken up into two output files
|
||||
// to limit compaction size.
|
||||
ASSERT_EQ(2, NumTableFilesAtLevel(1));
|
||||
}
|
||||
|
||||
TEST_F(DBRangeDelTest, OversizeCompactionPointKeyWithinRangetombstone) {
|
||||
// L2 has two files
|
||||
// L2_0: 0, 1, 2, 3, 4. L2_1: 6, 7, 8
|
||||
// L0 has [0, 9) and point key 5
|
||||
// max_compaction_bytes is less than the size of L2_0.
|
||||
// When compacting L0 into L1, the compaction should cut at point key 5.
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
options.target_file_size_base = 9 * 1024;
|
||||
options.max_compaction_bytes = 9 * 1024;
|
||||
DestroyAndReopen(options);
|
||||
Random rnd(301);
|
||||
for (int i = 0; i < 9; ++i) {
|
||||
if (i == 5) {
|
||||
++i;
|
||||
}
|
||||
ASSERT_OK(Put(Key(i), rnd.RandomString(3 << 10)));
|
||||
}
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(0));
|
||||
MoveFilesToLevel(2);
|
||||
ASSERT_EQ(2, NumTableFilesAtLevel(2));
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), Key(0),
|
||||
Key(9)));
|
||||
ASSERT_OK(Put(Key(5), rnd.RandomString(1 << 10)));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(0));
|
||||
ASSERT_OK(dbfull()->TEST_CompactRange(0, nullptr, nullptr, nullptr,
|
||||
true /* disallow_trivial_move */));
|
||||
ASSERT_EQ(2, NumTableFilesAtLevel(1));
|
||||
}
|
||||
|
||||
TEST_F(DBRangeDelTest, OverlappedTombstones) {
|
||||
const int kNumPerFile = 4, kNumFiles = 2;
|
||||
Options options = CurrentOptions();
|
||||
@@ -2093,6 +2304,7 @@ TEST_F(DBRangeDelTest, NonOverlappingTombstonAtBoundary) {
|
||||
options.compression = kNoCompression;
|
||||
options.disable_auto_compactions = true;
|
||||
options.target_file_size_base = 2 * 1024;
|
||||
options.level_compaction_dynamic_file_size = false;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
Random rnd(301);
|
||||
@@ -2508,7 +2720,7 @@ TEST_F(DBRangeDelTest, LeftSentinelKeyTest) {
|
||||
options.compression = kNoCompression;
|
||||
options.disable_auto_compactions = true;
|
||||
options.target_file_size_base = 3 * 1024;
|
||||
options.max_compaction_bytes = 1024;
|
||||
options.max_compaction_bytes = 2048;
|
||||
|
||||
DestroyAndReopen(options);
|
||||
// L2
|
||||
@@ -2554,7 +2766,7 @@ TEST_F(DBRangeDelTest, LeftSentinelKeyTestWithNewerKey) {
|
||||
options.compression = kNoCompression;
|
||||
options.disable_auto_compactions = true;
|
||||
options.target_file_size_base = 3 * 1024;
|
||||
options.max_compaction_bytes = 1024;
|
||||
options.max_compaction_bytes = 3 * 1024;
|
||||
|
||||
DestroyAndReopen(options);
|
||||
// L2
|
||||
@@ -3015,6 +3227,183 @@ TEST_F(DBRangeDelTest, DoubleCountRangeTombstoneCompensatedSize) {
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
}
|
||||
|
||||
TEST_F(DBRangeDelTest, AddRangeDelsSameLowerAndUpperBound) {
|
||||
// Test for an edge case where CompactionOutputs::AddRangeDels()
|
||||
// is called with an empty range: `range_tombstone_lower_bound_` is not empty
|
||||
// and have the same user_key and sequence number as `next_table_min_key.
|
||||
// This used to cause file's smallest and largest key to be incorrectly set
|
||||
// such that smallest > largest, and fail some assertions in iterator and/or
|
||||
// assertion in VersionSet::ApproximateSize().
|
||||
Options opts = CurrentOptions();
|
||||
opts.disable_auto_compactions = true;
|
||||
opts.target_file_size_base = 1 << 10;
|
||||
opts.level_compaction_dynamic_file_size = false;
|
||||
DestroyAndReopen(opts);
|
||||
|
||||
Random rnd(301);
|
||||
// Create file at bottommost level so the manual compaction below is
|
||||
// non-bottommost level and goes through code path like compensate range
|
||||
// tombstone size.
|
||||
ASSERT_OK(Put(Key(1), "v1"));
|
||||
ASSERT_OK(Put(Key(4), "v2"));
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(6);
|
||||
|
||||
ASSERT_OK(Put(Key(1), rnd.RandomString(4 << 10)));
|
||||
ASSERT_OK(Put(Key(3), rnd.RandomString(4 << 10)));
|
||||
// So Key(3) does not get dropped.
|
||||
const Snapshot* snapshot = db_->GetSnapshot();
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), Key(2),
|
||||
Key(4)));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
ASSERT_OK(Put(Key(3), rnd.RandomString(4 << 10)));
|
||||
ASSERT_OK(Put(Key(4), rnd.RandomString(4 << 10)));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
MoveFilesToLevel(1);
|
||||
// Each file will have two keys, with Key(3) straddle between two files.
|
||||
// File 1: Key(1)@1, Key(3)@6, DeleteRange ends at Key(3)@6
|
||||
// File 2: Key(3)@4, Key(4)@7, DeleteRange start from Key(3)@4
|
||||
ASSERT_EQ(NumTableFilesAtLevel(1), 2);
|
||||
|
||||
// Manually update compaction output file cutting decisions
|
||||
// to cut before range tombstone sentinel Key(3)@4
|
||||
// and the point key Key(3)@4 itself
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionOutputs::ShouldStopBefore::manual_decision", [opts](void* p) {
|
||||
auto* pair = (std::pair<bool*, const Slice>*)p;
|
||||
if ((opts.comparator->Compare(ExtractUserKey(pair->second), Key(3)) ==
|
||||
0) &&
|
||||
(GetInternalKeySeqno(pair->second) <= 4)) {
|
||||
*(pair->first) = true;
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
std::string begin_key = Key(0);
|
||||
std::string end_key = Key(5);
|
||||
Slice begin_slice{begin_key};
|
||||
Slice end_slice{end_key};
|
||||
ASSERT_OK(dbfull()->RunManualCompaction(
|
||||
static_cast_with_check<ColumnFamilyHandleImpl>(db_->DefaultColumnFamily())
|
||||
->cfd(),
|
||||
1, 2, CompactRangeOptions(), &begin_slice, &end_slice, true,
|
||||
true /* disallow_trivial_move */,
|
||||
std::numeric_limits<uint64_t>::max() /*max_file_num_to_ignore*/,
|
||||
"" /*trim_ts*/));
|
||||
// iterate through to check if any assertion breaks
|
||||
std::unique_ptr<Iterator> iter{db_->NewIterator(ReadOptions())};
|
||||
iter->SeekToFirst();
|
||||
std::vector<int> expected{1, 3, 4};
|
||||
for (auto i : expected) {
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(iter->key(), Key(i));
|
||||
iter->Next();
|
||||
}
|
||||
ASSERT_TRUE(iter->status().ok() && !iter->Valid());
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
}
|
||||
|
||||
TEST_F(DBRangeDelTest, AddRangeDelsSingleUserKeyTombstoneOnlyFile) {
|
||||
// Test for an edge case where CompactionOutputs::AddRangeDels()
|
||||
// is called with an SST file that has no point keys, and that
|
||||
// the lower bound and upper bound have the same user key.
|
||||
// This could cause a file's smallest and largest key to be incorrectly set
|
||||
// such that smallest > largest, and fail some assertions in iterator and/or
|
||||
// assertion in VersionSet::ApproximateSize().
|
||||
Options opts = CurrentOptions();
|
||||
opts.disable_auto_compactions = true;
|
||||
opts.target_file_size_base = 1 << 10;
|
||||
opts.level_compaction_dynamic_file_size = false;
|
||||
DestroyAndReopen(opts);
|
||||
|
||||
Random rnd(301);
|
||||
// Create file at bottommost level so the manual compaction below is
|
||||
// non-bottommost level and goes through code path like compensate range
|
||||
// tombstone size.
|
||||
ASSERT_OK(Put(Key(1), "v1"));
|
||||
ASSERT_OK(Put(Key(4), "v2"));
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(6);
|
||||
|
||||
ASSERT_OK(Put(Key(1), rnd.RandomString(10)));
|
||||
// Key(3)@4
|
||||
ASSERT_OK(Put(Key(3), rnd.RandomString(10)));
|
||||
const Snapshot* snapshot1 = db_->GetSnapshot();
|
||||
// Key(3)@5
|
||||
ASSERT_OK(Put(Key(3), rnd.RandomString(10)));
|
||||
const Snapshot* snapshot2 = db_->GetSnapshot();
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), Key(2),
|
||||
Key(4)));
|
||||
// Key(3)@7
|
||||
ASSERT_OK(Put(Key(3), rnd.RandomString(10)));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
// L0 -> L1 compaction: cut output into two files:
|
||||
// File 1: Key(1), Key(3)@7, Range tombstone ends at Key(3)@7
|
||||
// File 2: Key(3)@5, Key(3)@4, Range tombstone starts from Key(3)@5
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionOutputs::ShouldStopBefore::manual_decision", [opts](void* p) {
|
||||
auto* pair = (std::pair<bool*, const Slice>*)p;
|
||||
if ((opts.comparator->Compare(ExtractUserKey(pair->second), Key(3)) ==
|
||||
0) &&
|
||||
(GetInternalKeySeqno(pair->second) <= 6)) {
|
||||
*(pair->first) = true;
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
std::string begin_key = Key(0);
|
||||
std::string end_key = Key(5);
|
||||
Slice begin_slice{begin_key};
|
||||
Slice end_slice{end_key};
|
||||
ASSERT_OK(dbfull()->RunManualCompaction(
|
||||
static_cast_with_check<ColumnFamilyHandleImpl>(db_->DefaultColumnFamily())
|
||||
->cfd(),
|
||||
0, 1, CompactRangeOptions(), &begin_slice, &end_slice, true,
|
||||
true /* disallow_trivial_move */,
|
||||
std::numeric_limits<uint64_t>::max() /*max_file_num_to_ignore*/,
|
||||
"" /*trim_ts*/));
|
||||
ASSERT_EQ(NumTableFilesAtLevel(1), 2);
|
||||
|
||||
// L1 -> L2 compaction, drop the snapshot protecting Key(3)@5.
|
||||
// Let ShouldStopBefore() return true for Key(3)@5 (delete range sentinel)
|
||||
// and Key(3)@4.
|
||||
// Output should have two files:
|
||||
// File 1: Key(1), Key(3)@7, range tombstone ends at Key(3)@7
|
||||
// File dropped: range tombstone only file (from Key(3)@5 to Key(3)@4)
|
||||
// File 2: Range tombstone starting from Key(3)@4, Key(3)@4
|
||||
db_->ReleaseSnapshot(snapshot2);
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionOutputs::ShouldStopBefore::manual_decision", [opts](void* p) {
|
||||
auto* pair = (std::pair<bool*, const Slice>*)p;
|
||||
if ((opts.comparator->Compare(ExtractUserKey(pair->second), Key(3)) ==
|
||||
0) &&
|
||||
(GetInternalKeySeqno(pair->second) <= 6)) {
|
||||
*(pair->first) = true;
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
ASSERT_OK(dbfull()->RunManualCompaction(
|
||||
static_cast_with_check<ColumnFamilyHandleImpl>(db_->DefaultColumnFamily())
|
||||
->cfd(),
|
||||
1, 2, CompactRangeOptions(), &begin_slice, &end_slice, true,
|
||||
true /* disallow_trivial_move */,
|
||||
std::numeric_limits<uint64_t>::max() /*max_file_num_to_ignore*/,
|
||||
"" /*trim_ts*/));
|
||||
ASSERT_EQ(NumTableFilesAtLevel(2), 2);
|
||||
// iterate through to check if any assertion breaks
|
||||
std::unique_ptr<Iterator> iter{db_->NewIterator(ReadOptions())};
|
||||
iter->SeekToFirst();
|
||||
std::vector<int> expected{1, 3, 4};
|
||||
for (auto i : expected) {
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(iter->key(), Key(i));
|
||||
iter->Next();
|
||||
}
|
||||
ASSERT_TRUE(iter->status().ok() && !iter->Valid());
|
||||
db_->ReleaseSnapshot(snapshot1);
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
|
||||
+2
-76
@@ -909,82 +909,6 @@ class TestPutOperator : public MergeOperator {
|
||||
virtual const char* Name() const override { return "TestPutOperator"; }
|
||||
};
|
||||
|
||||
// A wrapper around Cache that can easily be extended with instrumentation,
|
||||
// etc.
|
||||
class CacheWrapper : public Cache {
|
||||
public:
|
||||
explicit CacheWrapper(std::shared_ptr<Cache> target)
|
||||
: target_(std::move(target)) {}
|
||||
|
||||
const char* Name() const override { return target_->Name(); }
|
||||
|
||||
Status Insert(const Slice& key, ObjectPtr value,
|
||||
const CacheItemHelper* helper, size_t charge,
|
||||
Handle** handle = nullptr,
|
||||
Priority priority = Priority::LOW) override {
|
||||
return target_->Insert(key, value, helper, charge, handle, priority);
|
||||
}
|
||||
|
||||
Handle* Lookup(const Slice& key, const CacheItemHelper* helper,
|
||||
CreateContext* create_context,
|
||||
Priority priority = Priority::LOW, bool wait = true,
|
||||
Statistics* stats = nullptr) override {
|
||||
return target_->Lookup(key, helper, create_context, priority, wait, stats);
|
||||
}
|
||||
|
||||
bool Ref(Handle* handle) override { return target_->Ref(handle); }
|
||||
|
||||
using Cache::Release;
|
||||
bool Release(Handle* handle, bool erase_if_last_ref = false) override {
|
||||
return target_->Release(handle, erase_if_last_ref);
|
||||
}
|
||||
|
||||
ObjectPtr Value(Handle* handle) override { return target_->Value(handle); }
|
||||
|
||||
void Erase(const Slice& key) override { target_->Erase(key); }
|
||||
uint64_t NewId() override { return target_->NewId(); }
|
||||
|
||||
void SetCapacity(size_t capacity) override { target_->SetCapacity(capacity); }
|
||||
|
||||
void SetStrictCapacityLimit(bool strict_capacity_limit) override {
|
||||
target_->SetStrictCapacityLimit(strict_capacity_limit);
|
||||
}
|
||||
|
||||
bool HasStrictCapacityLimit() const override {
|
||||
return target_->HasStrictCapacityLimit();
|
||||
}
|
||||
|
||||
size_t GetCapacity() const override { return target_->GetCapacity(); }
|
||||
|
||||
size_t GetUsage() const override { return target_->GetUsage(); }
|
||||
|
||||
size_t GetUsage(Handle* handle) const override {
|
||||
return target_->GetUsage(handle);
|
||||
}
|
||||
|
||||
size_t GetPinnedUsage() const override { return target_->GetPinnedUsage(); }
|
||||
|
||||
size_t GetCharge(Handle* handle) const override {
|
||||
return target_->GetCharge(handle);
|
||||
}
|
||||
|
||||
const CacheItemHelper* GetCacheItemHelper(Handle* handle) const override {
|
||||
return target_->GetCacheItemHelper(handle);
|
||||
}
|
||||
|
||||
void ApplyToAllEntries(
|
||||
const std::function<void(const Slice& key, ObjectPtr value, size_t charge,
|
||||
const CacheItemHelper* helper)>& callback,
|
||||
const ApplyToAllEntriesOptions& opts) override {
|
||||
target_->ApplyToAllEntries(callback, opts);
|
||||
}
|
||||
|
||||
void EraseUnRefEntries() override { target_->EraseUnRefEntries(); }
|
||||
|
||||
protected:
|
||||
std::shared_ptr<Cache> target_;
|
||||
};
|
||||
|
||||
/*
|
||||
* A cache wrapper that tracks certain CacheEntryRole's cache charge, its
|
||||
* peaks and increments
|
||||
@@ -1002,6 +926,8 @@ class TargetCacheChargeTrackingCache : public CacheWrapper {
|
||||
public:
|
||||
explicit TargetCacheChargeTrackingCache(std::shared_ptr<Cache> target);
|
||||
|
||||
const char* Name() const override { return "TargetCacheChargeTrackingCache"; }
|
||||
|
||||
Status Insert(const Slice& key, ObjectPtr value,
|
||||
const CacheItemHelper* helper, size_t charge,
|
||||
Handle** handle = nullptr,
|
||||
|
||||
+14
-32
@@ -1633,6 +1633,8 @@ TEST_F(DBWALTest, RaceInstallFlushResultsWithWalObsoletion) {
|
||||
|
||||
TEST_F(DBWALTest, FixSyncWalOnObseletedWalWithNewManifestCausingMissingWAL) {
|
||||
Options options = CurrentOptions();
|
||||
// Small size to force manifest creation
|
||||
options.max_manifest_file_size = 1;
|
||||
options.track_and_verify_wals_in_manifest = true;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
@@ -1649,53 +1651,33 @@ TEST_F(DBWALTest, FixSyncWalOnObseletedWalWithNewManifestCausingMissingWAL) {
|
||||
// (2) SyncWAL() proceeds with the lock. It
|
||||
// creates a new manifest and syncs all the inactive wals before the latest
|
||||
// (i.e, active log), which is 4.log. Note that SyncWAL() is not aware of the
|
||||
// fact that 4.log has marked as to be obseleted. Prior to the fix, such wal
|
||||
// fact that 4.log has marked as to be obseleted. Such wal
|
||||
// sync will then add a WAL addition record of 4.log to the new manifest
|
||||
// without any special treatment.
|
||||
// (3) BackgroundFlush() will eventually purge 4.log.
|
||||
// without any special treatment. Prior to the fix, there is no WAL deletion
|
||||
// record to offset it. (3) BackgroundFlush() will eventually purge 4.log.
|
||||
|
||||
bool wal_synced = false;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"FindObsoleteFiles::PostMutexUnlock", [&](void*) {
|
||||
ASSERT_OK(env_->FileExists(wal_file_path));
|
||||
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"VersionSet::ProcessManifestWrites:"
|
||||
"PostDecidingCreateNewManifestOrNot",
|
||||
[&](void* arg) {
|
||||
bool* new_descriptor_log = (bool*)arg;
|
||||
*new_descriptor_log = true;
|
||||
});
|
||||
|
||||
uint64_t pre_sync_wal_manifest_no =
|
||||
dbfull()->TEST_Current_Manifest_FileNo();
|
||||
ASSERT_OK(db_->SyncWAL());
|
||||
uint64_t post_sync_wal_manifest_no =
|
||||
dbfull()->TEST_Current_Manifest_FileNo();
|
||||
bool new_manifest_created =
|
||||
post_sync_wal_manifest_no == pre_sync_wal_manifest_no + 1;
|
||||
ASSERT_TRUE(new_manifest_created);
|
||||
wal_synced = true;
|
||||
});
|
||||
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::DeleteObsoleteFileImpl:AfterDeletion2", [&](void* arg) {
|
||||
std::string* file_name = (std::string*)arg;
|
||||
if (*file_name == wal_file_path) {
|
||||
TEST_SYNC_POINT(
|
||||
"DBWALTest::"
|
||||
"FixSyncWalOnObseletedWalWithNewManifestCausingMissingWAL::"
|
||||
"PostDeleteWAL");
|
||||
}
|
||||
});
|
||||
|
||||
SyncPoint::GetInstance()->LoadDependency(
|
||||
{{"DBImpl::BackgroundCallFlush:FilesFound",
|
||||
"PreConfrimObsoletedWALSynced"},
|
||||
{"DBWALTest::FixSyncWalOnObseletedWalWithNewManifestCausingMissingWAL::"
|
||||
"PostDeleteWAL",
|
||||
"PreConfrimWALDeleted"}});
|
||||
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(dbfull()->TEST_WaitForBackgroundWork());
|
||||
|
||||
TEST_SYNC_POINT("PreConfrimObsoletedWALSynced");
|
||||
ASSERT_TRUE(wal_synced);
|
||||
|
||||
TEST_SYNC_POINT("PreConfrimWALDeleted");
|
||||
// BackgroundFlush() purged 4.log
|
||||
// because the memtable associated with the WAL was flushed and new WAL was
|
||||
// created (i.e, 8.log)
|
||||
|
||||
@@ -3892,6 +3892,32 @@ TEST_F(DBBasicTestWithTimestamp, RangeTombstoneApproximateSize) {
|
||||
std::numeric_limits<uint64_t>::max() /* max_file_num_to_ignore */,
|
||||
"" /*trim_ts*/));
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTestWithTimestamp, IterSeekToLastWithIterateUpperbound) {
|
||||
// Test for a bug fix where DBIter::SeekToLast() could fail when
|
||||
// iterate_upper_bound and iter_start_ts are both set.
|
||||
Options options = CurrentOptions();
|
||||
const size_t kTimestampSize = Timestamp(0, 0).size();
|
||||
TestComparator test_cmp(kTimestampSize);
|
||||
options.comparator = &test_cmp;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(1), Timestamp(2, 0), "val"));
|
||||
ReadOptions ro;
|
||||
std::string k = Key(1);
|
||||
Slice k_slice = k;
|
||||
ro.iterate_upper_bound = &k_slice;
|
||||
std::string ts = Timestamp(3, 0);
|
||||
Slice read_ts = ts;
|
||||
ro.timestamp = &read_ts;
|
||||
std::string start_ts = Timestamp(0, 0);
|
||||
Slice start_ts_slice = start_ts;
|
||||
ro.iter_start_ts = &start_ts_slice;
|
||||
std::unique_ptr<Iterator> iter{db_->NewIterator(ro)};
|
||||
iter->SeekToLast();
|
||||
ASSERT_FALSE(iter->Valid());
|
||||
ASSERT_OK(iter->status());
|
||||
}
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
+14
-5
@@ -19,6 +19,7 @@
|
||||
#include "util/random.h"
|
||||
#include "util/string_util.h"
|
||||
#include "utilities/fault_injection_env.h"
|
||||
#include "utilities/fault_injection_fs.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
@@ -608,11 +609,18 @@ TEST_P(DBWriteTest, IOErrorOnSwitchMemtable) {
|
||||
|
||||
// Test that db->LockWAL() flushes the WAL after locking, which can fail
|
||||
TEST_P(DBWriteTest, LockWALInEffect) {
|
||||
if (mem_env_ || encrypted_env_) {
|
||||
ROCKSDB_GTEST_SKIP("Test requires non-mem or non-encrypted environment");
|
||||
return;
|
||||
}
|
||||
Options options = GetOptions();
|
||||
std::unique_ptr<FaultInjectionTestEnv> mock_env(
|
||||
new FaultInjectionTestEnv(env_));
|
||||
options.env = mock_env.get();
|
||||
std::shared_ptr<FaultInjectionTestFS> fault_fs(
|
||||
new FaultInjectionTestFS(FileSystem::Default()));
|
||||
std::unique_ptr<Env> fault_fs_env(NewCompositeEnv(fault_fs));
|
||||
options.env = fault_fs_env.get();
|
||||
options.disable_auto_compactions = true;
|
||||
options.paranoid_checks = false;
|
||||
options.max_bgerror_resume_count = 0; // manual Resume()
|
||||
Reopen(options);
|
||||
// try the 1st WAL created during open
|
||||
ASSERT_OK(Put("key0", "value"));
|
||||
@@ -629,7 +637,7 @@ TEST_P(DBWriteTest, LockWALInEffect) {
|
||||
ASSERT_OK(db_->UnlockWAL());
|
||||
|
||||
// Fail the WAL flush if applicable
|
||||
mock_env->SetFilesystemActive(false);
|
||||
fault_fs->SetFilesystemActive(false);
|
||||
Status s = Put("key2", "value");
|
||||
if (options.manual_wal_flush) {
|
||||
ASSERT_OK(s);
|
||||
@@ -641,7 +649,8 @@ TEST_P(DBWriteTest, LockWALInEffect) {
|
||||
ASSERT_OK(db_->LockWAL());
|
||||
ASSERT_OK(db_->UnlockWAL());
|
||||
}
|
||||
mock_env->SetFilesystemActive(true);
|
||||
fault_fs->SetFilesystemActive(true);
|
||||
ASSERT_OK(db_->Resume());
|
||||
// Writes should work again
|
||||
ASSERT_OK(Put("key3", "value"));
|
||||
ASSERT_EQ(Get("key3"), "value");
|
||||
|
||||
+3
-1
@@ -86,8 +86,10 @@ inline bool IsValueType(ValueType t) {
|
||||
|
||||
// Checks whether a type is from user operation
|
||||
// kTypeRangeDeletion is in meta block so this API is separated from above
|
||||
// kTypeMaxValid can be from keys generated by
|
||||
// TruncatedRangeDelIterator::start_key()
|
||||
inline bool IsExtendedValueType(ValueType t) {
|
||||
return IsValueType(t) || t == kTypeRangeDeletion;
|
||||
return IsValueType(t) || t == kTypeRangeDeletion || t == kTypeMaxValid;
|
||||
}
|
||||
|
||||
// We leave eight bits empty at the bottom so a type and sequence#
|
||||
|
||||
@@ -2860,4 +2860,3 @@ int main(int argc, char** argv) {
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ namespace ROCKSDB_NAMESPACE {
|
||||
Status EventListener::CreateFromString(const ConfigOptions& config_options,
|
||||
const std::string& id,
|
||||
std::shared_ptr<EventListener>* result) {
|
||||
return LoadSharedObject<EventListener>(config_options, id, nullptr, result);
|
||||
return LoadSharedObject<EventListener>(config_options, id, result);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -1182,6 +1182,7 @@ TEST_F(ExternalSSTFileBasicTest, SyncFailure) {
|
||||
ASSERT_OK(sst_file_writer->Finish());
|
||||
|
||||
IngestExternalFileOptions ingest_opt;
|
||||
ASSERT_FALSE(ingest_opt.write_global_seqno); // new default
|
||||
if (i == 0) {
|
||||
ingest_opt.move_files = true;
|
||||
}
|
||||
|
||||
@@ -746,12 +746,6 @@ Status ExternalSstFileIngestionJob::GetIngestedFileInfo(
|
||||
|
||||
ParsedInternalKey key;
|
||||
ReadOptions ro;
|
||||
// During reading the external file we can cache blocks that we read into
|
||||
// the block cache, if we later change the global seqno of this file, we will
|
||||
// have block in cache that will include keys with wrong seqno.
|
||||
// We need to disable fill_cache so that we read from the file without
|
||||
// updating the block cache.
|
||||
ro.fill_cache = false;
|
||||
std::unique_ptr<InternalIterator> iter(table_reader->NewIterator(
|
||||
ro, sv->mutable_cf_options.prefix_extractor.get(), /*arena=*/nullptr,
|
||||
/*skip_filters=*/false, TableReaderCaller::kExternalSSTIngestion));
|
||||
|
||||
@@ -238,6 +238,9 @@ ForwardIterator::ForwardIterator(DBImpl* db, const ReadOptions& read_options,
|
||||
if (sv_) {
|
||||
RebuildIterators(false);
|
||||
}
|
||||
if (!cfd_->ioptions()->env->GetFileSystem()->use_async_io()) {
|
||||
read_options_.async_io = false;
|
||||
}
|
||||
|
||||
// immutable_status_ is a local aggregation of the
|
||||
// status of the immutable Iterators.
|
||||
|
||||
@@ -122,7 +122,7 @@ class ForwardIterator : public InternalIterator {
|
||||
void DeleteIterator(InternalIterator* iter, bool is_arena = false);
|
||||
|
||||
DBImpl* const db_;
|
||||
const ReadOptions read_options_;
|
||||
ReadOptions read_options_;
|
||||
ColumnFamilyData* const cfd_;
|
||||
const SliceTransform* const prefix_extractor_;
|
||||
const Comparator* user_comparator_;
|
||||
|
||||
@@ -82,6 +82,10 @@ class HistoryTrimmingIterator : public InternalIterator {
|
||||
|
||||
bool IsValuePinned() const override { return input_->IsValuePinned(); }
|
||||
|
||||
bool IsDeleteRangeSentinelKey() const override {
|
||||
return input_->IsDeleteRangeSentinelKey();
|
||||
}
|
||||
|
||||
private:
|
||||
InternalIterator* input_;
|
||||
const std::string filter_ts_;
|
||||
|
||||
@@ -34,8 +34,8 @@ Status ImportColumnFamilyJob::Prepare(uint64_t next_file_number,
|
||||
for (const auto& file_metadata : metadata_) {
|
||||
const auto file_path = file_metadata.db_path + "/" + file_metadata.name;
|
||||
IngestedFileInfo file_to_import;
|
||||
status =
|
||||
GetIngestedFileInfo(file_path, next_file_number++, &file_to_import, sv);
|
||||
status = GetIngestedFileInfo(file_path, next_file_number++, sv,
|
||||
file_metadata, &file_to_import);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
@@ -212,16 +212,20 @@ void ImportColumnFamilyJob::Cleanup(const Status& status) {
|
||||
|
||||
Status ImportColumnFamilyJob::GetIngestedFileInfo(
|
||||
const std::string& external_file, uint64_t new_file_number,
|
||||
IngestedFileInfo* file_to_import, SuperVersion* sv) {
|
||||
SuperVersion* sv, const LiveFileMetaData& file_meta,
|
||||
IngestedFileInfo* file_to_import) {
|
||||
file_to_import->external_file_path = external_file;
|
||||
|
||||
// Get external file size
|
||||
Status status = fs_->GetFileSize(external_file, IOOptions(),
|
||||
&file_to_import->file_size, nullptr);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
Status status;
|
||||
if (file_meta.size > 0) {
|
||||
file_to_import->file_size = file_meta.size;
|
||||
} else {
|
||||
// Get external file size
|
||||
status = fs_->GetFileSize(external_file, IOOptions(),
|
||||
&file_to_import->file_size, nullptr);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
}
|
||||
|
||||
// Assign FD with number
|
||||
file_to_import->fd =
|
||||
FileDescriptor(new_file_number, 0, file_to_import->file_size);
|
||||
@@ -262,37 +266,61 @@ Status ImportColumnFamilyJob::GetIngestedFileInfo(
|
||||
// Get number of entries in table
|
||||
file_to_import->num_entries = props->num_entries;
|
||||
|
||||
ParsedInternalKey key;
|
||||
ReadOptions ro;
|
||||
// During reading the external file we can cache blocks that we read into
|
||||
// the block cache, if we later change the global seqno of this file, we will
|
||||
// have block in cache that will include keys with wrong seqno.
|
||||
// We need to disable fill_cache so that we read from the file without
|
||||
// updating the block cache.
|
||||
ro.fill_cache = false;
|
||||
std::unique_ptr<InternalIterator> iter(table_reader->NewIterator(
|
||||
ro, sv->mutable_cf_options.prefix_extractor.get(), /*arena=*/nullptr,
|
||||
/*skip_filters=*/false, TableReaderCaller::kExternalSSTIngestion));
|
||||
// If the importing files were exported with Checkpoint::ExportColumnFamily(),
|
||||
// we cannot simply recompute smallest and largest used to truncate range
|
||||
// tombstones from file content, and we expect smallest and largest populated
|
||||
// in file_meta.
|
||||
if (file_meta.smallest.empty()) {
|
||||
assert(file_meta.largest.empty());
|
||||
ReadOptions ro;
|
||||
std::unique_ptr<InternalIterator> iter(table_reader->NewIterator(
|
||||
ro, sv->mutable_cf_options.prefix_extractor.get(), /*arena=*/nullptr,
|
||||
/*skip_filters=*/false, TableReaderCaller::kExternalSSTIngestion));
|
||||
|
||||
// Get first (smallest) key from file
|
||||
iter->SeekToFirst();
|
||||
Status pik_status =
|
||||
ParseInternalKey(iter->key(), &key, db_options_.allow_data_in_errors);
|
||||
if (!pik_status.ok()) {
|
||||
return Status::Corruption("Corrupted Key in external file. ",
|
||||
pik_status.getState());
|
||||
}
|
||||
file_to_import->smallest_internal_key.SetFrom(key);
|
||||
// Get first (smallest) key from file
|
||||
iter->SeekToFirst();
|
||||
bool bound_set = false;
|
||||
if (iter->Valid()) {
|
||||
file_to_import->smallest_internal_key.DecodeFrom(iter->key());
|
||||
iter->SeekToLast();
|
||||
file_to_import->largest_internal_key.DecodeFrom(iter->key());
|
||||
bound_set = true;
|
||||
}
|
||||
|
||||
// Get last (largest) key from file
|
||||
iter->SeekToLast();
|
||||
pik_status =
|
||||
ParseInternalKey(iter->key(), &key, db_options_.allow_data_in_errors);
|
||||
if (!pik_status.ok()) {
|
||||
return Status::Corruption("Corrupted Key in external file. ",
|
||||
pik_status.getState());
|
||||
std::unique_ptr<InternalIterator> range_del_iter{
|
||||
table_reader->NewRangeTombstoneIterator(ro)};
|
||||
if (range_del_iter != nullptr) {
|
||||
range_del_iter->SeekToFirst();
|
||||
if (range_del_iter->Valid()) {
|
||||
ParsedInternalKey key;
|
||||
Status pik_status = ParseInternalKey(range_del_iter->key(), &key,
|
||||
db_options_.allow_data_in_errors);
|
||||
if (!pik_status.ok()) {
|
||||
return Status::Corruption("Corrupted key in external file. ",
|
||||
pik_status.getState());
|
||||
}
|
||||
RangeTombstone tombstone(key, range_del_iter->value());
|
||||
InternalKey start_key = tombstone.SerializeKey();
|
||||
const InternalKeyComparator* icmp = &cfd_->internal_comparator();
|
||||
if (!bound_set ||
|
||||
icmp->Compare(start_key, file_to_import->smallest_internal_key) <
|
||||
0) {
|
||||
file_to_import->smallest_internal_key = start_key;
|
||||
}
|
||||
InternalKey end_key = tombstone.SerializeEndKey();
|
||||
if (!bound_set ||
|
||||
icmp->Compare(end_key, file_to_import->largest_internal_key) > 0) {
|
||||
file_to_import->largest_internal_key = end_key;
|
||||
}
|
||||
bound_set = true;
|
||||
}
|
||||
}
|
||||
assert(bound_set);
|
||||
} else {
|
||||
assert(!file_meta.largest.empty());
|
||||
file_to_import->smallest_internal_key.DecodeFrom(file_meta.smallest);
|
||||
file_to_import->largest_internal_key.DecodeFrom(file_meta.largest);
|
||||
}
|
||||
file_to_import->largest_internal_key.SetFrom(key);
|
||||
|
||||
file_to_import->cf_id = static_cast<uint32_t>(props->column_family_id);
|
||||
|
||||
|
||||
@@ -62,9 +62,9 @@ class ImportColumnFamilyJob {
|
||||
// Open the external file and populate `file_to_import` with all the
|
||||
// external information we need to import this file.
|
||||
Status GetIngestedFileInfo(const std::string& external_file,
|
||||
uint64_t new_file_number,
|
||||
IngestedFileInfo* file_to_import,
|
||||
SuperVersion* sv);
|
||||
uint64_t new_file_number, SuperVersion* sv,
|
||||
const LiveFileMetaData& file_meta,
|
||||
IngestedFileInfo* file_to_import);
|
||||
|
||||
SystemClock* clock_;
|
||||
VersionSet* versions_;
|
||||
|
||||
@@ -280,6 +280,57 @@ TEST_F(ImportColumnFamilyTest, ImportSSTFileWriterFilesWithOverlap) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ImportColumnFamilyTest, ImportSSTFileWriterFilesWithRangeTombstone) {
|
||||
// Test for a bug where import file's smallest and largest key did not
|
||||
// consider range tombstone.
|
||||
Options options = CurrentOptions();
|
||||
CreateAndReopenWithCF({"koko"}, options);
|
||||
|
||||
SstFileWriter sfw_cf1(EnvOptions(), options, handles_[1]);
|
||||
// cf1.sst
|
||||
const std::string cf1_sst_name = "cf1.sst";
|
||||
const std::string cf1_sst = sst_files_dir_ + cf1_sst_name;
|
||||
ASSERT_OK(sfw_cf1.Open(cf1_sst));
|
||||
ASSERT_OK(sfw_cf1.Put("K1", "V1"));
|
||||
ASSERT_OK(sfw_cf1.Put("K2", "V2"));
|
||||
ASSERT_OK(sfw_cf1.DeleteRange("K3", "K4"));
|
||||
ASSERT_OK(sfw_cf1.Finish());
|
||||
|
||||
// Import sst file corresponding to cf1 onto a new cf and verify
|
||||
ExportImportFilesMetaData metadata;
|
||||
metadata.files.push_back(
|
||||
LiveFileMetaDataInit(cf1_sst_name, sst_files_dir_, 0, 0, 19));
|
||||
metadata.db_comparator_name = options.comparator->Name();
|
||||
|
||||
ASSERT_OK(db_->CreateColumnFamilyWithImport(
|
||||
options, "toto", ImportColumnFamilyOptions(), metadata, &import_cfh_));
|
||||
ASSERT_NE(import_cfh_, nullptr);
|
||||
|
||||
ColumnFamilyMetaData import_cf_meta;
|
||||
db_->GetColumnFamilyMetaData(import_cfh_, &import_cf_meta);
|
||||
ASSERT_EQ(import_cf_meta.file_count, 1);
|
||||
const SstFileMetaData* file_meta = nullptr;
|
||||
for (const auto& level_meta : import_cf_meta.levels) {
|
||||
if (!level_meta.files.empty()) {
|
||||
file_meta = &(level_meta.files[0]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
ASSERT_TRUE(file_meta != nullptr);
|
||||
InternalKey largest;
|
||||
largest.DecodeFrom(file_meta->largest);
|
||||
ASSERT_EQ(largest.user_key(), "K4");
|
||||
|
||||
std::string value;
|
||||
ASSERT_OK(db_->Get(ReadOptions(), import_cfh_, "K1", &value));
|
||||
ASSERT_EQ(value, "V1");
|
||||
ASSERT_OK(db_->Get(ReadOptions(), import_cfh_, "K2", &value));
|
||||
ASSERT_EQ(value, "V2");
|
||||
ASSERT_OK(db_->DropColumnFamily(import_cfh_));
|
||||
ASSERT_OK(db_->DestroyColumnFamilyHandle(import_cfh_));
|
||||
import_cfh_ = nullptr;
|
||||
}
|
||||
|
||||
TEST_F(ImportColumnFamilyTest, ImportExportedSSTFromAnotherCF) {
|
||||
Options options = CurrentOptions();
|
||||
CreateAndReopenWithCF({"koko"}, options);
|
||||
@@ -444,6 +495,70 @@ TEST_F(ImportColumnFamilyTest, ImportExportedSSTFromAnotherDB) {
|
||||
ASSERT_OK(DestroyDir(env_, dbname_ + "/db_copy"));
|
||||
}
|
||||
|
||||
TEST_F(ImportColumnFamilyTest,
|
||||
ImportExportedSSTFromAnotherCFWithRangeTombstone) {
|
||||
// Test for a bug where import file's smallest and largest key did not
|
||||
// consider range tombstone.
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
CreateAndReopenWithCF({"koko"}, options);
|
||||
|
||||
for (int i = 10; i < 20; ++i) {
|
||||
ASSERT_OK(Put(1, Key(i), Key(i) + "_val"));
|
||||
}
|
||||
ASSERT_OK(Flush(1 /* cf */));
|
||||
MoveFilesToLevel(1 /* level */, 1 /* cf */);
|
||||
const Snapshot* snapshot = db_->GetSnapshot();
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), handles_[1], Key(0), Key(25)));
|
||||
ASSERT_OK(Put(1, Key(1), "t"));
|
||||
ASSERT_OK(Flush(1));
|
||||
// Tests importing a range tombstone only file
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), handles_[1], Key(0), Key(2)));
|
||||
|
||||
Checkpoint* checkpoint;
|
||||
ASSERT_OK(Checkpoint::Create(db_, &checkpoint));
|
||||
ASSERT_OK(checkpoint->ExportColumnFamily(handles_[1], export_files_dir_,
|
||||
&metadata_ptr_));
|
||||
ASSERT_NE(metadata_ptr_, nullptr);
|
||||
delete checkpoint;
|
||||
|
||||
ImportColumnFamilyOptions import_options;
|
||||
import_options.move_files = false;
|
||||
ASSERT_OK(db_->CreateColumnFamilyWithImport(options, "toto", import_options,
|
||||
*metadata_ptr_, &import_cfh_));
|
||||
ASSERT_NE(import_cfh_, nullptr);
|
||||
|
||||
import_options.move_files = true;
|
||||
ASSERT_OK(db_->CreateColumnFamilyWithImport(options, "yoyo", import_options,
|
||||
*metadata_ptr_, &import_cfh2_));
|
||||
ASSERT_NE(import_cfh2_, nullptr);
|
||||
delete metadata_ptr_;
|
||||
metadata_ptr_ = nullptr;
|
||||
|
||||
std::string value1, value2;
|
||||
ReadOptions ro_latest;
|
||||
ReadOptions ro_snapshot;
|
||||
ro_snapshot.snapshot = snapshot;
|
||||
|
||||
for (int i = 10; i < 20; ++i) {
|
||||
ASSERT_TRUE(db_->Get(ro_latest, import_cfh_, Key(i), &value1).IsNotFound());
|
||||
ASSERT_OK(db_->Get(ro_snapshot, import_cfh_, Key(i), &value1));
|
||||
ASSERT_EQ(Get(1, Key(i), snapshot), value1);
|
||||
}
|
||||
ASSERT_TRUE(db_->Get(ro_latest, import_cfh_, Key(1), &value1).IsNotFound());
|
||||
|
||||
for (int i = 10; i < 20; ++i) {
|
||||
ASSERT_TRUE(
|
||||
db_->Get(ro_latest, import_cfh2_, Key(i), &value1).IsNotFound());
|
||||
|
||||
ASSERT_OK(db_->Get(ro_snapshot, import_cfh2_, Key(i), &value2));
|
||||
ASSERT_EQ(Get(1, Key(i), snapshot), value2);
|
||||
}
|
||||
ASSERT_TRUE(db_->Get(ro_latest, import_cfh2_, Key(1), &value1).IsNotFound());
|
||||
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
}
|
||||
|
||||
TEST_F(ImportColumnFamilyTest, LevelFilesOverlappingAtEndpoints) {
|
||||
// Imports a column family containing a level where two files overlap at their
|
||||
// endpoints. "Overlap" means the largest user key in one file is the same as
|
||||
|
||||
+166
-69
@@ -23,6 +23,7 @@
|
||||
#include "cache/cache_entry_stats.h"
|
||||
#include "db/column_family.h"
|
||||
#include "db/db_impl/db_impl.h"
|
||||
#include "db/write_stall_stats.h"
|
||||
#include "port/port.h"
|
||||
#include "rocksdb/system_clock.h"
|
||||
#include "rocksdb/table.h"
|
||||
@@ -77,6 +78,10 @@ const std::map<InternalStats::InternalDBStatsType, DBStatInfo>
|
||||
DBStatInfo{"db.user_writes_with_wal"}},
|
||||
{InternalStats::kIntStatsWriteStallMicros,
|
||||
DBStatInfo{"db.user_write_stall_micros"}},
|
||||
{InternalStats::kIntStatsWriteBufferManagerLimitStopsCounts,
|
||||
DBStatInfo{WriteStallStatsMapKeys::CauseConditionCount(
|
||||
WriteStallCause::kWriteBufferManagerLimit,
|
||||
WriteStallCondition::kStopped)}},
|
||||
};
|
||||
|
||||
namespace {
|
||||
@@ -243,7 +248,9 @@ static const std::string cfstats = "cfstats";
|
||||
static const std::string cfstats_no_file_histogram =
|
||||
"cfstats-no-file-histogram";
|
||||
static const std::string cf_file_histogram = "cf-file-histogram";
|
||||
static const std::string cf_write_stall_stats = "cf-write-stall-stats";
|
||||
static const std::string dbstats = "dbstats";
|
||||
static const std::string db_write_stall_stats = "db-write-stall-stats";
|
||||
static const std::string levelstats = "levelstats";
|
||||
static const std::string block_cache_entry_stats = "block-cache-entry-stats";
|
||||
static const std::string fast_block_cache_entry_stats =
|
||||
@@ -323,6 +330,10 @@ const std::string DB::Properties::kCFStatsNoFileHistogram =
|
||||
rocksdb_prefix + cfstats_no_file_histogram;
|
||||
const std::string DB::Properties::kCFFileHistogram =
|
||||
rocksdb_prefix + cf_file_histogram;
|
||||
const std::string DB::Properties::kCFWriteStallStats =
|
||||
rocksdb_prefix + cf_write_stall_stats;
|
||||
const std::string DB::Properties::kDBWriteStallStats =
|
||||
rocksdb_prefix + db_write_stall_stats;
|
||||
const std::string DB::Properties::kDBStats = rocksdb_prefix + dbstats;
|
||||
const std::string DB::Properties::kLevelStats = rocksdb_prefix + levelstats;
|
||||
const std::string DB::Properties::kBlockCacheEntryStats =
|
||||
@@ -450,9 +461,15 @@ const UnorderedMap<std::string, DBPropertyInfo>
|
||||
{DB::Properties::kCFFileHistogram,
|
||||
{false, &InternalStats::HandleCFFileHistogram, nullptr, nullptr,
|
||||
nullptr}},
|
||||
{DB::Properties::kCFWriteStallStats,
|
||||
{false, &InternalStats::HandleCFWriteStallStats, nullptr,
|
||||
&InternalStats::HandleCFWriteStallStatsMap, nullptr}},
|
||||
{DB::Properties::kDBStats,
|
||||
{false, &InternalStats::HandleDBStats, nullptr,
|
||||
&InternalStats::HandleDBMapStats, nullptr}},
|
||||
{DB::Properties::kDBWriteStallStats,
|
||||
{false, &InternalStats::HandleDBWriteStallStats, nullptr,
|
||||
&InternalStats::HandleDBWriteStallStatsMap, nullptr}},
|
||||
{DB::Properties::kBlockCacheEntryStats,
|
||||
{true, &InternalStats::HandleBlockCacheEntryStats, nullptr,
|
||||
&InternalStats::HandleBlockCacheEntryStatsMap, nullptr}},
|
||||
@@ -658,11 +675,6 @@ void InternalStats::CollectCacheEntryStats(bool foreground) {
|
||||
min_interval_factor);
|
||||
}
|
||||
|
||||
std::function<void()> Blah() {
|
||||
static int x = 42;
|
||||
return [&]() { ++x; };
|
||||
}
|
||||
|
||||
std::function<void(const Slice& key, Cache::ObjectPtr value, size_t charge,
|
||||
const Cache::CacheItemHelper* helper)>
|
||||
InternalStats::CacheEntryRoleStats::GetEntryCallback() {
|
||||
@@ -1092,6 +1104,18 @@ bool InternalStats::HandleCFFileHistogram(std::string* value,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool InternalStats::HandleCFWriteStallStats(std::string* value,
|
||||
Slice /*suffix*/) {
|
||||
DumpCFStatsWriteStall(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool InternalStats::HandleCFWriteStallStatsMap(
|
||||
std::map<std::string, std::string>* value, Slice /*suffix*/) {
|
||||
DumpCFMapStatsWriteStall(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool InternalStats::HandleDBMapStats(
|
||||
std::map<std::string, std::string>* db_stats, Slice /*suffix*/) {
|
||||
DumpDBMapStats(db_stats);
|
||||
@@ -1103,6 +1127,18 @@ bool InternalStats::HandleDBStats(std::string* value, Slice /*suffix*/) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool InternalStats::HandleDBWriteStallStats(std::string* value,
|
||||
Slice /*suffix*/) {
|
||||
DumpDBStatsWriteStall(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool InternalStats::HandleDBWriteStallStatsMap(
|
||||
std::map<std::string, std::string>* value, Slice /*suffix*/) {
|
||||
DumpDBMapStatsWriteStall(value);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool InternalStats::HandleSsTables(std::string* value, Slice /*suffix*/) {
|
||||
auto* current = cfd_->current();
|
||||
*value = current->DebugString(true, true);
|
||||
@@ -1585,6 +1621,10 @@ void InternalStats::DumpDBStats(std::string* value) {
|
||||
10000.0 / std::max(interval_seconds_up, 0.001));
|
||||
value->append(buf);
|
||||
|
||||
std::string write_stall_stats;
|
||||
DumpDBStatsWriteStall(&write_stall_stats);
|
||||
value->append(write_stall_stats);
|
||||
|
||||
db_stats_snapshot_.seconds_up = seconds_up;
|
||||
db_stats_snapshot_.ingest_bytes = user_bytes_written;
|
||||
db_stats_snapshot_.write_other = write_other;
|
||||
@@ -1596,6 +1636,50 @@ void InternalStats::DumpDBStats(std::string* value) {
|
||||
db_stats_snapshot_.write_stall_micros = write_stall_micros;
|
||||
}
|
||||
|
||||
void InternalStats::DumpDBMapStatsWriteStall(
|
||||
std::map<std::string, std::string>* value) {
|
||||
constexpr uint32_t max_db_scope_write_stall_cause =
|
||||
static_cast<uint32_t>(WriteStallCause::kDBScopeWriteStallCauseEnumMax);
|
||||
|
||||
for (uint32_t i =
|
||||
max_db_scope_write_stall_cause - kNumDBScopeWriteStallCauses;
|
||||
i < max_db_scope_write_stall_cause; ++i) {
|
||||
for (uint32_t j = 0;
|
||||
j < static_cast<uint32_t>(WriteStallCondition::kNormal); ++j) {
|
||||
WriteStallCause cause = static_cast<WriteStallCause>(i);
|
||||
WriteStallCondition condition = static_cast<WriteStallCondition>(j);
|
||||
InternalStats::InternalDBStatsType internal_db_stat =
|
||||
InternalDBStat(cause, condition);
|
||||
|
||||
if (internal_db_stat == InternalStats::kIntStatsNumMax) {
|
||||
continue;
|
||||
}
|
||||
|
||||
std::string name =
|
||||
WriteStallStatsMapKeys::CauseConditionCount(cause, condition);
|
||||
uint64_t stat =
|
||||
db_stats_[static_cast<std::size_t>(internal_db_stat)].load(
|
||||
std::memory_order_relaxed);
|
||||
(*value)[name] = std::to_string(stat);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void InternalStats::DumpDBStatsWriteStall(std::string* value) {
|
||||
assert(value);
|
||||
|
||||
std::map<std::string, std::string> write_stall_stats_map;
|
||||
DumpDBMapStatsWriteStall(&write_stall_stats_map);
|
||||
|
||||
std::ostringstream str;
|
||||
str << "Write Stall (count): ";
|
||||
|
||||
for (const auto& name_and_stat : write_stall_stats_map) {
|
||||
str << name_and_stat.first << ": " << name_and_stat.second << ", ";
|
||||
}
|
||||
*value = str.str();
|
||||
}
|
||||
|
||||
/**
|
||||
* Dump Compaction Level stats to a map of stat name with "compaction." prefix
|
||||
* to value in double as string. The level in stat name is represented with
|
||||
@@ -1622,7 +1706,7 @@ void InternalStats::DumpCFMapStats(
|
||||
}
|
||||
}
|
||||
|
||||
DumpCFMapStatsIOStalls(cf_stats);
|
||||
DumpCFMapStatsWriteStall(cf_stats);
|
||||
}
|
||||
|
||||
void InternalStats::DumpCFMapStats(
|
||||
@@ -1714,36 +1798,81 @@ void InternalStats::DumpCFMapStatsByPriority(
|
||||
}
|
||||
}
|
||||
|
||||
void InternalStats::DumpCFMapStatsIOStalls(
|
||||
std::map<std::string, std::string>* cf_stats) {
|
||||
(*cf_stats)["io_stalls.level0_slowdown"] =
|
||||
std::to_string(cf_stats_count_[L0_FILE_COUNT_LIMIT_SLOWDOWNS]);
|
||||
(*cf_stats)["io_stalls.level0_slowdown_with_compaction"] =
|
||||
std::to_string(cf_stats_count_[LOCKED_L0_FILE_COUNT_LIMIT_SLOWDOWNS]);
|
||||
(*cf_stats)["io_stalls.level0_numfiles"] =
|
||||
std::to_string(cf_stats_count_[L0_FILE_COUNT_LIMIT_STOPS]);
|
||||
(*cf_stats)["io_stalls.level0_numfiles_with_compaction"] =
|
||||
std::to_string(cf_stats_count_[LOCKED_L0_FILE_COUNT_LIMIT_STOPS]);
|
||||
(*cf_stats)["io_stalls.stop_for_pending_compaction_bytes"] =
|
||||
std::to_string(cf_stats_count_[PENDING_COMPACTION_BYTES_LIMIT_STOPS]);
|
||||
(*cf_stats)["io_stalls.slowdown_for_pending_compaction_bytes"] =
|
||||
std::to_string(cf_stats_count_[PENDING_COMPACTION_BYTES_LIMIT_SLOWDOWNS]);
|
||||
(*cf_stats)["io_stalls.memtable_compaction"] =
|
||||
std::to_string(cf_stats_count_[MEMTABLE_LIMIT_STOPS]);
|
||||
(*cf_stats)["io_stalls.memtable_slowdown"] =
|
||||
std::to_string(cf_stats_count_[MEMTABLE_LIMIT_SLOWDOWNS]);
|
||||
void InternalStats::DumpCFMapStatsWriteStall(
|
||||
std::map<std::string, std::string>* value) {
|
||||
uint64_t total_delays = 0;
|
||||
uint64_t total_stops = 0;
|
||||
constexpr uint32_t max_cf_scope_write_stall_cause =
|
||||
static_cast<uint32_t>(WriteStallCause::kCFScopeWriteStallCauseEnumMax);
|
||||
|
||||
uint64_t total_stop = cf_stats_count_[L0_FILE_COUNT_LIMIT_STOPS] +
|
||||
cf_stats_count_[PENDING_COMPACTION_BYTES_LIMIT_STOPS] +
|
||||
cf_stats_count_[MEMTABLE_LIMIT_STOPS];
|
||||
for (uint32_t i =
|
||||
max_cf_scope_write_stall_cause - kNumCFScopeWriteStallCauses;
|
||||
i < max_cf_scope_write_stall_cause; ++i) {
|
||||
for (uint32_t j = 0;
|
||||
j < static_cast<uint32_t>(WriteStallCondition::kNormal); ++j) {
|
||||
WriteStallCause cause = static_cast<WriteStallCause>(i);
|
||||
WriteStallCondition condition = static_cast<WriteStallCondition>(j);
|
||||
InternalStats::InternalCFStatsType internal_cf_stat =
|
||||
InternalCFStat(cause, condition);
|
||||
|
||||
uint64_t total_slowdown =
|
||||
cf_stats_count_[L0_FILE_COUNT_LIMIT_SLOWDOWNS] +
|
||||
cf_stats_count_[PENDING_COMPACTION_BYTES_LIMIT_SLOWDOWNS] +
|
||||
cf_stats_count_[MEMTABLE_LIMIT_SLOWDOWNS];
|
||||
if (internal_cf_stat == InternalStats::INTERNAL_CF_STATS_ENUM_MAX) {
|
||||
continue;
|
||||
}
|
||||
|
||||
(*cf_stats)["io_stalls.total_stop"] = std::to_string(total_stop);
|
||||
(*cf_stats)["io_stalls.total_slowdown"] = std::to_string(total_slowdown);
|
||||
std::string name =
|
||||
WriteStallStatsMapKeys::CauseConditionCount(cause, condition);
|
||||
uint64_t stat =
|
||||
cf_stats_count_[static_cast<std::size_t>(internal_cf_stat)];
|
||||
(*value)[name] = std::to_string(stat);
|
||||
|
||||
if (condition == WriteStallCondition::kDelayed) {
|
||||
total_delays += stat;
|
||||
} else if (condition == WriteStallCondition::kStopped) {
|
||||
total_stops += stat;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
(*value)[WriteStallStatsMapKeys::
|
||||
CFL0FileCountLimitDelaysWithOngoingCompaction()] =
|
||||
std::to_string(
|
||||
cf_stats_count_[L0_FILE_COUNT_LIMIT_DELAYS_WITH_ONGOING_COMPACTION]);
|
||||
(*value)[WriteStallStatsMapKeys::
|
||||
CFL0FileCountLimitStopsWithOngoingCompaction()] =
|
||||
std::to_string(
|
||||
cf_stats_count_[L0_FILE_COUNT_LIMIT_STOPS_WITH_ONGOING_COMPACTION]);
|
||||
|
||||
(*value)[WriteStallStatsMapKeys::TotalStops()] = std::to_string(total_stops);
|
||||
(*value)[WriteStallStatsMapKeys::TotalDelays()] =
|
||||
std::to_string(total_delays);
|
||||
}
|
||||
|
||||
void InternalStats::DumpCFStatsWriteStall(std::string* value,
|
||||
uint64_t* total_stall_count) {
|
||||
assert(value);
|
||||
|
||||
std::map<std::string, std::string> write_stall_stats_map;
|
||||
DumpCFMapStatsWriteStall(&write_stall_stats_map);
|
||||
|
||||
std::ostringstream str;
|
||||
str << "Write Stall (count): ";
|
||||
|
||||
for (const auto& name_and_stat : write_stall_stats_map) {
|
||||
str << name_and_stat.first << ": " << name_and_stat.second << ", ";
|
||||
}
|
||||
|
||||
if (total_stall_count) {
|
||||
*total_stall_count =
|
||||
ParseUint64(
|
||||
write_stall_stats_map[WriteStallStatsMapKeys::TotalStops()]) +
|
||||
ParseUint64(
|
||||
write_stall_stats_map[WriteStallStatsMapKeys::TotalDelays()]);
|
||||
if (*total_stall_count > 0) {
|
||||
str << "interval: " << *total_stall_count - cf_stats_snapshot_.stall_count
|
||||
<< " total count\n";
|
||||
}
|
||||
}
|
||||
*value = str.str();
|
||||
}
|
||||
|
||||
void InternalStats::DumpCFStats(std::string* value) {
|
||||
@@ -1781,14 +1910,6 @@ void InternalStats::DumpCFStatsNoFileHistogram(bool is_periodic,
|
||||
uint64_t ingest_l0_files_addfile =
|
||||
cf_stats_value_[INGESTED_LEVEL0_NUM_FILES_TOTAL];
|
||||
uint64_t ingest_keys_addfile = cf_stats_value_[INGESTED_NUM_KEYS_TOTAL];
|
||||
// Cumulative summary
|
||||
uint64_t total_stall_count =
|
||||
cf_stats_count_[L0_FILE_COUNT_LIMIT_SLOWDOWNS] +
|
||||
cf_stats_count_[L0_FILE_COUNT_LIMIT_STOPS] +
|
||||
cf_stats_count_[PENDING_COMPACTION_BYTES_LIMIT_SLOWDOWNS] +
|
||||
cf_stats_count_[PENDING_COMPACTION_BYTES_LIMIT_STOPS] +
|
||||
cf_stats_count_[MEMTABLE_LIMIT_STOPS] +
|
||||
cf_stats_count_[MEMTABLE_LIMIT_SLOWDOWNS];
|
||||
// Interval summary
|
||||
uint64_t interval_flush_ingest =
|
||||
flush_ingest - cf_stats_snapshot_.ingest_bytes_flush;
|
||||
@@ -1910,34 +2031,10 @@ void InternalStats::DumpCFStatsNoFileHistogram(bool is_periodic,
|
||||
cf_stats_snapshot_.compact_micros = compact_micros;
|
||||
}
|
||||
|
||||
snprintf(buf, sizeof(buf),
|
||||
"Stalls(count): %" PRIu64
|
||||
" level0_slowdown, "
|
||||
"%" PRIu64
|
||||
" level0_slowdown_with_compaction, "
|
||||
"%" PRIu64
|
||||
" level0_numfiles, "
|
||||
"%" PRIu64
|
||||
" level0_numfiles_with_compaction, "
|
||||
"%" PRIu64
|
||||
" stop for pending_compaction_bytes, "
|
||||
"%" PRIu64
|
||||
" slowdown for pending_compaction_bytes, "
|
||||
"%" PRIu64
|
||||
" memtable_compaction, "
|
||||
"%" PRIu64
|
||||
" memtable_slowdown, "
|
||||
"interval %" PRIu64 " total count\n",
|
||||
cf_stats_count_[L0_FILE_COUNT_LIMIT_SLOWDOWNS],
|
||||
cf_stats_count_[LOCKED_L0_FILE_COUNT_LIMIT_SLOWDOWNS],
|
||||
cf_stats_count_[L0_FILE_COUNT_LIMIT_STOPS],
|
||||
cf_stats_count_[LOCKED_L0_FILE_COUNT_LIMIT_STOPS],
|
||||
cf_stats_count_[PENDING_COMPACTION_BYTES_LIMIT_STOPS],
|
||||
cf_stats_count_[PENDING_COMPACTION_BYTES_LIMIT_SLOWDOWNS],
|
||||
cf_stats_count_[MEMTABLE_LIMIT_STOPS],
|
||||
cf_stats_count_[MEMTABLE_LIMIT_SLOWDOWNS],
|
||||
total_stall_count - cf_stats_snapshot_.stall_count);
|
||||
value->append(buf);
|
||||
std::string write_stall_stats;
|
||||
uint64_t total_stall_count;
|
||||
DumpCFStatsWriteStall(&write_stall_stats, &total_stall_count);
|
||||
value->append(write_stall_stats);
|
||||
|
||||
if (is_periodic) {
|
||||
cf_stats_snapshot_.seconds_up = seconds_up;
|
||||
|
||||
+36
-7
@@ -104,15 +104,20 @@ class InternalStats {
|
||||
static const std::map<LevelStatType, LevelStat> compaction_level_stats;
|
||||
|
||||
enum InternalCFStatsType {
|
||||
L0_FILE_COUNT_LIMIT_SLOWDOWNS,
|
||||
LOCKED_L0_FILE_COUNT_LIMIT_SLOWDOWNS,
|
||||
MEMTABLE_LIMIT_DELAYS,
|
||||
MEMTABLE_LIMIT_STOPS,
|
||||
MEMTABLE_LIMIT_SLOWDOWNS,
|
||||
L0_FILE_COUNT_LIMIT_DELAYS,
|
||||
L0_FILE_COUNT_LIMIT_STOPS,
|
||||
LOCKED_L0_FILE_COUNT_LIMIT_STOPS,
|
||||
PENDING_COMPACTION_BYTES_LIMIT_SLOWDOWNS,
|
||||
PENDING_COMPACTION_BYTES_LIMIT_DELAYS,
|
||||
PENDING_COMPACTION_BYTES_LIMIT_STOPS,
|
||||
// Write slowdown caused by l0 file count limit while there is ongoing L0
|
||||
// compaction
|
||||
L0_FILE_COUNT_LIMIT_DELAYS_WITH_ONGOING_COMPACTION,
|
||||
// Write stop caused by l0 file count limit while there is ongoing L0
|
||||
// compaction
|
||||
L0_FILE_COUNT_LIMIT_STOPS_WITH_ONGOING_COMPACTION,
|
||||
WRITE_STALLS_ENUM_MAX,
|
||||
// End of all write stall stats
|
||||
BYTES_FLUSHED,
|
||||
BYTES_INGESTED_ADD_FILE,
|
||||
INGESTED_NUM_FILES_TOTAL,
|
||||
@@ -129,7 +134,18 @@ class InternalStats {
|
||||
kIntStatsWriteDoneByOther,
|
||||
kIntStatsWriteDoneBySelf,
|
||||
kIntStatsWriteWithWal,
|
||||
// TODO(hx235): Currently `kIntStatsWriteStallMicros` only measures
|
||||
// "delayed" time of CF-scope write stalls, not including the "stopped" time
|
||||
// nor any DB-scope write stalls (e.g, ones triggered by
|
||||
// `WriteBufferManager`).
|
||||
//
|
||||
// However, the word "write stall" includes both "delayed" and "stopped"
|
||||
// (see `WriteStallCondition`) and DB-scope writes stalls (see
|
||||
// `WriteStallCause`).
|
||||
//
|
||||
// So we should improve, rename or clarify it
|
||||
kIntStatsWriteStallMicros,
|
||||
kIntStatsWriteBufferManagerLimitStopsCounts,
|
||||
kIntStatsNumMax,
|
||||
};
|
||||
|
||||
@@ -599,6 +615,10 @@ class InternalStats {
|
||||
private:
|
||||
void DumpDBMapStats(std::map<std::string, std::string>* db_stats);
|
||||
void DumpDBStats(std::string* value);
|
||||
|
||||
void DumpDBMapStatsWriteStall(std::map<std::string, std::string>* value);
|
||||
void DumpDBStatsWriteStall(std::string* value);
|
||||
|
||||
void DumpCFMapStats(std::map<std::string, std::string>* cf_stats);
|
||||
void DumpCFMapStats(
|
||||
const VersionStorageInfo* vstorage,
|
||||
@@ -606,7 +626,6 @@ class InternalStats {
|
||||
CompactionStats* compaction_stats_sum);
|
||||
void DumpCFMapStatsByPriority(
|
||||
std::map<int, std::map<LevelStatType, double>>* priorities_stats);
|
||||
void DumpCFMapStatsIOStalls(std::map<std::string, std::string>* cf_stats);
|
||||
void DumpCFStats(std::string* value);
|
||||
// if is_periodic = true, it is an internal call by RocksDB periodically to
|
||||
// dump the status.
|
||||
@@ -615,6 +634,10 @@ class InternalStats {
|
||||
// dump the status.
|
||||
void DumpCFFileHistogram(std::string* value);
|
||||
|
||||
void DumpCFMapStatsWriteStall(std::map<std::string, std::string>* value);
|
||||
void DumpCFStatsWriteStall(std::string* value,
|
||||
uint64_t* total_stall_count = nullptr);
|
||||
|
||||
Cache* GetBlockCacheForStats();
|
||||
Cache* GetBlobCacheForStats();
|
||||
|
||||
@@ -648,7 +671,7 @@ class InternalStats {
|
||||
// ColumnFamily-level stats
|
||||
CompactionStats comp_stats;
|
||||
uint64_t ingest_bytes_flush; // Bytes written to L0 (Flush)
|
||||
uint64_t stall_count; // Stall count
|
||||
uint64_t stall_count; // Total counts of CF-scope write stalls
|
||||
// Stats from compaction jobs - bytes written, bytes read, duration.
|
||||
uint64_t compact_bytes_write;
|
||||
uint64_t compact_bytes_read;
|
||||
@@ -743,9 +766,15 @@ class InternalStats {
|
||||
bool HandleCFStatsNoFileHistogram(std::string* value, Slice suffix);
|
||||
bool HandleCFFileHistogram(std::string* value, Slice suffix);
|
||||
bool HandleCFStatsPeriodic(std::string* value, Slice suffix);
|
||||
bool HandleCFWriteStallStats(std::string* value, Slice suffix);
|
||||
bool HandleCFWriteStallStatsMap(std::map<std::string, std::string>* values,
|
||||
Slice suffix);
|
||||
bool HandleDBMapStats(std::map<std::string, std::string>* compaction_stats,
|
||||
Slice suffix);
|
||||
bool HandleDBStats(std::string* value, Slice suffix);
|
||||
bool HandleDBWriteStallStats(std::string* value, Slice suffix);
|
||||
bool HandleDBWriteStallStatsMap(std::map<std::string, std::string>* values,
|
||||
Slice suffix);
|
||||
bool HandleSsTables(std::string* value, Slice suffix);
|
||||
bool HandleAggregatedTableProperties(std::string* value, Slice suffix);
|
||||
bool HandleAggregatedTablePropertiesAtLevel(std::string* value, Slice suffix);
|
||||
|
||||
+8
-6
@@ -515,10 +515,11 @@ unsigned int Reader::ReadPhysicalRecord(Slice* result, size_t* drop_size,
|
||||
|
||||
size_t uncompressed_size = 0;
|
||||
int remaining = 0;
|
||||
const char* input = header + header_size;
|
||||
do {
|
||||
remaining = uncompress_->Uncompress(header + header_size, length,
|
||||
uncompressed_buffer_.get(),
|
||||
&uncompressed_size);
|
||||
remaining = uncompress_->Uncompress(
|
||||
input, length, uncompressed_buffer_.get(), &uncompressed_size);
|
||||
input = nullptr;
|
||||
if (remaining < 0) {
|
||||
buffer_.clear();
|
||||
return kBadRecord;
|
||||
@@ -830,10 +831,11 @@ bool FragmentBufferedReader::TryReadFragment(
|
||||
uncompressed_record_.clear();
|
||||
size_t uncompressed_size = 0;
|
||||
int remaining = 0;
|
||||
const char* input = header + header_size;
|
||||
do {
|
||||
remaining = uncompress_->Uncompress(header + header_size, length,
|
||||
uncompressed_buffer_.get(),
|
||||
&uncompressed_size);
|
||||
remaining = uncompress_->Uncompress(
|
||||
input, length, uncompressed_buffer_.get(), &uncompressed_size);
|
||||
input = nullptr;
|
||||
if (remaining < 0) {
|
||||
buffer_.clear();
|
||||
*fragment_type_or_err = kBadRecord;
|
||||
|
||||
+35
-2
@@ -979,6 +979,38 @@ TEST_P(CompressionLogTest, Fragmentation) {
|
||||
ASSERT_EQ("EOF", Read());
|
||||
}
|
||||
|
||||
TEST_P(CompressionLogTest, AlignedFragmentation) {
|
||||
CompressionType compression_type = std::get<2>(GetParam());
|
||||
if (!StreamingCompressionTypeSupported(compression_type)) {
|
||||
ROCKSDB_GTEST_SKIP("Test requires support for compression type");
|
||||
return;
|
||||
}
|
||||
ASSERT_OK(SetupTestEnv());
|
||||
Random rnd(301);
|
||||
int num_filler_records = 0;
|
||||
// Keep writing small records until the next record will be aligned at the
|
||||
// beginning of the block.
|
||||
while ((WrittenBytes() & (kBlockSize - 1)) >= kHeaderSize) {
|
||||
char entry = 'a';
|
||||
ASSERT_OK(writer_->AddRecord(Slice(&entry, 1)));
|
||||
num_filler_records++;
|
||||
}
|
||||
const std::vector<std::string> wal_entries = {
|
||||
rnd.RandomBinaryString(3 * kBlockSize),
|
||||
};
|
||||
for (const std::string& wal_entry : wal_entries) {
|
||||
Write(wal_entry);
|
||||
}
|
||||
|
||||
for (int i = 0; i < num_filler_records; ++i) {
|
||||
ASSERT_EQ("a", Read());
|
||||
}
|
||||
for (const std::string& wal_entry : wal_entries) {
|
||||
ASSERT_EQ(wal_entry, Read());
|
||||
}
|
||||
ASSERT_EQ("EOF", Read());
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(
|
||||
Compression, CompressionLogTest,
|
||||
::testing::Combine(::testing::Values(0, 1), ::testing::Bool(),
|
||||
@@ -1026,10 +1058,11 @@ TEST_P(StreamingCompressionTest, Basic) {
|
||||
for (int i = 0; i < (int)compressed_buffers.size(); i++) {
|
||||
// Call uncompress till either the entire input is consumed or the output
|
||||
// buffer size is equal to the allocated output buffer size.
|
||||
const char* input = compressed_buffers[i].c_str();
|
||||
do {
|
||||
ret_val = uncompress->Uncompress(compressed_buffers[i].c_str(),
|
||||
compressed_buffers[i].size(),
|
||||
ret_val = uncompress->Uncompress(input, compressed_buffers[i].size(),
|
||||
uncompressed_output_buffer, &output_pos);
|
||||
input = nullptr;
|
||||
if (output_pos > 0) {
|
||||
std::string uncompressed_fragment;
|
||||
uncompressed_fragment.assign(uncompressed_output_buffer, output_pos);
|
||||
|
||||
+18
-10
@@ -1078,7 +1078,7 @@ static bool SaveValue(void* arg, const char* entry) {
|
||||
*(s->value) = std::move(result);
|
||||
} else {
|
||||
assert(s->columns);
|
||||
s->columns->SetPlainValue(result);
|
||||
s->columns->SetPlainValue(std::move(result));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1152,7 +1152,7 @@ static bool SaveValue(void* arg, const char* entry) {
|
||||
/* op_failure_scope */ nullptr);
|
||||
|
||||
if (s->status->ok()) {
|
||||
*(s->status) = s->columns->SetWideColumnValue(result);
|
||||
*(s->status) = s->columns->SetWideColumnValue(std::move(result));
|
||||
}
|
||||
}
|
||||
} else if (s->value) {
|
||||
@@ -1200,7 +1200,7 @@ static bool SaveValue(void* arg, const char* entry) {
|
||||
*(s->value) = std::move(result);
|
||||
} else {
|
||||
assert(s->columns);
|
||||
s->columns->SetPlainValue(result);
|
||||
s->columns->SetPlainValue(std::move(result));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -1230,6 +1230,8 @@ static bool SaveValue(void* arg, const char* entry) {
|
||||
*(s->merge_in_progress) = true;
|
||||
merge_context->PushOperand(
|
||||
v, s->inplace_update_support == false /* operand_pinned */);
|
||||
PERF_COUNTER_ADD(internal_merge_point_lookup_count, 1);
|
||||
|
||||
if (s->do_merge && merge_operator->ShouldMerge(
|
||||
merge_context->GetOperandsDirectionBackward())) {
|
||||
if (s->value || s->columns) {
|
||||
@@ -1249,7 +1251,7 @@ static bool SaveValue(void* arg, const char* entry) {
|
||||
*(s->value) = std::move(result);
|
||||
} else {
|
||||
assert(s->columns);
|
||||
s->columns->SetPlainValue(result);
|
||||
s->columns->SetPlainValue(std::move(result));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1452,18 +1454,24 @@ void MemTable::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
||||
}
|
||||
SequenceNumber dummy_seq;
|
||||
GetFromTable(*(iter->lkey), iter->max_covering_tombstone_seq, true,
|
||||
callback, &iter->is_blob_index, iter->value->GetSelf(),
|
||||
/*columns=*/nullptr, iter->timestamp, iter->s,
|
||||
&(iter->merge_context), &dummy_seq, &found_final_value,
|
||||
&merge_in_progress);
|
||||
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);
|
||||
|
||||
if (!found_final_value && merge_in_progress) {
|
||||
*(iter->s) = Status::MergeInProgress();
|
||||
}
|
||||
|
||||
if (found_final_value) {
|
||||
iter->value->PinSelf();
|
||||
range->AddValueSize(iter->value->size());
|
||||
if (iter->value) {
|
||||
iter->value->PinSelf();
|
||||
range->AddValueSize(iter->value->size());
|
||||
} else {
|
||||
assert(iter->columns);
|
||||
range->AddValueSize(iter->columns->serialized_size());
|
||||
}
|
||||
|
||||
range->MarkKeyDone(iter);
|
||||
RecordTick(moptions_.statistics, MEMTABLE_HIT);
|
||||
if (range->GetValueSize() > read_options.value_size_soft_limit) {
|
||||
|
||||
+10
-5
@@ -113,7 +113,7 @@ Status MergeHelper::TimedFullMerge(
|
||||
|
||||
if (!success) {
|
||||
RecordTick(statistics, NUMBER_MERGE_FAILURES);
|
||||
return Status::Corruption("Error: Could not perform merge.");
|
||||
return Status::Corruption(Status::SubCode::kMergeOperatorFailed);
|
||||
}
|
||||
|
||||
return Status::OK();
|
||||
@@ -231,6 +231,10 @@ Status MergeHelper::MergeUntil(InternalIterator* iter,
|
||||
s = Status::ShutdownInProgress();
|
||||
return s;
|
||||
}
|
||||
// Skip range tombstones emitted by the compaction iterator.
|
||||
if (iter->IsDeleteRangeSentinelKey()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
ParsedInternalKey ikey;
|
||||
assert(keys_.size() == merge_context_.GetNumOperands());
|
||||
@@ -578,14 +582,15 @@ CompactionFilter::Decision MergeHelper::FilterMerge(const Slice& user_key,
|
||||
}
|
||||
compaction_filter_value_.clear();
|
||||
compaction_filter_skip_until_.Clear();
|
||||
auto ret = compaction_filter_->FilterV2(
|
||||
level_, user_key, CompactionFilter::ValueType::kMergeOperand, value_slice,
|
||||
&compaction_filter_value_, compaction_filter_skip_until_.rep());
|
||||
auto ret = compaction_filter_->FilterV3(
|
||||
level_, user_key, CompactionFilter::ValueType::kMergeOperand,
|
||||
&value_slice, /* existing_columns */ nullptr, &compaction_filter_value_,
|
||||
/* new_columns */ nullptr, compaction_filter_skip_until_.rep());
|
||||
if (ret == CompactionFilter::Decision::kRemoveAndSkipUntil) {
|
||||
if (user_comparator_->Compare(*compaction_filter_skip_until_.rep(),
|
||||
user_key) <= 0) {
|
||||
// Invalid skip_until returned from compaction filter.
|
||||
// Keep the key as per FilterV2 documentation.
|
||||
// Keep the key as per FilterV2/FilterV3 documentation.
|
||||
ret = CompactionFilter::Decision::kKeep;
|
||||
} else {
|
||||
compaction_filter_skip_until_.ConvertFromUserKey(kMaxSequenceNumber,
|
||||
|
||||
@@ -964,6 +964,157 @@ TEST_F(PerfContextTest, CPUTimer) {
|
||||
ASSERT_EQ(count, get_perf_context()->iter_seek_cpu_nanos);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(PerfContextTest, MergeOperandCount) {
|
||||
ASSERT_OK(DestroyDB(kDbName, Options()));
|
||||
|
||||
DB* db = nullptr;
|
||||
Options options;
|
||||
options.create_if_missing = true;
|
||||
options.merge_operator = MergeOperators::CreateStringAppendOperator();
|
||||
|
||||
ASSERT_OK(DB::Open(options, kDbName, &db));
|
||||
std::unique_ptr<DB> db_guard(db);
|
||||
|
||||
constexpr size_t num_keys = 3;
|
||||
const std::string key_prefix("key");
|
||||
const std::string value_prefix("value");
|
||||
|
||||
std::vector<std::string> keys;
|
||||
keys.reserve(num_keys);
|
||||
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
keys.emplace_back(key_prefix + std::to_string(i));
|
||||
}
|
||||
|
||||
// Write three keys with one Put each followed by 1, 2, and 3
|
||||
// Merge operations respectively.
|
||||
constexpr size_t total_merges = num_keys * (num_keys + 1) / 2;
|
||||
|
||||
std::vector<ManagedSnapshot> snapshots;
|
||||
snapshots.reserve(total_merges);
|
||||
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
const std::string suffix = std::to_string(i);
|
||||
const std::string value = value_prefix + suffix;
|
||||
|
||||
ASSERT_OK(db->Put(WriteOptions(), keys[i], value));
|
||||
|
||||
for (size_t j = 0; j <= i; ++j) {
|
||||
// Take a snapshot before each Merge so they are preserved and not
|
||||
// collapsed during flush.
|
||||
snapshots.emplace_back(db);
|
||||
|
||||
ASSERT_OK(db->Merge(WriteOptions(), keys[i], value + std::to_string(j)));
|
||||
}
|
||||
}
|
||||
|
||||
auto verify = [&]() {
|
||||
get_perf_context()->Reset();
|
||||
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
// Get
|
||||
{
|
||||
PinnableSlice result;
|
||||
ASSERT_OK(db->Get(ReadOptions(), db->DefaultColumnFamily(), keys[i],
|
||||
&result));
|
||||
ASSERT_EQ(get_perf_context()->internal_merge_point_lookup_count, i + 1);
|
||||
|
||||
get_perf_context()->Reset();
|
||||
}
|
||||
|
||||
// GetEntity
|
||||
{
|
||||
PinnableWideColumns result;
|
||||
ASSERT_OK(db->GetEntity(ReadOptions(), db->DefaultColumnFamily(),
|
||||
keys[i], &result));
|
||||
ASSERT_EQ(get_perf_context()->internal_merge_point_lookup_count, i + 1);
|
||||
|
||||
get_perf_context()->Reset();
|
||||
}
|
||||
}
|
||||
|
||||
{
|
||||
std::vector<Slice> key_slices;
|
||||
key_slices.reserve(num_keys);
|
||||
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
key_slices.emplace_back(keys[i]);
|
||||
}
|
||||
|
||||
// MultiGet
|
||||
{
|
||||
std::vector<PinnableSlice> results(num_keys);
|
||||
std::vector<Status> statuses(num_keys);
|
||||
|
||||
db->MultiGet(ReadOptions(), db->DefaultColumnFamily(), num_keys,
|
||||
&key_slices[0], &results[0], &statuses[0]);
|
||||
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
ASSERT_OK(statuses[i]);
|
||||
}
|
||||
|
||||
ASSERT_EQ(get_perf_context()->internal_merge_point_lookup_count,
|
||||
total_merges);
|
||||
|
||||
get_perf_context()->Reset();
|
||||
}
|
||||
|
||||
// MultiGetEntity
|
||||
{
|
||||
std::vector<PinnableWideColumns> results(num_keys);
|
||||
std::vector<Status> statuses(num_keys);
|
||||
|
||||
db->MultiGetEntity(ReadOptions(), db->DefaultColumnFamily(), num_keys,
|
||||
&key_slices[0], &results[0], &statuses[0]);
|
||||
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
ASSERT_OK(statuses[i]);
|
||||
}
|
||||
|
||||
ASSERT_EQ(get_perf_context()->internal_merge_point_lookup_count,
|
||||
total_merges);
|
||||
|
||||
get_perf_context()->Reset();
|
||||
}
|
||||
}
|
||||
|
||||
std::unique_ptr<Iterator> it(db->NewIterator(ReadOptions()));
|
||||
|
||||
// Forward iteration
|
||||
{
|
||||
size_t i = 0;
|
||||
|
||||
for (it->SeekToFirst(); it->Valid(); it->Next(), ++i) {
|
||||
ASSERT_EQ(it->key(), keys[i]);
|
||||
ASSERT_EQ(get_perf_context()->internal_merge_count, i + 1);
|
||||
|
||||
get_perf_context()->Reset();
|
||||
}
|
||||
}
|
||||
|
||||
// Backward iteration
|
||||
{
|
||||
size_t i = num_keys - 1;
|
||||
|
||||
for (it->SeekToLast(); it->Valid(); it->Prev(), --i) {
|
||||
ASSERT_EQ(it->key(), keys[i]);
|
||||
ASSERT_EQ(get_perf_context()->internal_merge_count, i + 1);
|
||||
|
||||
get_perf_context()->Reset();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Verify counters when reading from memtable
|
||||
verify();
|
||||
|
||||
// Verify counters when reading from table files
|
||||
db->Flush(FlushOptions());
|
||||
|
||||
verify();
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
+48
-17
@@ -30,12 +30,15 @@ TruncatedRangeDelIterator::TruncatedRangeDelIterator(
|
||||
icmp_(icmp),
|
||||
smallest_ikey_(smallest),
|
||||
largest_ikey_(largest) {
|
||||
// Set up bounds such that range tombstones from this iterator are
|
||||
// truncated to range [smallest_, largest_).
|
||||
if (smallest != nullptr) {
|
||||
pinned_bounds_.emplace_back();
|
||||
auto& parsed_smallest = pinned_bounds_.back();
|
||||
Status pik_status = ParseInternalKey(smallest->Encode(), &parsed_smallest,
|
||||
false /* log_err_key */); // TODO
|
||||
pik_status.PermitUncheckedError();
|
||||
parsed_smallest.type = kTypeMaxValid;
|
||||
assert(pik_status.ok());
|
||||
smallest_ = &parsed_smallest;
|
||||
}
|
||||
@@ -63,6 +66,8 @@ TruncatedRangeDelIterator::TruncatedRangeDelIterator(
|
||||
//
|
||||
// Therefore, we will never truncate a range tombstone at largest, so we
|
||||
// can leave it unchanged.
|
||||
// TODO: maybe use kMaxValid here to ensure range tombstone having
|
||||
// distinct key from point keys.
|
||||
} else {
|
||||
// The same user key may straddle two sstable boundaries. To ensure that
|
||||
// the truncated end key can cover the largest key in this sstable, reduce
|
||||
@@ -70,7 +75,7 @@ TruncatedRangeDelIterator::TruncatedRangeDelIterator(
|
||||
parsed_largest.sequence -= 1;
|
||||
// This line is not needed for correctness, but it ensures that the
|
||||
// truncated end key is not covering keys from the next SST file.
|
||||
parsed_largest.type = kValueTypeForSeek;
|
||||
parsed_largest.type = kTypeMaxValid;
|
||||
}
|
||||
largest_ = &parsed_largest;
|
||||
}
|
||||
@@ -101,6 +106,24 @@ void TruncatedRangeDelIterator::Seek(const Slice& target) {
|
||||
iter_->Seek(target);
|
||||
}
|
||||
|
||||
void TruncatedRangeDelIterator::SeekInternalKey(const Slice& target) {
|
||||
if (largest_ && icmp_->Compare(*largest_, target) <= 0) {
|
||||
iter_->Invalidate();
|
||||
return;
|
||||
}
|
||||
if (smallest_ && icmp_->Compare(target, *smallest_) < 0) {
|
||||
// Since target < smallest, target < largest_.
|
||||
// This seek must land on a range tombstone where end_key() > target,
|
||||
// so there is no need to check again.
|
||||
iter_->Seek(smallest_->user_key);
|
||||
} else {
|
||||
iter_->Seek(ExtractUserKey(target));
|
||||
while (Valid() && icmp_->Compare(end_key(), target) <= 0) {
|
||||
Next();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: target is a user key, with timestamp if enabled.
|
||||
void TruncatedRangeDelIterator::SeekForPrev(const Slice& target) {
|
||||
if (smallest_ != nullptr &&
|
||||
@@ -393,21 +416,20 @@ bool CompactionRangeDelAggregator::ShouldDelete(const ParsedInternalKey& parsed,
|
||||
namespace {
|
||||
|
||||
// Produce a sorted (by start internal key) stream of range tombstones from
|
||||
// `children`. lower_bound and upper_bound on user key can be
|
||||
// `children`. lower_bound and upper_bound on internal key can be
|
||||
// optionally specified. Range tombstones that ends before lower_bound or starts
|
||||
// after upper_bound are excluded.
|
||||
// If user-defined timestamp is enabled, lower_bound and upper_bound should
|
||||
// contain timestamp, but comparison is done ignoring timestamps.
|
||||
// contain timestamp.
|
||||
class TruncatedRangeDelMergingIter : public InternalIterator {
|
||||
public:
|
||||
TruncatedRangeDelMergingIter(
|
||||
const InternalKeyComparator* icmp, const Slice* lower_bound,
|
||||
const Slice* upper_bound, bool upper_bound_inclusive,
|
||||
const Slice* upper_bound,
|
||||
const std::vector<std::unique_ptr<TruncatedRangeDelIterator>>& children)
|
||||
: icmp_(icmp),
|
||||
lower_bound_(lower_bound),
|
||||
upper_bound_(upper_bound),
|
||||
upper_bound_inclusive_(upper_bound_inclusive),
|
||||
heap_(StartKeyMinComparator(icmp)),
|
||||
ts_sz_(icmp_->user_comparator()->timestamp_size()) {
|
||||
for (auto& child : children) {
|
||||
@@ -420,7 +442,7 @@ class TruncatedRangeDelMergingIter : public InternalIterator {
|
||||
}
|
||||
|
||||
bool Valid() const override {
|
||||
return !heap_.empty() && BeforeEndKey(heap_.top());
|
||||
return !heap_.empty() && !AfterEndKey(heap_.top());
|
||||
}
|
||||
Status status() const override { return Status::OK(); }
|
||||
|
||||
@@ -428,7 +450,13 @@ class TruncatedRangeDelMergingIter : public InternalIterator {
|
||||
heap_.clear();
|
||||
for (auto& child : children_) {
|
||||
if (lower_bound_ != nullptr) {
|
||||
child->Seek(*lower_bound_);
|
||||
child->Seek(ExtractUserKey(*lower_bound_));
|
||||
// Since the above `Seek()` operates on a user key while `lower_bound_`
|
||||
// is an internal key, we may need to advance `child` farther for it to
|
||||
// be in bounds.
|
||||
while (child->Valid() && BeforeStartKey(child)) {
|
||||
child->InternalNext();
|
||||
}
|
||||
} else {
|
||||
child->SeekToFirst();
|
||||
}
|
||||
@@ -481,19 +509,23 @@ class TruncatedRangeDelMergingIter : public InternalIterator {
|
||||
void SeekToLast() override { assert(false); }
|
||||
|
||||
private:
|
||||
bool BeforeEndKey(const TruncatedRangeDelIterator* iter) const {
|
||||
if (upper_bound_ == nullptr) {
|
||||
return true;
|
||||
bool BeforeStartKey(const TruncatedRangeDelIterator* iter) const {
|
||||
if (lower_bound_ == nullptr) {
|
||||
return false;
|
||||
}
|
||||
int cmp = icmp_->user_comparator()->CompareWithoutTimestamp(
|
||||
iter->start_key().user_key, *upper_bound_);
|
||||
return upper_bound_inclusive_ ? cmp <= 0 : cmp < 0;
|
||||
return icmp_->Compare(iter->end_key(), *lower_bound_) <= 0;
|
||||
}
|
||||
|
||||
bool AfterEndKey(const TruncatedRangeDelIterator* iter) const {
|
||||
if (upper_bound_ == nullptr) {
|
||||
return false;
|
||||
}
|
||||
return icmp_->Compare(iter->start_key(), *upper_bound_) > 0;
|
||||
}
|
||||
|
||||
const InternalKeyComparator* icmp_;
|
||||
const Slice* lower_bound_;
|
||||
const Slice* upper_bound_;
|
||||
bool upper_bound_inclusive_;
|
||||
BinaryHeap<TruncatedRangeDelIterator*, StartKeyMinComparator> heap_;
|
||||
std::vector<TruncatedRangeDelIterator*> children_;
|
||||
|
||||
@@ -506,11 +538,10 @@ class TruncatedRangeDelMergingIter : public InternalIterator {
|
||||
|
||||
std::unique_ptr<FragmentedRangeTombstoneIterator>
|
||||
CompactionRangeDelAggregator::NewIterator(const Slice* lower_bound,
|
||||
const Slice* upper_bound,
|
||||
bool upper_bound_inclusive) {
|
||||
const Slice* upper_bound) {
|
||||
InvalidateRangeDelMapPositions();
|
||||
auto merging_iter = std::make_unique<TruncatedRangeDelMergingIter>(
|
||||
icmp_, lower_bound, upper_bound, upper_bound_inclusive, parent_iters_);
|
||||
icmp_, lower_bound, upper_bound, parent_iters_);
|
||||
|
||||
auto fragmented_tombstone_list =
|
||||
std::make_shared<FragmentedRangeTombstoneList>(
|
||||
|
||||
@@ -49,6 +49,9 @@ class TruncatedRangeDelIterator {
|
||||
// REQUIRES: target is a user key.
|
||||
void Seek(const Slice& target);
|
||||
|
||||
// Seeks to the first range tombstone with end_key() > target.
|
||||
void SeekInternalKey(const Slice& target);
|
||||
|
||||
// Seeks to the tombstone with the highest visible sequence number that covers
|
||||
// target (a user key). If no such tombstone exists, the position will be at
|
||||
// the latest tombstone that starts before target.
|
||||
@@ -452,16 +455,15 @@ class CompactionRangeDelAggregator : public RangeDelAggregator {
|
||||
}
|
||||
|
||||
// Creates an iterator over all the range tombstones in the aggregator, for
|
||||
// use in compaction. Nullptr arguments indicate that the iterator range is
|
||||
// unbounded.
|
||||
// NOTE: the boundaries are used for optimization purposes to reduce the
|
||||
// number of tombstones that are passed to the fragmenter; they do not
|
||||
// guarantee that the resulting iterator only contains range tombstones that
|
||||
// cover keys in the provided range. If required, these bounds must be
|
||||
// use in compaction.
|
||||
//
|
||||
// NOTE: the internal key boundaries are used for optimization purposes to
|
||||
// reduce the number of tombstones that are passed to the fragmenter; they do
|
||||
// not guarantee that the resulting iterator only contains range tombstones
|
||||
// that cover keys in the provided range. If required, these bounds must be
|
||||
// enforced during iteration.
|
||||
std::unique_ptr<FragmentedRangeTombstoneIterator> NewIterator(
|
||||
const Slice* lower_bound = nullptr, const Slice* upper_bound = nullptr,
|
||||
bool upper_bound_inclusive = false);
|
||||
const Slice* lower_bound = nullptr, const Slice* upper_bound = nullptr);
|
||||
|
||||
private:
|
||||
std::vector<std::unique_ptr<TruncatedRangeDelIterator>> parent_iters_;
|
||||
|
||||
@@ -224,26 +224,32 @@ TEST_F(RangeDelAggregatorTest, UntruncatedIter) {
|
||||
TruncatedRangeDelIterator iter(std::move(input_iter), &bytewise_icmp, nullptr,
|
||||
nullptr);
|
||||
|
||||
VerifyIterator(&iter, bytewise_icmp,
|
||||
{{UncutEndpoint("a"), UncutEndpoint("e"), 10},
|
||||
{UncutEndpoint("e"), UncutEndpoint("g"), 8},
|
||||
{UncutEndpoint("j"), UncutEndpoint("n"), 4}});
|
||||
VerifyIterator(
|
||||
&iter, bytewise_icmp,
|
||||
{{InternalValue("a", 10, kTypeRangeDeletion), UncutEndpoint("e"), 10},
|
||||
{InternalValue("e", 8, kTypeRangeDeletion), UncutEndpoint("g"), 8},
|
||||
{InternalValue("j", 4, kTypeRangeDeletion), UncutEndpoint("n"), 4}});
|
||||
|
||||
VerifySeek(
|
||||
&iter, bytewise_icmp,
|
||||
{{"d", UncutEndpoint("a"), UncutEndpoint("e"), 10},
|
||||
{"e", UncutEndpoint("e"), UncutEndpoint("g"), 8},
|
||||
{"ia", UncutEndpoint("j"), UncutEndpoint("n"), 4},
|
||||
{"n", UncutEndpoint(""), UncutEndpoint(""), 0, true /* invalid */},
|
||||
{"", UncutEndpoint("a"), UncutEndpoint("e"), 10}});
|
||||
{{"d", InternalValue("a", 10, kTypeRangeDeletion), UncutEndpoint("e"),
|
||||
10},
|
||||
{"e", InternalValue("e", 8, kTypeRangeDeletion), UncutEndpoint("g"), 8},
|
||||
{"ia", InternalValue("j", 4, kTypeRangeDeletion), UncutEndpoint("n"), 4},
|
||||
{"n", InternalValue("", 0, kTypeRangeDeletion), UncutEndpoint(""), 0,
|
||||
true /* invalid */},
|
||||
{"", InternalValue("a", 10, kTypeRangeDeletion), UncutEndpoint("e"),
|
||||
10}});
|
||||
|
||||
VerifySeekForPrev(
|
||||
&iter, bytewise_icmp,
|
||||
{{"d", UncutEndpoint("a"), UncutEndpoint("e"), 10},
|
||||
{"e", UncutEndpoint("e"), UncutEndpoint("g"), 8},
|
||||
{"ia", UncutEndpoint("e"), UncutEndpoint("g"), 8},
|
||||
{"n", UncutEndpoint("j"), UncutEndpoint("n"), 4},
|
||||
{"", UncutEndpoint(""), UncutEndpoint(""), 0, true /* invalid */}});
|
||||
{{"d", InternalValue("a", 10, kTypeRangeDeletion), UncutEndpoint("e"),
|
||||
10},
|
||||
{"e", InternalValue("e", 8, kTypeRangeDeletion), UncutEndpoint("g"), 8},
|
||||
{"ia", InternalValue("e", 8, kTypeRangeDeletion), UncutEndpoint("g"), 8},
|
||||
{"n", InternalValue("j", 4, kTypeRangeDeletion), UncutEndpoint("n"), 4},
|
||||
{"", InternalValue("", 0, kTypeRangeDeletion), UncutEndpoint(""), 0,
|
||||
true /* invalid */}});
|
||||
}
|
||||
|
||||
TEST_F(RangeDelAggregatorTest, UntruncatedIterWithSnapshot) {
|
||||
@@ -258,25 +264,29 @@ TEST_F(RangeDelAggregatorTest, UntruncatedIterWithSnapshot) {
|
||||
TruncatedRangeDelIterator iter(std::move(input_iter), &bytewise_icmp, nullptr,
|
||||
nullptr);
|
||||
|
||||
VerifyIterator(&iter, bytewise_icmp,
|
||||
{{UncutEndpoint("e"), UncutEndpoint("g"), 8},
|
||||
{UncutEndpoint("j"), UncutEndpoint("n"), 4}});
|
||||
VerifyIterator(
|
||||
&iter, bytewise_icmp,
|
||||
{{InternalValue("e", 8, kTypeRangeDeletion), UncutEndpoint("g"), 8},
|
||||
{InternalValue("j", 4, kTypeRangeDeletion), UncutEndpoint("n"), 4}});
|
||||
|
||||
VerifySeek(
|
||||
&iter, bytewise_icmp,
|
||||
{{"d", UncutEndpoint("e"), UncutEndpoint("g"), 8},
|
||||
{"e", UncutEndpoint("e"), UncutEndpoint("g"), 8},
|
||||
{"ia", UncutEndpoint("j"), UncutEndpoint("n"), 4},
|
||||
{"n", UncutEndpoint(""), UncutEndpoint(""), 0, true /* invalid */},
|
||||
{"", UncutEndpoint("e"), UncutEndpoint("g"), 8}});
|
||||
{{"d", InternalValue("e", 8, kTypeRangeDeletion), UncutEndpoint("g"), 8},
|
||||
{"e", InternalValue("e", 8, kTypeRangeDeletion), UncutEndpoint("g"), 8},
|
||||
{"ia", InternalValue("j", 4, kTypeRangeDeletion), UncutEndpoint("n"), 4},
|
||||
{"n", InternalValue("", 0, kTypeRangeDeletion), UncutEndpoint(""), 0,
|
||||
true /* invalid */},
|
||||
{"", InternalValue("e", 8, kTypeRangeDeletion), UncutEndpoint("g"), 8}});
|
||||
|
||||
VerifySeekForPrev(
|
||||
&iter, bytewise_icmp,
|
||||
{{"d", UncutEndpoint(""), UncutEndpoint(""), 0, true /* invalid */},
|
||||
{"e", UncutEndpoint("e"), UncutEndpoint("g"), 8},
|
||||
{"ia", UncutEndpoint("e"), UncutEndpoint("g"), 8},
|
||||
{"n", UncutEndpoint("j"), UncutEndpoint("n"), 4},
|
||||
{"", UncutEndpoint(""), UncutEndpoint(""), 0, true /* invalid */}});
|
||||
{{"d", InternalValue("", 0, kTypeRangeDeletion), UncutEndpoint(""), 0,
|
||||
true /* invalid */},
|
||||
{"e", InternalValue("e", 8, kTypeRangeDeletion), UncutEndpoint("g"), 8},
|
||||
{"ia", InternalValue("e", 8, kTypeRangeDeletion), UncutEndpoint("g"), 8},
|
||||
{"n", InternalValue("j", 4, kTypeRangeDeletion), UncutEndpoint("n"), 4},
|
||||
{"", InternalValue("", 0, kTypeRangeDeletion), UncutEndpoint(""), 0,
|
||||
true /* invalid */}});
|
||||
}
|
||||
|
||||
TEST_F(RangeDelAggregatorTest, TruncatedIterPartiallyCutTombstones) {
|
||||
@@ -295,27 +305,30 @@ TEST_F(RangeDelAggregatorTest, TruncatedIterPartiallyCutTombstones) {
|
||||
|
||||
VerifyIterator(
|
||||
&iter, bytewise_icmp,
|
||||
{{InternalValue("d", 7), UncutEndpoint("e"), 10},
|
||||
{UncutEndpoint("e"), UncutEndpoint("g"), 8},
|
||||
{UncutEndpoint("j"), InternalValue("m", 8, kValueTypeForSeek), 4}});
|
||||
{{InternalValue("d", 7, kTypeMaxValid), UncutEndpoint("e"), 10},
|
||||
{InternalValue("e", 8, kTypeRangeDeletion), UncutEndpoint("g"), 8},
|
||||
{InternalValue("j", 4, kTypeRangeDeletion),
|
||||
InternalValue("m", 8, kTypeMaxValid), 4}});
|
||||
|
||||
VerifySeek(
|
||||
&iter, bytewise_icmp,
|
||||
{{"d", InternalValue("d", 7), UncutEndpoint("e"), 10},
|
||||
{"e", UncutEndpoint("e"), UncutEndpoint("g"), 8},
|
||||
{"ia", UncutEndpoint("j"), InternalValue("m", 8, kValueTypeForSeek), 4,
|
||||
false /* invalid */},
|
||||
{"n", UncutEndpoint(""), UncutEndpoint(""), 0, true /* invalid */},
|
||||
{"", InternalValue("d", 7), UncutEndpoint("e"), 10}});
|
||||
{{"d", InternalValue("d", 7, kTypeMaxValid), UncutEndpoint("e"), 10},
|
||||
{"e", InternalValue("e", 8, kTypeRangeDeletion), UncutEndpoint("g"), 8},
|
||||
{"ia", InternalValue("j", 4, kTypeRangeDeletion),
|
||||
InternalValue("m", 8, kTypeMaxValid), 4, false /* invalid */},
|
||||
{"n", InternalValue("", 0, kTypeRangeDeletion), UncutEndpoint(""), 0,
|
||||
true /* invalid */},
|
||||
{"", InternalValue("d", 7, kTypeMaxValid), UncutEndpoint("e"), 10}});
|
||||
|
||||
VerifySeekForPrev(
|
||||
&iter, bytewise_icmp,
|
||||
{{"d", InternalValue("d", 7), UncutEndpoint("e"), 10},
|
||||
{"e", UncutEndpoint("e"), UncutEndpoint("g"), 8},
|
||||
{"ia", UncutEndpoint("e"), UncutEndpoint("g"), 8},
|
||||
{"n", UncutEndpoint("j"), InternalValue("m", 8, kValueTypeForSeek), 4,
|
||||
false /* invalid */},
|
||||
{"", UncutEndpoint(""), UncutEndpoint(""), 0, true /* invalid */}});
|
||||
{{"d", InternalValue("d", 7, kTypeMaxValid), UncutEndpoint("e"), 10},
|
||||
{"e", InternalValue("e", 8, kTypeRangeDeletion), UncutEndpoint("g"), 8},
|
||||
{"ia", InternalValue("e", 8, kTypeRangeDeletion), UncutEndpoint("g"), 8},
|
||||
{"n", InternalValue("j", 4, kTypeRangeDeletion),
|
||||
InternalValue("m", 8, kTypeMaxValid), 4, false /* invalid */},
|
||||
{"", InternalValue("", 0, kTypeRangeDeletion), UncutEndpoint(""), 0,
|
||||
true /* invalid */}});
|
||||
}
|
||||
|
||||
TEST_F(RangeDelAggregatorTest, TruncatedIterFullyCutTombstones) {
|
||||
@@ -332,20 +345,23 @@ TEST_F(RangeDelAggregatorTest, TruncatedIterFullyCutTombstones) {
|
||||
TruncatedRangeDelIterator iter(std::move(input_iter), &bytewise_icmp,
|
||||
&smallest, &largest);
|
||||
|
||||
VerifyIterator(&iter, bytewise_icmp,
|
||||
{{InternalValue("f", 7), UncutEndpoint("g"), 8}});
|
||||
VerifyIterator(
|
||||
&iter, bytewise_icmp,
|
||||
{{InternalValue("f", 7, kTypeMaxValid), UncutEndpoint("g"), 8}});
|
||||
|
||||
VerifySeek(
|
||||
&iter, bytewise_icmp,
|
||||
{{"d", InternalValue("f", 7), UncutEndpoint("g"), 8},
|
||||
{"f", InternalValue("f", 7), UncutEndpoint("g"), 8},
|
||||
{"j", UncutEndpoint(""), UncutEndpoint(""), 0, true /* invalid */}});
|
||||
{{"d", InternalValue("f", 7, kTypeMaxValid), UncutEndpoint("g"), 8},
|
||||
{"f", InternalValue("f", 7, kTypeMaxValid), UncutEndpoint("g"), 8},
|
||||
{"j", InternalValue("", 0, kTypeRangeDeletion), UncutEndpoint(""), 0,
|
||||
true /* invalid */}});
|
||||
|
||||
VerifySeekForPrev(
|
||||
&iter, bytewise_icmp,
|
||||
{{"d", UncutEndpoint(""), UncutEndpoint(""), 0, true /* invalid */},
|
||||
{"f", InternalValue("f", 7), UncutEndpoint("g"), 8},
|
||||
{"j", InternalValue("f", 7), UncutEndpoint("g"), 8}});
|
||||
{{"d", InternalValue("", 0, kTypeRangeDeletion), UncutEndpoint(""), 0,
|
||||
true /* invalid */},
|
||||
{"f", InternalValue("f", 7, kTypeMaxValid), UncutEndpoint("g"), 8},
|
||||
{"j", InternalValue("f", 7, kTypeMaxValid), UncutEndpoint("g"), 8}});
|
||||
}
|
||||
|
||||
TEST_F(RangeDelAggregatorTest, SingleIterInAggregator) {
|
||||
@@ -627,15 +643,12 @@ TEST_F(RangeDelAggregatorTest, CompactionAggregatorEmptyIteratorRight) {
|
||||
range_del_agg.AddTombstones(std::move(input_iter));
|
||||
}
|
||||
|
||||
Slice start("p");
|
||||
Slice end("q");
|
||||
auto range_del_compaction_iter1 =
|
||||
range_del_agg.NewIterator(&start, &end, false /* end_key_inclusive */);
|
||||
VerifyFragmentedRangeDels(range_del_compaction_iter1.get(), {});
|
||||
|
||||
auto range_del_compaction_iter2 =
|
||||
range_del_agg.NewIterator(&start, &end, true /* end_key_inclusive */);
|
||||
VerifyFragmentedRangeDels(range_del_compaction_iter2.get(), {});
|
||||
InternalKey start_buf("p", 0, kTypeRangeDeletion);
|
||||
InternalKey end_buf("q", 0, kTypeRangeDeletion);
|
||||
Slice start = start_buf.Encode();
|
||||
Slice end = end_buf.Encode();
|
||||
auto range_del_compaction_iter = range_del_agg.NewIterator(&start, &end);
|
||||
VerifyFragmentedRangeDels(range_del_compaction_iter.get(), {});
|
||||
}
|
||||
|
||||
TEST_F(RangeDelAggregatorTest, CompactionAggregatorBoundedIterator) {
|
||||
@@ -652,18 +665,13 @@ TEST_F(RangeDelAggregatorTest, CompactionAggregatorBoundedIterator) {
|
||||
range_del_agg.AddTombstones(std::move(input_iter));
|
||||
}
|
||||
|
||||
Slice start("bb");
|
||||
Slice end("e");
|
||||
auto range_del_compaction_iter1 =
|
||||
range_del_agg.NewIterator(&start, &end, false /* end_key_inclusive */);
|
||||
VerifyFragmentedRangeDels(range_del_compaction_iter1.get(),
|
||||
InternalKey start_buf("bb", 0, kTypeRangeDeletion);
|
||||
InternalKey end_buf("e", 9, kTypeRangeDeletion);
|
||||
Slice start = start_buf.Encode();
|
||||
Slice end = end_buf.Encode();
|
||||
auto range_del_compaction_iter = range_del_agg.NewIterator(&start, &end);
|
||||
VerifyFragmentedRangeDels(range_del_compaction_iter.get(),
|
||||
{{"a", "c", 10}, {"c", "e", 10}, {"c", "e", 8}});
|
||||
|
||||
auto range_del_compaction_iter2 =
|
||||
range_del_agg.NewIterator(&start, &end, true /* end_key_inclusive */);
|
||||
VerifyFragmentedRangeDels(
|
||||
range_del_compaction_iter2.get(),
|
||||
{{"a", "c", 10}, {"c", "e", 10}, {"c", "e", 8}, {"e", "g", 8}});
|
||||
}
|
||||
|
||||
TEST_F(RangeDelAggregatorTest,
|
||||
@@ -681,29 +689,19 @@ TEST_F(RangeDelAggregatorTest,
|
||||
range_del_agg.AddTombstones(std::move(input_iter));
|
||||
}
|
||||
|
||||
Slice start("bb");
|
||||
Slice end("e");
|
||||
auto range_del_compaction_iter1 =
|
||||
range_del_agg.NewIterator(&start, &end, false /* end_key_inclusive */);
|
||||
VerifyFragmentedRangeDels(range_del_compaction_iter1.get(), {{"a", "b", 10},
|
||||
{"b", "c", 20},
|
||||
{"b", "c", 10},
|
||||
{"c", "d", 10},
|
||||
{"c", "d", 8},
|
||||
{"d", "f", 30},
|
||||
{"d", "f", 8},
|
||||
{"f", "g", 8}});
|
||||
|
||||
auto range_del_compaction_iter2 =
|
||||
range_del_agg.NewIterator(&start, &end, true /* end_key_inclusive */);
|
||||
VerifyFragmentedRangeDels(range_del_compaction_iter2.get(), {{"a", "b", 10},
|
||||
{"b", "c", 20},
|
||||
{"b", "c", 10},
|
||||
{"c", "d", 10},
|
||||
{"c", "d", 8},
|
||||
{"d", "f", 30},
|
||||
{"d", "f", 8},
|
||||
{"f", "g", 8}});
|
||||
InternalKey start_buf("bb", 0, kTypeRangeDeletion);
|
||||
InternalKey end_buf("e", 0, kTypeRangeDeletion);
|
||||
Slice start = start_buf.Encode();
|
||||
Slice end = end_buf.Encode();
|
||||
auto range_del_compaction_iter = range_del_agg.NewIterator(&start, &end);
|
||||
VerifyFragmentedRangeDels(range_del_compaction_iter.get(), {{"a", "b", 10},
|
||||
{"b", "c", 20},
|
||||
{"b", "c", 10},
|
||||
{"c", "d", 10},
|
||||
{"c", "d", 8},
|
||||
{"d", "f", 30},
|
||||
{"d", "f", 8},
|
||||
{"f", "g", 8}});
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
@@ -218,8 +218,7 @@ class FragmentedRangeTombstoneIterator : public InternalIterator {
|
||||
}
|
||||
|
||||
ParsedInternalKey parsed_start_key() const {
|
||||
return ParsedInternalKey(pos_->start_key, kMaxSequenceNumber,
|
||||
kTypeRangeDeletion);
|
||||
return ParsedInternalKey(pos_->start_key, seq(), kTypeRangeDeletion);
|
||||
}
|
||||
ParsedInternalKey parsed_end_key() const {
|
||||
return ParsedInternalKey(pos_->end_key, kMaxSequenceNumber,
|
||||
|
||||
+1
-1
@@ -20,8 +20,8 @@
|
||||
#include "db/wal_edit.h"
|
||||
#include "memory/arena.h"
|
||||
#include "port/malloc.h"
|
||||
#include "rocksdb/advanced_cache.h"
|
||||
#include "rocksdb/advanced_options.h"
|
||||
#include "rocksdb/cache.h"
|
||||
#include "table/table_reader.h"
|
||||
#include "table/unique_id_impl.h"
|
||||
#include "util/autovector.h"
|
||||
|
||||
@@ -27,7 +27,7 @@ void VersionEditHandlerBase::Iterate(log::Reader& reader,
|
||||
assert(log_read_status);
|
||||
assert(log_read_status->ok());
|
||||
|
||||
size_t recovered_edits = 0;
|
||||
[[maybe_unused]] size_t recovered_edits = 0;
|
||||
Status s = Initialize();
|
||||
while (reader.LastRecordEnd() < max_manifest_read_size_ && s.ok() &&
|
||||
reader.ReadRecord(&record, &scratch) && log_read_status->ok()) {
|
||||
|
||||
+83
-44
@@ -23,7 +23,6 @@
|
||||
#include "db/blob/blob_fetcher.h"
|
||||
#include "db/blob/blob_file_cache.h"
|
||||
#include "db/blob/blob_file_reader.h"
|
||||
#include "db/blob/blob_index.h"
|
||||
#include "db/blob/blob_log_format.h"
|
||||
#include "db/blob/blob_source.h"
|
||||
#include "db/compaction/compaction.h"
|
||||
@@ -39,6 +38,8 @@
|
||||
#include "db/table_cache.h"
|
||||
#include "db/version_builder.h"
|
||||
#include "db/version_edit_handler.h"
|
||||
#include "table/compaction_merging_iterator.h"
|
||||
|
||||
#if USE_COROUTINES
|
||||
#include "folly/experimental/coro/BlockingWait.h"
|
||||
#include "folly/experimental/coro/Collect.h"
|
||||
@@ -1775,6 +1776,8 @@ void Version::GetColumnFamilyMetaData(ColumnFamilyMetaData* cf_meta) {
|
||||
file->file_checksum, file->file_checksum_func_name);
|
||||
files.back().num_entries = file->num_entries;
|
||||
files.back().num_deletions = file->num_deletions;
|
||||
files.back().smallest = file->smallest.Encode().ToString();
|
||||
files.back().largest = file->largest.Encode().ToString();
|
||||
level_size += file->fd.GetFileSize();
|
||||
}
|
||||
cf_meta->levels.emplace_back(level, level_size, std::move(files));
|
||||
@@ -2118,7 +2121,8 @@ Version::Version(ColumnFamilyData* column_family_data, VersionSet* vset,
|
||||
max_file_size_for_l0_meta_pin_(
|
||||
MaxFileSizeForL0MetaPin(mutable_cf_options_)),
|
||||
version_number_(version_number),
|
||||
io_tracer_(io_tracer) {}
|
||||
io_tracer_(io_tracer),
|
||||
use_async_io_(env_->GetFileSystem()->use_async_io()) {}
|
||||
|
||||
Status Version::GetBlob(const ReadOptions& read_options, const Slice& user_key,
|
||||
const Slice& blob_index_slice,
|
||||
@@ -2177,26 +2181,35 @@ void Version::MultiGetBlob(
|
||||
|
||||
autovector<BlobReadRequest> blob_reqs_in_file;
|
||||
BlobReadContexts& blobs_in_file = ctx.second;
|
||||
for (const auto& blob : blobs_in_file) {
|
||||
const BlobIndex& blob_index = blob.first;
|
||||
const KeyContext& key_context = blob.second;
|
||||
for (auto& blob : blobs_in_file) {
|
||||
const BlobIndex& blob_index = blob.blob_index;
|
||||
const KeyContext* const key_context = blob.key_context;
|
||||
assert(key_context);
|
||||
assert(key_context->get_context);
|
||||
assert(key_context->s);
|
||||
|
||||
if (key_context->value) {
|
||||
key_context->value->Reset();
|
||||
} else {
|
||||
assert(key_context->columns);
|
||||
key_context->columns->Reset();
|
||||
}
|
||||
|
||||
if (!blob_file_meta) {
|
||||
*key_context.s = Status::Corruption("Invalid blob file number");
|
||||
*key_context->s = Status::Corruption("Invalid blob file number");
|
||||
continue;
|
||||
}
|
||||
|
||||
if (blob_index.HasTTL() || blob_index.IsInlined()) {
|
||||
*key_context.s =
|
||||
*key_context->s =
|
||||
Status::Corruption("Unexpected TTL/inlined blob index");
|
||||
continue;
|
||||
}
|
||||
|
||||
key_context.value->Reset();
|
||||
blob_reqs_in_file.emplace_back(
|
||||
key_context.get_context->ukey_to_get_blob_value(),
|
||||
key_context->get_context->ukey_to_get_blob_value(),
|
||||
blob_index.offset(), blob_index.size(), blob_index.compression(),
|
||||
key_context.value, key_context.s);
|
||||
&blob.result, key_context->s);
|
||||
}
|
||||
if (blob_reqs_in_file.size() > 0) {
|
||||
const auto file_size = blob_file_meta->GetBlobFileSize();
|
||||
@@ -2211,18 +2224,29 @@ void Version::MultiGetBlob(
|
||||
|
||||
for (auto& ctx : blob_ctxs) {
|
||||
BlobReadContexts& blobs_in_file = ctx.second;
|
||||
for (const auto& blob : blobs_in_file) {
|
||||
const KeyContext& key_context = blob.second;
|
||||
if (key_context.s->ok()) {
|
||||
range.AddValueSize(key_context.value->size());
|
||||
if (range.GetValueSize() > read_options.value_size_soft_limit) {
|
||||
*key_context.s = Status::Aborted();
|
||||
for (auto& blob : blobs_in_file) {
|
||||
const KeyContext* const key_context = blob.key_context;
|
||||
assert(key_context);
|
||||
assert(key_context->get_context);
|
||||
assert(key_context->s);
|
||||
|
||||
if (key_context->s->ok()) {
|
||||
if (key_context->value) {
|
||||
*key_context->value = std::move(blob.result);
|
||||
range.AddValueSize(key_context->value->size());
|
||||
} else {
|
||||
assert(key_context->columns);
|
||||
key_context->columns->SetPlainValue(std::move(blob.result));
|
||||
range.AddValueSize(key_context->columns->serialized_size());
|
||||
}
|
||||
} else if (key_context.s->IsIncomplete()) {
|
||||
|
||||
if (range.GetValueSize() > read_options.value_size_soft_limit) {
|
||||
*key_context->s = Status::Aborted();
|
||||
}
|
||||
} else if (key_context->s->IsIncomplete()) {
|
||||
// read_options.read_tier == kBlockCacheTier
|
||||
// Cannot read blob(s): no disk I/O allowed
|
||||
assert(key_context.get_context);
|
||||
auto& get_context = *(key_context.get_context);
|
||||
auto& get_context = *(key_context->get_context);
|
||||
get_context.MarkKeyMayExist();
|
||||
}
|
||||
}
|
||||
@@ -2369,8 +2393,7 @@ void Version::Get(const ReadOptions& read_options, const LookupKey& k,
|
||||
*value = std::move(result);
|
||||
} else {
|
||||
assert(columns);
|
||||
columns->Reset();
|
||||
columns->SetPlainValue(result);
|
||||
columns->SetPlainValue(std::move(result));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2388,6 +2411,9 @@ void Version::Get(const ReadOptions& read_options, const LookupKey& k,
|
||||
"Encounter unexpected blob index. Please open DB with "
|
||||
"ROCKSDB_NAMESPACE::blob_db::BlobDB instead.");
|
||||
return;
|
||||
case GetContext::kMergeOperatorFailed:
|
||||
*status = Status::Corruption(Status::SubCode::kMergeOperatorFailed);
|
||||
return;
|
||||
}
|
||||
f = fp.GetNextFile();
|
||||
}
|
||||
@@ -2421,7 +2447,7 @@ void Version::Get(const ReadOptions& read_options, const LookupKey& k,
|
||||
value->PinSelf();
|
||||
} else {
|
||||
assert(columns != nullptr);
|
||||
columns->SetPlainValue(result);
|
||||
columns->SetPlainValue(std::move(result));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2457,7 +2483,7 @@ void Version::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
||||
get_ctx.emplace_back(
|
||||
user_comparator(), merge_operator_, info_log_, db_statistics_,
|
||||
iter->s->ok() ? GetContext::kNotFound : GetContext::kMerge,
|
||||
iter->ukey_with_ts, iter->value, /*columns=*/nullptr, iter->timestamp,
|
||||
iter->ukey_with_ts, iter->value, iter->columns, iter->timestamp,
|
||||
nullptr, &(iter->merge_context), true,
|
||||
&iter->max_covering_tombstone_seq, clock_, nullptr,
|
||||
merge_operator_ ? &pinned_iters_mgr : nullptr, callback,
|
||||
@@ -2480,7 +2506,7 @@ void Version::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
||||
MultiGetRange keys_with_blobs_range(*range, range->begin(), range->end());
|
||||
#if USE_COROUTINES
|
||||
if (read_options.async_io && read_options.optimize_multiget_for_io &&
|
||||
using_coroutines()) {
|
||||
using_coroutines() && use_async_io_) {
|
||||
s = MultiGetAsync(read_options, range, &blob_ctxs);
|
||||
} else
|
||||
#endif // USE_COROUTINES
|
||||
@@ -2506,7 +2532,7 @@ void Version::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
||||
// Avoid using the coroutine version if we're looking in a L0 file, since
|
||||
// L0 files won't be parallelized anyway. The regular synchronous version
|
||||
// is faster.
|
||||
if (!read_options.async_io || !using_coroutines() ||
|
||||
if (!read_options.async_io || !using_coroutines() || !use_async_io_ ||
|
||||
fp.GetHitFileLevel() == 0 || !fp.RemainingOverlapInLevel()) {
|
||||
if (f) {
|
||||
bool skip_filters =
|
||||
@@ -2657,23 +2683,29 @@ void Version::MultiGet(const ReadOptions& read_options, MultiGetRange* range,
|
||||
}
|
||||
// merge_operands are in saver and we hit the beginning of the key history
|
||||
// do a final merge of nullptr and operands;
|
||||
std::string* str_value =
|
||||
iter->value != nullptr ? iter->value->GetSelf() : nullptr;
|
||||
std::string result;
|
||||
|
||||
// `op_failure_scope` (an output parameter) is not provided (set to
|
||||
// nullptr) since a failure must be propagated regardless of its value.
|
||||
*status = MergeHelper::TimedFullMerge(
|
||||
merge_operator_, user_key, nullptr, iter->merge_context.GetOperands(),
|
||||
str_value, info_log_, db_statistics_, clock_,
|
||||
&result, info_log_, db_statistics_, clock_,
|
||||
/* result_operand */ nullptr, /* update_num_ops_stats */ true,
|
||||
/* op_failure_scope */ nullptr);
|
||||
if (LIKELY(iter->value != nullptr)) {
|
||||
*iter->value->GetSelf() = std::move(result);
|
||||
iter->value->PinSelf();
|
||||
range->AddValueSize(iter->value->size());
|
||||
range->MarkKeyDone(iter);
|
||||
if (range->GetValueSize() > read_options.value_size_soft_limit) {
|
||||
s = Status::Aborted();
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
assert(iter->columns);
|
||||
iter->columns->SetPlainValue(std::move(result));
|
||||
range->AddValueSize(iter->columns->serialized_size());
|
||||
}
|
||||
|
||||
range->MarkKeyDone(iter);
|
||||
if (range->GetValueSize() > read_options.value_size_soft_limit) {
|
||||
s = Status::Aborted();
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
range->MarkKeyDone(iter);
|
||||
@@ -5077,15 +5109,10 @@ Status VersionSet::ProcessManifestWrites(
|
||||
if (!descriptor_log_ ||
|
||||
manifest_file_size_ > db_options_->max_manifest_file_size) {
|
||||
TEST_SYNC_POINT("VersionSet::ProcessManifestWrites:BeforeNewManifest");
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"VersionSet::ProcessManifestWrites:BeforeNewManifest", nullptr);
|
||||
new_descriptor_log = true;
|
||||
} else {
|
||||
pending_manifest_file_number_ = manifest_file_number_;
|
||||
}
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"VersionSet::ProcessManifestWrites:PostDecidingCreateNewManifestOrNot",
|
||||
&new_descriptor_log);
|
||||
|
||||
// Local cached copy of state variable(s). WriteCurrentStateToManifest()
|
||||
// reads its content after releasing db mutex to avoid race with
|
||||
@@ -6612,6 +6639,14 @@ InternalIterator* VersionSet::MakeInputIterator(
|
||||
c->num_input_levels() - 1
|
||||
: c->num_input_levels());
|
||||
InternalIterator** list = new InternalIterator*[space];
|
||||
// First item in the pair is a pointer to range tombstones.
|
||||
// Second item is a pointer to a member of a LevelIterator,
|
||||
// that will be initialized to where CompactionMergingIterator stores
|
||||
// pointer to its range tombstones. This is used by LevelIterator
|
||||
// to update pointer to range tombstones as it traverse different SST files.
|
||||
std::vector<
|
||||
std::pair<TruncatedRangeDelIterator*, TruncatedRangeDelIterator***>>
|
||||
range_tombstones;
|
||||
size_t num = 0;
|
||||
for (size_t which = 0; which < c->num_input_levels(); which++) {
|
||||
if (c->input_levels(which)->num_files != 0) {
|
||||
@@ -6632,7 +6667,7 @@ InternalIterator* VersionSet::MakeInputIterator(
|
||||
end.value(), fmd.smallest.user_key()) < 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
TruncatedRangeDelIterator* range_tombstone_iter = nullptr;
|
||||
list[num++] = cfd->table_cache()->NewIterator(
|
||||
read_options, file_options_compactions,
|
||||
cfd->internal_comparator(), fmd, range_del_agg,
|
||||
@@ -6645,10 +6680,13 @@ InternalIterator* VersionSet::MakeInputIterator(
|
||||
MaxFileSizeForL0MetaPin(*c->mutable_cf_options()),
|
||||
/*smallest_compaction_key=*/nullptr,
|
||||
/*largest_compaction_key=*/nullptr,
|
||||
/*allow_unprepared_value=*/false);
|
||||
/*allow_unprepared_value=*/false,
|
||||
/*range_del_iter=*/&range_tombstone_iter);
|
||||
range_tombstones.emplace_back(range_tombstone_iter, nullptr);
|
||||
}
|
||||
} else {
|
||||
// Create concatenating iterator for the files from this level
|
||||
TruncatedRangeDelIterator*** tombstone_iter_ptr = nullptr;
|
||||
list[num++] = new LevelIterator(
|
||||
cfd->table_cache(), read_options, file_options_compactions,
|
||||
cfd->internal_comparator(), c->input_levels(which),
|
||||
@@ -6657,14 +6695,15 @@ InternalIterator* VersionSet::MakeInputIterator(
|
||||
/*no per level latency histogram=*/nullptr,
|
||||
TableReaderCaller::kCompaction, /*skip_filters=*/false,
|
||||
/*level=*/static_cast<int>(c->level(which)), range_del_agg,
|
||||
c->boundaries(which));
|
||||
c->boundaries(which), false, &tombstone_iter_ptr);
|
||||
range_tombstones.emplace_back(nullptr, tombstone_iter_ptr);
|
||||
}
|
||||
}
|
||||
}
|
||||
assert(num <= space);
|
||||
InternalIterator* result =
|
||||
NewMergingIterator(&c->column_family_data()->internal_comparator(), list,
|
||||
static_cast<int>(num));
|
||||
InternalIterator* result = NewCompactionMergingIterator(
|
||||
&c->column_family_data()->internal_comparator(), list,
|
||||
static_cast<int>(num), range_tombstones);
|
||||
delete[] list;
|
||||
return result;
|
||||
}
|
||||
|
||||
+11
-2
@@ -33,6 +33,7 @@
|
||||
|
||||
#include "cache/cache_helpers.h"
|
||||
#include "db/blob/blob_file_meta.h"
|
||||
#include "db/blob/blob_index.h"
|
||||
#include "db/column_family.h"
|
||||
#include "db/compaction/compaction.h"
|
||||
#include "db/compaction/compaction_picker.h"
|
||||
@@ -889,8 +890,15 @@ class Version {
|
||||
FilePrefetchBuffer* prefetch_buffer, PinnableSlice* value,
|
||||
uint64_t* bytes_read) const;
|
||||
|
||||
using BlobReadContext =
|
||||
std::pair<BlobIndex, std::reference_wrapper<const KeyContext>>;
|
||||
struct BlobReadContext {
|
||||
BlobReadContext(const BlobIndex& blob_idx, const KeyContext* key_ctx)
|
||||
: blob_index(blob_idx), key_context(key_ctx) {}
|
||||
|
||||
BlobIndex blob_index;
|
||||
const KeyContext* key_context;
|
||||
PinnableSlice result;
|
||||
};
|
||||
|
||||
using BlobReadContexts = std::vector<BlobReadContext>;
|
||||
void MultiGetBlob(const ReadOptions& read_options, MultiGetRange& range,
|
||||
std::unordered_map<uint64_t, BlobReadContexts>& blob_ctxs);
|
||||
@@ -1067,6 +1075,7 @@ class Version {
|
||||
// used for debugging and logging purposes only.
|
||||
uint64_t version_number_;
|
||||
std::shared_ptr<IOTracer> io_tracer_;
|
||||
bool use_async_io_;
|
||||
|
||||
Version(ColumnFamilyData* cfd, VersionSet* vset, const FileOptions& file_opt,
|
||||
MutableCFOptions mutable_cf_options,
|
||||
|
||||
Loaded 100 of 246 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user