mirror of
https://github.com/facebook/rocksdb.git
synced 2026-11-06 02:52:35 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c2452696d9 | ||
|
|
d56ad2328b | ||
|
|
540d5aae51 | ||
|
|
0e860cb42a | ||
|
|
dcd4435c9f | ||
|
|
b91628b4cb | ||
|
|
be73bf1b02 | ||
|
|
35bc98893d | ||
|
|
7c138a6358 | ||
|
|
354fa5f635 | ||
|
|
a58fae99c0 | ||
|
|
70ba7cbb28 | ||
|
|
dcc3eb3576 | ||
|
|
efdb428edc | ||
|
|
faa0f9723c | ||
|
|
7506c1a4ca | ||
|
|
fb579a221c | ||
|
|
dcb6a3be4e | ||
|
|
69a18b9bad | ||
|
|
a543773bbc | ||
|
|
00e68e7a30 | ||
|
|
a3acf2ef87 | ||
|
|
66685d6aa1 | ||
|
|
c8b20d469d | ||
|
|
6ce0b2ca34 | ||
|
|
7e1b417824 | ||
|
|
5f9fe7f21e | ||
|
|
86c2d0a95d | ||
|
|
9645e66fc9 | ||
|
|
b283f041f5 | ||
|
|
769b156e65 | ||
|
|
7679f22a89 | ||
|
|
7e2004a123 | ||
|
|
2f13f5f7d0 | ||
|
|
96206531bc | ||
|
|
28586be8ec | ||
|
|
fc51b7f33a | ||
|
|
c987eb4712 | ||
|
|
c277aeb42c | ||
|
|
8debfe2b21 | ||
|
|
e6c5e0ab9a | ||
|
|
071fe39c05 | ||
|
|
2acbf386a3 | ||
|
|
177b2fa341 | ||
|
|
f9cfc6a808 | ||
|
|
0ff7713112 | ||
|
|
caced09e79 | ||
|
|
9eced1a344 | ||
|
|
a9565ccb26 | ||
|
|
d6b9c4ae26 | ||
|
|
a9117a3490 | ||
|
|
54f678cd86 | ||
|
|
e716bda010 | ||
|
|
11215e0f3a | ||
|
|
4428c76181 | ||
|
|
4f51101d31 | ||
|
|
720ab355f9 | ||
|
|
b397dcd390 | ||
|
|
056e08d6c4 | ||
|
|
20754b3654 | ||
|
|
17a6f7faaf | ||
|
|
b87c355772 | ||
|
|
57a0e2f304 | ||
|
|
c2dc4c0c52 | ||
|
|
490fcac078 | ||
|
|
6115254416 | ||
|
|
12bfd519de | ||
|
|
28f2d3cca6 | ||
|
|
b6cfda1283 | ||
|
|
d96febeeaa | ||
|
|
ca81b80d83 | ||
|
|
d7ebb58cb5 | ||
|
|
c6055cba30 | ||
|
|
a1eb02f089 | ||
|
|
0d1e0722ef | ||
|
|
d3de59255a | ||
|
|
c73d2a9d18 | ||
|
|
f322f273b0 | ||
|
|
8e63d90ff8 | ||
|
|
410ca2efd2 | ||
|
|
246d469750 | ||
|
|
052666aed5 | ||
|
|
725df120e9 | ||
|
|
6061905790 | ||
|
|
2a3792edfc | ||
|
|
17a1d65e3a | ||
|
|
ba1f62ddfb | ||
|
|
b52620ab0e | ||
|
|
f81ea75df7 | ||
|
|
5879053fd0 | ||
|
|
2352e2dfda | ||
|
|
c073ed7601 | ||
|
|
e103b87296 | ||
|
|
a16e2ff82a | ||
|
|
36fefd7e22 | ||
|
|
9586dcf1ce | ||
|
|
42c631b339 | ||
|
|
84210c9489 | ||
|
|
d654888b8f | ||
|
|
f7605ec655 | ||
|
|
30141461f9 | ||
|
|
b012d23557 | ||
|
|
3afed7408c | ||
|
|
deff48bcef | ||
|
|
4207872fc3 | ||
|
|
8f59c41cc7 | ||
|
|
501543573a | ||
|
|
ccb4f047ae | ||
|
|
6358e1b967 |
No files matched your search
+15
-1
@@ -206,7 +206,11 @@ commands:
|
||||
destination: test_logs
|
||||
- run:
|
||||
name: Send benchmark report to visualisation
|
||||
command: ./build_tools/benchmark_log_tool.py --tsvfile /tmp/benchmark-results/report.tsv --esdocument https://search-rocksdb-bench-k2izhptfeap2hjfxteolsgsynm.us-west-2.es.amazonaws.com/bench_test3/_doc
|
||||
command: |
|
||||
set +e
|
||||
set +o pipefail
|
||||
./build_tools/benchmark_log_tool.py --tsvfile /tmp/benchmark-results/report.tsv --esdocument https://search-rocksdb-bench-k2izhptfeap2hjfxteolsgsynm.us-west-2.es.amazonaws.com/bench_test3/_doc
|
||||
true
|
||||
|
||||
executors:
|
||||
windows-2xlarge:
|
||||
@@ -964,6 +968,16 @@ workflows:
|
||||
build-fuzzers:
|
||||
jobs:
|
||||
- build-fuzzers
|
||||
benchmark-linux:
|
||||
triggers:
|
||||
- schedule:
|
||||
cron: "0 * * * *"
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- main
|
||||
jobs:
|
||||
- benchmark-linux
|
||||
nightly:
|
||||
triggers:
|
||||
- schedule:
|
||||
|
||||
@@ -627,7 +627,11 @@ set(SOURCES
|
||||
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/compaction_outputs.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
|
||||
@@ -666,6 +670,7 @@ set(SOURCES
|
||||
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
|
||||
@@ -1231,6 +1236,7 @@ if(WITH_TESTS)
|
||||
db/compaction/compaction_iterator_test.cc
|
||||
db/compaction/compaction_picker_test.cc
|
||||
db/compaction/compaction_service_test.cc
|
||||
db/compaction/tiered_compaction_test.cc
|
||||
db/comparator_db_test.cc
|
||||
db/corruption_test.cc
|
||||
db/cuckoo_table_db_test.cc
|
||||
@@ -1291,6 +1297,7 @@ if(WITH_TESTS)
|
||||
db/perf_context_test.cc
|
||||
db/periodic_work_scheduler_test.cc
|
||||
db/plain_table_db_test.cc
|
||||
db/seqno_time_test.cc
|
||||
db/prefix_test.cc
|
||||
db/range_del_aggregator_test.cc
|
||||
db/range_tombstone_fragmenter_test.cc
|
||||
@@ -1301,6 +1308,7 @@ if(WITH_TESTS)
|
||||
db/version_set_test.cc
|
||||
db/wal_manager_test.cc
|
||||
db/wal_edit_test.cc
|
||||
db/wide/db_wide_basic_test.cc
|
||||
db/wide/wide_column_serialization_test.cc
|
||||
db/write_batch_test.cc
|
||||
db/write_callback_test.cc
|
||||
|
||||
+59
@@ -1,4 +1,58 @@
|
||||
# Rocksdb Change Log
|
||||
## 7.5.4 (11/03/2022)
|
||||
* Fixed a bug where blobs read during compaction would pollute the cache.
|
||||
|
||||
## 7.5.3 (08/24/2022)
|
||||
* Fixed a regression where L0's compaction score could become very large.
|
||||
|
||||
## 7.5.2 (08/02/2022)
|
||||
### Bug Fixes
|
||||
* Fix a bug starting in 7.4.0 in which some fsync operations might be skipped in a DB after any DropColumnFamily on that DB, until it is re-opened. This can lead to data loss on power loss. (For custom FileSystem implementations, this could lead to `FSDirectory::Fsync` or `FSDirectory::Close` after the first `FSDirectory::Close`; Also, valgrind could report call to `close()` with `fd=-1`.)
|
||||
|
||||
## 7.5.1 (08/01/2022)
|
||||
### Bug Fixes
|
||||
* Fix a bug where rate_limiter_parameter is not passed into `PartitionedFilterBlockReader::GetFilterPartitionBlock`.
|
||||
|
||||
## 7.5.0 (07/15/2022)
|
||||
### New Features
|
||||
* Mempurge option flag `experimental_mempurge_threshold` is now a ColumnFamilyOptions and can now be dynamically configured using `SetOptions()`.
|
||||
* Support backward iteration when `ReadOptions::iter_start_ts` is set.
|
||||
* Provide support for ReadOptions.async_io with direct_io to improve Seek latency by using async IO to parallelize child iterator seek and doing asynchronous prefetching on sequential scans.
|
||||
* Added support for blob caching in order to cache frequently used blobs for BlobDB.
|
||||
* User can configure the new ColumnFamilyOptions `blob_cache` to enable/disable blob caching.
|
||||
* Either sharing the backend cache with the block cache or using a completely separate cache is supported.
|
||||
* A new abstraction interface called `BlobSource` for blob read logic gives all users access to blobs, whether they are in the blob cache, secondary cache, or (remote) storage. Blobs can be potentially read both while handling user reads (`Get`, `MultiGet`, or iterator) and during compaction (while dealing with compaction filters, Merges, or garbage collection) but eventually all blob reads go through `Version::GetBlob` or, for MultiGet, `Version::MultiGetBlob` (and then get dispatched to the interface -- `BlobSource`).
|
||||
* Add experimental tiered compaction feature `AdvancedColumnFamilyOptions::preclude_last_level_data_seconds`, which makes sure the new data inserted within preclude_last_level_data_seconds won't be placed on cold tier (the feature is not complete).
|
||||
|
||||
### Public API changes
|
||||
* Add metadata related structs and functions in C API, including
|
||||
* `rocksdb_get_column_family_metadata()` and `rocksdb_get_column_family_metadata_cf()` to obtain `rocksdb_column_family_metadata_t`.
|
||||
* `rocksdb_column_family_metadata_t` and its get functions & destroy function.
|
||||
* `rocksdb_level_metadata_t` and its and its get functions & destroy function.
|
||||
* `rocksdb_file_metadata_t` and its and get functions & destroy functions.
|
||||
* Add suggest_compact_range() and suggest_compact_range_cf() to C API.
|
||||
* When using block cache strict capacity limit (`LRUCache` with `strict_capacity_limit=true`), DB operations now fail with Status code `kAborted` subcode `kMemoryLimit` (`IsMemoryLimit()`) instead of `kIncomplete` (`IsIncomplete()`) when the capacity limit is reached, because Incomplete can mean other specific things for some operations. In more detail, `Cache::Insert()` now returns the updated Status code and this usually propagates through RocksDB to the user on failure.
|
||||
* NewClockCache calls temporarily return an LRUCache (with similar characteristics as the desired ClockCache). This is because ClockCache is being replaced by a new version (the old one had unknown bugs) but this is still under development.
|
||||
* Add two functions `int ReserveThreads(int threads_to_be_reserved)` and `int ReleaseThreads(threads_to_be_released)` into `Env` class. In the default implementation, both return 0. Newly added `xxxEnv` class that inherits `Env` should implement these two functions for thread reservation/releasing features.
|
||||
* Removed Customizable support for RateLimiter and removed its CreateFromString() and Type() functions.
|
||||
|
||||
### Bug Fixes
|
||||
* Fix a bug in which backup/checkpoint can include a WAL deleted by RocksDB.
|
||||
* Fix a bug where concurrent compactions might cause unnecessary further write stalling. In some cases, this might cause write rate to drop to minimum.
|
||||
* Fix a bug in Logger where if dbname and db_log_dir are on different filesystems, dbname creation would fail wrt to db_log_dir path returning an error and fails to open the DB.
|
||||
* Fix a CPU and memory efficiency issue introduce by https://github.com/facebook/rocksdb/pull/8336 which made InternalKeyComparator configurable as an unintended side effect
|
||||
* Fix a bug where `GenericRateLimiter` could revert the bandwidth set dynamically using `SetBytesPerSecond()` when a user configures a structure enclosing it, e.g., using `GetOptionsFromString()` to configure an `Options` that references an existing `RateLimiter` object.
|
||||
|
||||
## Behavior Change
|
||||
* In leveled compaction with dynamic levelling, level multiplier is not anymore adjusted due to oversized L0. Instead, compaction score is adjusted by increasing size level target by adding incoming bytes from upper levels. This would deprioritize compactions from upper levels if more data from L0 is coming. This is to fix some unnecessary full stalling due to drastic change of level targets, while not wasting write bandwidth for compaction while writes are overloaded.
|
||||
* For track_and_verify_wals_in_manifest, revert to the original behavior before #10087: syncing of live WAL file is not tracked, and we track only the synced sizes of **closed** WALs. (PR #10330).
|
||||
* WAL compression now computes/verifies checksum during compression/decompression.
|
||||
|
||||
### Performance Improvements
|
||||
* Rather than doing total sort against all files in a level, SortFileByOverlappingRatio() to only find the top 50 files based on score. This can improve write throughput for the use cases where data is loaded in increasing key order and there are a lot of files in one LSM-tree, where applying compaction results is the bottleneck.
|
||||
* In leveled compaction, L0->L1 trivial move will allow more than one file to be moved in one compaction. This would allow L0 files to be moved down faster when data is loaded in sequential order, making slowdown or stop condition harder to hit. Also seek L0->L1 trivial move when only some files qualify.
|
||||
* In leveled compaction, try to trivial move more than one files if possible, up to 4 files or max_compaction_bytes. This is to allow higher write throughput for some use cases where data is loaded in sequential order, where appying compaction results is the bottleneck.
|
||||
|
||||
## 7.4.0 (06/19/2022)
|
||||
### Bug Fixes
|
||||
* Fixed a bug in calculating key-value integrity protection for users of in-place memtable updates. In particular, the affected users would be those who configure `protection_bytes_per_key > 0` on `WriteBatch` or `WriteOptions`, and configure `inplace_callback != nullptr`.
|
||||
@@ -13,6 +67,7 @@
|
||||
* Avoid a crash if the IDENTITY file is accidentally truncated to empty. A new DB ID will be written and generated on Open.
|
||||
* Fixed a possible corruption for users of `manual_wal_flush` and/or `FlushWAL(true /* sync */)`, together with `track_and_verify_wals_in_manifest == true`. For those users, losing unsynced data (e.g., due to power loss) could make future DB opens fail with a `Status::Corruption` complaining about missing WAL data.
|
||||
* Fixed a bug in `WriteBatchInternal::Append()` where WAL termination point in write batch was not considered and the function appends an incorrect number of checksums.
|
||||
* Fixed a crash bug introduced in 7.3.0 affecting users of MultiGet with `kDataBlockBinaryAndHash`.
|
||||
|
||||
### Public API changes
|
||||
* Add new API GetUnixTime in Snapshot class which returns the unix time at which Snapshot is taken.
|
||||
@@ -39,6 +94,10 @@
|
||||
* Add support for timestamped snapshots (#9879)
|
||||
* Provide support for AbortIO in posix to cancel submitted asynchronous requests using io_uring.
|
||||
* Add support for rate-limiting batched `MultiGet()` APIs
|
||||
* Added several new tickers, perf context statistics, and DB properties to BlobDB
|
||||
* Added new DB properties "rocksdb.blob-cache-capacity", "rocksdb.blob-cache-usage", "rocksdb.blob-cache-pinned-usage" to show blob cache usage.
|
||||
* Added new perf context statistics `blob_cache_hit_count`, `blob_read_count`, `blob_read_byte`, `blob_read_time`, `blob_checksum_time` and `blob_decompress_time`.
|
||||
* Added new tickers `BLOB_DB_CACHE_MISS`, `BLOB_DB_CACHE_HIT`, `BLOB_DB_CACHE_ADD`, `BLOB_DB_CACHE_ADD_FAILURES`, `BLOB_DB_CACHE_BYTES_READ` and `BLOB_DB_CACHE_BYTES_WRITE`.
|
||||
|
||||
### Behavior changes
|
||||
* DB::Open(), DB::OpenAsSecondary() will fail if a Logger cannot be created (#9984)
|
||||
|
||||
@@ -136,6 +136,7 @@ CXXFLAGS += $(PLATFORM_SHARED_CFLAGS) -DROCKSDB_DLL
|
||||
CFLAGS += $(PLATFORM_SHARED_CFLAGS) -DROCKSDB_DLL
|
||||
endif
|
||||
|
||||
GIT_COMMAND ?= git
|
||||
ifeq ($(USE_COROUTINES), 1)
|
||||
USE_FOLLY = 1
|
||||
OPT += -DUSE_COROUTINES
|
||||
@@ -1383,6 +1384,9 @@ db_blob_compaction_test: $(OBJ_DIR)/db/blob/db_blob_compaction_test.o $(TEST_LIB
|
||||
db_readonly_with_timestamp_test: $(OBJ_DIR)/db/db_readonly_with_timestamp_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
db_wide_basic_test: $(OBJ_DIR)/db/wide/db_wide_basic_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
db_with_timestamp_basic_test: $(OBJ_DIR)/db/db_with_timestamp_basic_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
@@ -1500,6 +1504,9 @@ db_table_properties_test: $(OBJ_DIR)/db/db_table_properties_test.o $(TEST_LIBRAR
|
||||
log_write_bench: $(OBJ_DIR)/util/log_write_bench.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK) $(PROFILING_FLAGS)
|
||||
|
||||
seqno_time_test: $(OBJ_DIR)/db/seqno_time_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
plain_table_db_test: $(OBJ_DIR)/db/plain_table_db_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
@@ -1779,6 +1786,9 @@ write_unprepared_transaction_test: $(OBJ_DIR)/utilities/transactions/write_unpre
|
||||
timestamped_snapshot_test: $(OBJ_DIR)/utilities/transactions/timestamped_snapshot_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
tiered_compaction_test: $(OBJ_DIR)/db/compaction/tiered_compaction_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
sst_dump: $(OBJ_DIR)/tools/sst_dump.o $(TOOLS_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
@@ -2353,9 +2363,9 @@ commit_prereq:
|
||||
# integration.
|
||||
checkout_folly:
|
||||
if [ -e third-party/folly ]; then \
|
||||
cd third-party/folly && git fetch origin; \
|
||||
cd third-party/folly && ${GIT_COMMAND} fetch origin; \
|
||||
else \
|
||||
cd third-party && git clone https://github.com/facebook/folly.git; \
|
||||
cd third-party && ${GIT_COMMAND} clone https://github.com/facebook/folly.git; \
|
||||
fi
|
||||
@# Pin to a particular version for public CI, so that PR authors don't
|
||||
@# need to worry about folly breaking our integration. Update periodically
|
||||
|
||||
@@ -38,11 +38,15 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"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",
|
||||
@@ -81,6 +85,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"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",
|
||||
@@ -368,11 +373,15 @@ cpp_library_wrapper(name="rocksdb_whole_archive_lib", srcs=[
|
||||
"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",
|
||||
@@ -411,6 +420,7 @@ cpp_library_wrapper(name="rocksdb_whole_archive_lib", srcs=[
|
||||
"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",
|
||||
@@ -5240,6 +5250,12 @@ cpp_unittest_wrapper(name="db_wal_test",
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="db_wide_basic_test",
|
||||
srcs=["db/wide/db_wide_basic_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="db_with_timestamp_basic_test",
|
||||
srcs=["db/db_with_timestamp_basic_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
@@ -5674,6 +5690,12 @@ cpp_unittest_wrapper(name="ribbon_test",
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="seqno_time_test",
|
||||
srcs=["db/seqno_time_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="sim_cache_test",
|
||||
srcs=["utilities/simulator_cache/sim_cache_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
@@ -5758,6 +5780,12 @@ cpp_unittest_wrapper(name="thread_local_test",
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="tiered_compaction_test",
|
||||
srcs=["db/compaction/tiered_compaction_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="timer_queue_test",
|
||||
srcs=["util/timer_queue_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
|
||||
@@ -30,45 +30,56 @@ class BenchmarkResultException(Exception):
|
||||
|
||||
class BenchmarkUtils:
|
||||
|
||||
expected_keys = ['ops_sec', 'mb_sec', 'total_size_gb', 'level0_size_gb', 'sum_gb', 'write_amplification',
|
||||
'write_mbps', 'usec_op', 'percentile_50', 'percentile_75',
|
||||
'percentile_99', 'percentile_99.9', 'percentile_99.99', 'uptime',
|
||||
'stall_time', 'stall_percent', 'test_name', 'test_date', 'rocksdb_version',
|
||||
'job_id', 'timestamp']
|
||||
|
||||
metric_keys = ['ops_sec', 'mb_sec', 'total_size_gb', 'level0_size_gb', 'sum_gb', 'write_amplification',
|
||||
'write_mbps', 'usec_op', 'percentile_50', 'percentile_75',
|
||||
'percentile_99', 'percentile_99.9', 'percentile_99.99', 'uptime',
|
||||
'stall_time', 'stall_percent']
|
||||
expected_keys = ['ops_sec', 'mb_sec', 'lsm_sz', 'blob_sz', 'c_wgb', 'w_amp',
|
||||
'c_mbps', 'c_wsecs', 'c_csecs', 'b_rgb', 'b_wgb', 'usec_op',
|
||||
'p50', 'p99', 'p99.9', 'p99.99', 'pmax',
|
||||
'uptime', 'stall%', 'Nstall', 'u_cpu', 's_cpu', 'rss', 'test', 'date', 'version', 'job_id']
|
||||
|
||||
def sanity_check(row):
|
||||
if not 'test_name' in row:
|
||||
if not 'test' in row:
|
||||
logging.debug(f"not 'test' in row: {row}")
|
||||
return False
|
||||
if row['test_name'] == '':
|
||||
if row['test'] == '':
|
||||
logging.debug(f"row['test'] == '': {row}")
|
||||
return False
|
||||
if not 'test_date' in row:
|
||||
if not 'date' in row:
|
||||
logging.debug(f"not 'date' in row: {row}")
|
||||
return False
|
||||
if not 'ops_sec' in row:
|
||||
logging.debug(f"not 'ops_sec' in row: {row}")
|
||||
return False
|
||||
try:
|
||||
v = int(row['ops_sec'])
|
||||
except (ValueError, TypeError):
|
||||
logging.debug(f"int(row['ops_sec']): {row}")
|
||||
return False
|
||||
try:
|
||||
(_, _) = parser.parse(row['date'], fuzzy_with_tokens=True)
|
||||
except (parser.ParserError):
|
||||
logging.error(f"parser.parse((row['date']): not a valid format for date in row: {row}")
|
||||
return False
|
||||
return True
|
||||
|
||||
def conform_opensearch(row):
|
||||
(dt, _) = parser.parse(row['test_date'], fuzzy_with_tokens=True)
|
||||
(dt, _) = parser.parse(row['date'], fuzzy_with_tokens=True)
|
||||
row['test_date'] = dt.isoformat()
|
||||
return dict((key.replace('.', '_'), value)
|
||||
for (key, value) in row.items())
|
||||
|
||||
|
||||
class ResultParser:
|
||||
def __init__(self, field="(\w|[+-:.])+", intrafield="(\s)+", separator="\t"):
|
||||
def __init__(self, field="(\w|[+-:.%])+", intrafield="(\s)+", separator="\t"):
|
||||
self.field = re.compile(field)
|
||||
self.intra = re.compile(intrafield)
|
||||
self.sep = re.compile(separator)
|
||||
|
||||
def ignore(self, l_in: str):
|
||||
if len(l_in) == 0:
|
||||
return True
|
||||
if l_in[0:1] == '#':
|
||||
return True
|
||||
return False
|
||||
|
||||
def line(self, l_in: str):
|
||||
'''Parse a line into items
|
||||
Being clever about separators
|
||||
@@ -102,7 +113,7 @@ class ResultParser:
|
||||
|
||||
def parse(self, lines):
|
||||
'''Parse something that iterates lines'''
|
||||
rows = [self.line(line) for line in lines]
|
||||
rows = [self.line(line) for line in lines if not self.ignore(line)]
|
||||
header = rows[0]
|
||||
width = len(header)
|
||||
records = [{k: v for (k, v) in itertools.zip_longest(
|
||||
@@ -123,7 +134,7 @@ def load_report_from_tsv(filename: str):
|
||||
def push_report_to_opensearch(report, esdocument):
|
||||
sanitized = [BenchmarkUtils.conform_opensearch(row)
|
||||
for row in report if BenchmarkUtils.sanity_check(row)]
|
||||
logging.debug(f"upload {len(sanitized)} benchmarks to opensearch")
|
||||
logging.debug(f"upload {len(sanitized)} sane of {len(report)} benchmarks to opensearch")
|
||||
for single_benchmark in sanitized:
|
||||
logging.debug(f"upload benchmark: {single_benchmark}")
|
||||
response = requests.post(
|
||||
@@ -133,6 +144,13 @@ def push_report_to_opensearch(report, esdocument):
|
||||
f"Sent to OpenSearch, status: {response.status_code}, result: {response.text}")
|
||||
response.raise_for_status()
|
||||
|
||||
def push_report_to_null(report):
|
||||
|
||||
for row in report:
|
||||
if BenchmarkUtils.sanity_check(row):
|
||||
logging.debug(f"row {row}")
|
||||
conformed = BenchmarkUtils.conform_opensearch(row)
|
||||
logging.debug(f"conformed row {conformed}")
|
||||
|
||||
def main():
|
||||
'''Tool for fetching, parsing and uploading benchmark results to OpenSearch / ElasticSearch
|
||||
@@ -151,11 +169,15 @@ def main():
|
||||
parser.add_argument('--tsvfile', default='build_tools/circle_api_scraper_input.txt',
|
||||
help='File from which to read tsv report')
|
||||
parser.add_argument('--esdocument', help='ElasticSearch/OpenSearch document URL to upload report into')
|
||||
parser.add_argument('--upload', choices=['opensearch', 'none'], default='opensearch')
|
||||
|
||||
args = parser.parse_args()
|
||||
logging.debug(f"Arguments: {args}")
|
||||
reports = load_report_from_tsv(args.tsvfile)
|
||||
push_report_to_opensearch(reports, args.esdocument)
|
||||
if (args.upload == 'opensearch'):
|
||||
push_report_to_opensearch(reports, args.esdocument)
|
||||
else:
|
||||
push_report_to_null(reports)
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -17,12 +17,18 @@ if [ "$?" != "1" ]; then
|
||||
BAD=1
|
||||
fi
|
||||
|
||||
git grep -n '<rocksdb/' -- ':!build_tools/check-sources.sh'
|
||||
git grep -n 'include <rocksdb/' -- ':!build_tools/check-sources.sh'
|
||||
if [ "$?" != "1" ]; then
|
||||
echo '^^^^^ Use double-quotes as in #include "rocksdb/something.h"'
|
||||
BAD=1
|
||||
fi
|
||||
|
||||
git grep -n 'include "include/rocksdb/' -- ':!build_tools/check-sources.sh'
|
||||
if [ "$?" != "1" ]; then
|
||||
echo '^^^^^ Use #include "rocksdb/something.h" instead of #include "include/rocksdb/something.h"'
|
||||
BAD=1
|
||||
fi
|
||||
|
||||
git grep -n 'using namespace' -- ':!build_tools' ':!docs' \
|
||||
':!third-party/folly/folly/lang/Align.h' \
|
||||
':!third-party/gtest-1.8.1/fused-src/gtest/gtest.h'
|
||||
|
||||
@@ -6,7 +6,7 @@ GLIBC_BASE=/mnt/gvfs/third-party2/glibc/45ce3375cdc77ecb2520bbf8f0ecddd3f98efd7a
|
||||
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/ec6573523b0ce55ef6373a4801189027cf07bb2c/1.9.1/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
|
||||
|
||||
Vendored
+33
-15
@@ -3,6 +3,7 @@
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#include "cache_key.h"
|
||||
#ifdef GFLAGS
|
||||
#include <cinttypes>
|
||||
#include <cstddef>
|
||||
@@ -12,6 +13,7 @@
|
||||
#include <set>
|
||||
#include <sstream>
|
||||
|
||||
#include "cache/clock_cache.h"
|
||||
#include "cache/fast_lru_cache.h"
|
||||
#include "db/db_impl/db_impl.h"
|
||||
#include "monitoring/histogram.h"
|
||||
@@ -72,6 +74,11 @@ DEFINE_uint32(
|
||||
DEFINE_uint32(gather_stats_entries_per_lock, 256,
|
||||
"For Cache::ApplyToAllEntries");
|
||||
DEFINE_bool(skewed, false, "If true, skew the key access distribution");
|
||||
|
||||
DEFINE_bool(lean, false,
|
||||
"If true, no additional computation is performed besides cache "
|
||||
"operations.");
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
DEFINE_string(secondary_cache_uri, "",
|
||||
"Full URI for creating a custom secondary cache object");
|
||||
@@ -214,7 +221,8 @@ struct KeyGen {
|
||||
EncodeFixed64(key_data + 10, key);
|
||||
key_data[18] = char{4};
|
||||
EncodeFixed64(key_data + 19, key);
|
||||
return Slice(&key_data[off], sizeof(key_data) - off);
|
||||
assert(27 >= kCacheKeySize);
|
||||
return Slice(&key_data[off], kCacheKeySize);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -282,7 +290,9 @@ class CacheBench {
|
||||
}
|
||||
|
||||
if (FLAGS_cache_type == "clock_cache") {
|
||||
cache_ = NewClockCache(FLAGS_cache_size, FLAGS_num_shard_bits);
|
||||
cache_ = ExperimentalNewClockCache(
|
||||
FLAGS_cache_size, FLAGS_value_bytes, FLAGS_num_shard_bits,
|
||||
false /*strict_capacity_limit*/, kDefaultCacheMetadataChargePolicy);
|
||||
if (!cache_) {
|
||||
fprintf(stderr, "Clock cache not supported.\n");
|
||||
exit(1);
|
||||
@@ -321,8 +331,9 @@ class CacheBench {
|
||||
Random64 rnd(1);
|
||||
KeyGen keygen;
|
||||
for (uint64_t i = 0; i < 2 * FLAGS_cache_size; i += FLAGS_value_bytes) {
|
||||
cache_->Insert(keygen.GetRand(rnd, max_key_, max_log_), createValue(rnd),
|
||||
&helper1, FLAGS_value_bytes);
|
||||
Status s = cache_->Insert(keygen.GetRand(rnd, max_key_, max_log_),
|
||||
createValue(rnd), &helper1, FLAGS_value_bytes);
|
||||
assert(s.ok());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -516,7 +527,6 @@ class CacheBench {
|
||||
StopWatchNano timer(clock);
|
||||
|
||||
for (uint64_t i = 0; i < FLAGS_ops_per_thread; i++) {
|
||||
timer.Start();
|
||||
Slice key = gen.GetRand(thread->rnd, max_key_, max_log_);
|
||||
uint64_t random_op = thread->rnd.Next();
|
||||
Cache::CreateCallback create_cb = [](const void* buf, size_t size,
|
||||
@@ -528,6 +538,8 @@ class CacheBench {
|
||||
return Status::OK();
|
||||
};
|
||||
|
||||
timer.Start();
|
||||
|
||||
if (random_op < lookup_insert_threshold_) {
|
||||
if (handle) {
|
||||
cache_->Release(handle);
|
||||
@@ -537,13 +549,16 @@ class CacheBench {
|
||||
handle = cache_->Lookup(key, &helper2, create_cb, Cache::Priority::LOW,
|
||||
true);
|
||||
if (handle) {
|
||||
// do something with the data
|
||||
result += NPHash64(static_cast<char*>(cache_->Value(handle)),
|
||||
FLAGS_value_bytes);
|
||||
if (!FLAGS_lean) {
|
||||
// do something with the data
|
||||
result += NPHash64(static_cast<char*>(cache_->Value(handle)),
|
||||
FLAGS_value_bytes);
|
||||
}
|
||||
} else {
|
||||
// do insert
|
||||
cache_->Insert(key, createValue(thread->rnd), &helper2,
|
||||
FLAGS_value_bytes, &handle);
|
||||
Status s = cache_->Insert(key, createValue(thread->rnd), &helper2,
|
||||
FLAGS_value_bytes, &handle);
|
||||
assert(s.ok());
|
||||
}
|
||||
} else if (random_op < insert_threshold_) {
|
||||
if (handle) {
|
||||
@@ -551,8 +566,9 @@ class CacheBench {
|
||||
handle = nullptr;
|
||||
}
|
||||
// do insert
|
||||
cache_->Insert(key, createValue(thread->rnd), &helper3,
|
||||
FLAGS_value_bytes, &handle);
|
||||
Status s = cache_->Insert(key, createValue(thread->rnd), &helper3,
|
||||
FLAGS_value_bytes, &handle);
|
||||
assert(s.ok());
|
||||
} else if (random_op < lookup_threshold_) {
|
||||
if (handle) {
|
||||
cache_->Release(handle);
|
||||
@@ -562,9 +578,11 @@ class CacheBench {
|
||||
handle = cache_->Lookup(key, &helper2, create_cb, Cache::Priority::LOW,
|
||||
true);
|
||||
if (handle) {
|
||||
// do something with the data
|
||||
result += NPHash64(static_cast<char*>(cache_->Value(handle)),
|
||||
FLAGS_value_bytes);
|
||||
if (!FLAGS_lean) {
|
||||
// do something with the data
|
||||
result += NPHash64(static_cast<char*>(cache_->Value(handle)),
|
||||
FLAGS_value_bytes);
|
||||
}
|
||||
}
|
||||
} else if (random_op < erase_threshold_) {
|
||||
// do erase
|
||||
|
||||
Vendored
+20
@@ -84,6 +84,16 @@ class CacheHandleGuard {
|
||||
Cache::Handle* GetCacheHandle() const { return handle_; }
|
||||
T* GetValue() const { return value_; }
|
||||
|
||||
void TransferTo(Cleanable* cleanable) {
|
||||
if (cleanable) {
|
||||
if (handle_ != nullptr) {
|
||||
assert(cache_);
|
||||
cleanable->RegisterCleanup(&ReleaseCacheHandle, cache_, handle_);
|
||||
}
|
||||
}
|
||||
ResetFields();
|
||||
}
|
||||
|
||||
void Reset() {
|
||||
ReleaseHandle();
|
||||
ResetFields();
|
||||
@@ -105,6 +115,16 @@ class CacheHandleGuard {
|
||||
value_ = nullptr;
|
||||
}
|
||||
|
||||
static void ReleaseCacheHandle(void* arg1, void* arg2) {
|
||||
Cache* const cache = static_cast<Cache*>(arg1);
|
||||
assert(cache);
|
||||
|
||||
Cache::Handle* const cache_handle = static_cast<Cache::Handle*>(arg2);
|
||||
assert(cache_handle);
|
||||
|
||||
cache->Release(cache_handle);
|
||||
}
|
||||
|
||||
private:
|
||||
Cache* cache_ = nullptr;
|
||||
Cache::Handle* handle_ = nullptr;
|
||||
|
||||
Vendored
+2
@@ -65,6 +65,8 @@ class CacheKey {
|
||||
uint64_t offset_etc64_;
|
||||
};
|
||||
|
||||
constexpr uint8_t kCacheKeySize = static_cast<uint8_t>(sizeof(CacheKey));
|
||||
|
||||
// A file-specific generator of cache keys, sometimes referred to as the
|
||||
// "base" cache key for a file because all the cache keys for various offsets
|
||||
// within the file are computed using simple arithmetic. The basis for the
|
||||
|
||||
+2
-2
@@ -147,7 +147,7 @@ TEST(CacheReservationManagerIncreaseReservcationOnFullCacheTest,
|
||||
|
||||
std::size_t new_mem_used = kSmallCacheCapacity + 1;
|
||||
Status s = test_cache_rev_mng->UpdateCacheReservation(new_mem_used);
|
||||
EXPECT_EQ(s, Status::Incomplete())
|
||||
EXPECT_EQ(s, Status::MemoryLimit())
|
||||
<< "Failed to return status to indicate failure of dummy entry insertion "
|
||||
"during cache reservation on full cache";
|
||||
EXPECT_GE(test_cache_rev_mng->GetTotalReservedCacheSize(),
|
||||
@@ -192,7 +192,7 @@ TEST(CacheReservationManagerIncreaseReservcationOnFullCacheTest,
|
||||
// Create cache full again for subsequent tests
|
||||
new_mem_used = kSmallCacheCapacity + 1;
|
||||
s = test_cache_rev_mng->UpdateCacheReservation(new_mem_used);
|
||||
EXPECT_EQ(s, Status::Incomplete())
|
||||
EXPECT_EQ(s, Status::MemoryLimit())
|
||||
<< "Failed to return status to indicate failure of dummy entry insertion "
|
||||
"during cache reservation on full cache";
|
||||
EXPECT_GE(test_cache_rev_mng->GetTotalReservedCacheSize(),
|
||||
|
||||
Vendored
+52
-25
@@ -77,7 +77,7 @@ class CacheTest : public testing::TestWithParam<std::string> {
|
||||
static std::string type_;
|
||||
|
||||
static void Deleter(const Slice& key, void* v) {
|
||||
if (type_ == kFast) {
|
||||
if (type_ == kFast || type_ == kClock) {
|
||||
current_->deleted_keys_.push_back(DecodeKey16Bytes(key));
|
||||
} else {
|
||||
current_->deleted_keys_.push_back(DecodeKey32Bits(key));
|
||||
@@ -111,7 +111,9 @@ class CacheTest : public testing::TestWithParam<std::string> {
|
||||
return NewLRUCache(capacity);
|
||||
}
|
||||
if (type == kClock) {
|
||||
return NewClockCache(capacity);
|
||||
return ExperimentalNewClockCache(
|
||||
capacity, 1 /*estimated_value_size*/, -1 /*num_shard_bits*/,
|
||||
false /*strict_capacity_limit*/, kDefaultCacheMetadataChargePolicy);
|
||||
}
|
||||
if (type == kFast) {
|
||||
return NewFastLRUCache(
|
||||
@@ -135,8 +137,9 @@ class CacheTest : public testing::TestWithParam<std::string> {
|
||||
return NewLRUCache(co);
|
||||
}
|
||||
if (type == kClock) {
|
||||
return NewClockCache(capacity, num_shard_bits, strict_capacity_limit,
|
||||
charge_policy);
|
||||
return ExperimentalNewClockCache(capacity, 1 /*estimated_value_size*/,
|
||||
num_shard_bits, strict_capacity_limit,
|
||||
charge_policy);
|
||||
}
|
||||
if (type == kFast) {
|
||||
return NewFastLRUCache(capacity, 1 /*estimated_value_size*/,
|
||||
@@ -152,7 +155,8 @@ class CacheTest : public testing::TestWithParam<std::string> {
|
||||
// LRUCache and ClockCache don't, so the encoding depends on
|
||||
// the cache type.
|
||||
std::string EncodeKey(int k) {
|
||||
if (GetParam() == kFast) {
|
||||
auto type = GetParam();
|
||||
if (type == kFast || type == kClock) {
|
||||
return EncodeKey16Bytes(k);
|
||||
} else {
|
||||
return EncodeKey32Bits(k);
|
||||
@@ -160,7 +164,8 @@ class CacheTest : public testing::TestWithParam<std::string> {
|
||||
}
|
||||
|
||||
int DecodeKey(const Slice& k) {
|
||||
if (GetParam() == kFast) {
|
||||
auto type = GetParam();
|
||||
if (type == kFast || type == kClock) {
|
||||
return DecodeKey16Bytes(k);
|
||||
} else {
|
||||
return DecodeKey32Bits(k);
|
||||
@@ -217,8 +222,9 @@ std::string CacheTest::type_;
|
||||
class LRUCacheTest : public CacheTest {};
|
||||
|
||||
TEST_P(CacheTest, UsageTest) {
|
||||
if (GetParam() == kFast) {
|
||||
ROCKSDB_GTEST_BYPASS("FastLRUCache requires 16 byte keys.");
|
||||
auto type = GetParam();
|
||||
if (type == kFast || type == kClock) {
|
||||
ROCKSDB_GTEST_BYPASS("FastLRUCache and ClockCache require 16-byte keys.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -266,8 +272,9 @@ TEST_P(CacheTest, UsageTest) {
|
||||
}
|
||||
|
||||
TEST_P(CacheTest, PinnedUsageTest) {
|
||||
if (GetParam() == kFast) {
|
||||
ROCKSDB_GTEST_BYPASS("FastLRUCache requires 16 byte keys.");
|
||||
auto type = GetParam();
|
||||
if (type == kFast || type == kClock) {
|
||||
ROCKSDB_GTEST_BYPASS("FastLRUCache and ClockCache require 16-byte keys.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -492,8 +499,10 @@ TEST_P(CacheTest, EvictionPolicyRef) {
|
||||
Insert(302, 103);
|
||||
Insert(303, 104);
|
||||
|
||||
// Insert entries much more than Cache capacity
|
||||
for (int i = 0; i < kCacheSize * 2; i++) {
|
||||
// Insert entries much more than cache capacity.
|
||||
double load_factor =
|
||||
std::min(fast_lru_cache::kLoadFactor, clock_cache::kLoadFactor);
|
||||
for (int i = 0; i < 2 * static_cast<int>(kCacheSize / load_factor); i++) {
|
||||
Insert(1000 + i, 2000 + i);
|
||||
}
|
||||
|
||||
@@ -523,8 +532,9 @@ TEST_P(CacheTest, EvictionPolicyRef) {
|
||||
}
|
||||
|
||||
TEST_P(CacheTest, EvictEmptyCache) {
|
||||
if (GetParam() == kFast) {
|
||||
ROCKSDB_GTEST_BYPASS("FastLRUCache requires 16 byte keys.");
|
||||
auto type = GetParam();
|
||||
if (type == kFast || type == kClock) {
|
||||
ROCKSDB_GTEST_BYPASS("FastLRUCache and ClockCache require 16-byte keys.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -534,8 +544,9 @@ TEST_P(CacheTest, EvictEmptyCache) {
|
||||
}
|
||||
|
||||
TEST_P(CacheTest, EraseFromDeleter) {
|
||||
if (GetParam() == kFast) {
|
||||
ROCKSDB_GTEST_BYPASS("FastLRUCache requires 16 byte keys.");
|
||||
auto type = GetParam();
|
||||
if (type == kFast || type == kClock) {
|
||||
ROCKSDB_GTEST_BYPASS("FastLRUCache and ClockCache require 16-byte keys.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -650,6 +661,12 @@ TEST_P(CacheTest, ReleaseWithoutErase) {
|
||||
}
|
||||
|
||||
TEST_P(CacheTest, SetCapacity) {
|
||||
auto type = GetParam();
|
||||
if (type == kFast || type == kClock) {
|
||||
ROCKSDB_GTEST_BYPASS(
|
||||
"FastLRUCache and ClockCache don't support capacity adjustments.");
|
||||
return;
|
||||
}
|
||||
// test1: increase capacity
|
||||
// lets create a cache with capacity 5,
|
||||
// then, insert 5 elements, then increase capacity
|
||||
@@ -698,6 +715,14 @@ TEST_P(CacheTest, SetCapacity) {
|
||||
}
|
||||
|
||||
TEST_P(LRUCacheTest, SetStrictCapacityLimit) {
|
||||
auto type = GetParam();
|
||||
if (type == kFast || type == kClock) {
|
||||
ROCKSDB_GTEST_BYPASS(
|
||||
"FastLRUCache and ClockCache don't support an unbounded number of "
|
||||
"inserts beyond "
|
||||
"capacity.");
|
||||
return;
|
||||
}
|
||||
// test1: set the flag to false. Insert more keys than capacity. See if they
|
||||
// all go through.
|
||||
std::shared_ptr<Cache> cache = NewCache(5, 0, false);
|
||||
@@ -717,7 +742,7 @@ TEST_P(LRUCacheTest, SetStrictCapacityLimit) {
|
||||
cache->SetStrictCapacityLimit(true);
|
||||
Cache::Handle* handle;
|
||||
s = cache->Insert(extra_key, extra_value, 1, &deleter, &handle);
|
||||
ASSERT_TRUE(s.IsIncomplete());
|
||||
ASSERT_TRUE(s.IsMemoryLimit());
|
||||
ASSERT_EQ(nullptr, handle);
|
||||
ASSERT_EQ(10, cache->GetUsage());
|
||||
|
||||
@@ -734,7 +759,7 @@ TEST_P(LRUCacheTest, SetStrictCapacityLimit) {
|
||||
ASSERT_NE(nullptr, handles[i]);
|
||||
}
|
||||
s = cache2->Insert(extra_key, extra_value, 1, &deleter, &handle);
|
||||
ASSERT_TRUE(s.IsIncomplete());
|
||||
ASSERT_TRUE(s.IsMemoryLimit());
|
||||
ASSERT_EQ(nullptr, handle);
|
||||
// test insert without handle
|
||||
s = cache2->Insert(extra_key, extra_value, 1, &deleter);
|
||||
@@ -749,6 +774,12 @@ TEST_P(LRUCacheTest, SetStrictCapacityLimit) {
|
||||
}
|
||||
|
||||
TEST_P(CacheTest, OverCapacity) {
|
||||
auto type = GetParam();
|
||||
if (type == kFast || type == kClock) {
|
||||
ROCKSDB_GTEST_BYPASS(
|
||||
"FastLRUCache and ClockCache don't support capacity adjustments.");
|
||||
return;
|
||||
}
|
||||
size_t n = 10;
|
||||
|
||||
// a LRUCache with n entries and one shard only
|
||||
@@ -924,15 +955,11 @@ TEST_P(CacheTest, GetChargeAndDeleter) {
|
||||
cache_->Release(h1);
|
||||
}
|
||||
|
||||
#ifdef SUPPORT_CLOCK_CACHE
|
||||
std::shared_ptr<Cache> (*new_clock_cache_func)(
|
||||
size_t, int, bool, CacheMetadataChargePolicy) = NewClockCache;
|
||||
std::shared_ptr<Cache> (*new_clock_cache_func)(size_t, size_t, int, bool,
|
||||
CacheMetadataChargePolicy) =
|
||||
ExperimentalNewClockCache;
|
||||
INSTANTIATE_TEST_CASE_P(CacheTestInstance, CacheTest,
|
||||
testing::Values(kLRU, kClock, kFast));
|
||||
#else
|
||||
INSTANTIATE_TEST_CASE_P(CacheTestInstance, CacheTest,
|
||||
testing::Values(kLRU, kFast));
|
||||
#endif // SUPPORT_CLOCK_CACHE
|
||||
INSTANTIATE_TEST_CASE_P(CacheTestInstance, LRUCacheTest,
|
||||
testing::Values(kLRU, kFast));
|
||||
|
||||
|
||||
Vendored
+592
-733
File diff suppressed because it is too large.
Load diff
Vendored
+757
-3
@@ -9,8 +9,762 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "rocksdb/cache.h"
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#if defined(TBB) && !defined(ROCKSDB_LITE)
|
||||
#define SUPPORT_CLOCK_CACHE
|
||||
#include "cache/cache_key.h"
|
||||
#include "cache/sharded_cache.h"
|
||||
#include "port/lang.h"
|
||||
#include "port/malloc.h"
|
||||
#include "port/port.h"
|
||||
#include "rocksdb/cache.h"
|
||||
#include "rocksdb/secondary_cache.h"
|
||||
#include "util/autovector.h"
|
||||
#include "util/distributed_mutex.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
namespace clock_cache {
|
||||
|
||||
// Block cache implementation using a lock-free open-address hash table
|
||||
// and clock eviction.
|
||||
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Part 1: Handles
|
||||
//
|
||||
// Every slot in the hash table is a ClockHandle. A handle can be in a few
|
||||
// different states, that stem from the fact that handles can be externally
|
||||
// referenced and, thus, can't always be immediately evicted when a delete
|
||||
// operation is executed or when they are replaced by a new version (via an
|
||||
// insert of the same key). Concretely, the state of a handle is defined by the
|
||||
// following two properties:
|
||||
// (R) Externally referenced: A handle can be referenced externally, or not.
|
||||
// Importantly, a handle can be evicted if and only if it's not
|
||||
// referenced. In particular, when an handle becomes referenced, it's
|
||||
// temporarily taken out of clock until all references to it are released.
|
||||
// (M) Marked for deletion (or invisible): An handle is marked for deletion
|
||||
// when an operation attempts to delete it, but the handle is externally
|
||||
// referenced, so it can't be immediately deleted. When this mark is placed,
|
||||
// lookups will no longer be able to find it. Consequently, no more external
|
||||
// references will be taken to the handle. When a handle is marked for
|
||||
// deletion, we also say it's invisible.
|
||||
// These properties induce 4 different states, with transitions defined as
|
||||
// follows:
|
||||
// - Not M --> M: When a handle is deleted or replaced by a new version, but
|
||||
// not immediately evicted.
|
||||
// - M --> not M: This cannot happen. Once a handle is marked for deletion,
|
||||
// there is no can't go back.
|
||||
// - R --> not R: When all references to an handle are released.
|
||||
// - Not R --> R: When an unreferenced handle becomes referenced. This can only
|
||||
// happen if the handle is visible, since references to an handle can only be
|
||||
// created when it's visible.
|
||||
//
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Part 2: Hash table structure
|
||||
//
|
||||
// Internally, the cache uses an open-addressed hash table to index the handles.
|
||||
// We use tombstone counters to keep track of displacements. Probes are
|
||||
// generated with double-hashing (but the code can be easily modified to use
|
||||
// other probing schemes, like linear hashing). Because of the tombstones and
|
||||
// the two possible visibility states of a handle, the table slots (we use the
|
||||
// word "slot" to refer to handles that are not necessary valid key-value
|
||||
// elements) can be in 4 different states:
|
||||
// 1. Visible element: The slot contains an element in not M state.
|
||||
// 2. To-be-deleted element: The slot contains an element in M state.
|
||||
// 3. Tombstone: The slot doesn't contain an element, but there is some other
|
||||
// element that probed this slot during its insertion.
|
||||
// 4. Empty: The slot is unused.
|
||||
// When a ghost is removed from the table, it can either transition to being a
|
||||
// tombstone or an empty slot, depending on the number of displacements of the
|
||||
// slot. In any case, the slot becomes available. When a handle is inserted
|
||||
// into that slot, it becomes a visible element again.
|
||||
//
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Part 3: The clock algorithm
|
||||
//
|
||||
// We maintain a circular buffer with the handles available for eviction,
|
||||
// which the clock algorithm traverses (using a "clock pointer") to pick the
|
||||
// next victim. We use the hash table array as the circular buffer, and mark
|
||||
// the handles that are evictable. For this we use different clock flags, namely
|
||||
// NONE, LOW, MEDIUM, HIGH, that represent priorities: LOW, MEDIUM and HIGH
|
||||
// represent how close an element is from being evictable, LOW being immediately
|
||||
// evictable. NONE means the slot is not evictable. This is due to one of the
|
||||
// following reasons:
|
||||
// (i) the slot doesn't contain an element, or
|
||||
// (ii) the slot contains an element that is in R state, or
|
||||
// (iii) the slot contains an element that was in R state but it's
|
||||
// not any more, and the clock pointer has not swept through the
|
||||
// slot since the element stopped being referenced.
|
||||
//
|
||||
// The priority NONE is really only important for case (iii), as in the other
|
||||
// two cases there are other metadata fields that already capture the state.
|
||||
// When an element stops being referenced (and is not deleted), the clock
|
||||
// algorithm must acknowledge this, and assign a non-NONE priority to make
|
||||
// the element evictable again.
|
||||
//
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
// Part 4: Synchronization
|
||||
//
|
||||
// We provide the following synchronization guarantees:
|
||||
// - Lookup is lock-free.
|
||||
// - Release is lock-free, unless (i) no references to the element are left,
|
||||
// and (ii) it was marked for deletion or the user wishes to delete if
|
||||
// releasing the last reference.
|
||||
// - Insert and Erase still use a per-shard lock.
|
||||
//
|
||||
// Our hash table is lock-free, in the sense that system-wide progress is
|
||||
// guaranteed, i.e., some thread is always able to make progress.
|
||||
//
|
||||
///////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
// The load factor p is a real number in (0, 1) such that at all
|
||||
// times at most a fraction p of all slots, without counting tombstones,
|
||||
// are occupied by elements. This means that the probability that a
|
||||
// random probe hits an empty slot is at most p, and thus at most 1/p probes
|
||||
// are required on average. For example, p = 70% implies that between 1 and 2
|
||||
// probes are needed on average (bear in mind that this reasoning doesn't
|
||||
// consider the effects of clustering over time).
|
||||
// Because the size of the hash table is always rounded up to the next
|
||||
// power of 2, p is really an upper bound on the actual load factor---the
|
||||
// actual load factor is anywhere between p/2 and p. This is a bit wasteful,
|
||||
// but bear in mind that slots only hold metadata, not actual values.
|
||||
// Since space cost is dominated by the values (the LSM blocks),
|
||||
// overprovisioning the table with metadata only increases the total cache space
|
||||
// usage by a tiny fraction.
|
||||
constexpr double kLoadFactor = 0.35;
|
||||
|
||||
// The user can exceed kLoadFactor if the sizes of the inserted values don't
|
||||
// match estimated_value_size, or if strict_capacity_limit == false. To
|
||||
// avoid performance to plunge, we set a strict upper bound on the load factor.
|
||||
constexpr double kStrictLoadFactor = 0.7;
|
||||
|
||||
// Arbitrary seeds.
|
||||
constexpr uint32_t kProbingSeed1 = 0xbc9f1d34;
|
||||
constexpr uint32_t kProbingSeed2 = 0x7a2bb9d5;
|
||||
|
||||
// An experimental (under development!) alternative to LRUCache.
|
||||
|
||||
struct ClockHandle {
|
||||
void* value;
|
||||
Cache::DeleterFn deleter;
|
||||
uint32_t hash;
|
||||
size_t total_charge;
|
||||
std::array<char, kCacheKeySize> key_data;
|
||||
|
||||
static constexpr uint8_t kExternalRefsOffset = 0;
|
||||
static constexpr uint8_t kSharedRefsOffset = 15;
|
||||
static constexpr uint8_t kExclusiveRefOffset = 30;
|
||||
static constexpr uint8_t kWillBeDeletedOffset = 31;
|
||||
|
||||
enum Refs : uint32_t {
|
||||
// Number of external references to the slot.
|
||||
EXTERNAL_REFS = ((uint32_t{1} << 15) - 1)
|
||||
<< kExternalRefsOffset, // Bits 0, ..., 14
|
||||
// Number of internal references plus external references to the slot.
|
||||
SHARED_REFS = ((uint32_t{1} << 15) - 1)
|
||||
<< kSharedRefsOffset, // Bits 15, ..., 29
|
||||
// Whether a thread has an exclusive reference to the slot.
|
||||
EXCLUSIVE_REF = uint32_t{1} << kExclusiveRefOffset, // Bit 30
|
||||
// Whether the handle will be deleted soon. When this bit is set, new
|
||||
// internal
|
||||
// or external references to this handle stop being accepted.
|
||||
// There is an exception: external references can be created from
|
||||
// existing external references, or converting from existing internal
|
||||
// references.
|
||||
WILL_BE_DELETED = uint32_t{1} << kWillBeDeletedOffset // Bit 31
|
||||
|
||||
// Shared references (i.e., external and internal references) and exclusive
|
||||
// references are our custom implementation of RW locks---external and
|
||||
// internal references are read locks, and exclusive references are write
|
||||
// locks. We prioritize readers, which never block; in fact, they don't even
|
||||
// use compare-and-swap operations. Using our own implementation of RW locks
|
||||
// allows us to save many atomic operations by packing data more carefully.
|
||||
// In particular:
|
||||
// - Combining EXTERNAL_REFS and SHARED_REFS allows us to convert an
|
||||
// internal
|
||||
// reference into an external reference in a single atomic arithmetic
|
||||
// operation.
|
||||
// - Combining SHARED_REFS and WILL_BE_DELETED allows us to attempt to take
|
||||
// a shared reference and check whether the entry is marked for deletion
|
||||
// in a single atomic arithmetic operation.
|
||||
};
|
||||
|
||||
static constexpr uint32_t kOneInternalRef = 0x8000;
|
||||
static constexpr uint32_t kOneExternalRef = 0x8001;
|
||||
|
||||
std::atomic<uint32_t> refs;
|
||||
|
||||
static constexpr uint8_t kIsElementOffset = 1;
|
||||
static constexpr uint8_t kClockPriorityOffset = 2;
|
||||
static constexpr uint8_t kIsHitOffset = 4;
|
||||
static constexpr uint8_t kCachePriorityOffset = 5;
|
||||
|
||||
enum Flags : uint8_t {
|
||||
// Whether the slot is in use by an element.
|
||||
IS_ELEMENT = 1 << kIsElementOffset,
|
||||
// Clock priorities. Represents how close a handle is from being evictable.
|
||||
CLOCK_PRIORITY = 3 << kClockPriorityOffset,
|
||||
// Whether the handle has been looked up after its insertion.
|
||||
HAS_HIT = 1 << kIsHitOffset,
|
||||
// The value of Cache::Priority for the handle.
|
||||
CACHE_PRIORITY = 1 << kCachePriorityOffset,
|
||||
};
|
||||
|
||||
std::atomic<uint8_t> flags;
|
||||
|
||||
enum ClockPriority : uint8_t {
|
||||
NONE = (0 << kClockPriorityOffset),
|
||||
LOW = (1 << kClockPriorityOffset),
|
||||
MEDIUM = (2 << kClockPriorityOffset),
|
||||
HIGH = (3 << kClockPriorityOffset)
|
||||
};
|
||||
|
||||
// The number of elements that hash to this slot or a lower one, but wind
|
||||
// up in this slot or a higher one.
|
||||
std::atomic<uint32_t> displacements;
|
||||
|
||||
// Synchronization rules:
|
||||
// - Use a shared reference when we want the handle's identity
|
||||
// members (key_data, hash, value and IS_ELEMENT flag) to
|
||||
// remain untouched, but not modify them. The only updates
|
||||
// that a shared reference allows are:
|
||||
// * set CLOCK_PRIORITY to NONE;
|
||||
// * set the HAS_HIT bit.
|
||||
// Notice that these two types of updates are idempotent, so
|
||||
// they don't require synchronization across shared references.
|
||||
// - Use an exclusive reference when we want identity members
|
||||
// to remain untouched, as well as modify any identity member
|
||||
// or flag.
|
||||
// - displacements can be modified without holding a reference.
|
||||
// - refs is only modified through appropriate functions to
|
||||
// take or release references.
|
||||
|
||||
ClockHandle()
|
||||
: value(nullptr),
|
||||
deleter(nullptr),
|
||||
hash(0),
|
||||
total_charge(0),
|
||||
refs(0),
|
||||
flags(0),
|
||||
displacements(0) {
|
||||
SetWillBeDeleted(false);
|
||||
SetIsElement(false);
|
||||
SetClockPriority(ClockPriority::NONE);
|
||||
SetCachePriority(Cache::Priority::LOW);
|
||||
key_data.fill(0);
|
||||
}
|
||||
|
||||
ClockHandle(const ClockHandle& other) { *this = other; }
|
||||
|
||||
void operator=(const ClockHandle& other) {
|
||||
value = other.value;
|
||||
deleter = other.deleter;
|
||||
hash = other.hash;
|
||||
total_charge = other.total_charge;
|
||||
refs.store(other.refs);
|
||||
key_data = other.key_data;
|
||||
flags.store(other.flags);
|
||||
SetWillBeDeleted(other.WillBeDeleted());
|
||||
SetIsElement(other.IsElement());
|
||||
SetClockPriority(other.GetClockPriority());
|
||||
SetCachePriority(other.GetCachePriority());
|
||||
displacements.store(other.displacements);
|
||||
}
|
||||
|
||||
Slice key() const { return Slice(key_data.data(), kCacheKeySize); }
|
||||
|
||||
bool HasExternalRefs() const { return (refs & EXTERNAL_REFS) > 0; }
|
||||
|
||||
bool IsElement() const { return flags & IS_ELEMENT; }
|
||||
|
||||
void SetIsElement(bool is_element) {
|
||||
if (is_element) {
|
||||
flags |= IS_ELEMENT;
|
||||
} else {
|
||||
flags &= static_cast<uint8_t>(~IS_ELEMENT);
|
||||
}
|
||||
}
|
||||
|
||||
bool HasHit() const { return flags & HAS_HIT; }
|
||||
|
||||
void SetHit() { flags |= HAS_HIT; }
|
||||
|
||||
bool IsInClock() const {
|
||||
return GetClockPriority() != ClockHandle::ClockPriority::NONE;
|
||||
}
|
||||
|
||||
Cache::Priority GetCachePriority() const {
|
||||
return static_cast<Cache::Priority>(flags & CACHE_PRIORITY);
|
||||
}
|
||||
|
||||
void SetCachePriority(Cache::Priority priority) {
|
||||
if (priority == Cache::Priority::HIGH) {
|
||||
flags |= Flags::CACHE_PRIORITY;
|
||||
} else {
|
||||
flags &= static_cast<uint8_t>(~Flags::CACHE_PRIORITY);
|
||||
}
|
||||
}
|
||||
|
||||
ClockPriority GetClockPriority() const {
|
||||
return static_cast<ClockPriority>(flags & Flags::CLOCK_PRIORITY);
|
||||
}
|
||||
|
||||
void SetClockPriority(ClockPriority priority) {
|
||||
flags &= static_cast<uint8_t>(~Flags::CLOCK_PRIORITY);
|
||||
flags |= priority;
|
||||
}
|
||||
|
||||
void DecreaseClockPriority() {
|
||||
uint8_t p = static_cast<uint8_t>(flags & Flags::CLOCK_PRIORITY) >>
|
||||
kClockPriorityOffset;
|
||||
assert(p > 0);
|
||||
p--;
|
||||
flags &= static_cast<uint8_t>(~Flags::CLOCK_PRIORITY);
|
||||
ClockPriority new_priority =
|
||||
static_cast<ClockPriority>(p << kClockPriorityOffset);
|
||||
flags |= new_priority;
|
||||
}
|
||||
|
||||
void FreeData() {
|
||||
if (deleter) {
|
||||
(*deleter)(key(), value);
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate the memory usage by metadata.
|
||||
inline size_t CalcMetaCharge(
|
||||
CacheMetadataChargePolicy metadata_charge_policy) const {
|
||||
if (metadata_charge_policy != kFullChargeCacheMetadata) {
|
||||
return 0;
|
||||
} else {
|
||||
// #ifdef ROCKSDB_MALLOC_USABLE_SIZE
|
||||
// return malloc_usable_size(
|
||||
// const_cast<void*>(static_cast<const void*>(this)));
|
||||
// #else
|
||||
// TODO(Guido) malloc_usable_size only works when we call it on
|
||||
// a pointer allocated with malloc. Because our handles are all
|
||||
// allocated in a single shot as an array, the user can't call
|
||||
// CalcMetaCharge (or CalcTotalCharge or GetCharge) on a handle
|
||||
// pointer returned by the cache. Moreover, malloc_usable_size
|
||||
// expects a heap-allocated handle, but sometimes in our code we
|
||||
// wish to pass a stack-allocated handle (this is only a performance
|
||||
// concern).
|
||||
// What is the right way to compute metadata charges with pre-allocated
|
||||
// handles?
|
||||
return sizeof(ClockHandle);
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
|
||||
inline void CalcTotalCharge(
|
||||
size_t charge, CacheMetadataChargePolicy metadata_charge_policy) {
|
||||
total_charge = charge + CalcMetaCharge(metadata_charge_policy);
|
||||
}
|
||||
|
||||
inline size_t GetCharge(
|
||||
CacheMetadataChargePolicy metadata_charge_policy) const {
|
||||
size_t meta_charge = CalcMetaCharge(metadata_charge_policy);
|
||||
assert(total_charge >= meta_charge);
|
||||
return total_charge - meta_charge;
|
||||
}
|
||||
|
||||
inline bool IsEmpty() const {
|
||||
return !this->IsElement() && this->displacements == 0;
|
||||
}
|
||||
|
||||
inline bool IsTombstone() const {
|
||||
return !this->IsElement() && this->displacements > 0;
|
||||
}
|
||||
|
||||
inline bool Matches(const Slice& some_key, uint32_t some_hash) const {
|
||||
return this->IsElement() && this->hash == some_hash &&
|
||||
this->key() == some_key;
|
||||
}
|
||||
|
||||
bool WillBeDeleted() const { return refs & WILL_BE_DELETED; }
|
||||
|
||||
void SetWillBeDeleted(bool will_be_deleted) {
|
||||
if (will_be_deleted) {
|
||||
refs |= WILL_BE_DELETED;
|
||||
} else {
|
||||
refs &= ~WILL_BE_DELETED;
|
||||
}
|
||||
}
|
||||
|
||||
// The following functions are for taking and releasing refs.
|
||||
|
||||
// Tries to take an external ref. Returns true iff it succeeds.
|
||||
inline bool TryExternalRef() {
|
||||
if (!((refs += kOneExternalRef) & (EXCLUSIVE_REF | WILL_BE_DELETED))) {
|
||||
return true;
|
||||
}
|
||||
refs -= kOneExternalRef;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Releases an external ref. Returns the new value (this is useful to
|
||||
// avoid an extra atomic read).
|
||||
inline uint32_t ReleaseExternalRef() { return refs -= kOneExternalRef; }
|
||||
|
||||
// Take an external ref, assuming there is already one external ref
|
||||
// to the handle.
|
||||
void Ref() {
|
||||
// TODO(Guido) Is it okay to assume that the existing external reference
|
||||
// survives until this function returns?
|
||||
refs += kOneExternalRef;
|
||||
}
|
||||
|
||||
// Tries to take an internal ref. Returns true iff it succeeds.
|
||||
inline bool TryInternalRef() {
|
||||
if (!((refs += kOneInternalRef) & (EXCLUSIVE_REF | WILL_BE_DELETED))) {
|
||||
return true;
|
||||
}
|
||||
refs -= kOneInternalRef;
|
||||
return false;
|
||||
}
|
||||
|
||||
inline void ReleaseInternalRef() { refs -= kOneInternalRef; }
|
||||
|
||||
// Tries to take an exclusive ref. Returns true iff it succeeds.
|
||||
inline bool TryExclusiveRef() {
|
||||
uint32_t will_be_deleted = refs & WILL_BE_DELETED;
|
||||
uint32_t expected = will_be_deleted;
|
||||
return refs.compare_exchange_strong(expected,
|
||||
EXCLUSIVE_REF | will_be_deleted);
|
||||
}
|
||||
|
||||
// Repeatedly tries to take an exclusive reference, but stops as soon
|
||||
// as an external reference is detected (in this case the wait would
|
||||
// presumably be too long).
|
||||
inline bool TrySpinExclusiveRef() {
|
||||
uint32_t expected = 0;
|
||||
uint32_t will_be_deleted = 0;
|
||||
while (!refs.compare_exchange_strong(expected,
|
||||
EXCLUSIVE_REF | will_be_deleted)) {
|
||||
if (expected & EXTERNAL_REFS) {
|
||||
return false;
|
||||
}
|
||||
will_be_deleted = expected & WILL_BE_DELETED;
|
||||
expected = will_be_deleted;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
inline void ReleaseExclusiveRef() { refs.fetch_and(~EXCLUSIVE_REF); }
|
||||
|
||||
// The following functions are for upgrading and downgrading refs.
|
||||
// They guarantee atomicity, i.e., no exclusive refs to the handle
|
||||
// can be taken by a different thread during the conversion.
|
||||
|
||||
inline void ExclusiveToInternalRef() {
|
||||
refs += kOneInternalRef;
|
||||
ReleaseExclusiveRef();
|
||||
}
|
||||
|
||||
inline void ExclusiveToExternalRef() {
|
||||
refs += kOneExternalRef;
|
||||
ReleaseExclusiveRef();
|
||||
}
|
||||
|
||||
// TODO(Guido) Do we want to bound the loop and prepare the
|
||||
// algorithms to react to a failure?
|
||||
inline void InternalToExclusiveRef() {
|
||||
uint32_t expected = kOneInternalRef;
|
||||
uint32_t will_be_deleted = 0;
|
||||
while (!refs.compare_exchange_strong(expected,
|
||||
EXCLUSIVE_REF | will_be_deleted)) {
|
||||
will_be_deleted = expected & WILL_BE_DELETED;
|
||||
expected = kOneInternalRef | will_be_deleted;
|
||||
}
|
||||
}
|
||||
|
||||
inline void InternalToExternalRef() {
|
||||
refs += kOneExternalRef - kOneInternalRef;
|
||||
}
|
||||
|
||||
// TODO(Guido) Same concern.
|
||||
inline void ExternalToExclusiveRef() {
|
||||
uint32_t expected = kOneExternalRef;
|
||||
uint32_t will_be_deleted = 0;
|
||||
while (!refs.compare_exchange_strong(expected,
|
||||
EXCLUSIVE_REF | will_be_deleted)) {
|
||||
will_be_deleted = expected & WILL_BE_DELETED;
|
||||
expected = kOneExternalRef | will_be_deleted;
|
||||
}
|
||||
}
|
||||
|
||||
}; // struct ClockHandle
|
||||
|
||||
class ClockHandleTable {
|
||||
public:
|
||||
explicit ClockHandleTable(int hash_bits);
|
||||
~ClockHandleTable();
|
||||
|
||||
// Returns a pointer to a visible element matching the key/hash, or
|
||||
// nullptr if not present.
|
||||
ClockHandle* Lookup(const Slice& key, uint32_t hash);
|
||||
|
||||
// Inserts a copy of h into the hash table.
|
||||
// Returns a pointer to the inserted handle, or nullptr if no slot
|
||||
// available was found. If an existing visible element matching the
|
||||
// key/hash is already present in the hash table, the argument old
|
||||
// is set to point to it; otherwise, it's set to nullptr.
|
||||
// Returns an exclusive reference to h, and no references to old.
|
||||
ClockHandle* Insert(ClockHandle* h, ClockHandle** old);
|
||||
|
||||
// Removes h from the hash table. The handle must already be off clock.
|
||||
void Remove(ClockHandle* h);
|
||||
|
||||
// Extracts the element information from a handle (src), and assigns it
|
||||
// to a hash table slot (dst). Doesn't touch displacements and refs,
|
||||
// which are maintained by the hash table algorithm.
|
||||
void Assign(ClockHandle* dst, ClockHandle* src);
|
||||
|
||||
template <typename T>
|
||||
void ApplyToEntriesRange(T func, uint32_t index_begin, uint32_t index_end,
|
||||
bool apply_if_will_be_deleted) {
|
||||
for (uint32_t i = index_begin; i < index_end; i++) {
|
||||
ClockHandle* h = &array_[i];
|
||||
if (h->TryExclusiveRef()) {
|
||||
if (h->IsElement() &&
|
||||
(apply_if_will_be_deleted || !h->WillBeDeleted())) {
|
||||
// Hand the internal ref over to func, which is now responsible
|
||||
// to release it.
|
||||
func(h);
|
||||
} else {
|
||||
h->ReleaseExclusiveRef();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
void ConstApplyToEntriesRange(T func, uint32_t index_begin,
|
||||
uint32_t index_end,
|
||||
bool apply_if_will_be_deleted) const {
|
||||
for (uint32_t i = index_begin; i < index_end; i++) {
|
||||
ClockHandle* h = &array_[i];
|
||||
if (h->TryExclusiveRef()) {
|
||||
if (h->IsElement() &&
|
||||
(apply_if_will_be_deleted || !h->WillBeDeleted())) {
|
||||
func(h);
|
||||
}
|
||||
h->ReleaseExclusiveRef();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t GetTableSize() const { return uint32_t{1} << length_bits_; }
|
||||
|
||||
int GetLengthBits() const { return length_bits_; }
|
||||
|
||||
uint32_t GetOccupancyLimit() const { return occupancy_limit_; }
|
||||
|
||||
uint32_t GetOccupancy() const { return occupancy_; }
|
||||
|
||||
// Returns x mod 2^{length_bits_}.
|
||||
uint32_t ModTableSize(uint32_t x) { return x & length_bits_mask_; }
|
||||
|
||||
private:
|
||||
friend class ClockCacheShard;
|
||||
|
||||
int FindElement(const Slice& key, uint32_t hash, uint32_t& probe);
|
||||
|
||||
int FindAvailableSlot(const Slice& key, uint32_t& probe);
|
||||
|
||||
int FindElementOrAvailableSlot(const Slice& key, uint32_t hash,
|
||||
uint32_t& probe);
|
||||
|
||||
// Returns the index of the first slot probed (hashing with
|
||||
// the given key) with a handle e such that match(e) is true.
|
||||
// At every step, the function first tests whether match(e) holds.
|
||||
// If it's false, it evaluates abort(e) to decide whether the
|
||||
// search should be aborted, and in the affirmative returns -1.
|
||||
// For every handle e probed except the last one, the function runs
|
||||
// update(e). We say a probe to a handle e is aborting if match(e) is
|
||||
// false and abort(e) is true. The argument probe is one more than the
|
||||
// last non-aborting probe during the call. This is so that that the
|
||||
// variable can be used to keep track of progress across consecutive
|
||||
// calls to FindSlot.
|
||||
inline int FindSlot(const Slice& key, std::function<bool(ClockHandle*)> match,
|
||||
std::function<bool(ClockHandle*)> stop,
|
||||
std::function<void(ClockHandle*)> update,
|
||||
uint32_t& probe);
|
||||
|
||||
// After a failed FindSlot call (i.e., with answer -1), this function
|
||||
// decrements all displacements, starting from the 0-th probe.
|
||||
void Rollback(const Slice& key, uint32_t probe);
|
||||
|
||||
// Number of hash bits used for table index.
|
||||
// The size of the table is 1 << length_bits_.
|
||||
int length_bits_;
|
||||
|
||||
// For faster computation of ModTableSize.
|
||||
const uint32_t length_bits_mask_;
|
||||
|
||||
// Number of elements in the table.
|
||||
uint32_t occupancy_;
|
||||
|
||||
// Maximum number of elements the user can store in the table.
|
||||
uint32_t occupancy_limit_;
|
||||
|
||||
std::unique_ptr<ClockHandle[]> array_;
|
||||
}; // class ClockHandleTable
|
||||
|
||||
// A single shard of sharded cache.
|
||||
class ALIGN_AS(CACHE_LINE_SIZE) ClockCacheShard final : public CacheShard {
|
||||
public:
|
||||
ClockCacheShard(size_t capacity, size_t estimated_value_size,
|
||||
bool strict_capacity_limit,
|
||||
CacheMetadataChargePolicy metadata_charge_policy);
|
||||
~ClockCacheShard() override = default;
|
||||
|
||||
// Separate from constructor so caller can easily make an array of ClockCache
|
||||
// if current usage is more than new capacity, the function will attempt to
|
||||
// free the needed space.
|
||||
void SetCapacity(size_t capacity) override;
|
||||
|
||||
// Set the flag to reject insertion if cache if full.
|
||||
void SetStrictCapacityLimit(bool strict_capacity_limit) override;
|
||||
|
||||
// Like Cache methods, but with an extra "hash" parameter.
|
||||
// Insert an item into the hash table and, if handle is null, make it
|
||||
// evictable by the clock algorithm. 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 Insert(const Slice& key, uint32_t hash, void* value, size_t charge,
|
||||
Cache::DeleterFn deleter, Cache::Handle** handle,
|
||||
Cache::Priority priority) override;
|
||||
|
||||
Status Insert(const Slice& key, uint32_t hash, void* value,
|
||||
const Cache::CacheItemHelper* helper, size_t charge,
|
||||
Cache::Handle** handle, Cache::Priority priority) override {
|
||||
return Insert(key, hash, value, charge, helper->del_cb, handle, priority);
|
||||
}
|
||||
|
||||
Cache::Handle* Lookup(const Slice& key, uint32_t hash,
|
||||
const Cache::CacheItemHelper* /*helper*/,
|
||||
const Cache::CreateCallback& /*create_cb*/,
|
||||
Cache::Priority /*priority*/, bool /*wait*/,
|
||||
Statistics* /*stats*/) override {
|
||||
return Lookup(key, hash);
|
||||
}
|
||||
Cache::Handle* Lookup(const Slice& key, uint32_t hash) override;
|
||||
|
||||
bool Release(Cache::Handle* handle, bool /*useful*/,
|
||||
bool erase_if_last_ref) override {
|
||||
return Release(handle, erase_if_last_ref);
|
||||
}
|
||||
bool IsReady(Cache::Handle* /*handle*/) override { return true; }
|
||||
void Wait(Cache::Handle* /*handle*/) override {}
|
||||
|
||||
bool Ref(Cache::Handle* handle) override;
|
||||
bool Release(Cache::Handle* handle, bool erase_if_last_ref = false) override;
|
||||
void Erase(const Slice& key, uint32_t hash) override;
|
||||
|
||||
size_t GetUsage() const override;
|
||||
size_t GetPinnedUsage() const override;
|
||||
|
||||
void ApplyToSomeEntries(
|
||||
const std::function<void(const Slice& key, void* value, size_t charge,
|
||||
DeleterFn deleter)>& callback,
|
||||
uint32_t average_entries_per_lock, uint32_t* state) override;
|
||||
|
||||
void EraseUnRefEntries() override;
|
||||
|
||||
std::string GetPrintableOptions() const override;
|
||||
|
||||
private:
|
||||
friend class ClockCache;
|
||||
|
||||
// Makes an element evictable by clock.
|
||||
void ClockOn(ClockHandle* h);
|
||||
|
||||
// Makes an element non-evictable.
|
||||
void ClockOff(ClockHandle* h);
|
||||
|
||||
// Requires an exclusive ref on h.
|
||||
void Evict(ClockHandle* h);
|
||||
|
||||
// Free some space following strict clock policy until enough space
|
||||
// to hold (usage_ + charge) is freed or there are no evictable elements.
|
||||
void EvictFromClock(size_t charge, autovector<ClockHandle>* deleted);
|
||||
|
||||
// Returns the charge of a single handle.
|
||||
static size_t CalcEstimatedHandleCharge(
|
||||
size_t estimated_value_size,
|
||||
CacheMetadataChargePolicy metadata_charge_policy);
|
||||
|
||||
// Returns the number of bits used to hash an element in the hash
|
||||
// table.
|
||||
static int CalcHashBits(size_t capacity, size_t estimated_value_size,
|
||||
CacheMetadataChargePolicy metadata_charge_policy);
|
||||
|
||||
// Initialized before use.
|
||||
size_t capacity_;
|
||||
|
||||
// Whether to reject insertion if cache reaches its full capacity.
|
||||
bool strict_capacity_limit_;
|
||||
|
||||
uint32_t clock_pointer_;
|
||||
|
||||
// ------------^^^^^^^^^^^^^-----------
|
||||
// Not frequently modified data members
|
||||
// ------------------------------------
|
||||
//
|
||||
// We separate data members that are updated frequently from the ones that
|
||||
// are not frequently updated so that they don't share the same cache line
|
||||
// which will lead into false cache sharing
|
||||
//
|
||||
// ------------------------------------
|
||||
// Frequently modified data members
|
||||
// ------------vvvvvvvvvvvvv-----------
|
||||
ClockHandleTable table_;
|
||||
|
||||
// Memory size for entries residing in the cache.
|
||||
size_t usage_;
|
||||
|
||||
// mutex_ protects the following state.
|
||||
// We don't count mutex_ as the cache's internal state so semantically we
|
||||
// don't mind mutex_ invoking the non-const actions.
|
||||
mutable DMutex mutex_;
|
||||
}; // class ClockCacheShard
|
||||
|
||||
class ClockCache
|
||||
#ifdef NDEBUG
|
||||
final
|
||||
#endif
|
||||
: public ShardedCache {
|
||||
public:
|
||||
ClockCache(size_t capacity, size_t estimated_value_size, int num_shard_bits,
|
||||
bool strict_capacity_limit,
|
||||
CacheMetadataChargePolicy metadata_charge_policy =
|
||||
kDontChargeCacheMetadata);
|
||||
~ClockCache() override;
|
||||
const char* Name() const override { return "ClockCache"; }
|
||||
CacheShard* GetShard(uint32_t shard) override;
|
||||
const CacheShard* GetShard(uint32_t shard) const override;
|
||||
void* Value(Handle* handle) override;
|
||||
size_t GetCharge(Handle* handle) const override;
|
||||
uint32_t GetHash(Handle* handle) const override;
|
||||
DeleterFn GetDeleter(Handle* handle) const override;
|
||||
void DisownData() override;
|
||||
|
||||
private:
|
||||
ClockCacheShard* shards_ = nullptr;
|
||||
int num_shards_ = 0;
|
||||
}; // class ClockCache
|
||||
|
||||
} // namespace clock_cache
|
||||
|
||||
// Only for internal testing, temporarily replacing NewClockCache.
|
||||
// TODO(Guido) Remove once NewClockCache constructs a ClockCache again.
|
||||
extern std::shared_ptr<Cache> ExperimentalNewClockCache(
|
||||
size_t capacity, size_t estimated_value_size, int num_shard_bits,
|
||||
bool strict_capacity_limit,
|
||||
CacheMetadataChargePolicy metadata_charge_policy);
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
Vendored
+1
-1
@@ -139,7 +139,7 @@ std::string CompressedSecondaryCache::GetPrintableOptions() const {
|
||||
snprintf(buffer, kBufferSize, " compression_type : %s\n",
|
||||
CompressionTypeToString(cache_options_.compression_type).c_str());
|
||||
ret.append(buffer);
|
||||
snprintf(buffer, kBufferSize, " compression_type : %d\n",
|
||||
snprintf(buffer, kBufferSize, " compress_format_version : %d\n",
|
||||
cache_options_.compress_format_version);
|
||||
ret.append(buffer);
|
||||
return ret;
|
||||
|
||||
Vendored
+281
-171
@@ -12,15 +12,15 @@
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <functional>
|
||||
|
||||
#include "monitoring/perf_context_imp.h"
|
||||
#include "monitoring/statistics.h"
|
||||
#include "port/lang.h"
|
||||
#include "util/distributed_mutex.h"
|
||||
|
||||
#define KEY_LENGTH \
|
||||
16 // TODO(guido) Make use of this symbol in other parts of the source code
|
||||
// (e.g., cache_key.h, cache_test.cc, etc.)
|
||||
#include "util/hash.h"
|
||||
#include "util/math.h"
|
||||
#include "util/random.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
@@ -28,58 +28,155 @@ namespace fast_lru_cache {
|
||||
|
||||
LRUHandleTable::LRUHandleTable(int hash_bits)
|
||||
: length_bits_(hash_bits),
|
||||
list_(new LRUHandle* [size_t{1} << length_bits_] {}) {}
|
||||
length_bits_mask_((uint32_t{1} << length_bits_) - 1),
|
||||
occupancy_(0),
|
||||
occupancy_limit_(static_cast<uint32_t>((uint32_t{1} << length_bits_) *
|
||||
kStrictLoadFactor)),
|
||||
array_(new LRUHandle[size_t{1} << length_bits_]) {
|
||||
assert(hash_bits <= 32);
|
||||
}
|
||||
|
||||
LRUHandleTable::~LRUHandleTable() {
|
||||
ApplyToEntriesRange(
|
||||
[](LRUHandle* h) {
|
||||
if (!h->HasRefs()) {
|
||||
h->Free();
|
||||
}
|
||||
},
|
||||
0, uint32_t{1} << length_bits_);
|
||||
ApplyToEntriesRange([](LRUHandle* h) { h->FreeData(); }, 0, GetTableSize());
|
||||
}
|
||||
|
||||
LRUHandle* LRUHandleTable::Lookup(const Slice& key, uint32_t hash) {
|
||||
return *FindPointer(key, hash);
|
||||
int probe = 0;
|
||||
int slot = FindVisibleElement(key, hash, probe, 0);
|
||||
return (slot == -1) ? nullptr : &array_[slot];
|
||||
}
|
||||
|
||||
inline LRUHandle** LRUHandleTable::Head(uint32_t hash) {
|
||||
return &list_[hash >> (32 - length_bits_)];
|
||||
}
|
||||
|
||||
LRUHandle* LRUHandleTable::Insert(LRUHandle* h) {
|
||||
LRUHandle** ptr = FindPointer(h->key(), h->hash);
|
||||
LRUHandle* old = *ptr;
|
||||
h->next_hash = (old == nullptr ? nullptr : old->next_hash);
|
||||
*ptr = h;
|
||||
return old;
|
||||
}
|
||||
|
||||
LRUHandle* LRUHandleTable::Remove(const Slice& key, uint32_t hash) {
|
||||
LRUHandle** ptr = FindPointer(key, hash);
|
||||
LRUHandle* result = *ptr;
|
||||
if (result != nullptr) {
|
||||
*ptr = result->next_hash;
|
||||
LRUHandle* LRUHandleTable::Insert(LRUHandle* h, LRUHandle** old) {
|
||||
int probe = 0;
|
||||
int slot = FindVisibleElementOrAvailableSlot(h->key(), h->hash, probe,
|
||||
1 /*displacement*/);
|
||||
*old = nullptr;
|
||||
if (slot == -1) {
|
||||
// TODO(Guido) Don't we need to roll back displacements here?
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
if (array_[slot].IsEmpty() || array_[slot].IsTombstone()) {
|
||||
bool empty = array_[slot].IsEmpty();
|
||||
Assign(slot, h);
|
||||
LRUHandle* new_entry = &array_[slot];
|
||||
if (empty) {
|
||||
// This used to be an empty slot.
|
||||
return new_entry;
|
||||
}
|
||||
// It used to be a tombstone, so there may already be a copy of the
|
||||
// key in the table.
|
||||
slot = FindVisibleElement(h->key(), h->hash, probe, 0 /*displacement*/);
|
||||
if (slot == -1) {
|
||||
// No existing copy of the key.
|
||||
return new_entry;
|
||||
}
|
||||
*old = &array_[slot];
|
||||
return new_entry;
|
||||
} else {
|
||||
// There is an existing copy of the key.
|
||||
*old = &array_[slot];
|
||||
// Find an available slot for the new element.
|
||||
array_[slot].displacements++;
|
||||
slot = FindAvailableSlot(h->key(), probe, 1 /*displacement*/);
|
||||
if (slot == -1) {
|
||||
// No available slots. Roll back displacements.
|
||||
probe = 0;
|
||||
slot = FindVisibleElement(h->key(), h->hash, probe, -1);
|
||||
array_[slot].displacements--;
|
||||
FindAvailableSlot(h->key(), probe, -1);
|
||||
return nullptr;
|
||||
}
|
||||
Assign(slot, h);
|
||||
return &array_[slot];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
LRUHandle** LRUHandleTable::FindPointer(const Slice& key, uint32_t hash) {
|
||||
LRUHandle** ptr = &list_[hash >> (32 - length_bits_)];
|
||||
while (*ptr != nullptr && ((*ptr)->hash != hash || key != (*ptr)->key())) {
|
||||
ptr = &(*ptr)->next_hash;
|
||||
void LRUHandleTable::Remove(LRUHandle* h) {
|
||||
assert(h->next == nullptr &&
|
||||
h->prev == nullptr); // Already off the LRU list.
|
||||
int probe = 0;
|
||||
FindSlot(
|
||||
h->key(), [&h](LRUHandle* e) { return e == h; }, probe,
|
||||
-1 /*displacement*/);
|
||||
h->SetIsVisible(false);
|
||||
h->SetIsElement(false);
|
||||
occupancy_--;
|
||||
}
|
||||
|
||||
void LRUHandleTable::Assign(int slot, LRUHandle* h) {
|
||||
LRUHandle* dst = &array_[slot];
|
||||
uint32_t disp = dst->displacements;
|
||||
*dst = *h;
|
||||
dst->displacements = disp;
|
||||
dst->SetIsVisible(true);
|
||||
dst->SetIsElement(true);
|
||||
occupancy_++;
|
||||
}
|
||||
|
||||
void LRUHandleTable::Exclude(LRUHandle* h) { h->SetIsVisible(false); }
|
||||
|
||||
int LRUHandleTable::FindVisibleElement(const Slice& key, uint32_t hash,
|
||||
int& probe, int displacement) {
|
||||
return FindSlot(
|
||||
key,
|
||||
[&](LRUHandle* h) { return h->Matches(key, hash) && h->IsVisible(); },
|
||||
probe, displacement);
|
||||
}
|
||||
|
||||
int LRUHandleTable::FindAvailableSlot(const Slice& key, int& probe,
|
||||
int displacement) {
|
||||
return FindSlot(
|
||||
key, [](LRUHandle* h) { return h->IsEmpty() || h->IsTombstone(); }, probe,
|
||||
displacement);
|
||||
}
|
||||
|
||||
int LRUHandleTable::FindVisibleElementOrAvailableSlot(const Slice& key,
|
||||
uint32_t hash, int& probe,
|
||||
int displacement) {
|
||||
return FindSlot(
|
||||
key,
|
||||
[&](LRUHandle* h) {
|
||||
return h->IsEmpty() || h->IsTombstone() ||
|
||||
(h->Matches(key, hash) && h->IsVisible());
|
||||
},
|
||||
probe, displacement);
|
||||
}
|
||||
|
||||
inline int LRUHandleTable::FindSlot(const Slice& key,
|
||||
std::function<bool(LRUHandle*)> cond,
|
||||
int& probe, int displacement) {
|
||||
uint32_t base = ModTableSize(Hash(key.data(), key.size(), kProbingSeed1));
|
||||
uint32_t increment =
|
||||
ModTableSize((Hash(key.data(), key.size(), kProbingSeed2) << 1) | 1);
|
||||
uint32_t current = ModTableSize(base + probe * increment);
|
||||
while (true) {
|
||||
LRUHandle* h = &array_[current];
|
||||
probe++;
|
||||
if (current == base && probe > 1) {
|
||||
// We looped back.
|
||||
return -1;
|
||||
}
|
||||
if (cond(h)) {
|
||||
return current;
|
||||
}
|
||||
if (h->IsEmpty()) {
|
||||
// We check emptyness after the condition, because
|
||||
// the condition may be emptyness.
|
||||
return -1;
|
||||
}
|
||||
h->displacements += displacement;
|
||||
current = ModTableSize(current + increment);
|
||||
}
|
||||
return ptr;
|
||||
}
|
||||
|
||||
LRUCacheShard::LRUCacheShard(size_t capacity, size_t estimated_value_size,
|
||||
bool strict_capacity_limit,
|
||||
CacheMetadataChargePolicy metadata_charge_policy)
|
||||
: capacity_(0),
|
||||
: capacity_(capacity),
|
||||
strict_capacity_limit_(strict_capacity_limit),
|
||||
table_(
|
||||
GetHashBits(capacity, estimated_value_size, metadata_charge_policy)),
|
||||
CalcHashBits(capacity, estimated_value_size, metadata_charge_policy)),
|
||||
usage_(0),
|
||||
lru_usage_(0) {
|
||||
set_metadata_charge_policy(metadata_charge_policy);
|
||||
@@ -87,29 +184,27 @@ LRUCacheShard::LRUCacheShard(size_t capacity, size_t estimated_value_size,
|
||||
lru_.next = &lru_;
|
||||
lru_.prev = &lru_;
|
||||
lru_low_pri_ = &lru_;
|
||||
SetCapacity(capacity);
|
||||
}
|
||||
|
||||
void LRUCacheShard::EraseUnRefEntries() {
|
||||
autovector<LRUHandle*> last_reference_list;
|
||||
autovector<LRUHandle> last_reference_list;
|
||||
{
|
||||
DMutexLock l(mutex_);
|
||||
while (lru_.next != &lru_) {
|
||||
LRUHandle* old = lru_.next;
|
||||
// LRU list contains only elements which can be evicted.
|
||||
assert(old->InCache() && !old->HasRefs());
|
||||
assert(old->IsVisible() && !old->HasRefs());
|
||||
LRU_Remove(old);
|
||||
table_.Remove(old->key(), old->hash);
|
||||
old->SetInCache(false);
|
||||
table_.Remove(old);
|
||||
assert(usage_ >= old->total_charge);
|
||||
usage_ -= old->total_charge;
|
||||
last_reference_list.push_back(old);
|
||||
last_reference_list.push_back(*old);
|
||||
}
|
||||
}
|
||||
|
||||
// Free the entries here outside of mutex for performance reasons.
|
||||
for (auto entry : last_reference_list) {
|
||||
entry->Free();
|
||||
for (auto& h : last_reference_list) {
|
||||
h.FreeData();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -122,7 +217,7 @@ void LRUCacheShard::ApplyToSomeEntries(
|
||||
// hash bits for table indexes.
|
||||
DMutexLock l(mutex_);
|
||||
uint32_t length_bits = table_.GetLengthBits();
|
||||
uint32_t length = uint32_t{1} << length_bits;
|
||||
uint32_t length = table_.GetTableSize();
|
||||
|
||||
assert(average_entries_per_lock > 0);
|
||||
// Assuming we are called with same average_entries_per_lock repeatedly,
|
||||
@@ -148,65 +243,64 @@ void LRUCacheShard::ApplyToSomeEntries(
|
||||
index_begin, index_end);
|
||||
}
|
||||
|
||||
void LRUCacheShard::LRU_Remove(LRUHandle* e) {
|
||||
assert(e->next != nullptr);
|
||||
assert(e->prev != nullptr);
|
||||
e->next->prev = e->prev;
|
||||
e->prev->next = e->next;
|
||||
e->prev = e->next = nullptr;
|
||||
assert(lru_usage_ >= e->total_charge);
|
||||
lru_usage_ -= e->total_charge;
|
||||
void LRUCacheShard::LRU_Remove(LRUHandle* h) {
|
||||
assert(h->next != nullptr);
|
||||
assert(h->prev != nullptr);
|
||||
h->next->prev = h->prev;
|
||||
h->prev->next = h->next;
|
||||
h->prev = h->next = nullptr;
|
||||
assert(lru_usage_ >= h->total_charge);
|
||||
lru_usage_ -= h->total_charge;
|
||||
}
|
||||
|
||||
void LRUCacheShard::LRU_Insert(LRUHandle* e) {
|
||||
assert(e->next == nullptr);
|
||||
assert(e->prev == nullptr);
|
||||
// Inset "e" to head of LRU list.
|
||||
e->next = &lru_;
|
||||
e->prev = lru_.prev;
|
||||
e->prev->next = e;
|
||||
e->next->prev = e;
|
||||
lru_usage_ += e->total_charge;
|
||||
void LRUCacheShard::LRU_Insert(LRUHandle* h) {
|
||||
assert(h->next == nullptr);
|
||||
assert(h->prev == nullptr);
|
||||
// Insert h to head of LRU list.
|
||||
h->next = &lru_;
|
||||
h->prev = lru_.prev;
|
||||
h->prev->next = h;
|
||||
h->next->prev = h;
|
||||
lru_usage_ += h->total_charge;
|
||||
}
|
||||
|
||||
void LRUCacheShard::EvictFromLRU(size_t charge,
|
||||
autovector<LRUHandle*>* deleted) {
|
||||
autovector<LRUHandle>* deleted) {
|
||||
while ((usage_ + charge) > capacity_ && lru_.next != &lru_) {
|
||||
LRUHandle* old = lru_.next;
|
||||
// LRU list contains only elements which can be evicted.
|
||||
assert(old->InCache() && !old->HasRefs());
|
||||
assert(old->IsVisible() && !old->HasRefs());
|
||||
LRU_Remove(old);
|
||||
table_.Remove(old->key(), old->hash);
|
||||
old->SetInCache(false);
|
||||
table_.Remove(old);
|
||||
assert(usage_ >= old->total_charge);
|
||||
usage_ -= old->total_charge;
|
||||
deleted->push_back(old);
|
||||
deleted->push_back(*old);
|
||||
}
|
||||
}
|
||||
|
||||
int LRUCacheShard::GetHashBits(
|
||||
size_t LRUCacheShard::CalcEstimatedHandleCharge(
|
||||
size_t estimated_value_size,
|
||||
CacheMetadataChargePolicy metadata_charge_policy) {
|
||||
LRUHandle h;
|
||||
h.CalcTotalCharge(estimated_value_size, metadata_charge_policy);
|
||||
return h.total_charge;
|
||||
}
|
||||
|
||||
int LRUCacheShard::CalcHashBits(
|
||||
size_t capacity, size_t estimated_value_size,
|
||||
CacheMetadataChargePolicy metadata_charge_policy) {
|
||||
LRUHandle* e = reinterpret_cast<LRUHandle*>(
|
||||
new char[sizeof(LRUHandle) - 1 + KEY_LENGTH]);
|
||||
e->key_length = KEY_LENGTH;
|
||||
e->deleter = nullptr;
|
||||
e->refs = 0;
|
||||
e->flags = 0;
|
||||
e->refs = 0;
|
||||
|
||||
e->CalcTotalCharge(estimated_value_size, metadata_charge_policy);
|
||||
size_t num_entries = capacity / e->total_charge;
|
||||
e->Free();
|
||||
int num_hash_bits = 0;
|
||||
while (num_entries >>= 1) {
|
||||
++num_hash_bits;
|
||||
}
|
||||
return num_hash_bits;
|
||||
size_t handle_charge =
|
||||
CalcEstimatedHandleCharge(estimated_value_size, metadata_charge_policy);
|
||||
assert(handle_charge > 0);
|
||||
uint32_t num_entries =
|
||||
static_cast<uint32_t>(capacity / (kLoadFactor * handle_charge)) + 1;
|
||||
assert(num_entries <= uint32_t{1} << 31);
|
||||
return FloorLog2((num_entries << 1) - 1);
|
||||
}
|
||||
|
||||
void LRUCacheShard::SetCapacity(size_t capacity) {
|
||||
autovector<LRUHandle*> last_reference_list;
|
||||
assert(false); // Not supported. TODO(Guido) Support it?
|
||||
autovector<LRUHandle> last_reference_list;
|
||||
{
|
||||
DMutexLock l(mutex_);
|
||||
capacity_ = capacity;
|
||||
@@ -214,8 +308,8 @@ void LRUCacheShard::SetCapacity(size_t capacity) {
|
||||
}
|
||||
|
||||
// Free the entries here outside of mutex for performance reasons.
|
||||
for (auto entry : last_reference_list) {
|
||||
entry->Free();
|
||||
for (auto& h : last_reference_list) {
|
||||
h.FreeData();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -224,83 +318,124 @@ void LRUCacheShard::SetStrictCapacityLimit(bool strict_capacity_limit) {
|
||||
strict_capacity_limit_ = strict_capacity_limit;
|
||||
}
|
||||
|
||||
Status LRUCacheShard::InsertItem(LRUHandle* e, Cache::Handle** handle,
|
||||
bool free_handle_on_fail) {
|
||||
Status LRUCacheShard::Insert(const Slice& key, uint32_t hash, void* value,
|
||||
size_t charge, Cache::DeleterFn deleter,
|
||||
Cache::Handle** handle,
|
||||
Cache::Priority /*priority*/) {
|
||||
if (key.size() != kCacheKeySize) {
|
||||
return Status::NotSupported("FastLRUCache only supports key size " +
|
||||
std::to_string(kCacheKeySize) + "B");
|
||||
}
|
||||
|
||||
LRUHandle tmp;
|
||||
tmp.value = value;
|
||||
tmp.deleter = deleter;
|
||||
tmp.hash = hash;
|
||||
tmp.CalcTotalCharge(charge, metadata_charge_policy_);
|
||||
for (int i = 0; i < kCacheKeySize; i++) {
|
||||
tmp.key_data[i] = key.data()[i];
|
||||
}
|
||||
|
||||
Status s = Status::OK();
|
||||
autovector<LRUHandle*> last_reference_list;
|
||||
autovector<LRUHandle> last_reference_list;
|
||||
{
|
||||
DMutexLock l(mutex_);
|
||||
assert(table_.GetOccupancy() <= table_.GetOccupancyLimit());
|
||||
|
||||
// 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);
|
||||
EvictFromLRU(tmp.total_charge, &last_reference_list);
|
||||
if ((usage_ + tmp.total_charge > capacity_ &&
|
||||
(strict_capacity_limit_ || handle == nullptr)) ||
|
||||
table_.GetOccupancy() == table_.GetOccupancyLimit()) {
|
||||
// There are two measures of capacity:
|
||||
// - Space (or charge) capacity: The maximum possible sum of the charges
|
||||
// of the elements.
|
||||
// - Table capacity: The number of slots in the hash table.
|
||||
// These are incomparable, in the sense that one doesn't imply the other.
|
||||
// Typically we will reach space capacity before table capacity---
|
||||
// if the user always inserts values with size equal to
|
||||
// estimated_value_size, then at most a kLoadFactor fraction of slots
|
||||
// will ever be occupied. But in some cases we may reach table capacity
|
||||
// before space capacity---if the user initially claims a very large
|
||||
// estimated_value_size but then inserts tiny values, more elements than
|
||||
// initially estimated will be inserted.
|
||||
|
||||
if ((usage_ + e->total_charge) > capacity_ &&
|
||||
(strict_capacity_limit_ || handle == nullptr)) {
|
||||
e->SetInCache(false);
|
||||
// TODO(Guido) Some tests (at least two from cache_test, as well as the
|
||||
// stress tests) currently assume the table capacity is unbounded.
|
||||
if (handle == nullptr) {
|
||||
// Don't insert the entry but still return ok, as if the entry inserted
|
||||
// into cache and get evicted immediately.
|
||||
last_reference_list.push_back(e);
|
||||
last_reference_list.push_back(tmp);
|
||||
} else {
|
||||
if (free_handle_on_fail) {
|
||||
delete[] reinterpret_cast<char*>(e);
|
||||
*handle = nullptr;
|
||||
if (table_.GetOccupancy() == table_.GetOccupancyLimit()) {
|
||||
// TODO: Consider using a distinct status for this case, but usually
|
||||
// it will be handled the same way as reaching charge capacity limit
|
||||
s = Status::MemoryLimit(
|
||||
"Insert failed because all slots in the hash table are full.");
|
||||
} else {
|
||||
s = Status::MemoryLimit(
|
||||
"Insert failed because the total charge has exceeded the "
|
||||
"capacity.");
|
||||
}
|
||||
s = Status::Incomplete("Insert failed due to LRU cache being full.");
|
||||
}
|
||||
} else {
|
||||
// Insert into the cache. Note that the cache might get larger than its
|
||||
// capacity if not enough space was freed up.
|
||||
LRUHandle* old = table_.Insert(e);
|
||||
usage_ += e->total_charge;
|
||||
LRUHandle* old;
|
||||
LRUHandle* h = table_.Insert(&tmp, &old);
|
||||
assert(h != nullptr); // We're below occupancy, so this insertion should
|
||||
// never fail.
|
||||
usage_ += h->total_charge;
|
||||
if (old != nullptr) {
|
||||
s = Status::OkOverwritten();
|
||||
assert(old->InCache());
|
||||
old->SetInCache(false);
|
||||
assert(old->IsVisible());
|
||||
table_.Exclude(old);
|
||||
if (!old->HasRefs()) {
|
||||
// old is on LRU because it's in cache and its reference count is 0.
|
||||
LRU_Remove(old);
|
||||
table_.Remove(old);
|
||||
assert(usage_ >= old->total_charge);
|
||||
usage_ -= old->total_charge;
|
||||
last_reference_list.push_back(old);
|
||||
last_reference_list.push_back(*old);
|
||||
}
|
||||
}
|
||||
if (handle == nullptr) {
|
||||
LRU_Insert(e);
|
||||
LRU_Insert(h);
|
||||
} else {
|
||||
// If caller already holds a ref, no need to take one here.
|
||||
if (!e->HasRefs()) {
|
||||
e->Ref();
|
||||
if (!h->HasRefs()) {
|
||||
h->Ref();
|
||||
}
|
||||
*handle = reinterpret_cast<Cache::Handle*>(e);
|
||||
*handle = reinterpret_cast<Cache::Handle*>(h);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Free the entries here outside of mutex for performance reasons.
|
||||
for (auto entry : last_reference_list) {
|
||||
entry->Free();
|
||||
for (auto& h : last_reference_list) {
|
||||
h.FreeData();
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
Cache::Handle* LRUCacheShard::Lookup(const Slice& key, uint32_t hash) {
|
||||
LRUHandle* e = nullptr;
|
||||
LRUHandle* h = nullptr;
|
||||
{
|
||||
DMutexLock l(mutex_);
|
||||
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);
|
||||
h = table_.Lookup(key, hash);
|
||||
if (h != nullptr) {
|
||||
assert(h->IsVisible());
|
||||
if (!h->HasRefs()) {
|
||||
// The entry is in LRU since it's in hash and has no external
|
||||
// references.
|
||||
LRU_Remove(h);
|
||||
}
|
||||
e->Ref();
|
||||
h->Ref();
|
||||
}
|
||||
}
|
||||
return reinterpret_cast<Cache::Handle*>(e);
|
||||
return reinterpret_cast<Cache::Handle*>(h);
|
||||
}
|
||||
|
||||
bool LRUCacheShard::Ref(Cache::Handle* h) {
|
||||
@@ -316,91 +451,64 @@ bool LRUCacheShard::Release(Cache::Handle* handle, bool erase_if_last_ref) {
|
||||
if (handle == nullptr) {
|
||||
return false;
|
||||
}
|
||||
LRUHandle* e = reinterpret_cast<LRUHandle*>(handle);
|
||||
LRUHandle* h = reinterpret_cast<LRUHandle*>(handle);
|
||||
LRUHandle copy;
|
||||
bool last_reference = false;
|
||||
{
|
||||
DMutexLock l(mutex_);
|
||||
last_reference = e->Unref();
|
||||
if (last_reference && e->InCache()) {
|
||||
last_reference = h->Unref();
|
||||
if (last_reference && h->IsVisible()) {
|
||||
// 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.
|
||||
assert(lru_.next == &lru_ || erase_if_last_ref);
|
||||
// Take this opportunity and remove the item.
|
||||
table_.Remove(e->key(), e->hash);
|
||||
e->SetInCache(false);
|
||||
table_.Remove(h);
|
||||
} else {
|
||||
// Put the item back on the LRU list, and don't free it.
|
||||
LRU_Insert(e);
|
||||
LRU_Insert(h);
|
||||
last_reference = false;
|
||||
}
|
||||
}
|
||||
// If it was the last reference, then decrement the cache usage.
|
||||
if (last_reference) {
|
||||
assert(usage_ >= e->total_charge);
|
||||
usage_ -= e->total_charge;
|
||||
assert(usage_ >= h->total_charge);
|
||||
usage_ -= h->total_charge;
|
||||
copy = *h;
|
||||
}
|
||||
}
|
||||
|
||||
// Free the entry here outside of mutex for performance reasons.
|
||||
if (last_reference) {
|
||||
e->Free();
|
||||
copy.FreeData();
|
||||
}
|
||||
return last_reference;
|
||||
}
|
||||
|
||||
Status LRUCacheShard::Insert(const Slice& key, uint32_t hash, void* value,
|
||||
size_t charge, Cache::DeleterFn deleter,
|
||||
Cache::Handle** handle,
|
||||
Cache::Priority /*priority*/) {
|
||||
if (key.size() != KEY_LENGTH) {
|
||||
return Status::NotSupported("FastLRUCache only supports key size " +
|
||||
std::to_string(KEY_LENGTH) + "B");
|
||||
}
|
||||
|
||||
// Allocate the memory here outside of the mutex.
|
||||
// If the cache is full, we'll have to release it.
|
||||
// It shouldn't happen very often though.
|
||||
LRUHandle* e = reinterpret_cast<LRUHandle*>(
|
||||
new char[sizeof(LRUHandle) - 1 + key.size()]);
|
||||
|
||||
e->value = value;
|
||||
e->flags = 0;
|
||||
e->deleter = deleter;
|
||||
e->key_length = key.size();
|
||||
e->hash = hash;
|
||||
e->refs = 0;
|
||||
e->next = e->prev = nullptr;
|
||||
e->SetInCache(true);
|
||||
e->CalcTotalCharge(charge, metadata_charge_policy_);
|
||||
memcpy(e->key_data, key.data(), key.size());
|
||||
|
||||
return InsertItem(e, handle, /* free_handle_on_fail */ true);
|
||||
}
|
||||
|
||||
void LRUCacheShard::Erase(const Slice& key, uint32_t hash) {
|
||||
LRUHandle* e;
|
||||
LRUHandle copy;
|
||||
bool last_reference = false;
|
||||
{
|
||||
DMutexLock l(mutex_);
|
||||
e = table_.Remove(key, hash);
|
||||
if (e != nullptr) {
|
||||
assert(e->InCache());
|
||||
e->SetInCache(false);
|
||||
if (!e->HasRefs()) {
|
||||
// The entry is in LRU since it's in hash and has no external references
|
||||
LRU_Remove(e);
|
||||
assert(usage_ >= e->total_charge);
|
||||
usage_ -= e->total_charge;
|
||||
LRUHandle* h = table_.Lookup(key, hash);
|
||||
if (h != nullptr) {
|
||||
table_.Exclude(h);
|
||||
if (!h->HasRefs()) {
|
||||
// The entry is in LRU since it's in cache and has no external
|
||||
// references.
|
||||
LRU_Remove(h);
|
||||
table_.Remove(h);
|
||||
assert(usage_ >= h->total_charge);
|
||||
usage_ -= h->total_charge;
|
||||
last_reference = true;
|
||||
copy = *h;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Free the entry here outside of mutex for performance reasons.
|
||||
// last_reference will only be true if e != nullptr.
|
||||
if (last_reference) {
|
||||
e->Free();
|
||||
copy.FreeData();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -421,6 +529,8 @@ LRUCache::LRUCache(size_t capacity, size_t estimated_value_size,
|
||||
int num_shard_bits, bool strict_capacity_limit,
|
||||
CacheMetadataChargePolicy metadata_charge_policy)
|
||||
: ShardedCache(capacity, num_shard_bits, strict_capacity_limit) {
|
||||
assert(estimated_value_size > 0 ||
|
||||
metadata_charge_policy != kDontChargeCacheMetadata);
|
||||
num_shards_ = 1 << num_shard_bits;
|
||||
shards_ = reinterpret_cast<LRUCacheShard*>(
|
||||
port::cacheline_aligned_alloc(sizeof(LRUCacheShard) * num_shards_));
|
||||
|
||||
Vendored
+220
-57
@@ -8,9 +8,11 @@
|
||||
// found in the LICENSE file. See the AUTHORS file for names of contributors.
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
|
||||
#include "cache/cache_key.h"
|
||||
#include "cache/sharded_cache.h"
|
||||
#include "port/lang.h"
|
||||
#include "port/malloc.h"
|
||||
@@ -20,33 +22,124 @@
|
||||
#include "util/distributed_mutex.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
namespace fast_lru_cache {
|
||||
|
||||
// Forward declaration of friend class.
|
||||
class FastLRUCacheTest;
|
||||
|
||||
// LRU cache implementation using an open-address hash table.
|
||||
//
|
||||
// Every slot in the hash table is an LRUHandle. Because handles can be
|
||||
// referenced externally, we can't discard them immediately once they are
|
||||
// deleted (via a delete or an LRU eviction) or replaced by a new version
|
||||
// (via an insert of the same key). The state of an element is defined by
|
||||
// the following two properties:
|
||||
// (R) Referenced: An element can be referenced externally (refs > 0), or not.
|
||||
// Importantly, an element can be evicted if and only if it's not
|
||||
// referenced. In particular, when an element becomes referenced, it's
|
||||
// temporarily taken out of the LRU list until all references to it
|
||||
// are dropped.
|
||||
// (V) Visible: An element can visible for lookups (IS_VISIBLE set), or not.
|
||||
// Initially, every element is visible. An element that is not visible is
|
||||
// called a ghost.
|
||||
// These properties induce 4 different states, with transitions defined as
|
||||
// follows:
|
||||
// - V --> not V: When a visible element is deleted or replaced by a new
|
||||
// version.
|
||||
// - Not V --> V: This cannot happen. A ghost remains in that state until it's
|
||||
// not referenced any more, at which point it's ready to be removed from the
|
||||
// hash table. (A ghost simply waits to transition to the afterlife---it will
|
||||
// never be visible again.)
|
||||
// - R --> not R: When all references to an element are dropped.
|
||||
// - Not R --> R: When an unreferenced element becomes referenced. This can only
|
||||
// happen if the element is V, since references to an element can only be
|
||||
// created when it's visible.
|
||||
//
|
||||
// Internally, the cache uses an open-addressed hash table to index the handles.
|
||||
// We use tombstone counters to keep track of displacements.
|
||||
// Because of the tombstones and the two possible visibility states of an
|
||||
// element, the table slots can be in 4 different states:
|
||||
// 1. Visible element (IS_ELEMENT set and IS_VISIBLE set): The slot contains a
|
||||
// key-value element.
|
||||
// 2. Ghost element (IS_ELEMENT set and IS_VISIBLE unset): The slot contains an
|
||||
// element that has been removed, but it's still referenced. It's invisible
|
||||
// to lookups.
|
||||
// 3. Tombstone (IS_ELEMENT unset and displacements > 0): The slot contains a
|
||||
// tombstone.
|
||||
// 4. Empty (IS_ELEMENT unset and displacements == 0): The slot is unused.
|
||||
// A slot that is an element can further have IS_VISIBLE set or not.
|
||||
// When a ghost is removed from the table, it can either transition to being a
|
||||
// tombstone or an empty slot, depending on the number of displacements of the
|
||||
// slot. In any case, the slot becomes available. When a handle is inserted
|
||||
// into that slot, it becomes a visible element again.
|
||||
|
||||
// The load factor p is a real number in (0, 1) such that at all
|
||||
// times at most a fraction p of all slots, without counting tombstones,
|
||||
// are occupied by elements. This means that the probability that a
|
||||
// random probe hits an empty slot is at most p, and thus at most 1/p probes
|
||||
// are required on average. For example, p = 70% implies that between 1 and 2
|
||||
// probes are needed on average (bear in mind that this reasoning doesn't
|
||||
// consider the effects of clustering over time).
|
||||
// Because the size of the hash table is always rounded up to the next
|
||||
// power of 2, p is really an upper bound on the actual load factor---the
|
||||
// actual load factor is anywhere between p/2 and p. This is a bit wasteful,
|
||||
// but bear in mind that slots only hold metadata, not actual values.
|
||||
// Since space cost is dominated by the values (the LSM blocks),
|
||||
// overprovisioning the table with metadata only increases the total cache space
|
||||
// usage by a tiny fraction.
|
||||
constexpr double kLoadFactor = 0.35;
|
||||
|
||||
// The user can exceed kLoadFactor if the sizes of the inserted values don't
|
||||
// match estimated_value_size, or if strict_capacity_limit == false. To
|
||||
// avoid performance to plunge, we set a strict upper bound on the load factor.
|
||||
constexpr double kStrictLoadFactor = 0.7;
|
||||
|
||||
// Arbitrary seeds.
|
||||
constexpr uint32_t kProbingSeed1 = 0xbc9f1d34;
|
||||
constexpr uint32_t kProbingSeed2 = 0x7a2bb9d5;
|
||||
|
||||
// An experimental (under development!) alternative to LRUCache
|
||||
|
||||
struct LRUHandle {
|
||||
void* value;
|
||||
Cache::DeleterFn deleter;
|
||||
LRUHandle* next_hash;
|
||||
LRUHandle* next;
|
||||
LRUHandle* prev;
|
||||
size_t total_charge; // TODO(opt): Only allow uint32_t?
|
||||
size_t key_length;
|
||||
// The hash of key(). Used for fast sharding and comparisons.
|
||||
uint32_t hash;
|
||||
// The number of external refs to this entry. The cache itself is not counted.
|
||||
// The number of external refs to this entry.
|
||||
uint32_t refs;
|
||||
|
||||
enum Flags : uint8_t {
|
||||
// Whether this entry is referenced by the hash table.
|
||||
IN_CACHE = (1 << 0),
|
||||
// Whether the handle is visible to Lookups.
|
||||
IS_VISIBLE = (1 << 0),
|
||||
// Whether the slot is in use by an element.
|
||||
IS_ELEMENT = (1 << 1),
|
||||
};
|
||||
uint8_t flags;
|
||||
|
||||
// Beginning of the key (MUST BE THE LAST FIELD IN THIS STRUCT!)
|
||||
char key_data[1];
|
||||
// The number of elements that hash to this slot or a lower one,
|
||||
// but wind up in a higher slot.
|
||||
uint32_t displacements;
|
||||
|
||||
Slice key() const { return Slice(key_data, key_length); }
|
||||
std::array<char, kCacheKeySize> key_data;
|
||||
|
||||
LRUHandle() {
|
||||
value = nullptr;
|
||||
deleter = nullptr;
|
||||
next = nullptr;
|
||||
prev = nullptr;
|
||||
total_charge = 0;
|
||||
hash = 0;
|
||||
refs = 0;
|
||||
flags = 0;
|
||||
displacements = 0;
|
||||
key_data.fill(0);
|
||||
}
|
||||
|
||||
Slice key() const { return Slice(key_data.data(), kCacheKeySize); }
|
||||
|
||||
// Increase the reference count by 1.
|
||||
void Ref() { refs++; }
|
||||
@@ -61,22 +154,31 @@ struct LRUHandle {
|
||||
// Return true if there are external refs, false otherwise.
|
||||
bool HasRefs() const { return refs > 0; }
|
||||
|
||||
bool InCache() const { return flags & IN_CACHE; }
|
||||
bool IsVisible() const { return flags & IS_VISIBLE; }
|
||||
|
||||
void SetInCache(bool in_cache) {
|
||||
if (in_cache) {
|
||||
flags |= IN_CACHE;
|
||||
void SetIsVisible(bool is_visible) {
|
||||
if (is_visible) {
|
||||
flags |= IS_VISIBLE;
|
||||
} else {
|
||||
flags &= ~IN_CACHE;
|
||||
flags &= ~IS_VISIBLE;
|
||||
}
|
||||
}
|
||||
|
||||
void Free() {
|
||||
bool IsElement() const { return flags & IS_ELEMENT; }
|
||||
|
||||
void SetIsElement(bool is_element) {
|
||||
if (is_element) {
|
||||
flags |= IS_ELEMENT;
|
||||
} else {
|
||||
flags &= ~IS_ELEMENT;
|
||||
}
|
||||
}
|
||||
|
||||
void FreeData() {
|
||||
assert(refs == 0);
|
||||
if (deleter) {
|
||||
(*deleter)(key(), value);
|
||||
}
|
||||
delete[] reinterpret_cast<char*>(this);
|
||||
}
|
||||
|
||||
// Calculate the memory usage by metadata.
|
||||
@@ -85,13 +187,22 @@ struct LRUHandle {
|
||||
if (metadata_charge_policy != kFullChargeCacheMetadata) {
|
||||
return 0;
|
||||
} else {
|
||||
#ifdef ROCKSDB_MALLOC_USABLE_SIZE
|
||||
return malloc_usable_size(
|
||||
const_cast<void*>(static_cast<const void*>(this)));
|
||||
#else
|
||||
// This is the size that is used when a new handle is created.
|
||||
return sizeof(LRUHandle) - 1 + key_length;
|
||||
#endif
|
||||
// #ifdef ROCKSDB_MALLOC_USABLE_SIZE
|
||||
// return malloc_usable_size(
|
||||
// const_cast<void*>(static_cast<const void*>(this)));
|
||||
// #else
|
||||
// TODO(Guido) malloc_usable_size only works when we call it on
|
||||
// a pointer allocated with malloc. Because our handles are all
|
||||
// allocated in a single shot as an array, the user can't call
|
||||
// CalcMetaCharge (or CalcTotalCharge or GetCharge) on a handle
|
||||
// pointer returned by the cache. Moreover, malloc_usable_size
|
||||
// expects a heap-allocated handle, but sometimes in our code we
|
||||
// wish to pass a stack-allocated handle (this is only a performance
|
||||
// concern).
|
||||
// What is the right way to compute metadata charges with pre-allocated
|
||||
// handles?
|
||||
return sizeof(LRUHandle);
|
||||
// #endif
|
||||
}
|
||||
}
|
||||
|
||||
@@ -106,54 +217,102 @@ struct LRUHandle {
|
||||
assert(total_charge >= meta_charge);
|
||||
return total_charge - meta_charge;
|
||||
}
|
||||
|
||||
inline bool IsEmpty() {
|
||||
return !this->IsElement() && this->displacements == 0;
|
||||
}
|
||||
|
||||
inline bool IsTombstone() {
|
||||
return !this->IsElement() && this->displacements > 0;
|
||||
}
|
||||
|
||||
inline bool Matches(const Slice& some_key, uint32_t some_hash) {
|
||||
return this->IsElement() && this->hash == some_hash &&
|
||||
this->key() == some_key;
|
||||
}
|
||||
};
|
||||
|
||||
// We provide our own simple hash table since it removes a whole bunch
|
||||
// of porting hacks and is also faster than some of the built-in hash
|
||||
// table implementations in some of the compiler/runtime combinations
|
||||
// we have tested. E.g., readrandom speeds up by ~5% over the g++
|
||||
// 4.4.3's builtin hashtable.
|
||||
|
||||
class LRUHandleTable {
|
||||
public:
|
||||
explicit LRUHandleTable(int hash_bits);
|
||||
~LRUHandleTable();
|
||||
|
||||
// Returns a pointer to a visible element matching the key/hash, or
|
||||
// nullptr if not present.
|
||||
LRUHandle* Lookup(const Slice& key, uint32_t hash);
|
||||
LRUHandle* Insert(LRUHandle* h);
|
||||
LRUHandle* Remove(const Slice& key, uint32_t hash);
|
||||
|
||||
// Inserts a copy of h into the hash table.
|
||||
// Returns a pointer to the inserted handle, or nullptr if no slot
|
||||
// available was found. If an existing visible element matching the
|
||||
// key/hash is already present in the hash table, the argument old
|
||||
// is set to pointe to it; otherwise, it's set to nullptr.
|
||||
LRUHandle* Insert(LRUHandle* h, LRUHandle** old);
|
||||
|
||||
// Removes h from the hash table. The handle must already be off
|
||||
// the LRU list.
|
||||
void Remove(LRUHandle* h);
|
||||
|
||||
// Turns a visible element h into a ghost (i.e., not visible).
|
||||
void Exclude(LRUHandle* h);
|
||||
|
||||
// Assigns a copy of h to the given slot.
|
||||
void Assign(int slot, LRUHandle* h);
|
||||
|
||||
template <typename T>
|
||||
void ApplyToEntriesRange(T func, uint32_t index_begin, uint32_t index_end) {
|
||||
for (uint32_t i = index_begin; i < index_end; i++) {
|
||||
LRUHandle* h = list_[i];
|
||||
while (h != nullptr) {
|
||||
auto n = h->next_hash;
|
||||
assert(h->InCache());
|
||||
LRUHandle* h = &array_[i];
|
||||
if (h->IsVisible()) {
|
||||
func(h);
|
||||
h = n;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uint32_t GetTableSize() const { return uint32_t{1} << length_bits_; }
|
||||
|
||||
int GetLengthBits() const { return length_bits_; }
|
||||
|
||||
// Return the address of the head of the chain in the bucket given
|
||||
// by the hash.
|
||||
inline LRUHandle** Head(uint32_t hash);
|
||||
uint32_t GetOccupancyLimit() const { return occupancy_limit_; }
|
||||
|
||||
uint32_t GetOccupancy() const { return occupancy_; }
|
||||
|
||||
// Returns x mod 2^{length_bits_}.
|
||||
uint32_t ModTableSize(uint32_t x) { return x & length_bits_mask_; }
|
||||
|
||||
private:
|
||||
// Return a pointer to slot that points to a cache entry that
|
||||
// matches key/hash. If there is no such cache entry, return a
|
||||
// pointer to the trailing slot in the corresponding linked list.
|
||||
LRUHandle** FindPointer(const Slice& key, uint32_t hash);
|
||||
int FindVisibleElement(const Slice& key, uint32_t hash, int& probe,
|
||||
int displacement);
|
||||
|
||||
// Number of hash bits (upper because lower bits used for sharding)
|
||||
// used for table index. Length == 1 << length_bits_
|
||||
int FindAvailableSlot(const Slice& key, int& probe, int displacement);
|
||||
|
||||
int FindVisibleElementOrAvailableSlot(const Slice& key, uint32_t hash,
|
||||
int& probe, int displacement);
|
||||
|
||||
// Returns the index of the first slot probed (hashing with
|
||||
// the given key) with a handle e such that cond(e) is true.
|
||||
// Otherwise, if no match is found, returns -1.
|
||||
// For every handle e probed except the final slot, updates
|
||||
// e->displacements += displacement.
|
||||
// The argument probe is modified such that consecutive calls
|
||||
// to FindSlot continue probing right after where the previous
|
||||
// call left.
|
||||
int FindSlot(const Slice& key, std::function<bool(LRUHandle*)> cond,
|
||||
int& probe, int displacement);
|
||||
|
||||
// Number of hash bits used for table index.
|
||||
// The size of the table is 1 << length_bits_.
|
||||
int length_bits_;
|
||||
|
||||
// The table consists of an array of buckets where each bucket is
|
||||
// a linked list of cache entries that hash into the bucket.
|
||||
std::unique_ptr<LRUHandle*[]> list_;
|
||||
const uint32_t length_bits_mask_;
|
||||
|
||||
// Number of elements in the table.
|
||||
uint32_t occupancy_;
|
||||
|
||||
// Maximum number of elements the user can store in the table.
|
||||
uint32_t occupancy_limit_;
|
||||
|
||||
std::unique_ptr<LRUHandle[]> array_;
|
||||
};
|
||||
|
||||
// A single shard of sharded cache.
|
||||
@@ -173,6 +332,10 @@ class ALIGN_AS(CACHE_LINE_SIZE) LRUCacheShard final : public CacheShard {
|
||||
void SetStrictCapacityLimit(bool strict_capacity_limit) override;
|
||||
|
||||
// Like Cache methods, but with an extra "hash" parameter.
|
||||
// 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 Insert(const Slice& key, uint32_t hash, void* value, size_t charge,
|
||||
Cache::DeleterFn deleter, Cache::Handle** handle,
|
||||
Cache::Priority priority) override;
|
||||
@@ -217,26 +380,26 @@ class ALIGN_AS(CACHE_LINE_SIZE) LRUCacheShard final : public CacheShard {
|
||||
|
||||
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, Cache::Handle** handle,
|
||||
bool free_handle_on_fail);
|
||||
friend class FastLRUCacheTest;
|
||||
|
||||
void LRU_Remove(LRUHandle* e);
|
||||
void LRU_Insert(LRUHandle* e);
|
||||
|
||||
// Free some space following strict LRU policy until enough space
|
||||
// to hold (usage_ + charge) is freed or the lru list is empty
|
||||
// to hold (usage_ + charge) is freed or the LRU list is empty
|
||||
// This function is not thread safe - it needs to be executed while
|
||||
// holding the mutex_.
|
||||
void EvictFromLRU(size_t charge, autovector<LRUHandle*>* deleted);
|
||||
void EvictFromLRU(size_t charge, autovector<LRUHandle>* deleted);
|
||||
|
||||
// Returns the number of bits used to hash an element in the per-shard
|
||||
// Returns the charge of a single handle.
|
||||
static size_t CalcEstimatedHandleCharge(
|
||||
size_t estimated_value_size,
|
||||
CacheMetadataChargePolicy metadata_charge_policy);
|
||||
|
||||
// Returns the number of bits used to hash an element in the hash
|
||||
// table.
|
||||
static int GetHashBits(size_t capacity, size_t estimated_value_size,
|
||||
CacheMetadataChargePolicy metadata_charge_policy);
|
||||
static int CalcHashBits(size_t capacity, size_t estimated_value_size,
|
||||
CacheMetadataChargePolicy metadata_charge_policy);
|
||||
|
||||
// Initialized before use.
|
||||
size_t capacity_;
|
||||
|
||||
Vendored
+12
-1
@@ -332,7 +332,7 @@ Status LRUCacheShard::InsertItem(LRUHandle* e, Cache::Handle** handle,
|
||||
delete[] reinterpret_cast<char*>(e);
|
||||
*handle = nullptr;
|
||||
}
|
||||
s = Status::Incomplete("Insert failed due to LRU cache being full.");
|
||||
s = Status::MemoryLimit("Insert failed due to LRU cache being full.");
|
||||
}
|
||||
} else {
|
||||
// Insert into the cache. Note that the cache might get larger than its
|
||||
@@ -757,6 +757,17 @@ void LRUCache::WaitAll(std::vector<Handle*>& handles) {
|
||||
}
|
||||
}
|
||||
|
||||
std::string LRUCache::GetPrintableOptions() const {
|
||||
std::string ret;
|
||||
ret.reserve(20000);
|
||||
ret.append(ShardedCache::GetPrintableOptions());
|
||||
if (secondary_cache_) {
|
||||
ret.append(" secondary_cache:\n");
|
||||
ret.append(secondary_cache_->GetPrintableOptions());
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
} // namespace lru_cache
|
||||
|
||||
std::shared_ptr<Cache> NewLRUCache(
|
||||
|
||||
Vendored
+1
@@ -482,6 +482,7 @@ class LRUCache
|
||||
virtual DeleterFn GetDeleter(Handle* handle) const override;
|
||||
virtual void DisownData() override;
|
||||
virtual void WaitAll(std::vector<Handle*>& handles) override;
|
||||
std::string GetPrintableOptions() const override;
|
||||
|
||||
// Retrieves number of elements in LRU, for unit test purpose only.
|
||||
size_t TEST_GetLRUSize();
|
||||
|
||||
Vendored
+225
-2
@@ -9,6 +9,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "cache/cache_key.h"
|
||||
#include "cache/clock_cache.h"
|
||||
#include "cache/fast_lru_cache.h"
|
||||
#include "db/db_test_util.h"
|
||||
#include "file/sst_file_manager_impl.h"
|
||||
@@ -206,8 +207,9 @@ TEST_F(LRUCacheTest, EntriesWithPriority) {
|
||||
ValidateLRUList({"e", "f", "g", "Z", "d"}, 2);
|
||||
}
|
||||
|
||||
// TODO(guido) Consolidate the following FastLRUCache tests with
|
||||
// that of LRUCache.
|
||||
namespace fast_lru_cache {
|
||||
|
||||
// TODO(guido) Replicate LRU policy tests from LRUCache here.
|
||||
class FastLRUCacheTest : public testing::Test {
|
||||
public:
|
||||
FastLRUCacheTest() {}
|
||||
@@ -238,6 +240,37 @@ class FastLRUCacheTest : public testing::Test {
|
||||
|
||||
Status Insert(char key, size_t len) { return Insert(std::string(len, key)); }
|
||||
|
||||
size_t CalcEstimatedHandleChargeWrapper(
|
||||
size_t estimated_value_size,
|
||||
CacheMetadataChargePolicy metadata_charge_policy) {
|
||||
return fast_lru_cache::LRUCacheShard::CalcEstimatedHandleCharge(
|
||||
estimated_value_size, metadata_charge_policy);
|
||||
}
|
||||
|
||||
int CalcHashBitsWrapper(size_t capacity, size_t estimated_value_size,
|
||||
CacheMetadataChargePolicy metadata_charge_policy) {
|
||||
return fast_lru_cache::LRUCacheShard::CalcHashBits(
|
||||
capacity, estimated_value_size, metadata_charge_policy);
|
||||
}
|
||||
|
||||
// Maximum number of items that a shard can hold.
|
||||
double CalcMaxOccupancy(size_t capacity, size_t estimated_value_size,
|
||||
CacheMetadataChargePolicy metadata_charge_policy) {
|
||||
size_t handle_charge =
|
||||
fast_lru_cache::LRUCacheShard::CalcEstimatedHandleCharge(
|
||||
estimated_value_size, metadata_charge_policy);
|
||||
return capacity / (fast_lru_cache::kLoadFactor * handle_charge);
|
||||
}
|
||||
|
||||
bool TableSizeIsAppropriate(int hash_bits, double max_occupancy) {
|
||||
if (hash_bits == 0) {
|
||||
return max_occupancy <= 1;
|
||||
} else {
|
||||
return (1 << hash_bits >= max_occupancy) &&
|
||||
(1 << (hash_bits - 1) <= max_occupancy);
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
fast_lru_cache::LRUCacheShard* cache_ = nullptr;
|
||||
};
|
||||
@@ -253,6 +286,196 @@ TEST_F(FastLRUCacheTest, ValidateKeySize) {
|
||||
EXPECT_NOK(Insert('f', 0));
|
||||
}
|
||||
|
||||
TEST_F(FastLRUCacheTest, CalcHashBitsTest) {
|
||||
size_t capacity;
|
||||
size_t estimated_value_size;
|
||||
double max_occupancy;
|
||||
int hash_bits;
|
||||
CacheMetadataChargePolicy metadata_charge_policy;
|
||||
|
||||
// Vary the cache capacity, fix the element charge.
|
||||
for (int i = 0; i < 2048; i++) {
|
||||
capacity = i;
|
||||
estimated_value_size = 0;
|
||||
metadata_charge_policy = kFullChargeCacheMetadata;
|
||||
max_occupancy = CalcMaxOccupancy(capacity, estimated_value_size,
|
||||
metadata_charge_policy);
|
||||
hash_bits = CalcHashBitsWrapper(capacity, estimated_value_size,
|
||||
metadata_charge_policy);
|
||||
EXPECT_TRUE(TableSizeIsAppropriate(hash_bits, max_occupancy));
|
||||
}
|
||||
|
||||
// Fix the cache capacity, vary the element charge.
|
||||
for (int i = 0; i < 1024; i++) {
|
||||
capacity = 1024;
|
||||
estimated_value_size = i;
|
||||
metadata_charge_policy = kFullChargeCacheMetadata;
|
||||
max_occupancy = CalcMaxOccupancy(capacity, estimated_value_size,
|
||||
metadata_charge_policy);
|
||||
hash_bits = CalcHashBitsWrapper(capacity, estimated_value_size,
|
||||
metadata_charge_policy);
|
||||
EXPECT_TRUE(TableSizeIsAppropriate(hash_bits, max_occupancy));
|
||||
}
|
||||
|
||||
// Zero-capacity cache, and only values have charge.
|
||||
capacity = 0;
|
||||
estimated_value_size = 1;
|
||||
metadata_charge_policy = kDontChargeCacheMetadata;
|
||||
hash_bits = CalcHashBitsWrapper(capacity, estimated_value_size,
|
||||
metadata_charge_policy);
|
||||
EXPECT_TRUE(TableSizeIsAppropriate(hash_bits, 0 /* max_occupancy */));
|
||||
|
||||
// Zero-capacity cache, and only metadata has charge.
|
||||
capacity = 0;
|
||||
estimated_value_size = 0;
|
||||
metadata_charge_policy = kFullChargeCacheMetadata;
|
||||
hash_bits = CalcHashBitsWrapper(capacity, estimated_value_size,
|
||||
metadata_charge_policy);
|
||||
EXPECT_TRUE(TableSizeIsAppropriate(hash_bits, 0 /* max_occupancy */));
|
||||
|
||||
// Small cache, large elements.
|
||||
capacity = 1024;
|
||||
estimated_value_size = 8192;
|
||||
metadata_charge_policy = kFullChargeCacheMetadata;
|
||||
hash_bits = CalcHashBitsWrapper(capacity, estimated_value_size,
|
||||
metadata_charge_policy);
|
||||
EXPECT_TRUE(TableSizeIsAppropriate(hash_bits, 0 /* max_occupancy */));
|
||||
|
||||
// Large capacity.
|
||||
capacity = 31924172;
|
||||
estimated_value_size = 8192;
|
||||
metadata_charge_policy = kFullChargeCacheMetadata;
|
||||
max_occupancy =
|
||||
CalcMaxOccupancy(capacity, estimated_value_size, metadata_charge_policy);
|
||||
hash_bits = CalcHashBitsWrapper(capacity, estimated_value_size,
|
||||
metadata_charge_policy);
|
||||
EXPECT_TRUE(TableSizeIsAppropriate(hash_bits, max_occupancy));
|
||||
}
|
||||
|
||||
} // namespace fast_lru_cache
|
||||
|
||||
namespace clock_cache {
|
||||
|
||||
class ClockCacheTest : public testing::Test {
|
||||
public:
|
||||
ClockCacheTest() {}
|
||||
~ClockCacheTest() override { DeleteShard(); }
|
||||
|
||||
void DeleteShard() {
|
||||
if (shard_ != nullptr) {
|
||||
shard_->~ClockCacheShard();
|
||||
port::cacheline_aligned_free(shard_);
|
||||
shard_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void NewShard(size_t capacity) {
|
||||
DeleteShard();
|
||||
shard_ = reinterpret_cast<ClockCacheShard*>(
|
||||
port::cacheline_aligned_alloc(sizeof(ClockCacheShard)));
|
||||
new (shard_) ClockCacheShard(capacity, 1, true /*strict_capacity_limit*/,
|
||||
kDontChargeCacheMetadata);
|
||||
}
|
||||
|
||||
Status Insert(const std::string& key,
|
||||
Cache::Priority priority = Cache::Priority::LOW) {
|
||||
return shard_->Insert(key, 0 /*hash*/, nullptr /*value*/, 1 /*charge*/,
|
||||
nullptr /*deleter*/, nullptr /*handle*/, priority);
|
||||
}
|
||||
|
||||
Status Insert(char key, Cache::Priority priority = Cache::Priority::LOW) {
|
||||
return Insert(std::string(kCacheKeySize, key), priority);
|
||||
}
|
||||
|
||||
Status Insert(char key, size_t len) { return Insert(std::string(len, key)); }
|
||||
|
||||
bool Lookup(const std::string& key) {
|
||||
auto handle = shard_->Lookup(key, 0 /*hash*/);
|
||||
if (handle) {
|
||||
shard_->Release(handle);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Lookup(char key) { return Lookup(std::string(kCacheKeySize, key)); }
|
||||
|
||||
void Erase(const std::string& key) { shard_->Erase(key, 0 /*hash*/); }
|
||||
|
||||
// void ValidateLRUList(std::vector<std::string> keys,
|
||||
// size_t num_high_pri_pool_keys = 0) {
|
||||
// LRUHandle* lru;
|
||||
// LRUHandle* lru_low_pri;
|
||||
// cache_->TEST_GetLRUList(&lru, &lru_low_pri);
|
||||
// LRUHandle* iter = lru;
|
||||
// bool in_high_pri_pool = false;
|
||||
// size_t high_pri_pool_keys = 0;
|
||||
// if (iter == lru_low_pri) {
|
||||
// in_high_pri_pool = true;
|
||||
// }
|
||||
// for (const auto& key : keys) {
|
||||
// iter = iter->next;
|
||||
// ASSERT_NE(lru, iter);
|
||||
// ASSERT_EQ(key, iter->key().ToString());
|
||||
// ASSERT_EQ(in_high_pri_pool, iter->InHighPriPool());
|
||||
// if (in_high_pri_pool) {
|
||||
// high_pri_pool_keys++;
|
||||
// }
|
||||
// if (iter == lru_low_pri) {
|
||||
// ASSERT_FALSE(in_high_pri_pool);
|
||||
// in_high_pri_pool = true;
|
||||
// }
|
||||
// }
|
||||
// ASSERT_EQ(lru, iter->next);
|
||||
// ASSERT_TRUE(in_high_pri_pool);
|
||||
// ASSERT_EQ(num_high_pri_pool_keys, high_pri_pool_keys);
|
||||
// }
|
||||
|
||||
private:
|
||||
clock_cache::ClockCacheShard* shard_ = nullptr;
|
||||
};
|
||||
|
||||
TEST_F(ClockCacheTest, Validate) {
|
||||
NewShard(3);
|
||||
EXPECT_OK(Insert('a', 16));
|
||||
EXPECT_NOK(Insert('b', 15));
|
||||
EXPECT_OK(Insert('b', 16));
|
||||
EXPECT_NOK(Insert('c', 17));
|
||||
EXPECT_NOK(Insert('d', 1000));
|
||||
EXPECT_NOK(Insert('e', 11));
|
||||
EXPECT_NOK(Insert('f', 0));
|
||||
}
|
||||
|
||||
TEST_F(ClockCacheTest, ClockPriorityTest) {
|
||||
clock_cache::ClockHandle handle;
|
||||
EXPECT_EQ(handle.GetClockPriority(),
|
||||
clock_cache::ClockHandle::ClockPriority::NONE);
|
||||
handle.SetClockPriority(clock_cache::ClockHandle::ClockPriority::HIGH);
|
||||
EXPECT_EQ(handle.GetClockPriority(),
|
||||
clock_cache::ClockHandle::ClockPriority::HIGH);
|
||||
handle.DecreaseClockPriority();
|
||||
EXPECT_EQ(handle.GetClockPriority(),
|
||||
clock_cache::ClockHandle::ClockPriority::MEDIUM);
|
||||
handle.DecreaseClockPriority();
|
||||
EXPECT_EQ(handle.GetClockPriority(),
|
||||
clock_cache::ClockHandle::ClockPriority::LOW);
|
||||
handle.SetClockPriority(clock_cache::ClockHandle::ClockPriority::MEDIUM);
|
||||
EXPECT_EQ(handle.GetClockPriority(),
|
||||
clock_cache::ClockHandle::ClockPriority::MEDIUM);
|
||||
handle.SetClockPriority(clock_cache::ClockHandle::ClockPriority::NONE);
|
||||
EXPECT_EQ(handle.GetClockPriority(),
|
||||
clock_cache::ClockHandle::ClockPriority::NONE);
|
||||
handle.SetClockPriority(clock_cache::ClockHandle::ClockPriority::MEDIUM);
|
||||
EXPECT_EQ(handle.GetClockPriority(),
|
||||
clock_cache::ClockHandle::ClockPriority::MEDIUM);
|
||||
handle.DecreaseClockPriority();
|
||||
handle.DecreaseClockPriority();
|
||||
EXPECT_EQ(handle.GetClockPriority(),
|
||||
clock_cache::ClockHandle::ClockPriority::NONE);
|
||||
}
|
||||
|
||||
} // namespace clock_cache
|
||||
|
||||
class TestSecondaryCache : public SecondaryCache {
|
||||
public:
|
||||
// Specifies what action to take on a lookup for a particular key
|
||||
|
||||
@@ -254,7 +254,7 @@ TEST_F(BlobFileCacheTest, GetBlobFileReader_CacheFull) {
|
||||
CacheHandleGuard<BlobFileReader> reader;
|
||||
|
||||
ASSERT_TRUE(blob_file_cache.GetBlobFileReader(blob_file_number, &reader)
|
||||
.IsIncomplete());
|
||||
.IsMemoryLimit());
|
||||
ASSERT_EQ(reader.GetValue(), nullptr);
|
||||
ASSERT_EQ(options.statistics->getTickerCount(NO_FILE_OPENS), 1);
|
||||
ASSERT_EQ(options.statistics->getTickerCount(NO_FILE_ERRORS), 1);
|
||||
|
||||
+78
-61
@@ -16,6 +16,7 @@
|
||||
#include "rocksdb/file_system.h"
|
||||
#include "rocksdb/slice.h"
|
||||
#include "rocksdb/status.h"
|
||||
#include "table/multiget_context.h"
|
||||
#include "test_util/sync_point.h"
|
||||
#include "util/compression.h"
|
||||
#include "util/crc32c.h"
|
||||
@@ -335,7 +336,9 @@ Status BlobFileReader::GetBlob(const ReadOptions& read_options,
|
||||
|
||||
if (!prefetched) {
|
||||
TEST_SYNC_POINT("BlobFileReader::GetBlob:ReadFromFile");
|
||||
|
||||
PERF_COUNTER_ADD(blob_read_count, 1);
|
||||
PERF_COUNTER_ADD(blob_read_byte, record_size);
|
||||
PERF_TIMER_GUARD(blob_read_time);
|
||||
const Status s = ReadFromFile(file_reader_.get(), record_offset,
|
||||
static_cast<size_t>(record_size), statistics_,
|
||||
&record_slice, &buf, &aligned_buf,
|
||||
@@ -372,40 +375,50 @@ Status BlobFileReader::GetBlob(const ReadOptions& read_options,
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
void BlobFileReader::MultiGetBlob(
|
||||
const ReadOptions& read_options,
|
||||
const autovector<std::reference_wrapper<const Slice>>& user_keys,
|
||||
const autovector<uint64_t>& offsets,
|
||||
const autovector<uint64_t>& value_sizes, autovector<Status*>& statuses,
|
||||
autovector<PinnableSlice*>& values, uint64_t* bytes_read) const {
|
||||
const size_t num_blobs = user_keys.size();
|
||||
void BlobFileReader::MultiGetBlob(const ReadOptions& read_options,
|
||||
autovector<BlobReadRequest*>& blob_reqs,
|
||||
uint64_t* bytes_read) const {
|
||||
const size_t num_blobs = blob_reqs.size();
|
||||
assert(num_blobs > 0);
|
||||
assert(num_blobs == offsets.size());
|
||||
assert(num_blobs == value_sizes.size());
|
||||
assert(num_blobs == statuses.size());
|
||||
assert(num_blobs == values.size());
|
||||
assert(num_blobs <= MultiGetContext::MAX_BATCH_SIZE);
|
||||
|
||||
#ifndef NDEBUG
|
||||
for (size_t i = 0; i < offsets.size() - 1; ++i) {
|
||||
assert(offsets[i] <= offsets[i + 1]);
|
||||
for (size_t i = 0; i < num_blobs - 1; ++i) {
|
||||
assert(blob_reqs[i]->offset <= blob_reqs[i + 1]->offset);
|
||||
}
|
||||
#endif // !NDEBUG
|
||||
|
||||
std::vector<FSReadRequest> read_reqs(num_blobs);
|
||||
std::vector<FSReadRequest> read_reqs;
|
||||
autovector<uint64_t> adjustments;
|
||||
uint64_t total_len = 0;
|
||||
read_reqs.reserve(num_blobs);
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
const size_t key_size = user_keys[i].get().size();
|
||||
assert(IsValidBlobOffset(offsets[i], key_size, value_sizes[i], file_size_));
|
||||
const size_t key_size = blob_reqs[i]->user_key->size();
|
||||
const uint64_t offset = blob_reqs[i]->offset;
|
||||
const uint64_t value_size = blob_reqs[i]->len;
|
||||
|
||||
if (!IsValidBlobOffset(offset, key_size, value_size, file_size_)) {
|
||||
*blob_reqs[i]->status = Status::Corruption("Invalid blob offset");
|
||||
continue;
|
||||
}
|
||||
if (blob_reqs[i]->compression != compression_type_) {
|
||||
*blob_reqs[i]->status =
|
||||
Status::Corruption("Compression type mismatch when reading a blob");
|
||||
continue;
|
||||
}
|
||||
|
||||
const uint64_t adjustment =
|
||||
read_options.verify_checksums
|
||||
? BlobLogRecord::CalculateAdjustmentForRecordHeader(key_size)
|
||||
: 0;
|
||||
assert(offsets[i] >= adjustment);
|
||||
assert(blob_reqs[i]->offset >= adjustment);
|
||||
adjustments.push_back(adjustment);
|
||||
read_reqs[i].offset = offsets[i] - adjustment;
|
||||
read_reqs[i].len = value_sizes[i] + adjustment;
|
||||
total_len += read_reqs[i].len;
|
||||
|
||||
FSReadRequest read_req;
|
||||
read_req.offset = blob_reqs[i]->offset - adjustment;
|
||||
read_req.len = blob_reqs[i]->len + adjustment;
|
||||
read_reqs.emplace_back(read_req);
|
||||
total_len += read_req.len;
|
||||
}
|
||||
|
||||
RecordTick(statistics_, BLOB_DB_BLOB_FILE_BYTES_READ, total_len);
|
||||
@@ -428,6 +441,8 @@ void BlobFileReader::MultiGetBlob(
|
||||
}
|
||||
}
|
||||
TEST_SYNC_POINT("BlobFileReader::MultiGetBlob:ReadFromFile");
|
||||
PERF_COUNTER_ADD(blob_read_count, num_blobs);
|
||||
PERF_COUNTER_ADD(blob_read_byte, total_len);
|
||||
s = file_reader_->MultiRead(IOOptions(), read_reqs.data(), read_reqs.size(),
|
||||
direct_io ? &aligned_buf : nullptr,
|
||||
read_options.rate_limiter_priority);
|
||||
@@ -435,64 +450,65 @@ void BlobFileReader::MultiGetBlob(
|
||||
for (auto& req : read_reqs) {
|
||||
req.status.PermitUncheckedError();
|
||||
}
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
assert(statuses[i]);
|
||||
*statuses[i] = s;
|
||||
for (auto& req : blob_reqs) {
|
||||
assert(req->status);
|
||||
if (!req->status->IsCorruption()) {
|
||||
// Avoid overwriting corruption status.
|
||||
*req->status = s;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
assert(s.ok());
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
auto& req = read_reqs[i];
|
||||
assert(statuses[i]);
|
||||
if (req.status.ok() && req.result.size() != req.len) {
|
||||
req.status = IOStatus::Corruption("Failed to read data from blob file");
|
||||
}
|
||||
*statuses[i] = req.status;
|
||||
}
|
||||
|
||||
if (read_options.verify_checksums) {
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
assert(statuses[i]);
|
||||
if (!statuses[i]->ok()) {
|
||||
continue;
|
||||
}
|
||||
const Slice& record_slice = read_reqs[i].result;
|
||||
s = VerifyBlob(record_slice, user_keys[i], value_sizes[i]);
|
||||
if (!s.ok()) {
|
||||
assert(statuses[i]);
|
||||
*statuses[i] = s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
assert(statuses[i]);
|
||||
if (!statuses[i]->ok()) {
|
||||
uint64_t total_bytes = 0;
|
||||
for (size_t i = 0, j = 0; i < num_blobs; ++i) {
|
||||
assert(blob_reqs[i]->status);
|
||||
if (!blob_reqs[i]->status->ok()) {
|
||||
continue;
|
||||
}
|
||||
const Slice& record_slice = read_reqs[i].result;
|
||||
const Slice value_slice(record_slice.data() + adjustments[i],
|
||||
value_sizes[i]);
|
||||
s = UncompressBlobIfNeeded(value_slice, compression_type_, clock_,
|
||||
statistics_, values[i]);
|
||||
if (!s.ok()) {
|
||||
*statuses[i] = s;
|
||||
|
||||
assert(j < read_reqs.size());
|
||||
auto& req = read_reqs[j++];
|
||||
const auto& record_slice = req.result;
|
||||
if (req.status.ok() && record_slice.size() != req.len) {
|
||||
req.status = IOStatus::Corruption("Failed to read data from blob file");
|
||||
}
|
||||
|
||||
*blob_reqs[i]->status = req.status;
|
||||
if (!blob_reqs[i]->status->ok()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Verify checksums if enabled
|
||||
if (read_options.verify_checksums) {
|
||||
*blob_reqs[i]->status =
|
||||
VerifyBlob(record_slice, *blob_reqs[i]->user_key, blob_reqs[i]->len);
|
||||
if (!blob_reqs[i]->status->ok()) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// Uncompress blob if needed
|
||||
Slice value_slice(record_slice.data() + adjustments[i], blob_reqs[i]->len);
|
||||
*blob_reqs[i]->status =
|
||||
UncompressBlobIfNeeded(value_slice, compression_type_, clock_,
|
||||
statistics_, blob_reqs[i]->result);
|
||||
if (blob_reqs[i]->status->ok()) {
|
||||
total_bytes += record_slice.size();
|
||||
}
|
||||
}
|
||||
|
||||
if (bytes_read) {
|
||||
uint64_t total_bytes = 0;
|
||||
for (const auto& req : read_reqs) {
|
||||
total_bytes += req.result.size();
|
||||
}
|
||||
*bytes_read = total_bytes;
|
||||
}
|
||||
}
|
||||
|
||||
Status BlobFileReader::VerifyBlob(const Slice& record_slice,
|
||||
const Slice& user_key, uint64_t value_size) {
|
||||
PERF_TIMER_GUARD(blob_checksum_time);
|
||||
|
||||
BlobLogRecord record;
|
||||
|
||||
const Slice header_slice(record_slice.data(), BlobLogRecord::kHeaderSize);
|
||||
@@ -557,6 +573,7 @@ Status BlobFileReader::UncompressBlobIfNeeded(const Slice& value_slice,
|
||||
CacheAllocationPtr output;
|
||||
|
||||
{
|
||||
PERF_TIMER_GUARD(blob_decompress_time);
|
||||
StopWatch stop_watch(clock, statistics, BLOB_DB_DECOMPRESSION_MICROS);
|
||||
output = UncompressData(info, value_slice.data(), value_slice.size(),
|
||||
&uncompressed_size, compression_format_version,
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <cinttypes>
|
||||
#include <memory>
|
||||
|
||||
#include "db/blob/blob_read_request.h"
|
||||
#include "file/random_access_file_reader.h"
|
||||
#include "rocksdb/compression_type.h"
|
||||
#include "rocksdb/rocksdb_namespace.h"
|
||||
@@ -47,12 +48,9 @@ class BlobFileReader {
|
||||
uint64_t* bytes_read) const;
|
||||
|
||||
// offsets must be sorted in ascending order by caller.
|
||||
void MultiGetBlob(
|
||||
const ReadOptions& read_options,
|
||||
const autovector<std::reference_wrapper<const Slice>>& user_keys,
|
||||
const autovector<uint64_t>& offsets,
|
||||
const autovector<uint64_t>& value_sizes, autovector<Status*>& statuses,
|
||||
autovector<PinnableSlice*>& values, uint64_t* bytes_read) const;
|
||||
void MultiGetBlob(const ReadOptions& read_options,
|
||||
autovector<BlobReadRequest*>& blob_reqs,
|
||||
uint64_t* bytes_read) const;
|
||||
|
||||
CompressionType GetCompressionType() const { return compression_type_; }
|
||||
|
||||
|
||||
@@ -194,21 +194,21 @@ TEST_F(BlobFileReaderTest, CreateReaderAndGetBlob) {
|
||||
// MultiGetBlob
|
||||
bytes_read = 0;
|
||||
size_t total_size = 0;
|
||||
autovector<std::reference_wrapper<const Slice>> key_refs;
|
||||
for (const auto& key_ref : keys) {
|
||||
key_refs.emplace_back(std::cref(key_ref));
|
||||
}
|
||||
autovector<uint64_t> offsets{blob_offsets[0], blob_offsets[1],
|
||||
blob_offsets[2]};
|
||||
autovector<uint64_t> sizes{blob_sizes[0], blob_sizes[1], blob_sizes[2]};
|
||||
|
||||
std::array<Status, num_blobs> statuses_buf;
|
||||
autovector<Status*> statuses{&statuses_buf[0], &statuses_buf[1],
|
||||
&statuses_buf[2]};
|
||||
std::array<PinnableSlice, num_blobs> value_buf;
|
||||
autovector<PinnableSlice*> values{&value_buf[0], &value_buf[1],
|
||||
&value_buf[2]};
|
||||
reader->MultiGetBlob(read_options, key_refs, offsets, sizes, statuses,
|
||||
values, &bytes_read);
|
||||
std::array<BlobReadRequest, num_blobs> requests_buf;
|
||||
autovector<BlobReadRequest*> blob_reqs;
|
||||
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
requests_buf[i] =
|
||||
BlobReadRequest(keys[i], blob_offsets[i], blob_sizes[i],
|
||||
kNoCompression, &value_buf[i], &statuses_buf[i]);
|
||||
blob_reqs.push_back(&requests_buf[i]);
|
||||
}
|
||||
|
||||
reader->MultiGetBlob(read_options, blob_reqs, &bytes_read);
|
||||
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
ASSERT_OK(statuses_buf[i]);
|
||||
ASSERT_EQ(value_buf[i], blobs[i]);
|
||||
@@ -300,15 +300,21 @@ TEST_F(BlobFileReaderTest, CreateReaderAndGetBlob) {
|
||||
blob_offsets[0],
|
||||
blob_offsets[1] - (keys[1].size() - key_refs[1].get().size()),
|
||||
blob_offsets[2]};
|
||||
autovector<uint64_t> sizes{blob_sizes[0], blob_sizes[1], blob_sizes[2]};
|
||||
|
||||
std::array<Status, num_blobs> statuses_buf;
|
||||
autovector<Status*> statuses{&statuses_buf[0], &statuses_buf[1],
|
||||
&statuses_buf[2]};
|
||||
std::array<PinnableSlice, num_blobs> value_buf;
|
||||
autovector<PinnableSlice*> values{&value_buf[0], &value_buf[1],
|
||||
&value_buf[2]};
|
||||
reader->MultiGetBlob(read_options, key_refs, offsets, sizes, statuses,
|
||||
values, &bytes_read);
|
||||
std::array<BlobReadRequest, num_blobs> requests_buf;
|
||||
autovector<BlobReadRequest*> blob_reqs;
|
||||
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
requests_buf[i] =
|
||||
BlobReadRequest(key_refs[i], offsets[i], blob_sizes[i],
|
||||
kNoCompression, &value_buf[i], &statuses_buf[i]);
|
||||
blob_reqs.push_back(&requests_buf[i]);
|
||||
}
|
||||
|
||||
reader->MultiGetBlob(read_options, blob_reqs, &bytes_read);
|
||||
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
if (i == 1) {
|
||||
ASSERT_TRUE(statuses_buf[i].IsCorruption());
|
||||
@@ -339,17 +345,21 @@ TEST_F(BlobFileReaderTest, CreateReaderAndGetBlob) {
|
||||
Slice wrong_key_slice(incorrect_key, sizeof(incorrect_key) - 1);
|
||||
key_refs[2] = std::cref(wrong_key_slice);
|
||||
|
||||
autovector<uint64_t> offsets{blob_offsets[0], blob_offsets[1],
|
||||
blob_offsets[2]};
|
||||
autovector<uint64_t> sizes{blob_sizes[0], blob_sizes[1], blob_sizes[2]};
|
||||
std::array<Status, num_blobs> statuses_buf;
|
||||
autovector<Status*> statuses{&statuses_buf[0], &statuses_buf[1],
|
||||
&statuses_buf[2]};
|
||||
std::array<PinnableSlice, num_blobs> value_buf;
|
||||
autovector<PinnableSlice*> values{&value_buf[0], &value_buf[1],
|
||||
&value_buf[2]};
|
||||
reader->MultiGetBlob(read_options, key_refs, offsets, sizes, statuses,
|
||||
values, &bytes_read);
|
||||
std::array<BlobReadRequest, num_blobs> requests_buf;
|
||||
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
requests_buf[i] =
|
||||
BlobReadRequest(key_refs[i], blob_offsets[i], blob_sizes[i],
|
||||
kNoCompression, &value_buf[i], &statuses_buf[i]);
|
||||
}
|
||||
|
||||
autovector<BlobReadRequest*> blob_reqs = {
|
||||
&requests_buf[0], &requests_buf[1], &requests_buf[2]};
|
||||
|
||||
reader->MultiGetBlob(read_options, blob_reqs, &bytes_read);
|
||||
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
if (i == num_blobs - 1) {
|
||||
ASSERT_TRUE(statuses_buf[i].IsCorruption());
|
||||
@@ -376,17 +386,26 @@ TEST_F(BlobFileReaderTest, CreateReaderAndGetBlob) {
|
||||
for (const auto& key_ref : keys) {
|
||||
key_refs.emplace_back(std::cref(key_ref));
|
||||
}
|
||||
autovector<uint64_t> offsets{blob_offsets[0], blob_offsets[1],
|
||||
blob_offsets[2]};
|
||||
autovector<uint64_t> sizes{blob_sizes[0], blob_sizes[1] + 1, blob_sizes[2]};
|
||||
|
||||
std::array<Status, num_blobs> statuses_buf;
|
||||
autovector<Status*> statuses{&statuses_buf[0], &statuses_buf[1],
|
||||
&statuses_buf[2]};
|
||||
std::array<PinnableSlice, num_blobs> value_buf;
|
||||
autovector<PinnableSlice*> values{&value_buf[0], &value_buf[1],
|
||||
&value_buf[2]};
|
||||
reader->MultiGetBlob(read_options, key_refs, offsets, sizes, statuses,
|
||||
values, &bytes_read);
|
||||
std::array<BlobReadRequest, num_blobs> requests_buf;
|
||||
|
||||
requests_buf[0] =
|
||||
BlobReadRequest(key_refs[0], blob_offsets[0], blob_sizes[0],
|
||||
kNoCompression, &value_buf[0], &statuses_buf[0]);
|
||||
requests_buf[1] =
|
||||
BlobReadRequest(key_refs[1], blob_offsets[1], blob_sizes[1] + 1,
|
||||
kNoCompression, &value_buf[1], &statuses_buf[1]);
|
||||
requests_buf[2] =
|
||||
BlobReadRequest(key_refs[2], blob_offsets[2], blob_sizes[2],
|
||||
kNoCompression, &value_buf[2], &statuses_buf[2]);
|
||||
|
||||
autovector<BlobReadRequest*> blob_reqs = {
|
||||
&requests_buf[0], &requests_buf[1], &requests_buf[2]};
|
||||
|
||||
reader->MultiGetBlob(read_options, blob_reqs, &bytes_read);
|
||||
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
if (i != 1) {
|
||||
ASSERT_OK(statuses_buf[i]);
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
// 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 <cinttypes>
|
||||
|
||||
#include "rocksdb/compression_type.h"
|
||||
#include "rocksdb/slice.h"
|
||||
#include "rocksdb/status.h"
|
||||
#include "util/autovector.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
// A read Blob request structure for use in BlobSource::MultiGetBlob and
|
||||
// BlobFileReader::MultiGetBlob.
|
||||
struct BlobReadRequest {
|
||||
// User key to lookup the paired blob
|
||||
const Slice* user_key = nullptr;
|
||||
|
||||
// File offset in bytes
|
||||
uint64_t offset = 0;
|
||||
|
||||
// Length to read in bytes
|
||||
size_t len = 0;
|
||||
|
||||
// Blob compression type
|
||||
CompressionType compression = kNoCompression;
|
||||
|
||||
// Output parameter set by MultiGetBlob() to point to the data buffer, and
|
||||
// the number of valid bytes
|
||||
PinnableSlice* result = nullptr;
|
||||
|
||||
// Status of read
|
||||
Status* status = nullptr;
|
||||
|
||||
BlobReadRequest(const Slice& _user_key, uint64_t _offset, size_t _len,
|
||||
CompressionType _compression, PinnableSlice* _result,
|
||||
Status* _status)
|
||||
: user_key(&_user_key),
|
||||
offset(_offset),
|
||||
len(_len),
|
||||
compression(_compression),
|
||||
result(_result),
|
||||
status(_status) {}
|
||||
|
||||
BlobReadRequest() = default;
|
||||
BlobReadRequest(const BlobReadRequest& other) = default;
|
||||
BlobReadRequest& operator=(const BlobReadRequest& other) = default;
|
||||
};
|
||||
|
||||
using BlobFileReadRequests =
|
||||
std::tuple<uint64_t /* file_number */, uint64_t /* file_size */,
|
||||
autovector<BlobReadRequest>>;
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
+256
-20
@@ -9,7 +9,11 @@
|
||||
#include <string>
|
||||
|
||||
#include "db/blob/blob_file_reader.h"
|
||||
#include "db/blob/blob_log_format.h"
|
||||
#include "monitoring/statistics.h"
|
||||
#include "options/cf_options.h"
|
||||
#include "table/get_context.h"
|
||||
#include "table/multiget_context.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
@@ -26,7 +30,7 @@ BlobSource::BlobSource(const ImmutableOptions* immutable_options,
|
||||
BlobSource::~BlobSource() = default;
|
||||
|
||||
Status BlobSource::GetBlobFromCache(const Slice& cache_key,
|
||||
CachableEntry<std::string>* blob) const {
|
||||
CacheHandleGuard<std::string>* blob) const {
|
||||
assert(blob);
|
||||
assert(blob->IsEmpty());
|
||||
assert(blob_cache_);
|
||||
@@ -35,9 +39,7 @@ Status BlobSource::GetBlobFromCache(const Slice& cache_key,
|
||||
Cache::Handle* cache_handle = nullptr;
|
||||
cache_handle = GetEntryFromCache(cache_key);
|
||||
if (cache_handle != nullptr) {
|
||||
blob->SetCachedValue(
|
||||
static_cast<std::string*>(blob_cache_->Value(cache_handle)),
|
||||
blob_cache_.get(), cache_handle);
|
||||
*blob = CacheHandleGuard<std::string>(blob_cache_.get(), cache_handle);
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
@@ -47,7 +49,7 @@ Status BlobSource::GetBlobFromCache(const Slice& cache_key,
|
||||
}
|
||||
|
||||
Status BlobSource::PutBlobIntoCache(const Slice& cache_key,
|
||||
CachableEntry<std::string>* cached_blob,
|
||||
CacheHandleGuard<std::string>* cached_blob,
|
||||
PinnableSlice* blob) const {
|
||||
assert(blob);
|
||||
assert(!cache_key.empty());
|
||||
@@ -70,12 +72,45 @@ Status BlobSource::PutBlobIntoCache(const Slice& cache_key,
|
||||
priority);
|
||||
if (s.ok()) {
|
||||
assert(cache_handle != nullptr);
|
||||
cached_blob->SetCachedValue(buf, blob_cache_.get(), cache_handle);
|
||||
*cached_blob =
|
||||
CacheHandleGuard<std::string>(blob_cache_.get(), cache_handle);
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
Cache::Handle* BlobSource::GetEntryFromCache(const Slice& key) const {
|
||||
Cache::Handle* cache_handle = nullptr;
|
||||
cache_handle = blob_cache_->Lookup(key, statistics_);
|
||||
if (cache_handle != nullptr) {
|
||||
PERF_COUNTER_ADD(blob_cache_hit_count, 1);
|
||||
RecordTick(statistics_, BLOB_DB_CACHE_HIT);
|
||||
RecordTick(statistics_, BLOB_DB_CACHE_BYTES_READ,
|
||||
blob_cache_->GetUsage(cache_handle));
|
||||
} else {
|
||||
RecordTick(statistics_, BLOB_DB_CACHE_MISS);
|
||||
}
|
||||
return cache_handle;
|
||||
}
|
||||
|
||||
Status BlobSource::InsertEntryIntoCache(const Slice& key, std::string* value,
|
||||
size_t charge,
|
||||
Cache::Handle** cache_handle,
|
||||
Cache::Priority priority) const {
|
||||
const Status s =
|
||||
blob_cache_->Insert(key, value, charge, &DeleteCacheEntry<std::string>,
|
||||
cache_handle, priority);
|
||||
if (s.ok()) {
|
||||
assert(*cache_handle != nullptr);
|
||||
RecordTick(statistics_, BLOB_DB_CACHE_ADD);
|
||||
RecordTick(statistics_, BLOB_DB_CACHE_BYTES_WRITE,
|
||||
blob_cache_->GetUsage(*cache_handle));
|
||||
} else {
|
||||
RecordTick(statistics_, BLOB_DB_CACHE_ADD_FAILURES);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
Status BlobSource::GetBlob(const ReadOptions& read_options,
|
||||
const Slice& user_key, uint64_t file_number,
|
||||
uint64_t offset, uint64_t file_size,
|
||||
@@ -89,29 +124,56 @@ Status BlobSource::GetBlob(const ReadOptions& read_options,
|
||||
|
||||
const CacheKey cache_key = GetCacheKey(file_number, file_size, offset);
|
||||
|
||||
CachableEntry<std::string> blob_entry;
|
||||
CacheHandleGuard<std::string> blob_handle;
|
||||
|
||||
// First, try to get the blob from the cache
|
||||
//
|
||||
// If blob cache is enabled, we'll try to read from it.
|
||||
if (blob_cache_) {
|
||||
Slice key = cache_key.AsSlice();
|
||||
s = GetBlobFromCache(key, &blob_entry);
|
||||
if (s.ok() && blob_entry.GetValue()) {
|
||||
assert(blob_entry.GetValue()->size() == value_size);
|
||||
if (bytes_read) {
|
||||
*bytes_read = value_size;
|
||||
s = GetBlobFromCache(key, &blob_handle);
|
||||
if (s.ok() && blob_handle.GetValue()) {
|
||||
{
|
||||
value->Reset();
|
||||
// To avoid copying the cached blob into the buffer provided by the
|
||||
// application, we can simply transfer ownership of the cache handle to
|
||||
// the target PinnableSlice. This has the potential to save a lot of
|
||||
// CPU, especially with large blob values.
|
||||
value->PinSlice(
|
||||
*blob_handle.GetValue(),
|
||||
[](void* arg1, void* arg2) {
|
||||
Cache* const cache = static_cast<Cache*>(arg1);
|
||||
Cache::Handle* const handle = static_cast<Cache::Handle*>(arg2);
|
||||
cache->Release(handle);
|
||||
},
|
||||
blob_handle.GetCache(), blob_handle.GetCacheHandle());
|
||||
// Make the CacheHandleGuard relinquish ownership of the handle.
|
||||
blob_handle.TransferTo(nullptr);
|
||||
}
|
||||
|
||||
// For consistency, the size of on-disk (possibly compressed) blob record
|
||||
// is assigned to bytes_read.
|
||||
uint64_t adjustment =
|
||||
read_options.verify_checksums
|
||||
? BlobLogRecord::CalculateAdjustmentForRecordHeader(
|
||||
user_key.size())
|
||||
: 0;
|
||||
assert(offset >= adjustment);
|
||||
|
||||
uint64_t record_size = value_size + adjustment;
|
||||
if (bytes_read) {
|
||||
*bytes_read = record_size;
|
||||
}
|
||||
value->PinSelf(*blob_entry.GetValue());
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
assert(blob_entry.IsEmpty());
|
||||
assert(blob_handle.IsEmpty());
|
||||
|
||||
const bool no_io = read_options.read_tier == kBlockCacheTier;
|
||||
if (no_io) {
|
||||
return Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
|
||||
s = Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
|
||||
return s;
|
||||
}
|
||||
|
||||
// Can't find the blob from the cache. Since I/O is allowed, read from the
|
||||
@@ -129,19 +191,23 @@ Status BlobSource::GetBlob(const ReadOptions& read_options,
|
||||
return Status::Corruption("Compression type mismatch when reading blob");
|
||||
}
|
||||
|
||||
uint64_t read_size = 0;
|
||||
s = blob_file_reader.GetValue()->GetBlob(
|
||||
read_options, user_key, offset, value_size, compression_type,
|
||||
prefetch_buffer, value, bytes_read);
|
||||
prefetch_buffer, value, &read_size);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
if (bytes_read) {
|
||||
*bytes_read = read_size;
|
||||
}
|
||||
}
|
||||
|
||||
if (blob_cache_ && read_options.fill_cache) {
|
||||
// If filling cache is allowed and a cache is configured, try to put the
|
||||
// blob to the cache.
|
||||
Slice key = cache_key.AsSlice();
|
||||
s = PutBlobIntoCache(key, &blob_entry, value);
|
||||
s = PutBlobIntoCache(key, &blob_handle, value);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -151,15 +217,185 @@ Status BlobSource::GetBlob(const ReadOptions& read_options,
|
||||
return s;
|
||||
}
|
||||
|
||||
void BlobSource::MultiGetBlob(const ReadOptions& read_options,
|
||||
autovector<BlobFileReadRequests>& blob_reqs,
|
||||
uint64_t* bytes_read) {
|
||||
assert(blob_reqs.size() > 0);
|
||||
|
||||
uint64_t total_bytes_read = 0;
|
||||
uint64_t bytes_read_in_file = 0;
|
||||
|
||||
for (auto& [file_number, file_size, blob_reqs_in_file] : blob_reqs) {
|
||||
// sort blob_reqs_in_file by file offset.
|
||||
std::sort(
|
||||
blob_reqs_in_file.begin(), blob_reqs_in_file.end(),
|
||||
[](const BlobReadRequest& lhs, const BlobReadRequest& rhs) -> bool {
|
||||
return lhs.offset < rhs.offset;
|
||||
});
|
||||
|
||||
MultiGetBlobFromOneFile(read_options, file_number, file_size,
|
||||
blob_reqs_in_file, &bytes_read_in_file);
|
||||
|
||||
total_bytes_read += bytes_read_in_file;
|
||||
}
|
||||
|
||||
if (bytes_read) {
|
||||
*bytes_read = total_bytes_read;
|
||||
}
|
||||
}
|
||||
|
||||
void BlobSource::MultiGetBlobFromOneFile(const ReadOptions& read_options,
|
||||
uint64_t file_number,
|
||||
uint64_t file_size,
|
||||
autovector<BlobReadRequest>& blob_reqs,
|
||||
uint64_t* bytes_read) {
|
||||
const size_t num_blobs = blob_reqs.size();
|
||||
assert(num_blobs > 0);
|
||||
assert(num_blobs <= MultiGetContext::MAX_BATCH_SIZE);
|
||||
|
||||
#ifndef NDEBUG
|
||||
for (size_t i = 0; i < num_blobs - 1; ++i) {
|
||||
assert(blob_reqs[i].offset <= blob_reqs[i + 1].offset);
|
||||
}
|
||||
#endif // !NDEBUG
|
||||
|
||||
using Mask = uint64_t;
|
||||
Mask cache_hit_mask = 0;
|
||||
|
||||
uint64_t total_bytes = 0;
|
||||
const OffsetableCacheKey base_cache_key(db_id_, db_session_id_, file_number,
|
||||
file_size);
|
||||
|
||||
if (blob_cache_) {
|
||||
size_t cached_blob_count = 0;
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
auto& req = blob_reqs[i];
|
||||
|
||||
CacheHandleGuard<std::string> blob_handle;
|
||||
const CacheKey cache_key = base_cache_key.WithOffset(req.offset);
|
||||
const Slice key = cache_key.AsSlice();
|
||||
|
||||
const Status s = GetBlobFromCache(key, &blob_handle);
|
||||
|
||||
if (s.ok() && blob_handle.GetValue()) {
|
||||
assert(req.status);
|
||||
*req.status = s;
|
||||
|
||||
{
|
||||
req.result->Reset();
|
||||
// To avoid copying the cached blob into the buffer provided by the
|
||||
// application, we can simply transfer ownership of the cache handle
|
||||
// to the target PinnableSlice. This has the potential to save a lot
|
||||
// of CPU, especially with large blob values.
|
||||
req.result->PinSlice(
|
||||
*blob_handle.GetValue(),
|
||||
[](void* arg1, void* arg2) {
|
||||
Cache* const cache = static_cast<Cache*>(arg1);
|
||||
Cache::Handle* const handle = static_cast<Cache::Handle*>(arg2);
|
||||
cache->Release(handle);
|
||||
},
|
||||
blob_handle.GetCache(), blob_handle.GetCacheHandle());
|
||||
// Make the CacheHandleGuard relinquish ownership of the handle.
|
||||
blob_handle.TransferTo(nullptr);
|
||||
}
|
||||
|
||||
// Update the counter for the number of valid blobs read from the cache.
|
||||
++cached_blob_count;
|
||||
|
||||
// For consistency, the size of each on-disk (possibly compressed) blob
|
||||
// record is accumulated to total_bytes.
|
||||
uint64_t adjustment =
|
||||
read_options.verify_checksums
|
||||
? BlobLogRecord::CalculateAdjustmentForRecordHeader(
|
||||
req.user_key->size())
|
||||
: 0;
|
||||
assert(req.offset >= adjustment);
|
||||
total_bytes += req.len + adjustment;
|
||||
cache_hit_mask |= (Mask{1} << i); // cache hit
|
||||
}
|
||||
}
|
||||
|
||||
// All blobs were read from the cache.
|
||||
if (cached_blob_count == num_blobs) {
|
||||
if (bytes_read) {
|
||||
*bytes_read = total_bytes;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const bool no_io = read_options.read_tier == kBlockCacheTier;
|
||||
if (no_io) {
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
if (!(cache_hit_mask & (Mask{1} << i))) {
|
||||
assert(blob_reqs[i].status);
|
||||
*blob_reqs[i].status =
|
||||
Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
{
|
||||
// Find the rest of blobs from the file since I/O is allowed.
|
||||
autovector<BlobReadRequest*> _blob_reqs;
|
||||
uint64_t _bytes_read = 0;
|
||||
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
if (!(cache_hit_mask & (Mask{1} << i))) {
|
||||
_blob_reqs.push_back(&blob_reqs[i]);
|
||||
}
|
||||
}
|
||||
|
||||
CacheHandleGuard<BlobFileReader> blob_file_reader;
|
||||
Status s =
|
||||
blob_file_cache_->GetBlobFileReader(file_number, &blob_file_reader);
|
||||
if (!s.ok()) {
|
||||
for (size_t i = 0; i < _blob_reqs.size(); ++i) {
|
||||
assert(_blob_reqs[i]->status);
|
||||
*_blob_reqs[i]->status = s;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
assert(blob_file_reader.GetValue());
|
||||
|
||||
blob_file_reader.GetValue()->MultiGetBlob(read_options, _blob_reqs,
|
||||
&_bytes_read);
|
||||
|
||||
if (blob_cache_ && read_options.fill_cache) {
|
||||
// If filling cache is allowed and a cache is configured, try to put
|
||||
// the blob(s) to the cache.
|
||||
for (size_t i = 0; i < _blob_reqs.size(); ++i) {
|
||||
if (_blob_reqs[i]->status->ok()) {
|
||||
CacheHandleGuard<std::string> blob_handle;
|
||||
const CacheKey cache_key =
|
||||
base_cache_key.WithOffset(_blob_reqs[i]->offset);
|
||||
const Slice key = cache_key.AsSlice();
|
||||
s = PutBlobIntoCache(key, &blob_handle, _blob_reqs[i]->result);
|
||||
if (!s.ok()) {
|
||||
*_blob_reqs[i]->status = s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
total_bytes += _bytes_read;
|
||||
if (bytes_read) {
|
||||
*bytes_read = total_bytes;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool BlobSource::TEST_BlobInCache(uint64_t file_number, uint64_t file_size,
|
||||
uint64_t offset) const {
|
||||
const CacheKey cache_key = GetCacheKey(file_number, file_size, offset);
|
||||
const Slice key = cache_key.AsSlice();
|
||||
|
||||
CachableEntry<std::string> blob_entry;
|
||||
const Status s = GetBlobFromCache(key, &blob_entry);
|
||||
CacheHandleGuard<std::string> blob_handle;
|
||||
const Status s = GetBlobFromCache(key, &blob_handle);
|
||||
|
||||
if (s.ok() && blob_entry.GetValue() != nullptr) {
|
||||
if (s.ok() && blob_handle.GetValue() != nullptr) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
+68
-18
@@ -10,9 +10,11 @@
|
||||
#include "cache/cache_helpers.h"
|
||||
#include "cache/cache_key.h"
|
||||
#include "db/blob/blob_file_cache.h"
|
||||
#include "include/rocksdb/cache.h"
|
||||
#include "db/blob/blob_read_request.h"
|
||||
#include "rocksdb/cache.h"
|
||||
#include "rocksdb/rocksdb_namespace.h"
|
||||
#include "table/block_based/cachable_entry.h"
|
||||
#include "util/autovector.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
@@ -36,23 +38,84 @@ class BlobSource {
|
||||
|
||||
~BlobSource();
|
||||
|
||||
// Read a blob from the underlying cache or one blob file.
|
||||
//
|
||||
// If successful, returns ok and sets "*value" to the newly retrieved
|
||||
// uncompressed blob. If there was an error while fetching the blob, sets
|
||||
// "*value" to empty and returns a non-ok status.
|
||||
//
|
||||
// Note: For consistency, whether the blob is found in the cache or on disk,
|
||||
// sets "*bytes_read" to the size of on-disk (possibly compressed) blob
|
||||
// record.
|
||||
Status GetBlob(const ReadOptions& read_options, const Slice& user_key,
|
||||
uint64_t file_number, uint64_t offset, uint64_t file_size,
|
||||
uint64_t value_size, CompressionType compression_type,
|
||||
FilePrefetchBuffer* prefetch_buffer, PinnableSlice* value,
|
||||
uint64_t* bytes_read);
|
||||
|
||||
// Read multiple blobs from the underlying cache or blob file(s).
|
||||
//
|
||||
// If successful, returns ok and sets "result" in the elements of "blob_reqs"
|
||||
// to the newly retrieved uncompressed blobs. If there was an error while
|
||||
// fetching one of blobs, sets its "result" to empty and sets its
|
||||
// corresponding "status" to a non-ok status.
|
||||
//
|
||||
// Note:
|
||||
// - The main difference between this function and MultiGetBlobFromOneFile is
|
||||
// that this function can read multiple blobs from multiple blob files.
|
||||
//
|
||||
// - For consistency, whether the blob is found in the cache or on disk, sets
|
||||
// "*bytes_read" to the total size of on-disk (possibly compressed) blob
|
||||
// records.
|
||||
void MultiGetBlob(const ReadOptions& read_options,
|
||||
autovector<BlobFileReadRequests>& blob_reqs,
|
||||
uint64_t* bytes_read);
|
||||
|
||||
// Read multiple blobs from the underlying cache or one blob file.
|
||||
//
|
||||
// If successful, returns ok and sets "result" in the elements of "blob_reqs"
|
||||
// to the newly retrieved uncompressed blobs. If there was an error while
|
||||
// fetching one of blobs, sets its "result" to empty and sets its
|
||||
// corresponding "status" to a non-ok status.
|
||||
//
|
||||
// Note:
|
||||
// - The main difference between this function and MultiGetBlob is that this
|
||||
// function is only used for the case where the demanded blobs are stored in
|
||||
// one blob file. MultiGetBlob will call this function multiple times if the
|
||||
// demanded blobs are stored in multiple blob files.
|
||||
//
|
||||
// - For consistency, whether the blob is found in the cache or on disk, sets
|
||||
// "*bytes_read" to the total size of on-disk (possibly compressed) blob
|
||||
// records.
|
||||
void MultiGetBlobFromOneFile(const ReadOptions& read_options,
|
||||
uint64_t file_number, uint64_t file_size,
|
||||
autovector<BlobReadRequest>& blob_reqs,
|
||||
uint64_t* bytes_read);
|
||||
|
||||
inline Status GetBlobFileReader(
|
||||
uint64_t blob_file_number,
|
||||
CacheHandleGuard<BlobFileReader>* blob_file_reader) {
|
||||
return blob_file_cache_->GetBlobFileReader(blob_file_number,
|
||||
blob_file_reader);
|
||||
}
|
||||
|
||||
bool TEST_BlobInCache(uint64_t file_number, uint64_t file_size,
|
||||
uint64_t offset) const;
|
||||
|
||||
private:
|
||||
Status GetBlobFromCache(const Slice& cache_key,
|
||||
CachableEntry<std::string>* blob) const;
|
||||
CacheHandleGuard<std::string>* blob) const;
|
||||
|
||||
Status PutBlobIntoCache(const Slice& cache_key,
|
||||
CachableEntry<std::string>* cached_blob,
|
||||
CacheHandleGuard<std::string>* cached_blob,
|
||||
PinnableSlice* blob) const;
|
||||
|
||||
Cache::Handle* GetEntryFromCache(const Slice& key) const;
|
||||
|
||||
Status InsertEntryIntoCache(const Slice& key, std::string* value,
|
||||
size_t charge, Cache::Handle** cache_handle,
|
||||
Cache::Priority priority) const;
|
||||
|
||||
inline CacheKey GetCacheKey(uint64_t file_number, uint64_t file_size,
|
||||
uint64_t offset) const {
|
||||
OffsetableCacheKey base_cache_key(db_id_, db_session_id_, file_number,
|
||||
@@ -60,21 +123,8 @@ class BlobSource {
|
||||
return base_cache_key.WithOffset(offset);
|
||||
}
|
||||
|
||||
inline Cache::Handle* GetEntryFromCache(const Slice& key) const {
|
||||
return blob_cache_->Lookup(key, statistics_);
|
||||
}
|
||||
|
||||
inline Status InsertEntryIntoCache(const Slice& key, std::string* value,
|
||||
size_t charge,
|
||||
Cache::Handle** cache_handle,
|
||||
Cache::Priority priority) const {
|
||||
return blob_cache_->Insert(key, value, charge,
|
||||
&DeleteCacheEntry<std::string>, cache_handle,
|
||||
priority);
|
||||
}
|
||||
|
||||
const std::string db_id_;
|
||||
const std::string db_session_id_;
|
||||
const std::string& db_id_;
|
||||
const std::string& db_session_id_;
|
||||
|
||||
Statistics* statistics_;
|
||||
|
||||
|
||||
+759
-29
@@ -12,6 +12,7 @@
|
||||
#include <string>
|
||||
|
||||
#include "db/blob/blob_file_cache.h"
|
||||
#include "db/blob/blob_file_reader.h"
|
||||
#include "db/blob/blob_log_format.h"
|
||||
#include "db/blob/blob_log_writer.h"
|
||||
#include "db/db_test_util.h"
|
||||
@@ -108,32 +109,38 @@ class BlobSourceTest : public DBTestBase {
|
||||
protected:
|
||||
public:
|
||||
explicit BlobSourceTest()
|
||||
: DBTestBase("blob_source_test", /*env_do_fsync=*/true) {}
|
||||
: DBTestBase("blob_source_test", /*env_do_fsync=*/true) {
|
||||
options_.env = env_;
|
||||
options_.enable_blob_files = true;
|
||||
options_.create_if_missing = true;
|
||||
|
||||
LRUCacheOptions co;
|
||||
co.capacity = 8 << 20;
|
||||
co.num_shard_bits = 2;
|
||||
co.metadata_charge_policy = kDontChargeCacheMetadata;
|
||||
options_.blob_cache = NewLRUCache(co);
|
||||
options_.lowest_used_cache_tier = CacheTier::kVolatileTier;
|
||||
|
||||
assert(db_->GetDbIdentity(db_id_).ok());
|
||||
assert(db_->GetDbSessionId(db_session_id_).ok());
|
||||
}
|
||||
|
||||
Options options_;
|
||||
std::string db_id_;
|
||||
std::string db_session_id_;
|
||||
};
|
||||
|
||||
TEST_F(BlobSourceTest, GetBlobsFromCache) {
|
||||
Options options;
|
||||
options.env = env_;
|
||||
options.cf_paths.emplace_back(
|
||||
options_.cf_paths.emplace_back(
|
||||
test::PerThreadDBPath(env_, "BlobSourceTest_GetBlobsFromCache"), 0);
|
||||
options.enable_blob_files = true;
|
||||
|
||||
LRUCacheOptions co;
|
||||
co.capacity = 2048;
|
||||
co.num_shard_bits = 2;
|
||||
co.metadata_charge_policy = kDontChargeCacheMetadata;
|
||||
options.blob_cache = NewLRUCache(co);
|
||||
options.lowest_used_cache_tier = CacheTier::kVolatileTier;
|
||||
options_.statistics = CreateDBStatistics();
|
||||
Statistics* statistics = options_.statistics.get();
|
||||
assert(statistics);
|
||||
|
||||
Reopen(options);
|
||||
DestroyAndReopen(options_);
|
||||
|
||||
std::string db_id;
|
||||
ASSERT_OK(db_->GetDbIdentity(db_id));
|
||||
|
||||
std::string db_session_id;
|
||||
ASSERT_OK(db_->GetDbSessionId(db_session_id));
|
||||
|
||||
ImmutableOptions immutable_options(options);
|
||||
ImmutableOptions immutable_options(options_);
|
||||
|
||||
constexpr uint32_t column_family_id = 1;
|
||||
constexpr bool has_ttl = false;
|
||||
@@ -178,7 +185,7 @@ TEST_F(BlobSourceTest, GetBlobsFromCache) {
|
||||
backing_cache.get(), &immutable_options, &file_options, column_family_id,
|
||||
blob_file_read_hist, nullptr /*IOTracer*/));
|
||||
|
||||
BlobSource blob_source(&immutable_options, db_id, db_session_id,
|
||||
BlobSource blob_source(&immutable_options, db_id_, db_session_id_,
|
||||
blob_file_cache.get());
|
||||
|
||||
ReadOptions read_options;
|
||||
@@ -190,8 +197,11 @@ TEST_F(BlobSourceTest, GetBlobsFromCache) {
|
||||
// GetBlob
|
||||
std::vector<PinnableSlice> values(keys.size());
|
||||
uint64_t bytes_read = 0;
|
||||
uint64_t blob_bytes = 0;
|
||||
uint64_t total_bytes = 0;
|
||||
|
||||
read_options.fill_cache = false;
|
||||
get_perf_context()->Reset();
|
||||
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
ASSERT_FALSE(blob_source.TEST_BlobInCache(blob_file_number, file_size,
|
||||
@@ -203,13 +213,32 @@ TEST_F(BlobSourceTest, GetBlobsFromCache) {
|
||||
&bytes_read));
|
||||
ASSERT_EQ(values[i], blobs[i]);
|
||||
ASSERT_EQ(bytes_read,
|
||||
blob_sizes[i] + keys[i].size() + BlobLogRecord::kHeaderSize);
|
||||
BlobLogRecord::kHeaderSize + keys[i].size() + blob_sizes[i]);
|
||||
|
||||
ASSERT_FALSE(blob_source.TEST_BlobInCache(blob_file_number, file_size,
|
||||
blob_offsets[i]));
|
||||
total_bytes += bytes_read;
|
||||
}
|
||||
|
||||
// Retrieved the blob cache num_blobs * 3 times via TEST_BlobInCache,
|
||||
// GetBlob, and TEST_BlobInCache.
|
||||
ASSERT_EQ((int)get_perf_context()->blob_cache_hit_count, 0);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_count, num_blobs);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_byte, total_bytes);
|
||||
ASSERT_GE((int)get_perf_context()->blob_checksum_time, 0);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_decompress_time, 0);
|
||||
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_MISS), num_blobs * 3);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_HIT), 0);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_ADD), 0);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_BYTES_READ), 0);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_BYTES_WRITE), 0);
|
||||
|
||||
read_options.fill_cache = true;
|
||||
blob_bytes = 0;
|
||||
total_bytes = 0;
|
||||
get_perf_context()->Reset();
|
||||
statistics->Reset().PermitUncheckedError();
|
||||
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
ASSERT_FALSE(blob_source.TEST_BlobInCache(blob_file_number, file_size,
|
||||
@@ -221,13 +250,38 @@ TEST_F(BlobSourceTest, GetBlobsFromCache) {
|
||||
&bytes_read));
|
||||
ASSERT_EQ(values[i], blobs[i]);
|
||||
ASSERT_EQ(bytes_read,
|
||||
blob_sizes[i] + keys[i].size() + BlobLogRecord::kHeaderSize);
|
||||
BlobLogRecord::kHeaderSize + keys[i].size() + blob_sizes[i]);
|
||||
|
||||
blob_bytes += blob_sizes[i];
|
||||
total_bytes += bytes_read;
|
||||
ASSERT_EQ((int)get_perf_context()->blob_cache_hit_count, i);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_count, i + 1);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_byte, total_bytes);
|
||||
|
||||
ASSERT_TRUE(blob_source.TEST_BlobInCache(blob_file_number, file_size,
|
||||
blob_offsets[i]));
|
||||
|
||||
ASSERT_EQ((int)get_perf_context()->blob_cache_hit_count, i + 1);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_count, i + 1);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_byte, total_bytes);
|
||||
}
|
||||
|
||||
ASSERT_EQ((int)get_perf_context()->blob_cache_hit_count, num_blobs);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_count, num_blobs);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_byte, total_bytes);
|
||||
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_MISS), num_blobs * 2);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_HIT), num_blobs);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_ADD), num_blobs);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_BYTES_READ), blob_bytes);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_BYTES_WRITE),
|
||||
blob_bytes);
|
||||
|
||||
read_options.fill_cache = true;
|
||||
total_bytes = 0;
|
||||
blob_bytes = 0;
|
||||
get_perf_context()->Reset();
|
||||
statistics->Reset().PermitUncheckedError();
|
||||
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
ASSERT_TRUE(blob_source.TEST_BlobInCache(blob_file_number, file_size,
|
||||
@@ -238,14 +292,34 @@ TEST_F(BlobSourceTest, GetBlobsFromCache) {
|
||||
kNoCompression, prefetch_buffer, &values[i],
|
||||
&bytes_read));
|
||||
ASSERT_EQ(values[i], blobs[i]);
|
||||
ASSERT_EQ(bytes_read, blob_sizes[i]);
|
||||
ASSERT_EQ(bytes_read,
|
||||
BlobLogRecord::kHeaderSize + keys[i].size() + blob_sizes[i]);
|
||||
|
||||
ASSERT_TRUE(blob_source.TEST_BlobInCache(blob_file_number, file_size,
|
||||
blob_offsets[i]));
|
||||
total_bytes += bytes_read; // on-disk blob record size
|
||||
blob_bytes += blob_sizes[i]; // cached blob value size
|
||||
}
|
||||
|
||||
// Retrieved the blob cache num_blobs * 3 times via TEST_BlobInCache,
|
||||
// GetBlob, and TEST_BlobInCache.
|
||||
ASSERT_EQ((int)get_perf_context()->blob_cache_hit_count, num_blobs * 3);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_count, 0); // without i/o
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_byte, 0); // without i/o
|
||||
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_MISS), 0);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_HIT), num_blobs * 3);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_ADD), 0);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_BYTES_READ),
|
||||
blob_bytes * 3);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_BYTES_WRITE), 0);
|
||||
|
||||
// Cache-only GetBlob
|
||||
read_options.read_tier = ReadTier::kBlockCacheTier;
|
||||
total_bytes = 0;
|
||||
blob_bytes = 0;
|
||||
get_perf_context()->Reset();
|
||||
statistics->Reset().PermitUncheckedError();
|
||||
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
ASSERT_TRUE(blob_source.TEST_BlobInCache(blob_file_number, file_size,
|
||||
@@ -256,14 +330,30 @@ TEST_F(BlobSourceTest, GetBlobsFromCache) {
|
||||
kNoCompression, prefetch_buffer, &values[i],
|
||||
&bytes_read));
|
||||
ASSERT_EQ(values[i], blobs[i]);
|
||||
ASSERT_EQ(bytes_read, blob_sizes[i]);
|
||||
ASSERT_EQ(bytes_read,
|
||||
BlobLogRecord::kHeaderSize + keys[i].size() + blob_sizes[i]);
|
||||
|
||||
ASSERT_TRUE(blob_source.TEST_BlobInCache(blob_file_number, file_size,
|
||||
blob_offsets[i]));
|
||||
total_bytes += bytes_read;
|
||||
blob_bytes += blob_sizes[i];
|
||||
}
|
||||
|
||||
// Retrieved the blob cache num_blobs * 3 times via TEST_BlobInCache,
|
||||
// GetBlob, and TEST_BlobInCache.
|
||||
ASSERT_EQ((int)get_perf_context()->blob_cache_hit_count, num_blobs * 3);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_count, 0); // without i/o
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_byte, 0); // without i/o
|
||||
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_MISS), 0);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_HIT), num_blobs * 3);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_ADD), 0);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_BYTES_READ),
|
||||
blob_bytes * 3);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_BYTES_WRITE), 0);
|
||||
}
|
||||
|
||||
options.blob_cache->EraseUnRefEntries();
|
||||
options_.blob_cache->EraseUnRefEntries();
|
||||
|
||||
{
|
||||
// Cache-only GetBlob
|
||||
@@ -272,21 +362,661 @@ TEST_F(BlobSourceTest, GetBlobsFromCache) {
|
||||
|
||||
read_options.read_tier = ReadTier::kBlockCacheTier;
|
||||
read_options.fill_cache = true;
|
||||
get_perf_context()->Reset();
|
||||
statistics->Reset().PermitUncheckedError();
|
||||
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
ASSERT_FALSE(blob_source.TEST_BlobInCache(blob_file_number, file_size,
|
||||
blob_offsets[i]));
|
||||
|
||||
ASSERT_NOK(blob_source.GetBlob(read_options, keys[i], blob_file_number,
|
||||
blob_offsets[i], file_size, blob_sizes[i],
|
||||
kNoCompression, prefetch_buffer,
|
||||
&values[i], &bytes_read));
|
||||
ASSERT_TRUE(blob_source
|
||||
.GetBlob(read_options, keys[i], blob_file_number,
|
||||
blob_offsets[i], file_size, blob_sizes[i],
|
||||
kNoCompression, prefetch_buffer, &values[i],
|
||||
&bytes_read)
|
||||
.IsIncomplete());
|
||||
ASSERT_TRUE(values[i].empty());
|
||||
ASSERT_EQ(bytes_read, 0);
|
||||
|
||||
ASSERT_FALSE(blob_source.TEST_BlobInCache(blob_file_number, file_size,
|
||||
blob_offsets[i]));
|
||||
}
|
||||
|
||||
// Retrieved the blob cache num_blobs * 3 times via TEST_BlobInCache,
|
||||
// GetBlob, and TEST_BlobInCache.
|
||||
ASSERT_EQ((int)get_perf_context()->blob_cache_hit_count, 0);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_count, 0);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_byte, 0);
|
||||
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_MISS), num_blobs * 3);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_HIT), 0);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_ADD), 0);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_BYTES_READ), 0);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_BYTES_WRITE), 0);
|
||||
}
|
||||
|
||||
{
|
||||
// GetBlob from non-existing file
|
||||
std::vector<PinnableSlice> values(keys.size());
|
||||
uint64_t bytes_read = 0;
|
||||
uint64_t file_number = 100; // non-existing file
|
||||
|
||||
read_options.read_tier = ReadTier::kReadAllTier;
|
||||
read_options.fill_cache = true;
|
||||
get_perf_context()->Reset();
|
||||
statistics->Reset().PermitUncheckedError();
|
||||
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
ASSERT_FALSE(blob_source.TEST_BlobInCache(file_number, file_size,
|
||||
blob_offsets[i]));
|
||||
|
||||
ASSERT_TRUE(blob_source
|
||||
.GetBlob(read_options, keys[i], file_number,
|
||||
blob_offsets[i], file_size, blob_sizes[i],
|
||||
kNoCompression, prefetch_buffer, &values[i],
|
||||
&bytes_read)
|
||||
.IsIOError());
|
||||
ASSERT_TRUE(values[i].empty());
|
||||
ASSERT_EQ(bytes_read, 0);
|
||||
|
||||
ASSERT_FALSE(blob_source.TEST_BlobInCache(file_number, file_size,
|
||||
blob_offsets[i]));
|
||||
}
|
||||
|
||||
// Retrieved the blob cache num_blobs * 3 times via TEST_BlobInCache,
|
||||
// GetBlob, and TEST_BlobInCache.
|
||||
ASSERT_EQ((int)get_perf_context()->blob_cache_hit_count, 0);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_count, 0);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_byte, 0);
|
||||
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_MISS), num_blobs * 3);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_HIT), 0);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_ADD), 0);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_BYTES_READ), 0);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_BYTES_WRITE), 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(BlobSourceTest, GetCompressedBlobs) {
|
||||
if (!Snappy_Supported()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const CompressionType compression = kSnappyCompression;
|
||||
|
||||
options_.cf_paths.emplace_back(
|
||||
test::PerThreadDBPath(env_, "BlobSourceTest_GetCompressedBlobs"), 0);
|
||||
|
||||
DestroyAndReopen(options_);
|
||||
|
||||
ImmutableOptions immutable_options(options_);
|
||||
|
||||
constexpr uint32_t column_family_id = 1;
|
||||
constexpr bool has_ttl = false;
|
||||
constexpr ExpirationRange expiration_range;
|
||||
constexpr size_t num_blobs = 256;
|
||||
|
||||
std::vector<std::string> key_strs;
|
||||
std::vector<std::string> blob_strs;
|
||||
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
key_strs.push_back("key" + std::to_string(i));
|
||||
blob_strs.push_back("blob" + std::to_string(i));
|
||||
}
|
||||
|
||||
std::vector<Slice> keys;
|
||||
std::vector<Slice> blobs;
|
||||
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
keys.push_back({key_strs[i]});
|
||||
blobs.push_back({blob_strs[i]});
|
||||
}
|
||||
|
||||
std::vector<uint64_t> blob_offsets(keys.size());
|
||||
std::vector<uint64_t> blob_sizes(keys.size());
|
||||
|
||||
constexpr size_t capacity = 1024;
|
||||
auto backing_cache = NewLRUCache(capacity); // Blob file cache
|
||||
|
||||
FileOptions file_options;
|
||||
std::unique_ptr<BlobFileCache> blob_file_cache(new BlobFileCache(
|
||||
backing_cache.get(), &immutable_options, &file_options, column_family_id,
|
||||
nullptr /*HistogramImpl*/, nullptr /*IOTracer*/));
|
||||
|
||||
BlobSource blob_source(&immutable_options, db_id_, db_session_id_,
|
||||
blob_file_cache.get());
|
||||
|
||||
ReadOptions read_options;
|
||||
read_options.verify_checksums = true;
|
||||
|
||||
uint64_t bytes_read = 0;
|
||||
std::vector<PinnableSlice> values(keys.size());
|
||||
|
||||
{
|
||||
// Snappy Compression
|
||||
const uint64_t file_number = 1;
|
||||
|
||||
read_options.read_tier = ReadTier::kReadAllTier;
|
||||
|
||||
WriteBlobFile(immutable_options, column_family_id, has_ttl,
|
||||
expiration_range, expiration_range, file_number, keys, blobs,
|
||||
compression, blob_offsets, blob_sizes);
|
||||
|
||||
CacheHandleGuard<BlobFileReader> blob_file_reader;
|
||||
ASSERT_OK(blob_source.GetBlobFileReader(file_number, &blob_file_reader));
|
||||
ASSERT_NE(blob_file_reader.GetValue(), nullptr);
|
||||
|
||||
const uint64_t file_size = blob_file_reader.GetValue()->GetFileSize();
|
||||
ASSERT_EQ(blob_file_reader.GetValue()->GetCompressionType(), compression);
|
||||
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
ASSERT_NE(blobs[i].size() /*uncompressed size*/,
|
||||
blob_sizes[i] /*compressed size*/);
|
||||
}
|
||||
|
||||
read_options.fill_cache = true;
|
||||
read_options.read_tier = ReadTier::kReadAllTier;
|
||||
get_perf_context()->Reset();
|
||||
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
ASSERT_FALSE(blob_source.TEST_BlobInCache(file_number, file_size,
|
||||
blob_offsets[i]));
|
||||
ASSERT_OK(blob_source.GetBlob(read_options, keys[i], file_number,
|
||||
blob_offsets[i], file_size, blob_sizes[i],
|
||||
compression, nullptr /*prefetch_buffer*/,
|
||||
&values[i], &bytes_read));
|
||||
ASSERT_EQ(values[i], blobs[i] /*uncompressed blob*/);
|
||||
ASSERT_NE(values[i].size(), blob_sizes[i] /*compressed size*/);
|
||||
ASSERT_EQ(bytes_read,
|
||||
BlobLogRecord::kHeaderSize + keys[i].size() + blob_sizes[i]);
|
||||
|
||||
ASSERT_TRUE(blob_source.TEST_BlobInCache(file_number, file_size,
|
||||
blob_offsets[i]));
|
||||
}
|
||||
|
||||
ASSERT_GE((int)get_perf_context()->blob_decompress_time, 0);
|
||||
|
||||
read_options.read_tier = ReadTier::kBlockCacheTier;
|
||||
get_perf_context()->Reset();
|
||||
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
ASSERT_TRUE(blob_source.TEST_BlobInCache(file_number, file_size,
|
||||
blob_offsets[i]));
|
||||
|
||||
// Compressed blob size is passed in GetBlob
|
||||
ASSERT_OK(blob_source.GetBlob(read_options, keys[i], file_number,
|
||||
blob_offsets[i], file_size, blob_sizes[i],
|
||||
compression, nullptr /*prefetch_buffer*/,
|
||||
&values[i], &bytes_read));
|
||||
ASSERT_EQ(values[i], blobs[i] /*uncompressed blob*/);
|
||||
ASSERT_NE(values[i].size(), blob_sizes[i] /*compressed size*/);
|
||||
ASSERT_EQ(bytes_read,
|
||||
BlobLogRecord::kHeaderSize + keys[i].size() + blob_sizes[i]);
|
||||
|
||||
ASSERT_TRUE(blob_source.TEST_BlobInCache(file_number, file_size,
|
||||
blob_offsets[i]));
|
||||
}
|
||||
|
||||
ASSERT_EQ((int)get_perf_context()->blob_decompress_time, 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(BlobSourceTest, MultiGetBlobsFromMultiFiles) {
|
||||
options_.cf_paths.emplace_back(
|
||||
test::PerThreadDBPath(env_, "BlobSourceTest_MultiGetBlobsFromMultiFiles"),
|
||||
0);
|
||||
|
||||
options_.statistics = CreateDBStatistics();
|
||||
Statistics* statistics = options_.statistics.get();
|
||||
assert(statistics);
|
||||
|
||||
DestroyAndReopen(options_);
|
||||
|
||||
ImmutableOptions immutable_options(options_);
|
||||
|
||||
constexpr uint32_t column_family_id = 1;
|
||||
constexpr bool has_ttl = false;
|
||||
constexpr ExpirationRange expiration_range;
|
||||
constexpr uint64_t blob_files = 2;
|
||||
constexpr size_t num_blobs = 32;
|
||||
|
||||
std::vector<std::string> key_strs;
|
||||
std::vector<std::string> blob_strs;
|
||||
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
key_strs.push_back("key" + std::to_string(i));
|
||||
blob_strs.push_back("blob" + std::to_string(i));
|
||||
}
|
||||
|
||||
std::vector<Slice> keys;
|
||||
std::vector<Slice> blobs;
|
||||
|
||||
uint64_t file_size = BlobLogHeader::kSize;
|
||||
uint64_t blob_value_bytes = 0;
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
keys.push_back({key_strs[i]});
|
||||
blobs.push_back({blob_strs[i]});
|
||||
blob_value_bytes += blobs[i].size();
|
||||
file_size += BlobLogRecord::kHeaderSize + keys[i].size() + blobs[i].size();
|
||||
}
|
||||
file_size += BlobLogFooter::kSize;
|
||||
const uint64_t blob_records_bytes =
|
||||
file_size - BlobLogHeader::kSize - BlobLogFooter::kSize;
|
||||
|
||||
std::vector<uint64_t> blob_offsets(keys.size());
|
||||
std::vector<uint64_t> blob_sizes(keys.size());
|
||||
|
||||
{
|
||||
// Write key/blob pairs to multiple blob files.
|
||||
for (size_t i = 0; i < blob_files; ++i) {
|
||||
const uint64_t file_number = i + 1;
|
||||
WriteBlobFile(immutable_options, column_family_id, has_ttl,
|
||||
expiration_range, expiration_range, file_number, keys,
|
||||
blobs, kNoCompression, blob_offsets, blob_sizes);
|
||||
}
|
||||
}
|
||||
|
||||
constexpr size_t capacity = 10;
|
||||
std::shared_ptr<Cache> backing_cache =
|
||||
NewLRUCache(capacity); // Blob file cache
|
||||
|
||||
FileOptions file_options;
|
||||
constexpr HistogramImpl* blob_file_read_hist = nullptr;
|
||||
|
||||
std::unique_ptr<BlobFileCache> blob_file_cache(new BlobFileCache(
|
||||
backing_cache.get(), &immutable_options, &file_options, column_family_id,
|
||||
blob_file_read_hist, nullptr /*IOTracer*/));
|
||||
|
||||
BlobSource blob_source(&immutable_options, db_id_, db_session_id_,
|
||||
blob_file_cache.get());
|
||||
|
||||
ReadOptions read_options;
|
||||
read_options.verify_checksums = true;
|
||||
|
||||
uint64_t bytes_read = 0;
|
||||
|
||||
{
|
||||
// MultiGetBlob
|
||||
read_options.fill_cache = true;
|
||||
read_options.read_tier = ReadTier::kReadAllTier;
|
||||
|
||||
autovector<BlobFileReadRequests> blob_reqs;
|
||||
std::array<autovector<BlobReadRequest>, blob_files> blob_reqs_in_file;
|
||||
std::array<PinnableSlice, num_blobs * blob_files> value_buf;
|
||||
std::array<Status, num_blobs * blob_files> statuses_buf;
|
||||
|
||||
for (size_t i = 0; i < blob_files; ++i) {
|
||||
const uint64_t file_number = i + 1;
|
||||
for (size_t j = 0; j < num_blobs; ++j) {
|
||||
blob_reqs_in_file[i].emplace_back(
|
||||
keys[j], blob_offsets[j], blob_sizes[j], kNoCompression,
|
||||
&value_buf[i * num_blobs + j], &statuses_buf[i * num_blobs + j]);
|
||||
}
|
||||
blob_reqs.emplace_back(file_number, file_size, blob_reqs_in_file[i]);
|
||||
}
|
||||
|
||||
get_perf_context()->Reset();
|
||||
statistics->Reset().PermitUncheckedError();
|
||||
|
||||
blob_source.MultiGetBlob(read_options, blob_reqs, &bytes_read);
|
||||
|
||||
for (size_t i = 0; i < blob_files; ++i) {
|
||||
const uint64_t file_number = i + 1;
|
||||
for (size_t j = 0; j < num_blobs; ++j) {
|
||||
ASSERT_OK(statuses_buf[i * num_blobs + j]);
|
||||
ASSERT_EQ(value_buf[i * num_blobs + j], blobs[j]);
|
||||
ASSERT_TRUE(blob_source.TEST_BlobInCache(file_number, file_size,
|
||||
blob_offsets[j]));
|
||||
}
|
||||
}
|
||||
|
||||
// Retrieved all blobs from 2 blob files twice via MultiGetBlob and
|
||||
// TEST_BlobInCache.
|
||||
ASSERT_EQ((int)get_perf_context()->blob_cache_hit_count,
|
||||
num_blobs * blob_files);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_count,
|
||||
num_blobs * blob_files); // blocking i/o
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_byte,
|
||||
blob_records_bytes * blob_files); // blocking i/o
|
||||
ASSERT_GE((int)get_perf_context()->blob_checksum_time, 0);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_decompress_time, 0);
|
||||
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_MISS),
|
||||
num_blobs * blob_files); // MultiGetBlob
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_HIT),
|
||||
num_blobs * blob_files); // TEST_BlobInCache
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_ADD),
|
||||
num_blobs * blob_files); // MultiGetBlob
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_BYTES_READ),
|
||||
blob_value_bytes * blob_files); // TEST_BlobInCache
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_BYTES_WRITE),
|
||||
blob_value_bytes * blob_files); // MultiGetBlob
|
||||
|
||||
get_perf_context()->Reset();
|
||||
statistics->Reset().PermitUncheckedError();
|
||||
|
||||
autovector<BlobReadRequest> fake_blob_reqs_in_file;
|
||||
std::array<PinnableSlice, num_blobs> fake_value_buf;
|
||||
std::array<Status, num_blobs> fake_statuses_buf;
|
||||
|
||||
const uint64_t fake_file_number = 100;
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
fake_blob_reqs_in_file.emplace_back(
|
||||
keys[i], blob_offsets[i], blob_sizes[i], kNoCompression,
|
||||
&fake_value_buf[i], &fake_statuses_buf[i]);
|
||||
}
|
||||
|
||||
// Add a fake multi-get blob request.
|
||||
blob_reqs.emplace_back(fake_file_number, file_size, fake_blob_reqs_in_file);
|
||||
|
||||
blob_source.MultiGetBlob(read_options, blob_reqs, &bytes_read);
|
||||
|
||||
// Check the real blob read requests.
|
||||
for (size_t i = 0; i < blob_files; ++i) {
|
||||
const uint64_t file_number = i + 1;
|
||||
for (size_t j = 0; j < num_blobs; ++j) {
|
||||
ASSERT_OK(statuses_buf[i * num_blobs + j]);
|
||||
ASSERT_EQ(value_buf[i * num_blobs + j], blobs[j]);
|
||||
ASSERT_TRUE(blob_source.TEST_BlobInCache(file_number, file_size,
|
||||
blob_offsets[j]));
|
||||
}
|
||||
}
|
||||
|
||||
// Check the fake blob request.
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
ASSERT_TRUE(fake_statuses_buf[i].IsIOError());
|
||||
ASSERT_TRUE(fake_value_buf[i].empty());
|
||||
ASSERT_FALSE(blob_source.TEST_BlobInCache(fake_file_number, file_size,
|
||||
blob_offsets[i]));
|
||||
}
|
||||
|
||||
// Retrieved all blobs from 3 blob files (including the fake one) twice
|
||||
// via MultiGetBlob and TEST_BlobInCache.
|
||||
ASSERT_EQ((int)get_perf_context()->blob_cache_hit_count,
|
||||
num_blobs * blob_files * 2);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_count,
|
||||
0); // blocking i/o
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_byte,
|
||||
0); // blocking i/o
|
||||
ASSERT_GE((int)get_perf_context()->blob_checksum_time, 0);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_decompress_time, 0);
|
||||
|
||||
// Fake blob requests: MultiGetBlob and TEST_BlobInCache
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_MISS), num_blobs * 2);
|
||||
// Real blob requests: MultiGetBlob and TEST_BlobInCache
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_HIT),
|
||||
num_blobs * blob_files * 2);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_ADD), 0);
|
||||
// Real blob requests: MultiGetBlob and TEST_BlobInCache
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_BYTES_READ),
|
||||
blob_value_bytes * blob_files * 2);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_BYTES_WRITE), 0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(BlobSourceTest, MultiGetBlobsFromCache) {
|
||||
options_.cf_paths.emplace_back(
|
||||
test::PerThreadDBPath(env_, "BlobSourceTest_MultiGetBlobsFromCache"), 0);
|
||||
|
||||
options_.statistics = CreateDBStatistics();
|
||||
Statistics* statistics = options_.statistics.get();
|
||||
assert(statistics);
|
||||
|
||||
DestroyAndReopen(options_);
|
||||
|
||||
ImmutableOptions immutable_options(options_);
|
||||
|
||||
constexpr uint32_t column_family_id = 1;
|
||||
constexpr bool has_ttl = false;
|
||||
constexpr ExpirationRange expiration_range;
|
||||
constexpr uint64_t blob_file_number = 1;
|
||||
constexpr size_t num_blobs = 16;
|
||||
|
||||
std::vector<std::string> key_strs;
|
||||
std::vector<std::string> blob_strs;
|
||||
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
key_strs.push_back("key" + std::to_string(i));
|
||||
blob_strs.push_back("blob" + std::to_string(i));
|
||||
}
|
||||
|
||||
std::vector<Slice> keys;
|
||||
std::vector<Slice> blobs;
|
||||
|
||||
uint64_t file_size = BlobLogHeader::kSize;
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
keys.push_back({key_strs[i]});
|
||||
blobs.push_back({blob_strs[i]});
|
||||
file_size += BlobLogRecord::kHeaderSize + keys[i].size() + blobs[i].size();
|
||||
}
|
||||
file_size += BlobLogFooter::kSize;
|
||||
|
||||
std::vector<uint64_t> blob_offsets(keys.size());
|
||||
std::vector<uint64_t> blob_sizes(keys.size());
|
||||
|
||||
WriteBlobFile(immutable_options, column_family_id, has_ttl, expiration_range,
|
||||
expiration_range, blob_file_number, keys, blobs, kNoCompression,
|
||||
blob_offsets, blob_sizes);
|
||||
|
||||
constexpr size_t capacity = 10;
|
||||
std::shared_ptr<Cache> backing_cache =
|
||||
NewLRUCache(capacity); // Blob file cache
|
||||
|
||||
FileOptions file_options;
|
||||
constexpr HistogramImpl* blob_file_read_hist = nullptr;
|
||||
|
||||
std::unique_ptr<BlobFileCache> blob_file_cache(new BlobFileCache(
|
||||
backing_cache.get(), &immutable_options, &file_options, column_family_id,
|
||||
blob_file_read_hist, nullptr /*IOTracer*/));
|
||||
|
||||
BlobSource blob_source(&immutable_options, db_id_, db_session_id_,
|
||||
blob_file_cache.get());
|
||||
|
||||
ReadOptions read_options;
|
||||
read_options.verify_checksums = true;
|
||||
|
||||
constexpr FilePrefetchBuffer* prefetch_buffer = nullptr;
|
||||
|
||||
{
|
||||
// MultiGetBlobFromOneFile
|
||||
uint64_t bytes_read = 0;
|
||||
std::array<Status, num_blobs> statuses_buf;
|
||||
std::array<PinnableSlice, num_blobs> value_buf;
|
||||
autovector<BlobReadRequest> blob_reqs;
|
||||
|
||||
for (size_t i = 0; i < num_blobs; i += 2) { // even index
|
||||
blob_reqs.emplace_back(keys[i], blob_offsets[i], blob_sizes[i],
|
||||
kNoCompression, &value_buf[i], &statuses_buf[i]);
|
||||
ASSERT_FALSE(blob_source.TEST_BlobInCache(blob_file_number, file_size,
|
||||
blob_offsets[i]));
|
||||
}
|
||||
|
||||
read_options.fill_cache = true;
|
||||
read_options.read_tier = ReadTier::kReadAllTier;
|
||||
get_perf_context()->Reset();
|
||||
statistics->Reset().PermitUncheckedError();
|
||||
|
||||
// Get half of blobs
|
||||
blob_source.MultiGetBlobFromOneFile(read_options, blob_file_number,
|
||||
file_size, blob_reqs, &bytes_read);
|
||||
|
||||
uint64_t fs_read_bytes = 0;
|
||||
uint64_t ca_read_bytes = 0;
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
if (i % 2 == 0) {
|
||||
ASSERT_OK(statuses_buf[i]);
|
||||
ASSERT_EQ(value_buf[i], blobs[i]);
|
||||
fs_read_bytes +=
|
||||
blob_sizes[i] + keys[i].size() + BlobLogRecord::kHeaderSize;
|
||||
ASSERT_TRUE(blob_source.TEST_BlobInCache(blob_file_number, file_size,
|
||||
blob_offsets[i]));
|
||||
ca_read_bytes += blob_sizes[i];
|
||||
} else {
|
||||
statuses_buf[i].PermitUncheckedError();
|
||||
ASSERT_TRUE(value_buf[i].empty());
|
||||
ASSERT_FALSE(blob_source.TEST_BlobInCache(blob_file_number, file_size,
|
||||
blob_offsets[i]));
|
||||
}
|
||||
}
|
||||
|
||||
constexpr int num_even_blobs = num_blobs / 2;
|
||||
ASSERT_EQ((int)get_perf_context()->blob_cache_hit_count, num_even_blobs);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_count,
|
||||
num_even_blobs); // blocking i/o
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_byte,
|
||||
fs_read_bytes); // blocking i/o
|
||||
ASSERT_GE((int)get_perf_context()->blob_checksum_time, 0);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_decompress_time, 0);
|
||||
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_MISS), num_blobs);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_HIT), num_even_blobs);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_ADD), num_even_blobs);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_BYTES_READ),
|
||||
ca_read_bytes);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_BYTES_WRITE),
|
||||
ca_read_bytes);
|
||||
|
||||
// Get the rest of blobs
|
||||
for (size_t i = 1; i < num_blobs; i += 2) { // odd index
|
||||
ASSERT_FALSE(blob_source.TEST_BlobInCache(blob_file_number, file_size,
|
||||
blob_offsets[i]));
|
||||
|
||||
ASSERT_OK(blob_source.GetBlob(read_options, keys[i], blob_file_number,
|
||||
blob_offsets[i], file_size, blob_sizes[i],
|
||||
kNoCompression, prefetch_buffer,
|
||||
&value_buf[i], &bytes_read));
|
||||
ASSERT_EQ(value_buf[i], blobs[i]);
|
||||
ASSERT_EQ(bytes_read,
|
||||
BlobLogRecord::kHeaderSize + keys[i].size() + blob_sizes[i]);
|
||||
|
||||
ASSERT_TRUE(blob_source.TEST_BlobInCache(blob_file_number, file_size,
|
||||
blob_offsets[i]));
|
||||
}
|
||||
|
||||
// Cache-only MultiGetBlobFromOneFile
|
||||
read_options.read_tier = ReadTier::kBlockCacheTier;
|
||||
get_perf_context()->Reset();
|
||||
statistics->Reset().PermitUncheckedError();
|
||||
|
||||
blob_reqs.clear();
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
blob_reqs.emplace_back(keys[i], blob_offsets[i], blob_sizes[i],
|
||||
kNoCompression, &value_buf[i], &statuses_buf[i]);
|
||||
}
|
||||
|
||||
blob_source.MultiGetBlobFromOneFile(read_options, blob_file_number,
|
||||
file_size, blob_reqs, &bytes_read);
|
||||
|
||||
uint64_t blob_bytes = 0;
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
ASSERT_OK(statuses_buf[i]);
|
||||
ASSERT_EQ(value_buf[i], blobs[i]);
|
||||
ASSERT_TRUE(blob_source.TEST_BlobInCache(blob_file_number, file_size,
|
||||
blob_offsets[i]));
|
||||
blob_bytes += blob_sizes[i];
|
||||
}
|
||||
|
||||
// Retrieved the blob cache num_blobs * 2 times via GetBlob and
|
||||
// TEST_BlobInCache.
|
||||
ASSERT_EQ((int)get_perf_context()->blob_cache_hit_count, num_blobs * 2);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_count, 0); // blocking i/o
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_byte, 0); // blocking i/o
|
||||
ASSERT_GE((int)get_perf_context()->blob_checksum_time, 0);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_decompress_time, 0);
|
||||
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_MISS), 0);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_HIT), num_blobs * 2);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_ADD), 0);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_BYTES_READ),
|
||||
blob_bytes * 2);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_BYTES_WRITE), 0);
|
||||
}
|
||||
|
||||
options_.blob_cache->EraseUnRefEntries();
|
||||
|
||||
{
|
||||
// Cache-only MultiGetBlobFromOneFile
|
||||
uint64_t bytes_read = 0;
|
||||
read_options.read_tier = ReadTier::kBlockCacheTier;
|
||||
|
||||
std::array<Status, num_blobs> statuses_buf;
|
||||
std::array<PinnableSlice, num_blobs> value_buf;
|
||||
autovector<BlobReadRequest> blob_reqs;
|
||||
|
||||
for (size_t i = 0; i < num_blobs; i++) {
|
||||
blob_reqs.emplace_back(keys[i], blob_offsets[i], blob_sizes[i],
|
||||
kNoCompression, &value_buf[i], &statuses_buf[i]);
|
||||
ASSERT_FALSE(blob_source.TEST_BlobInCache(blob_file_number, file_size,
|
||||
blob_offsets[i]));
|
||||
}
|
||||
|
||||
get_perf_context()->Reset();
|
||||
statistics->Reset().PermitUncheckedError();
|
||||
|
||||
blob_source.MultiGetBlobFromOneFile(read_options, blob_file_number,
|
||||
file_size, blob_reqs, &bytes_read);
|
||||
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
ASSERT_TRUE(statuses_buf[i].IsIncomplete());
|
||||
ASSERT_TRUE(value_buf[i].empty());
|
||||
ASSERT_FALSE(blob_source.TEST_BlobInCache(blob_file_number, file_size,
|
||||
blob_offsets[i]));
|
||||
}
|
||||
|
||||
ASSERT_EQ((int)get_perf_context()->blob_cache_hit_count, 0);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_count, 0); // blocking i/o
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_byte, 0); // blocking i/o
|
||||
ASSERT_EQ((int)get_perf_context()->blob_checksum_time, 0);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_decompress_time, 0);
|
||||
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_MISS), num_blobs * 2);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_HIT), 0);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_ADD), 0);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_BYTES_READ), 0);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_BYTES_WRITE), 0);
|
||||
}
|
||||
|
||||
{
|
||||
// MultiGetBlobFromOneFile from non-existing file
|
||||
uint64_t bytes_read = 0;
|
||||
uint64_t non_existing_file_number = 100;
|
||||
read_options.read_tier = ReadTier::kReadAllTier;
|
||||
|
||||
std::array<Status, num_blobs> statuses_buf;
|
||||
std::array<PinnableSlice, num_blobs> value_buf;
|
||||
autovector<BlobReadRequest> blob_reqs;
|
||||
|
||||
for (size_t i = 0; i < num_blobs; i++) {
|
||||
blob_reqs.emplace_back(keys[i], blob_offsets[i], blob_sizes[i],
|
||||
kNoCompression, &value_buf[i], &statuses_buf[i]);
|
||||
ASSERT_FALSE(blob_source.TEST_BlobInCache(non_existing_file_number,
|
||||
file_size, blob_offsets[i]));
|
||||
}
|
||||
|
||||
get_perf_context()->Reset();
|
||||
statistics->Reset().PermitUncheckedError();
|
||||
|
||||
blob_source.MultiGetBlobFromOneFile(read_options, non_existing_file_number,
|
||||
file_size, blob_reqs, &bytes_read);
|
||||
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
ASSERT_TRUE(statuses_buf[i].IsIOError());
|
||||
ASSERT_TRUE(value_buf[i].empty());
|
||||
ASSERT_FALSE(blob_source.TEST_BlobInCache(non_existing_file_number,
|
||||
file_size, blob_offsets[i]));
|
||||
}
|
||||
|
||||
ASSERT_EQ((int)get_perf_context()->blob_cache_hit_count, 0);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_count, 0); // blocking i/o
|
||||
ASSERT_EQ((int)get_perf_context()->blob_read_byte, 0); // blocking i/o
|
||||
ASSERT_EQ((int)get_perf_context()->blob_checksum_time, 0);
|
||||
ASSERT_EQ((int)get_perf_context()->blob_decompress_time, 0);
|
||||
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_MISS), num_blobs * 2);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_HIT), 0);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_ADD), 0);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_BYTES_READ), 0);
|
||||
ASSERT_EQ(statistics->getTickerCount(BLOB_DB_CACHE_BYTES_WRITE), 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
|
||||
#include <array>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
#include "db/blob/blob_index.h"
|
||||
#include "db/blob/blob_log_format.h"
|
||||
@@ -48,6 +49,177 @@ TEST_F(DBBlobBasicTest, GetBlob) {
|
||||
.IsIncomplete());
|
||||
}
|
||||
|
||||
TEST_F(DBBlobBasicTest, GetBlobFromCache) {
|
||||
Options options = GetDefaultOptions();
|
||||
|
||||
LRUCacheOptions co;
|
||||
co.capacity = 2048;
|
||||
co.num_shard_bits = 2;
|
||||
co.metadata_charge_policy = kDontChargeCacheMetadata;
|
||||
auto backing_cache = NewLRUCache(co);
|
||||
|
||||
options.enable_blob_files = true;
|
||||
options.blob_cache = backing_cache;
|
||||
|
||||
BlockBasedTableOptions block_based_options;
|
||||
block_based_options.no_block_cache = false;
|
||||
block_based_options.block_cache = backing_cache;
|
||||
block_based_options.cache_index_and_filter_blocks = true;
|
||||
options.table_factory.reset(NewBlockBasedTableFactory(block_based_options));
|
||||
|
||||
Reopen(options);
|
||||
|
||||
constexpr char key[] = "key";
|
||||
constexpr char blob_value[] = "blob_value";
|
||||
|
||||
ASSERT_OK(Put(key, blob_value));
|
||||
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
ReadOptions read_options;
|
||||
|
||||
read_options.fill_cache = false;
|
||||
|
||||
{
|
||||
PinnableSlice result;
|
||||
|
||||
read_options.read_tier = kReadAllTier;
|
||||
ASSERT_OK(db_->Get(read_options, db_->DefaultColumnFamily(), key, &result));
|
||||
ASSERT_EQ(result, blob_value);
|
||||
|
||||
result.Reset();
|
||||
read_options.read_tier = kBlockCacheTier;
|
||||
|
||||
// Try again with no I/O allowed. Since we didn't re-fill the cache, the
|
||||
// blob itself can only be read from the blob file, so the read should
|
||||
// return Incomplete.
|
||||
ASSERT_TRUE(db_->Get(read_options, db_->DefaultColumnFamily(), key, &result)
|
||||
.IsIncomplete());
|
||||
ASSERT_TRUE(result.empty());
|
||||
}
|
||||
|
||||
read_options.fill_cache = true;
|
||||
|
||||
{
|
||||
PinnableSlice result;
|
||||
|
||||
read_options.read_tier = kReadAllTier;
|
||||
ASSERT_OK(db_->Get(read_options, db_->DefaultColumnFamily(), key, &result));
|
||||
ASSERT_EQ(result, blob_value);
|
||||
|
||||
result.Reset();
|
||||
read_options.read_tier = kBlockCacheTier;
|
||||
|
||||
// Try again with no I/O allowed. The table and the necessary blocks/blobs
|
||||
// should already be in their respective caches.
|
||||
ASSERT_OK(db_->Get(read_options, db_->DefaultColumnFamily(), key, &result));
|
||||
ASSERT_EQ(result, blob_value);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBBlobBasicTest, IterateBlobsFromCache) {
|
||||
Options options = GetDefaultOptions();
|
||||
|
||||
LRUCacheOptions co;
|
||||
co.capacity = 2048;
|
||||
co.num_shard_bits = 2;
|
||||
co.metadata_charge_policy = kDontChargeCacheMetadata;
|
||||
auto backing_cache = NewLRUCache(co);
|
||||
|
||||
options.enable_blob_files = true;
|
||||
options.blob_cache = backing_cache;
|
||||
|
||||
BlockBasedTableOptions block_based_options;
|
||||
block_based_options.no_block_cache = false;
|
||||
block_based_options.block_cache = backing_cache;
|
||||
block_based_options.cache_index_and_filter_blocks = true;
|
||||
options.table_factory.reset(NewBlockBasedTableFactory(block_based_options));
|
||||
|
||||
Reopen(options);
|
||||
|
||||
int num_blobs = 5;
|
||||
std::vector<std::string> keys;
|
||||
std::vector<std::string> blobs;
|
||||
|
||||
for (int i = 0; i < num_blobs; ++i) {
|
||||
keys.push_back("key" + std::to_string(i));
|
||||
blobs.push_back("blob" + std::to_string(i));
|
||||
ASSERT_OK(Put(keys[i], blobs[i]));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
ReadOptions read_options;
|
||||
|
||||
{
|
||||
read_options.fill_cache = false;
|
||||
read_options.read_tier = kReadAllTier;
|
||||
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
ASSERT_OK(iter->status());
|
||||
|
||||
int i = 0;
|
||||
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
|
||||
ASSERT_OK(iter->status());
|
||||
ASSERT_EQ(iter->key().ToString(), keys[i]);
|
||||
ASSERT_EQ(iter->value().ToString(), blobs[i]);
|
||||
++i;
|
||||
}
|
||||
ASSERT_EQ(i, num_blobs);
|
||||
}
|
||||
|
||||
{
|
||||
read_options.fill_cache = false;
|
||||
read_options.read_tier = kBlockCacheTier;
|
||||
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
ASSERT_OK(iter->status());
|
||||
|
||||
// Try again with no I/O allowed. Since we didn't re-fill the cache,
|
||||
// the blob itself can only be read from the blob file, so iter->Valid()
|
||||
// should be false.
|
||||
iter->SeekToFirst();
|
||||
ASSERT_NOK(iter->status());
|
||||
ASSERT_FALSE(iter->Valid());
|
||||
}
|
||||
|
||||
{
|
||||
read_options.fill_cache = true;
|
||||
read_options.read_tier = kReadAllTier;
|
||||
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
ASSERT_OK(iter->status());
|
||||
|
||||
// Read blobs from the file and refill the cache.
|
||||
int i = 0;
|
||||
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
|
||||
ASSERT_OK(iter->status());
|
||||
ASSERT_EQ(iter->key().ToString(), keys[i]);
|
||||
ASSERT_EQ(iter->value().ToString(), blobs[i]);
|
||||
++i;
|
||||
}
|
||||
ASSERT_EQ(i, num_blobs);
|
||||
}
|
||||
|
||||
{
|
||||
read_options.fill_cache = false;
|
||||
read_options.read_tier = kBlockCacheTier;
|
||||
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
ASSERT_OK(iter->status());
|
||||
|
||||
// Try again with no I/O allowed. The table and the necessary blocks/blobs
|
||||
// should already be in their respective caches.
|
||||
int i = 0;
|
||||
for (iter->SeekToFirst(); iter->Valid(); iter->Next()) {
|
||||
ASSERT_OK(iter->status());
|
||||
ASSERT_EQ(iter->key().ToString(), keys[i]);
|
||||
ASSERT_EQ(iter->value().ToString(), blobs[i]);
|
||||
++i;
|
||||
}
|
||||
ASSERT_EQ(i, num_blobs);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBBlobBasicTest, MultiGetBlobs) {
|
||||
constexpr size_t min_blob_size = 6;
|
||||
|
||||
@@ -127,6 +299,141 @@ TEST_F(DBBlobBasicTest, MultiGetBlobs) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBBlobBasicTest, MultiGetBlobsFromCache) {
|
||||
Options options = GetDefaultOptions();
|
||||
|
||||
LRUCacheOptions co;
|
||||
co.capacity = 2048;
|
||||
co.num_shard_bits = 2;
|
||||
co.metadata_charge_policy = kDontChargeCacheMetadata;
|
||||
auto backing_cache = NewLRUCache(co);
|
||||
|
||||
constexpr size_t min_blob_size = 6;
|
||||
options.min_blob_size = min_blob_size;
|
||||
options.create_if_missing = true;
|
||||
options.enable_blob_files = true;
|
||||
options.blob_cache = backing_cache;
|
||||
|
||||
BlockBasedTableOptions block_based_options;
|
||||
block_based_options.no_block_cache = false;
|
||||
block_based_options.block_cache = backing_cache;
|
||||
block_based_options.cache_index_and_filter_blocks = true;
|
||||
options.table_factory.reset(NewBlockBasedTableFactory(block_based_options));
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Put then retrieve three key-values. The first value is below the size limit
|
||||
// and is thus stored inline; the other two are stored separately as blobs.
|
||||
constexpr size_t num_keys = 3;
|
||||
|
||||
constexpr char first_key[] = "first_key";
|
||||
constexpr char first_value[] = "short";
|
||||
static_assert(sizeof(first_value) - 1 < min_blob_size,
|
||||
"first_value too long to be inlined");
|
||||
|
||||
ASSERT_OK(Put(first_key, first_value));
|
||||
|
||||
constexpr char second_key[] = "second_key";
|
||||
constexpr char second_value[] = "long_value";
|
||||
static_assert(sizeof(second_value) - 1 >= min_blob_size,
|
||||
"second_value too short to be stored as blob");
|
||||
|
||||
ASSERT_OK(Put(second_key, second_value));
|
||||
|
||||
constexpr char third_key[] = "third_key";
|
||||
constexpr char third_value[] = "other_long_value";
|
||||
static_assert(sizeof(third_value) - 1 >= min_blob_size,
|
||||
"third_value too short to be stored as blob");
|
||||
|
||||
ASSERT_OK(Put(third_key, third_value));
|
||||
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
ReadOptions read_options;
|
||||
read_options.fill_cache = false;
|
||||
|
||||
std::array<Slice, num_keys> keys{{first_key, second_key, third_key}};
|
||||
|
||||
{
|
||||
std::array<PinnableSlice, num_keys> values;
|
||||
std::array<Status, num_keys> statuses;
|
||||
|
||||
db_->MultiGet(read_options, db_->DefaultColumnFamily(), num_keys, &keys[0],
|
||||
&values[0], &statuses[0]);
|
||||
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ(values[0], first_value);
|
||||
|
||||
ASSERT_OK(statuses[1]);
|
||||
ASSERT_EQ(values[1], second_value);
|
||||
|
||||
ASSERT_OK(statuses[2]);
|
||||
ASSERT_EQ(values[2], third_value);
|
||||
}
|
||||
|
||||
// Try again with no I/O allowed. The first (inlined) value should be
|
||||
// successfully read; however, the two blob values could only be read from the
|
||||
// blob file, so for those the read should return Incomplete.
|
||||
read_options.read_tier = kBlockCacheTier;
|
||||
|
||||
{
|
||||
std::array<PinnableSlice, num_keys> values;
|
||||
std::array<Status, num_keys> statuses;
|
||||
|
||||
db_->MultiGet(read_options, db_->DefaultColumnFamily(), num_keys, &keys[0],
|
||||
&values[0], &statuses[0]);
|
||||
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ(values[0], first_value);
|
||||
|
||||
ASSERT_TRUE(statuses[1].IsIncomplete());
|
||||
|
||||
ASSERT_TRUE(statuses[2].IsIncomplete());
|
||||
}
|
||||
|
||||
// Fill the cache when reading blobs from the blob file.
|
||||
read_options.read_tier = kReadAllTier;
|
||||
read_options.fill_cache = true;
|
||||
|
||||
{
|
||||
std::array<PinnableSlice, num_keys> values;
|
||||
std::array<Status, num_keys> statuses;
|
||||
|
||||
db_->MultiGet(read_options, db_->DefaultColumnFamily(), num_keys, &keys[0],
|
||||
&values[0], &statuses[0]);
|
||||
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ(values[0], first_value);
|
||||
|
||||
ASSERT_OK(statuses[1]);
|
||||
ASSERT_EQ(values[1], second_value);
|
||||
|
||||
ASSERT_OK(statuses[2]);
|
||||
ASSERT_EQ(values[2], third_value);
|
||||
}
|
||||
|
||||
// Try again with no I/O allowed. All blobs should be successfully read from
|
||||
// the cache.
|
||||
read_options.read_tier = kBlockCacheTier;
|
||||
|
||||
{
|
||||
std::array<PinnableSlice, num_keys> values;
|
||||
std::array<Status, num_keys> statuses;
|
||||
|
||||
db_->MultiGet(read_options, db_->DefaultColumnFamily(), num_keys, &keys[0],
|
||||
&values[0], &statuses[0]);
|
||||
|
||||
ASSERT_OK(statuses[0]);
|
||||
ASSERT_EQ(values[0], first_value);
|
||||
|
||||
ASSERT_OK(statuses[1]);
|
||||
ASSERT_EQ(values[1], second_value);
|
||||
|
||||
ASSERT_OK(statuses[2]);
|
||||
ASSERT_EQ(values[2], third_value);
|
||||
}
|
||||
}
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
TEST_F(DBBlobBasicTest, MultiGetWithDirectIO) {
|
||||
Options options = GetDefaultOptions();
|
||||
@@ -320,8 +627,23 @@ TEST_F(DBBlobBasicTest, MultiGetWithDirectIO) {
|
||||
|
||||
TEST_F(DBBlobBasicTest, MultiGetBlobsFromMultipleFiles) {
|
||||
Options options = GetDefaultOptions();
|
||||
options.enable_blob_files = true;
|
||||
|
||||
LRUCacheOptions co;
|
||||
co.capacity = 2 << 20; // 2MB
|
||||
co.num_shard_bits = 2;
|
||||
co.metadata_charge_policy = kDontChargeCacheMetadata;
|
||||
auto backing_cache = NewLRUCache(co);
|
||||
|
||||
options.min_blob_size = 0;
|
||||
options.create_if_missing = true;
|
||||
options.enable_blob_files = true;
|
||||
options.blob_cache = backing_cache;
|
||||
|
||||
BlockBasedTableOptions block_based_options;
|
||||
block_based_options.no_block_cache = false;
|
||||
block_based_options.block_cache = backing_cache;
|
||||
block_based_options.cache_index_and_filter_blocks = true;
|
||||
options.table_factory.reset(NewBlockBasedTableFactory(block_based_options));
|
||||
|
||||
Reopen(options);
|
||||
|
||||
@@ -347,14 +669,64 @@ TEST_F(DBBlobBasicTest, MultiGetBlobsFromMultipleFiles) {
|
||||
for (size_t i = 0; i < keys.size(); ++i) {
|
||||
keys[i] = key_strs[i];
|
||||
}
|
||||
std::array<PinnableSlice, kNumKeys> values;
|
||||
std::array<Status, kNumKeys> statuses;
|
||||
db_->MultiGet(ReadOptions(), db_->DefaultColumnFamily(), kNumKeys, &keys[0],
|
||||
&values[0], &statuses[0]);
|
||||
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
ASSERT_OK(statuses[i]);
|
||||
ASSERT_EQ(value_strs[i], values[i]);
|
||||
ReadOptions read_options;
|
||||
read_options.read_tier = kReadAllTier;
|
||||
read_options.fill_cache = false;
|
||||
|
||||
{
|
||||
std::array<PinnableSlice, kNumKeys> values;
|
||||
std::array<Status, kNumKeys> statuses;
|
||||
db_->MultiGet(read_options, db_->DefaultColumnFamily(), kNumKeys, &keys[0],
|
||||
&values[0], &statuses[0]);
|
||||
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
ASSERT_OK(statuses[i]);
|
||||
ASSERT_EQ(value_strs[i], values[i]);
|
||||
}
|
||||
}
|
||||
|
||||
read_options.read_tier = kBlockCacheTier;
|
||||
|
||||
{
|
||||
std::array<PinnableSlice, kNumKeys> values;
|
||||
std::array<Status, kNumKeys> statuses;
|
||||
db_->MultiGet(read_options, db_->DefaultColumnFamily(), kNumKeys, &keys[0],
|
||||
&values[0], &statuses[0]);
|
||||
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
ASSERT_TRUE(statuses[i].IsIncomplete());
|
||||
ASSERT_TRUE(values[i].empty());
|
||||
}
|
||||
}
|
||||
|
||||
read_options.read_tier = kReadAllTier;
|
||||
read_options.fill_cache = true;
|
||||
|
||||
{
|
||||
std::array<PinnableSlice, kNumKeys> values;
|
||||
std::array<Status, kNumKeys> statuses;
|
||||
db_->MultiGet(read_options, db_->DefaultColumnFamily(), kNumKeys, &keys[0],
|
||||
&values[0], &statuses[0]);
|
||||
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
ASSERT_OK(statuses[i]);
|
||||
ASSERT_EQ(value_strs[i], values[i]);
|
||||
}
|
||||
}
|
||||
|
||||
read_options.read_tier = kBlockCacheTier;
|
||||
|
||||
{
|
||||
std::array<PinnableSlice, kNumKeys> values;
|
||||
std::array<Status, kNumKeys> statuses;
|
||||
db_->MultiGet(read_options, db_->DefaultColumnFamily(), kNumKeys, &keys[0],
|
||||
&values[0], &statuses[0]);
|
||||
|
||||
for (size_t i = 0; i < kNumKeys; ++i) {
|
||||
ASSERT_OK(statuses[i]);
|
||||
ASSERT_EQ(value_strs[i], values[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -798,6 +798,42 @@ TEST_F(DBBlobCompactionTest, CompactionReadaheadMerge) {
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(DBBlobCompactionTest, CompactionDoNotFillCache) {
|
||||
Options options = GetDefaultOptions();
|
||||
|
||||
options.enable_blob_files = true;
|
||||
options.min_blob_size = 0;
|
||||
options.enable_blob_garbage_collection = true;
|
||||
options.blob_garbage_collection_age_cutoff = 1.0;
|
||||
options.disable_auto_compactions = true;
|
||||
options.statistics = CreateDBStatistics();
|
||||
|
||||
LRUCacheOptions cache_options;
|
||||
cache_options.capacity = 1 << 20;
|
||||
cache_options.metadata_charge_policy = kDontChargeCacheMetadata;
|
||||
|
||||
options.blob_cache = NewLRUCache(cache_options);
|
||||
|
||||
Reopen(options);
|
||||
|
||||
ASSERT_OK(Put("key", "lime"));
|
||||
ASSERT_OK(Put("foo", "bar"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
ASSERT_OK(Put("key", "pie"));
|
||||
ASSERT_OK(Put("foo", "baz"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
constexpr Slice* begin = nullptr;
|
||||
constexpr Slice* end = nullptr;
|
||||
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), begin, end));
|
||||
|
||||
ASSERT_EQ(options.statistics->getTickerCount(BLOB_DB_CACHE_ADD), 0);
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
+11
-1
@@ -66,7 +66,8 @@ Status BuildTable(
|
||||
SequenceNumber job_snapshot, SnapshotChecker* snapshot_checker,
|
||||
bool paranoid_file_checks, InternalStats* internal_stats,
|
||||
IOStatus* io_status, const std::shared_ptr<IOTracer>& io_tracer,
|
||||
BlobFileCreationReason blob_creation_reason, EventLogger* event_logger,
|
||||
BlobFileCreationReason blob_creation_reason,
|
||||
const SeqnoToTimeMapping& seqno_to_time_mapping, EventLogger* event_logger,
|
||||
int job_id, const Env::IOPriority io_priority,
|
||||
TableProperties* table_properties, Env::WriteLifeTimeHint write_hint,
|
||||
const std::string* full_history_ts_low,
|
||||
@@ -260,6 +261,15 @@ Status BuildTable(
|
||||
if (!s.ok() || empty) {
|
||||
builder->Abandon();
|
||||
} else {
|
||||
std::string seqno_time_mapping_str;
|
||||
seqno_to_time_mapping.Encode(
|
||||
seqno_time_mapping_str, meta->fd.smallest_seqno,
|
||||
meta->fd.largest_seqno, meta->file_creation_time);
|
||||
builder->SetSeqnoTimeTableProperties(
|
||||
seqno_time_mapping_str,
|
||||
ioptions.compaction_style == CompactionStyle::kCompactionStyleFIFO
|
||||
? meta->file_creation_time
|
||||
: meta->oldest_ancester_time);
|
||||
s = builder->Finish();
|
||||
}
|
||||
if (io_status->ok()) {
|
||||
|
||||
@@ -9,7 +9,9 @@
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "db/range_tombstone_fragmenter.h"
|
||||
#include "db/seqno_to_time_mapping.h"
|
||||
#include "db/table_properties_collector.h"
|
||||
#include "logging/event_logger.h"
|
||||
#include "options/cf_options.h"
|
||||
@@ -61,6 +63,7 @@ extern Status BuildTable(
|
||||
bool paranoid_file_checks, InternalStats* internal_stats,
|
||||
IOStatus* io_status, const std::shared_ptr<IOTracer>& io_tracer,
|
||||
BlobFileCreationReason blob_creation_reason,
|
||||
const SeqnoToTimeMapping& seqno_to_time_mapping,
|
||||
EventLogger* event_logger = nullptr, int job_id = 0,
|
||||
const Env::IOPriority io_priority = Env::IO_HIGH,
|
||||
TableProperties* table_properties = nullptr,
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
#include "rocksdb/convenience.h"
|
||||
#include "rocksdb/db.h"
|
||||
#include "rocksdb/env.h"
|
||||
#include "rocksdb/experimental.h"
|
||||
#include "rocksdb/filter_policy.h"
|
||||
#include "rocksdb/iterator.h"
|
||||
#include "rocksdb/memtablerep.h"
|
||||
@@ -40,6 +41,7 @@
|
||||
#include "rocksdb/utilities/db_ttl.h"
|
||||
#include "rocksdb/utilities/memory_util.h"
|
||||
#include "rocksdb/utilities/optimistic_transaction_db.h"
|
||||
#include "rocksdb/utilities/options_util.h"
|
||||
#include "rocksdb/utilities/table_properties_collectors.h"
|
||||
#include "rocksdb/utilities/transaction.h"
|
||||
#include "rocksdb/utilities/transaction_db.h"
|
||||
@@ -59,6 +61,7 @@ using ROCKSDB_NAMESPACE::Cache;
|
||||
using ROCKSDB_NAMESPACE::Checkpoint;
|
||||
using ROCKSDB_NAMESPACE::ColumnFamilyDescriptor;
|
||||
using ROCKSDB_NAMESPACE::ColumnFamilyHandle;
|
||||
using ROCKSDB_NAMESPACE::ColumnFamilyMetaData;
|
||||
using ROCKSDB_NAMESPACE::ColumnFamilyOptions;
|
||||
using ROCKSDB_NAMESPACE::CompactionFilter;
|
||||
using ROCKSDB_NAMESPACE::CompactionFilterFactory;
|
||||
@@ -78,6 +81,7 @@ using ROCKSDB_NAMESPACE::FlushOptions;
|
||||
using ROCKSDB_NAMESPACE::InfoLogLevel;
|
||||
using ROCKSDB_NAMESPACE::IngestExternalFileOptions;
|
||||
using ROCKSDB_NAMESPACE::Iterator;
|
||||
using ROCKSDB_NAMESPACE::LevelMetaData;
|
||||
using ROCKSDB_NAMESPACE::LiveFileMetaData;
|
||||
using ROCKSDB_NAMESPACE::Logger;
|
||||
using ROCKSDB_NAMESPACE::LRUCacheOptions;
|
||||
@@ -105,6 +109,7 @@ using ROCKSDB_NAMESPACE::Slice;
|
||||
using ROCKSDB_NAMESPACE::SliceParts;
|
||||
using ROCKSDB_NAMESPACE::SliceTransform;
|
||||
using ROCKSDB_NAMESPACE::Snapshot;
|
||||
using ROCKSDB_NAMESPACE::SstFileMetaData;
|
||||
using ROCKSDB_NAMESPACE::SstFileWriter;
|
||||
using ROCKSDB_NAMESPACE::Status;
|
||||
using ROCKSDB_NAMESPACE::TablePropertiesCollectorFactory;
|
||||
@@ -178,6 +183,15 @@ struct rocksdb_cache_t {
|
||||
};
|
||||
struct rocksdb_livefiles_t { std::vector<LiveFileMetaData> rep; };
|
||||
struct rocksdb_column_family_handle_t { ColumnFamilyHandle* rep; };
|
||||
struct rocksdb_column_family_metadata_t {
|
||||
ColumnFamilyMetaData rep;
|
||||
};
|
||||
struct rocksdb_level_metadata_t {
|
||||
const LevelMetaData* rep;
|
||||
};
|
||||
struct rocksdb_sst_file_metadata_t {
|
||||
const SstFileMetaData* rep;
|
||||
};
|
||||
struct rocksdb_envoptions_t { EnvOptions rep; };
|
||||
struct rocksdb_ingestexternalfileoptions_t { IngestExternalFileOptions rep; };
|
||||
struct rocksdb_sstfilewriter_t { SstFileWriter* rep; };
|
||||
@@ -1737,6 +1751,29 @@ void rocksdb_compact_range_cf(
|
||||
(limit_key ? (b = Slice(limit_key, limit_key_len), &b) : nullptr));
|
||||
}
|
||||
|
||||
void rocksdb_suggest_compact_range(rocksdb_t* db, const char* start_key,
|
||||
size_t start_key_len, const char* limit_key,
|
||||
size_t limit_key_len, char** errptr) {
|
||||
Slice a, b;
|
||||
Status s = ROCKSDB_NAMESPACE::experimental::SuggestCompactRange(
|
||||
db->rep,
|
||||
(start_key ? (a = Slice(start_key, start_key_len), &a) : nullptr),
|
||||
(limit_key ? (b = Slice(limit_key, limit_key_len), &b) : nullptr));
|
||||
SaveError(errptr, s);
|
||||
}
|
||||
|
||||
void rocksdb_suggest_compact_range_cf(
|
||||
rocksdb_t* db, rocksdb_column_family_handle_t* column_family,
|
||||
const char* start_key, size_t start_key_len, const char* limit_key,
|
||||
size_t limit_key_len, char** errptr) {
|
||||
Slice a, b;
|
||||
Status s = db->rep->SuggestCompactRange(
|
||||
column_family->rep,
|
||||
(start_key ? (a = Slice(start_key, start_key_len), &a) : nullptr),
|
||||
(limit_key ? (b = Slice(limit_key, limit_key_len), &b) : nullptr));
|
||||
SaveError(errptr, s);
|
||||
}
|
||||
|
||||
void rocksdb_compact_range_opt(rocksdb_t* db, rocksdb_compactoptions_t* opt,
|
||||
const char* start_key, size_t start_key_len,
|
||||
const char* limit_key, size_t limit_key_len) {
|
||||
@@ -2508,6 +2545,56 @@ void rocksdb_write_writebatch_wi(
|
||||
SaveError(errptr, db->rep->Write(options->rep, wb));
|
||||
}
|
||||
|
||||
void rocksdb_load_latest_options(
|
||||
const char* db_path, rocksdb_env_t* env, bool ignore_unknown_options,
|
||||
rocksdb_cache_t* cache, rocksdb_options_t** db_options,
|
||||
size_t* num_column_families, char*** list_column_family_names,
|
||||
rocksdb_options_t*** list_column_family_options, char** errptr) {
|
||||
DBOptions db_opt;
|
||||
std::vector<ColumnFamilyDescriptor> cf_descs;
|
||||
Status s = LoadLatestOptions(std::string(db_path), env->rep, &db_opt,
|
||||
&cf_descs, ignore_unknown_options, &cache->rep);
|
||||
if (s.ok()) {
|
||||
char** cf_names = (char**)malloc(cf_descs.size() * sizeof(char*));
|
||||
rocksdb_options_t** cf_options = (rocksdb_options_t**)malloc(
|
||||
cf_descs.size() * sizeof(rocksdb_options_t*));
|
||||
for (size_t i = 0; i < cf_descs.size(); ++i) {
|
||||
cf_names[i] = strdup(cf_descs[i].name.c_str());
|
||||
cf_options[i] = new rocksdb_options_t{
|
||||
Options(DBOptions(), std::move(cf_descs[i].options))};
|
||||
}
|
||||
*num_column_families = cf_descs.size();
|
||||
*db_options = new rocksdb_options_t{
|
||||
Options(std::move(db_opt), ColumnFamilyOptions())};
|
||||
*list_column_family_names = cf_names;
|
||||
*list_column_family_options = cf_options;
|
||||
} else {
|
||||
*num_column_families = 0;
|
||||
*db_options = nullptr;
|
||||
*list_column_family_names = nullptr;
|
||||
*list_column_family_options = nullptr;
|
||||
SaveError(errptr, s);
|
||||
}
|
||||
}
|
||||
|
||||
void rocksdb_load_latest_options_destroy(
|
||||
rocksdb_options_t* db_options, char** list_column_family_names,
|
||||
rocksdb_options_t** list_column_family_options, size_t len) {
|
||||
rocksdb_options_destroy(db_options);
|
||||
if (list_column_family_names) {
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
free(list_column_family_names[i]);
|
||||
}
|
||||
free(list_column_family_names);
|
||||
}
|
||||
if (list_column_family_options) {
|
||||
for (size_t i = 0; i < len; ++i) {
|
||||
rocksdb_options_destroy(list_column_family_options[i]);
|
||||
}
|
||||
free(list_column_family_options);
|
||||
}
|
||||
}
|
||||
|
||||
rocksdb_block_based_table_options_t*
|
||||
rocksdb_block_based_options_create() {
|
||||
return new rocksdb_block_based_table_options_t;
|
||||
@@ -3338,11 +3425,6 @@ unsigned char rocksdb_options_get_advise_random_on_open(
|
||||
return opt->rep.advise_random_on_open;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_experimental_mempurge_threshold(rocksdb_options_t* opt,
|
||||
double v) {
|
||||
opt->rep.experimental_mempurge_threshold = v;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_access_hint_on_compaction_start(
|
||||
rocksdb_options_t* opt, int v) {
|
||||
switch(v) {
|
||||
@@ -3528,6 +3610,16 @@ int rocksdb_options_get_max_background_flushes(rocksdb_options_t* opt) {
|
||||
return opt->rep.max_background_flushes;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_experimental_mempurge_threshold(rocksdb_options_t* opt,
|
||||
double v) {
|
||||
opt->rep.experimental_mempurge_threshold = v;
|
||||
}
|
||||
|
||||
double rocksdb_options_get_experimental_mempurge_threshold(
|
||||
rocksdb_options_t* opt) {
|
||||
return opt->rep.experimental_mempurge_threshold;
|
||||
}
|
||||
|
||||
void rocksdb_options_set_max_log_file_size(rocksdb_options_t* opt, size_t v) {
|
||||
opt->rep.max_log_file_size = v;
|
||||
}
|
||||
@@ -3986,6 +4078,18 @@ uint64_t rocksdb_perfcontext_metric(rocksdb_perfcontext_t* context,
|
||||
return rep->env_new_logger_nanos;
|
||||
case rocksdb_number_async_seek:
|
||||
return rep->number_async_seek;
|
||||
case rocksdb_blob_cache_hit_count:
|
||||
return rep->blob_cache_hit_count;
|
||||
case rocksdb_blob_read_count:
|
||||
return rep->blob_read_count;
|
||||
case rocksdb_blob_read_byte:
|
||||
return rep->blob_read_byte;
|
||||
case rocksdb_blob_read_time:
|
||||
return rep->blob_read_time;
|
||||
case rocksdb_blob_checksum_time:
|
||||
return rep->blob_checksum_time;
|
||||
case rocksdb_blob_decompress_time:
|
||||
return rep->blob_decompress_time;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
@@ -4583,6 +4687,11 @@ void rocksdb_lru_cache_options_set_capacity(rocksdb_lru_cache_options_t* opt,
|
||||
opt->rep.capacity = capacity;
|
||||
}
|
||||
|
||||
void rocksdb_lru_cache_options_set_num_shard_bits(
|
||||
rocksdb_lru_cache_options_t* opt, int num_shard_bits) {
|
||||
opt->rep.num_shard_bits = num_shard_bits;
|
||||
}
|
||||
|
||||
void rocksdb_lru_cache_options_set_memory_allocator(
|
||||
rocksdb_lru_cache_options_t* opt, rocksdb_memory_allocator_t* allocator) {
|
||||
opt->rep.memory_allocator = allocator->rep;
|
||||
@@ -5131,6 +5240,119 @@ void rocksdb_delete_file_in_range_cf(
|
||||
(limit_key ? (b = Slice(limit_key, limit_key_len), &b) : nullptr)));
|
||||
}
|
||||
|
||||
/* MetaData */
|
||||
|
||||
rocksdb_column_family_metadata_t* rocksdb_get_column_family_metadata(
|
||||
rocksdb_t* db) {
|
||||
rocksdb_column_family_metadata_t* meta = new rocksdb_column_family_metadata_t;
|
||||
db->rep->GetColumnFamilyMetaData(&meta->rep);
|
||||
return meta;
|
||||
}
|
||||
|
||||
rocksdb_column_family_metadata_t* rocksdb_get_column_family_metadata_cf(
|
||||
rocksdb_t* db, rocksdb_column_family_handle_t* column_family) {
|
||||
rocksdb_column_family_metadata_t* meta = new rocksdb_column_family_metadata_t;
|
||||
db->rep->GetColumnFamilyMetaData(column_family->rep, &meta->rep);
|
||||
return meta;
|
||||
}
|
||||
|
||||
void rocksdb_column_family_metadata_destroy(
|
||||
rocksdb_column_family_metadata_t* cf_meta) {
|
||||
delete cf_meta;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_column_family_metadata_get_size(
|
||||
rocksdb_column_family_metadata_t* cf_meta) {
|
||||
return cf_meta->rep.size;
|
||||
}
|
||||
|
||||
size_t rocksdb_column_family_metadata_get_file_count(
|
||||
rocksdb_column_family_metadata_t* cf_meta) {
|
||||
return cf_meta->rep.file_count;
|
||||
}
|
||||
|
||||
char* rocksdb_column_family_metadata_get_name(
|
||||
rocksdb_column_family_metadata_t* cf_meta) {
|
||||
return strdup(cf_meta->rep.name.c_str());
|
||||
}
|
||||
|
||||
size_t rocksdb_column_family_metadata_get_level_count(
|
||||
rocksdb_column_family_metadata_t* cf_meta) {
|
||||
return cf_meta->rep.levels.size();
|
||||
}
|
||||
|
||||
rocksdb_level_metadata_t* rocksdb_column_family_metadata_get_level_metadata(
|
||||
rocksdb_column_family_metadata_t* cf_meta, size_t i) {
|
||||
if (i >= cf_meta->rep.levels.size()) {
|
||||
return NULL;
|
||||
}
|
||||
rocksdb_level_metadata_t* level_meta =
|
||||
(rocksdb_level_metadata_t*)malloc(sizeof(rocksdb_level_metadata_t));
|
||||
level_meta->rep = &cf_meta->rep.levels[i];
|
||||
|
||||
return level_meta;
|
||||
}
|
||||
|
||||
void rocksdb_level_metadata_destroy(rocksdb_level_metadata_t* level_meta) {
|
||||
// Only free the base pointer as its parent rocksdb_column_family_metadata_t
|
||||
// has the ownership of its rep.
|
||||
free(level_meta);
|
||||
}
|
||||
|
||||
int rocksdb_level_metadata_get_level(rocksdb_level_metadata_t* level_meta) {
|
||||
return level_meta->rep->level;
|
||||
}
|
||||
|
||||
uint64_t rocksdb_level_metadata_get_size(rocksdb_level_metadata_t* level_meta) {
|
||||
return level_meta->rep->size;
|
||||
}
|
||||
|
||||
size_t rocksdb_level_metadata_get_file_count(
|
||||
rocksdb_level_metadata_t* level_meta) {
|
||||
return level_meta->rep->files.size();
|
||||
}
|
||||
|
||||
rocksdb_sst_file_metadata_t* rocksdb_level_metadata_get_sst_file_metadata(
|
||||
rocksdb_level_metadata_t* level_meta, size_t i) {
|
||||
if (i >= level_meta->rep->files.size()) {
|
||||
return nullptr;
|
||||
}
|
||||
rocksdb_sst_file_metadata_t* file_meta =
|
||||
(rocksdb_sst_file_metadata_t*)malloc(sizeof(rocksdb_sst_file_metadata_t));
|
||||
file_meta->rep = &level_meta->rep->files[i];
|
||||
return file_meta;
|
||||
}
|
||||
|
||||
void rocksdb_sst_file_metadata_destroy(rocksdb_sst_file_metadata_t* file_meta) {
|
||||
// Only free the base pointer as its parent rocksdb_level_metadata_t
|
||||
// has the ownership of its rep.
|
||||
free(file_meta);
|
||||
}
|
||||
|
||||
char* rocksdb_sst_file_metadata_get_relative_filename(
|
||||
rocksdb_sst_file_metadata_t* file_meta) {
|
||||
return strdup(file_meta->rep->relative_filename.c_str());
|
||||
}
|
||||
|
||||
uint64_t rocksdb_sst_file_metadata_get_size(
|
||||
rocksdb_sst_file_metadata_t* file_meta) {
|
||||
return file_meta->rep->size;
|
||||
}
|
||||
|
||||
char* rocksdb_sst_file_metadata_get_smallestkey(
|
||||
rocksdb_sst_file_metadata_t* file_meta, size_t* key_len) {
|
||||
*key_len = file_meta->rep->smallestkey.size();
|
||||
return CopyString(file_meta->rep->smallestkey);
|
||||
}
|
||||
|
||||
char* rocksdb_sst_file_metadata_get_largestkey(
|
||||
rocksdb_sst_file_metadata_t* file_meta, size_t* key_len) {
|
||||
*key_len = file_meta->rep->largestkey.size();
|
||||
return CopyString(file_meta->rep->largestkey);
|
||||
}
|
||||
|
||||
/* Transactions */
|
||||
|
||||
rocksdb_transactiondb_options_t* rocksdb_transactiondb_options_create() {
|
||||
return new rocksdb_transactiondb_options_t;
|
||||
}
|
||||
|
||||
+203
-2
@@ -272,6 +272,92 @@ static rocksdb_compactionfilter_t* CFilterCreate(
|
||||
CFilterName);
|
||||
}
|
||||
|
||||
void CheckMetaData(rocksdb_column_family_metadata_t* cf_meta,
|
||||
const char* expected_cf_name) {
|
||||
char* cf_name = rocksdb_column_family_metadata_get_name(cf_meta);
|
||||
assert(strcmp(cf_name, expected_cf_name) == 0);
|
||||
rocksdb_free(cf_name);
|
||||
|
||||
size_t cf_size = rocksdb_column_family_metadata_get_size(cf_meta);
|
||||
assert(cf_size > 0);
|
||||
size_t cf_file_count = rocksdb_column_family_metadata_get_size(cf_meta);
|
||||
assert(cf_file_count > 0);
|
||||
|
||||
uint64_t total_level_size = 0;
|
||||
size_t total_file_count = 0;
|
||||
size_t level_count = rocksdb_column_family_metadata_get_level_count(cf_meta);
|
||||
assert(level_count > 0);
|
||||
for (size_t l = 0; l < level_count; ++l) {
|
||||
rocksdb_level_metadata_t* level_meta =
|
||||
rocksdb_column_family_metadata_get_level_metadata(cf_meta, l);
|
||||
assert(level_meta);
|
||||
assert(rocksdb_level_metadata_get_level(level_meta) >= (int)l);
|
||||
uint64_t level_size = rocksdb_level_metadata_get_size(level_meta);
|
||||
uint64_t file_size_in_level = 0;
|
||||
|
||||
size_t file_count = rocksdb_level_metadata_get_file_count(level_meta);
|
||||
total_file_count += file_count;
|
||||
for (size_t f = 0; f < file_count; ++f) {
|
||||
rocksdb_sst_file_metadata_t* file_meta =
|
||||
rocksdb_level_metadata_get_sst_file_metadata(level_meta, f);
|
||||
assert(file_meta);
|
||||
|
||||
uint64_t file_size = rocksdb_sst_file_metadata_get_size(file_meta);
|
||||
assert(file_size > 0);
|
||||
file_size_in_level += file_size;
|
||||
|
||||
char* file_name =
|
||||
rocksdb_sst_file_metadata_get_relative_filename(file_meta);
|
||||
assert(file_name);
|
||||
assert(strlen(file_name) > 0);
|
||||
rocksdb_free(file_name);
|
||||
|
||||
size_t smallest_key_len;
|
||||
char* smallest_key = rocksdb_sst_file_metadata_get_smallestkey(
|
||||
file_meta, &smallest_key_len);
|
||||
assert(smallest_key);
|
||||
assert(smallest_key_len > 0);
|
||||
size_t largest_key_len;
|
||||
char* largest_key =
|
||||
rocksdb_sst_file_metadata_get_largestkey(file_meta, &largest_key_len);
|
||||
assert(largest_key);
|
||||
assert(largest_key_len > 0);
|
||||
rocksdb_free(smallest_key);
|
||||
rocksdb_free(largest_key);
|
||||
|
||||
rocksdb_sst_file_metadata_destroy(file_meta);
|
||||
}
|
||||
assert(level_size == file_size_in_level);
|
||||
total_level_size += level_size;
|
||||
rocksdb_level_metadata_destroy(level_meta);
|
||||
}
|
||||
assert(total_file_count > 0);
|
||||
assert(cf_size == total_level_size);
|
||||
}
|
||||
|
||||
void GetAndCheckMetaData(rocksdb_t* db) {
|
||||
rocksdb_column_family_metadata_t* cf_meta =
|
||||
rocksdb_get_column_family_metadata(db);
|
||||
|
||||
CheckMetaData(cf_meta, "default");
|
||||
|
||||
rocksdb_column_family_metadata_destroy(cf_meta);
|
||||
}
|
||||
|
||||
void GetAndCheckMetaDataCf(rocksdb_t* db,
|
||||
rocksdb_column_family_handle_t* handle,
|
||||
const char* cf_name) {
|
||||
// Compact to make sure we have at least one sst file to obtain datadata.
|
||||
rocksdb_compact_range_cf(db, handle, NULL, 0, NULL, 0);
|
||||
|
||||
rocksdb_column_family_metadata_t* cf_meta =
|
||||
rocksdb_get_column_family_metadata_cf(db, handle);
|
||||
|
||||
CheckMetaData(cf_meta, cf_name);
|
||||
|
||||
rocksdb_column_family_metadata_destroy(cf_meta);
|
||||
}
|
||||
|
||||
static rocksdb_t* CheckCompaction(rocksdb_t* db, rocksdb_options_t* options,
|
||||
rocksdb_readoptions_t* roptions,
|
||||
rocksdb_writeoptions_t* woptions) {
|
||||
@@ -304,6 +390,11 @@ static rocksdb_t* CheckCompaction(rocksdb_t* db, rocksdb_options_t* options,
|
||||
CheckGet(db, roptions, "foo", "foovalue");
|
||||
CheckGet(db, roptions, "bar", NULL);
|
||||
CheckGet(db, roptions, "baz", "newbazvalue");
|
||||
|
||||
rocksdb_suggest_compact_range(db, "bar", 3, "foo", 3, &err);
|
||||
GetAndCheckMetaData(db);
|
||||
CheckNoError(err);
|
||||
|
||||
return db;
|
||||
}
|
||||
|
||||
@@ -470,6 +561,71 @@ static void CheckTxnDBPinGetCF(rocksdb_transactiondb_t* txn_db,
|
||||
rocksdb_pinnableslice_destroy(p);
|
||||
}
|
||||
|
||||
static void LoadAndCheckLatestOptions(const char* db_name, rocksdb_env_t* env,
|
||||
bool ignore_unknown_options,
|
||||
rocksdb_cache_t* cache,
|
||||
rocksdb_comparator_t* cmp,
|
||||
const size_t expected_num_column_families,
|
||||
const char** expected_cf_names,
|
||||
const char* expected_open_err) {
|
||||
rocksdb_options_t* db_options;
|
||||
size_t num_column_families;
|
||||
char** list_column_family_names;
|
||||
rocksdb_options_t** list_column_family_options;
|
||||
char* err = 0;
|
||||
|
||||
// load the latest rocksdb option
|
||||
rocksdb_load_latest_options(db_name, env, ignore_unknown_options, cache,
|
||||
&db_options, &num_column_families,
|
||||
&list_column_family_names,
|
||||
&list_column_family_options, &err);
|
||||
assert(num_column_families == expected_num_column_families);
|
||||
CheckNoError(err);
|
||||
|
||||
// verify the loaded options by opening the db.
|
||||
rocksdb_options_set_error_if_exists(db_options, 0);
|
||||
|
||||
char** list_const_cf_names =
|
||||
(char**)malloc(num_column_families * sizeof(char*));
|
||||
rocksdb_options_t** list_const_cf_options = (rocksdb_options_t**)malloc(
|
||||
num_column_families * sizeof(rocksdb_options_t*));
|
||||
for (size_t i = 0; i < num_column_families; ++i) {
|
||||
assert(strcmp(list_column_family_names[i], expected_cf_names[i]) == 0);
|
||||
list_const_cf_names[i] = list_column_family_names[i];
|
||||
if (cmp) {
|
||||
rocksdb_options_set_comparator(list_column_family_options[i], cmp);
|
||||
}
|
||||
list_const_cf_options[i] = list_column_family_options[i];
|
||||
}
|
||||
rocksdb_column_family_handle_t** handles =
|
||||
(rocksdb_column_family_handle_t**)malloc(
|
||||
num_column_families * sizeof(rocksdb_column_family_handle_t*));
|
||||
|
||||
rocksdb_t* db = rocksdb_open_column_families(
|
||||
db_options, db_name, (int)num_column_families,
|
||||
(const char* const*)list_const_cf_names,
|
||||
(const rocksdb_options_t* const*)list_const_cf_options, handles, &err);
|
||||
if (expected_open_err == NULL) {
|
||||
CheckNoError(err);
|
||||
for (size_t i = 0; i < num_column_families; ++i) {
|
||||
rocksdb_column_family_handle_destroy(handles[i]);
|
||||
}
|
||||
free(handles);
|
||||
rocksdb_close(db);
|
||||
} else {
|
||||
assert(err != NULL);
|
||||
assert(strcmp(err, expected_open_err) == 0);
|
||||
free(handles);
|
||||
free(err);
|
||||
}
|
||||
|
||||
free(list_const_cf_names);
|
||||
free(list_const_cf_options);
|
||||
rocksdb_load_latest_options_destroy(db_options, list_column_family_names,
|
||||
list_column_family_options,
|
||||
num_column_families);
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
(void)argc;
|
||||
(void)argv;
|
||||
@@ -1250,7 +1406,6 @@ int main(int argc, char** argv) {
|
||||
rocksdb_merge(db, woptions, "bar", 3, "barvalue", 8, &err);
|
||||
CheckNoError(err);
|
||||
CheckGet(db, roptions, "bar", "fake");
|
||||
|
||||
}
|
||||
|
||||
StartPhase("columnfamilies");
|
||||
@@ -1262,7 +1417,16 @@ int main(int argc, char** argv) {
|
||||
rocksdb_options_t* db_options = rocksdb_options_create();
|
||||
rocksdb_options_set_create_if_missing(db_options, 1);
|
||||
db = rocksdb_open(db_options, dbname, &err);
|
||||
CheckNoError(err)
|
||||
CheckNoError(err);
|
||||
rocksdb_close(db);
|
||||
{
|
||||
const char* expected_cf_names[1] = {"default"};
|
||||
LoadAndCheckLatestOptions(dbname, env, false, cache, NULL, 1,
|
||||
expected_cf_names, NULL);
|
||||
}
|
||||
|
||||
rocksdb_options_set_create_if_missing(db_options, 0);
|
||||
db = rocksdb_open(db_options, dbname, &err);
|
||||
rocksdb_column_family_handle_t* cfh;
|
||||
cfh = rocksdb_create_column_family(db, db_options, "cf1", &err);
|
||||
rocksdb_column_family_handle_destroy(cfh);
|
||||
@@ -1282,6 +1446,10 @@ int main(int argc, char** argv) {
|
||||
const char* cf_names[2] = {"default", "cf1"};
|
||||
const rocksdb_options_t* cf_opts[2] = {cf_options, cf_options};
|
||||
rocksdb_column_family_handle_t* handles[2];
|
||||
|
||||
LoadAndCheckLatestOptions(dbname, env, false, cache, NULL, 2, cf_names,
|
||||
NULL);
|
||||
|
||||
db = rocksdb_open_column_families(db_options, dbname, 2, cf_names, cf_opts, handles, &err);
|
||||
CheckNoError(err);
|
||||
|
||||
@@ -1296,6 +1464,9 @@ int main(int argc, char** argv) {
|
||||
CheckNoError(err);
|
||||
rocksdb_put_cf(db, woptions, handles[1], "foobar4", 7, "hello4", 6, &err);
|
||||
CheckNoError(err);
|
||||
rocksdb_suggest_compact_range_cf(db, handles[1], "foo", 3, "foobar9", 7,
|
||||
&err);
|
||||
CheckNoError(err);
|
||||
|
||||
rocksdb_flushoptions_t *flush_options = rocksdb_flushoptions_create();
|
||||
rocksdb_flushoptions_set_wait(flush_options, 1);
|
||||
@@ -1443,12 +1614,20 @@ int main(int argc, char** argv) {
|
||||
CheckNoError(err);
|
||||
rocksdb_iter_destroy(iter);
|
||||
|
||||
GetAndCheckMetaDataCf(db, handles[1], cf_names[1]);
|
||||
|
||||
rocksdb_drop_column_family(db, handles[1], &err);
|
||||
CheckNoError(err);
|
||||
for (i = 0; i < 2; i++) {
|
||||
rocksdb_column_family_handle_destroy(handles[i]);
|
||||
}
|
||||
rocksdb_close(db);
|
||||
{
|
||||
// As column family has been dropped, we expect only one column family.
|
||||
const char* expected_cf_names[1] = {"default"};
|
||||
LoadAndCheckLatestOptions(dbname, env, false, cache, NULL, 1,
|
||||
expected_cf_names, NULL);
|
||||
}
|
||||
rocksdb_destroy_db(options, dbname, &err);
|
||||
rocksdb_options_destroy(db_options);
|
||||
rocksdb_options_destroy(cf_options);
|
||||
@@ -1510,6 +1689,16 @@ int main(int argc, char** argv) {
|
||||
rocksdb_readoptions_set_total_order_seek(roptions, 0);
|
||||
|
||||
rocksdb_close(db);
|
||||
|
||||
{
|
||||
const char* expected_cf_names[1] = {"default"};
|
||||
LoadAndCheckLatestOptions(dbname, env, false, cache, NULL, 1,
|
||||
expected_cf_names,
|
||||
"Invalid argument: leveldb.BytewiseComparator: "
|
||||
"does not match existing comparator foo");
|
||||
LoadAndCheckLatestOptions(dbname, env, false, cache, cmp, 1,
|
||||
expected_cf_names, NULL);
|
||||
}
|
||||
rocksdb_destroy_db(options, dbname, &err);
|
||||
}
|
||||
|
||||
@@ -1831,6 +2020,10 @@ int main(int argc, char** argv) {
|
||||
rocksdb_options_set_wal_compression(o, 1);
|
||||
CheckCondition(1 == rocksdb_options_get_wal_compression(o));
|
||||
|
||||
rocksdb_options_set_experimental_mempurge_threshold(o, 29.0);
|
||||
CheckCondition(29.0 ==
|
||||
rocksdb_options_get_experimental_mempurge_threshold(o));
|
||||
|
||||
/* Blob Options */
|
||||
rocksdb_options_set_enable_blob_files(o, 1);
|
||||
CheckCondition(1 == rocksdb_options_get_enable_blob_files(o));
|
||||
@@ -1961,6 +2154,8 @@ int main(int argc, char** argv) {
|
||||
CheckCondition(4 == rocksdb_options_get_bottommost_compression(copy));
|
||||
CheckCondition(2 == rocksdb_options_get_compaction_style(copy));
|
||||
CheckCondition(1 == rocksdb_options_get_atomic_flush(copy));
|
||||
CheckCondition(29.0 ==
|
||||
rocksdb_options_get_experimental_mempurge_threshold(copy));
|
||||
|
||||
// Copies should be independent.
|
||||
rocksdb_options_set_allow_ingest_behind(copy, 0);
|
||||
@@ -2309,6 +2504,12 @@ int main(int argc, char** argv) {
|
||||
CheckCondition(0 == rocksdb_options_get_atomic_flush(copy));
|
||||
CheckCondition(1 == rocksdb_options_get_atomic_flush(o));
|
||||
|
||||
rocksdb_options_set_experimental_mempurge_threshold(copy, 229.0);
|
||||
CheckCondition(229.0 ==
|
||||
rocksdb_options_get_experimental_mempurge_threshold(copy));
|
||||
CheckCondition(29.0 ==
|
||||
rocksdb_options_get_experimental_mempurge_threshold(o));
|
||||
|
||||
rocksdb_options_destroy(copy);
|
||||
rocksdb_options_destroy(o);
|
||||
}
|
||||
|
||||
+22
-20
@@ -17,6 +17,7 @@
|
||||
#include <vector>
|
||||
|
||||
#include "db/blob/blob_file_cache.h"
|
||||
#include "db/blob/blob_source.h"
|
||||
#include "db/compaction/compaction_picker.h"
|
||||
#include "db/compaction/compaction_picker_fifo.h"
|
||||
#include "db/compaction/compaction_picker_level.h"
|
||||
@@ -516,7 +517,7 @@ ColumnFamilyData::ColumnFamilyData(
|
||||
const ColumnFamilyOptions& cf_options, const ImmutableDBOptions& db_options,
|
||||
const FileOptions* file_options, ColumnFamilySet* column_family_set,
|
||||
BlockCacheTracer* const block_cache_tracer,
|
||||
const std::shared_ptr<IOTracer>& io_tracer,
|
||||
const std::shared_ptr<IOTracer>& io_tracer, const std::string& db_id,
|
||||
const std::string& db_session_id)
|
||||
: id_(id),
|
||||
name_(name),
|
||||
@@ -549,7 +550,8 @@ ColumnFamilyData::ColumnFamilyData(
|
||||
prev_compaction_needed_bytes_(0),
|
||||
allow_2pc_(db_options.allow_2pc),
|
||||
last_memtable_id_(0),
|
||||
db_paths_registered_(false) {
|
||||
db_paths_registered_(false),
|
||||
mempurge_used_(false) {
|
||||
if (id_ != kDummyColumnFamilyDataId) {
|
||||
// TODO(cc): RegisterDbPaths can be expensive, considering moving it
|
||||
// outside of this constructor which might be called with db mutex held.
|
||||
@@ -580,6 +582,8 @@ ColumnFamilyData::ColumnFamilyData(
|
||||
blob_file_cache_.reset(
|
||||
new BlobFileCache(_table_cache, ioptions(), soptions(), id_,
|
||||
internal_stats_->GetBlobFileReadHist(), io_tracer));
|
||||
blob_source_.reset(new BlobSource(ioptions(), db_id, db_session_id,
|
||||
blob_file_cache_.get()));
|
||||
|
||||
if (ioptions_.compaction_style == kCompactionStyleLevel) {
|
||||
compaction_picker_.reset(
|
||||
@@ -695,19 +699,6 @@ ColumnFamilyData::~ColumnFamilyData() {
|
||||
id_, name_.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
if (data_dirs_.size()) { // Explicitly close data directories
|
||||
Status s = Status::OK();
|
||||
for (auto& data_dir_ptr : data_dirs_) {
|
||||
if (data_dir_ptr) {
|
||||
s = data_dir_ptr->Close(IOOptions(), nullptr);
|
||||
if (!s.ok()) {
|
||||
// TODO(zichen): add `Status Close()` and `CloseDirectories()
|
||||
s.PermitUncheckedError();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool ColumnFamilyData::UnrefAndTryDelete() {
|
||||
@@ -1312,9 +1303,18 @@ void ColumnFamilyData::InstallSuperVersion(
|
||||
super_version_ = new_superversion;
|
||||
++super_version_number_;
|
||||
super_version_->version_number = super_version_number_;
|
||||
super_version_->write_stall_condition =
|
||||
RecalculateWriteStallConditions(mutable_cf_options);
|
||||
|
||||
if (old_superversion == nullptr || old_superversion->current != current() ||
|
||||
old_superversion->mem != mem_ ||
|
||||
old_superversion->imm != imm_.current()) {
|
||||
// Should not recalculate slow down condition if nothing has changed, since
|
||||
// currently RecalculateWriteStallConditions() treats it as further slowing
|
||||
// down is needed.
|
||||
super_version_->write_stall_condition =
|
||||
RecalculateWriteStallConditions(mutable_cf_options);
|
||||
} else {
|
||||
super_version_->write_stall_condition =
|
||||
old_superversion->write_stall_condition;
|
||||
}
|
||||
if (old_superversion != nullptr) {
|
||||
// Reset SuperVersions cached in thread local storage.
|
||||
// This should be done before old_superversion->Unref(). That's to ensure
|
||||
@@ -1504,13 +1504,14 @@ ColumnFamilySet::ColumnFamilySet(const std::string& dbname,
|
||||
WriteController* _write_controller,
|
||||
BlockCacheTracer* const block_cache_tracer,
|
||||
const std::shared_ptr<IOTracer>& io_tracer,
|
||||
const std::string& db_id,
|
||||
const std::string& db_session_id)
|
||||
: max_column_family_(0),
|
||||
file_options_(file_options),
|
||||
dummy_cfd_(new ColumnFamilyData(
|
||||
ColumnFamilyData::kDummyColumnFamilyDataId, "", nullptr, nullptr,
|
||||
nullptr, ColumnFamilyOptions(), *db_options, &file_options_, nullptr,
|
||||
block_cache_tracer, io_tracer, db_session_id)),
|
||||
block_cache_tracer, io_tracer, db_id, db_session_id)),
|
||||
default_cfd_cache_(nullptr),
|
||||
db_name_(dbname),
|
||||
db_options_(db_options),
|
||||
@@ -1519,6 +1520,7 @@ ColumnFamilySet::ColumnFamilySet(const std::string& dbname,
|
||||
write_controller_(_write_controller),
|
||||
block_cache_tracer_(block_cache_tracer),
|
||||
io_tracer_(io_tracer),
|
||||
db_id_(db_id),
|
||||
db_session_id_(db_session_id) {
|
||||
// initialize linked list
|
||||
dummy_cfd_->prev_ = dummy_cfd_;
|
||||
@@ -1586,7 +1588,7 @@ ColumnFamilyData* ColumnFamilySet::CreateColumnFamily(
|
||||
ColumnFamilyData* new_cfd = new ColumnFamilyData(
|
||||
id, name, dummy_versions, table_cache_, write_buffer_manager_, options,
|
||||
*db_options_, &file_options_, this, block_cache_tracer_, io_tracer_,
|
||||
db_session_id_);
|
||||
db_id_, db_session_id_);
|
||||
column_families_.insert({name, id});
|
||||
column_family_data_.insert({id, new_cfd});
|
||||
max_column_family_ = std::max(max_column_family_, id);
|
||||
|
||||
+15
-8
@@ -47,6 +47,7 @@ class InstrumentedMutex;
|
||||
class InstrumentedMutexLock;
|
||||
struct SuperVersionContext;
|
||||
class BlobFileCache;
|
||||
class BlobSource;
|
||||
|
||||
extern const double kIncSlowdownRatio;
|
||||
// This file contains a list of data structures for managing column family
|
||||
@@ -376,7 +377,7 @@ class ColumnFamilyData {
|
||||
SequenceNumber earliest_seq);
|
||||
|
||||
TableCache* table_cache() const { return table_cache_.get(); }
|
||||
BlobFileCache* blob_file_cache() const { return blob_file_cache_.get(); }
|
||||
BlobSource* blob_source() const { return blob_source_.get(); }
|
||||
|
||||
// See documentation in compaction_picker.h
|
||||
// REQUIRES: DB mutex held
|
||||
@@ -479,11 +480,8 @@ class ColumnFamilyData {
|
||||
const MutableCFOptions& mutable_cf_options,
|
||||
const ImmutableCFOptions& immutable_cf_options);
|
||||
|
||||
// Recalculate some small conditions, which are changed only during
|
||||
// compaction, adding new memtable and/or
|
||||
// recalculation of compaction score. These values are used in
|
||||
// DBImpl::MakeRoomForWrite function to decide, if it need to make
|
||||
// a write stall
|
||||
// Recalculate some stall conditions, which are changed only during
|
||||
// compaction, adding new memtable and/or recalculation of compaction score.
|
||||
WriteStallCondition RecalculateWriteStallConditions(
|
||||
const MutableCFOptions& mutable_cf_options);
|
||||
|
||||
@@ -526,8 +524,14 @@ class ColumnFamilyData {
|
||||
return file_metadata_cache_res_mgr_;
|
||||
}
|
||||
|
||||
SequenceNumber GetFirstMemtableSequenceNumber() const;
|
||||
|
||||
static const uint32_t kDummyColumnFamilyDataId;
|
||||
|
||||
// Keep track of whether the mempurge feature was ever used.
|
||||
void SetMempurgeUsed() { mempurge_used_ = true; }
|
||||
bool GetMempurgeUsed() { return mempurge_used_; }
|
||||
|
||||
private:
|
||||
friend class ColumnFamilySet;
|
||||
ColumnFamilyData(uint32_t id, const std::string& name,
|
||||
@@ -539,7 +543,7 @@ class ColumnFamilyData {
|
||||
ColumnFamilySet* column_family_set,
|
||||
BlockCacheTracer* const block_cache_tracer,
|
||||
const std::shared_ptr<IOTracer>& io_tracer,
|
||||
const std::string& db_session_id);
|
||||
const std::string& db_id, const std::string& db_session_id);
|
||||
|
||||
std::vector<std::string> GetDbPaths() const;
|
||||
|
||||
@@ -563,6 +567,7 @@ class ColumnFamilyData {
|
||||
|
||||
std::unique_ptr<TableCache> table_cache_;
|
||||
std::unique_ptr<BlobFileCache> blob_file_cache_;
|
||||
std::unique_ptr<BlobSource> blob_source_;
|
||||
|
||||
std::unique_ptr<InternalStats> internal_stats_;
|
||||
|
||||
@@ -627,6 +632,7 @@ class ColumnFamilyData {
|
||||
// For charging memory usage of file metadata created for newly added files to
|
||||
// a Version associated with this CFD
|
||||
std::shared_ptr<CacheReservationManager> file_metadata_cache_res_mgr_;
|
||||
bool mempurge_used_;
|
||||
};
|
||||
|
||||
// ColumnFamilySet has interesting thread-safety requirements
|
||||
@@ -673,7 +679,7 @@ class ColumnFamilySet {
|
||||
WriteController* _write_controller,
|
||||
BlockCacheTracer* const block_cache_tracer,
|
||||
const std::shared_ptr<IOTracer>& io_tracer,
|
||||
const std::string& db_session_id);
|
||||
const std::string& db_id, const std::string& db_session_id);
|
||||
~ColumnFamilySet();
|
||||
|
||||
ColumnFamilyData* GetDefault() const;
|
||||
@@ -735,6 +741,7 @@ class ColumnFamilySet {
|
||||
WriteController* write_controller_;
|
||||
BlockCacheTracer* const block_cache_tracer_;
|
||||
std::shared_ptr<IOTracer> io_tracer_;
|
||||
const std::string& db_id_;
|
||||
std::string db_session_id_;
|
||||
};
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include <cassert>
|
||||
|
||||
#include "rocksdb/comparator.h"
|
||||
#include "table/internal_iterator.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
@@ -19,7 +20,7 @@ namespace ROCKSDB_NAMESPACE {
|
||||
class ClippingIterator : public InternalIterator {
|
||||
public:
|
||||
ClippingIterator(InternalIterator* iter, const Slice* start, const Slice* end,
|
||||
const Comparator* cmp)
|
||||
const CompareInterface* cmp)
|
||||
: iter_(iter), start_(start), end_(end), cmp_(cmp), valid_(false) {
|
||||
assert(iter_);
|
||||
assert(cmp_);
|
||||
@@ -268,7 +269,7 @@ class ClippingIterator : public InternalIterator {
|
||||
InternalIterator* iter_;
|
||||
const Slice* start_;
|
||||
const Slice* end_;
|
||||
const Comparator* cmp_;
|
||||
const CompareInterface* cmp_;
|
||||
bool valid_;
|
||||
};
|
||||
|
||||
|
||||
+112
-8
@@ -77,11 +77,11 @@ void Compaction::SetInputVersion(Version* _input_version) {
|
||||
void Compaction::GetBoundaryKeys(
|
||||
VersionStorageInfo* vstorage,
|
||||
const std::vector<CompactionInputFiles>& inputs, Slice* smallest_user_key,
|
||||
Slice* largest_user_key) {
|
||||
Slice* largest_user_key, int exclude_level) {
|
||||
bool initialized = false;
|
||||
const Comparator* ucmp = vstorage->InternalComparator()->user_comparator();
|
||||
for (size_t i = 0; i < inputs.size(); ++i) {
|
||||
if (inputs[i].files.empty()) {
|
||||
if (inputs[i].files.empty() || inputs[i].level == exclude_level) {
|
||||
continue;
|
||||
}
|
||||
if (inputs[i].level == 0) {
|
||||
@@ -214,7 +214,8 @@ Compaction::Compaction(
|
||||
CompressionOptions _compression_opts, Temperature _output_temperature,
|
||||
uint32_t _max_subcompactions, std::vector<FileMetaData*> _grandparents,
|
||||
bool _manual_compaction, const std::string& _trim_ts, double _score,
|
||||
bool _deletion_compaction, CompactionReason _compaction_reason,
|
||||
bool _deletion_compaction, bool l0_files_might_overlap,
|
||||
CompactionReason _compaction_reason,
|
||||
BlobGarbageCollectionPolicy _blob_garbage_collection_policy,
|
||||
double _blob_garbage_collection_age_cutoff)
|
||||
: input_vstorage_(vstorage),
|
||||
@@ -233,6 +234,7 @@ Compaction::Compaction(
|
||||
output_compression_opts_(_compression_opts),
|
||||
output_temperature_(_output_temperature),
|
||||
deletion_compaction_(_deletion_compaction),
|
||||
l0_files_might_overlap_(l0_files_might_overlap),
|
||||
inputs_(PopulateWithAtomicBoundaries(vstorage, std::move(_inputs))),
|
||||
grandparents_(std::move(_grandparents)),
|
||||
score_(_score),
|
||||
@@ -241,6 +243,7 @@ Compaction::Compaction(
|
||||
is_manual_compaction_(_manual_compaction),
|
||||
trim_ts_(_trim_ts),
|
||||
is_trivial_move_(false),
|
||||
|
||||
compaction_reason_(_compaction_reason),
|
||||
notify_on_compaction_completion_(false),
|
||||
enable_blob_garbage_collection_(
|
||||
@@ -254,7 +257,9 @@ Compaction::Compaction(
|
||||
_blob_garbage_collection_age_cutoff < 0 ||
|
||||
_blob_garbage_collection_age_cutoff > 1
|
||||
? mutable_cf_options()->blob_garbage_collection_age_cutoff
|
||||
: _blob_garbage_collection_age_cutoff) {
|
||||
: _blob_garbage_collection_age_cutoff),
|
||||
penultimate_level_(EvaluatePenultimateLevel(
|
||||
immutable_options_, start_level_, output_level_)) {
|
||||
MarkFilesBeingCompacted(true);
|
||||
if (is_manual_compaction_) {
|
||||
compaction_reason_ = CompactionReason::kManualCompaction;
|
||||
@@ -279,6 +284,39 @@ Compaction::Compaction(
|
||||
}
|
||||
|
||||
GetBoundaryKeys(vstorage, inputs_, &smallest_user_key_, &largest_user_key_);
|
||||
|
||||
// Every compaction regardless of any compaction reason may respect the
|
||||
// existing compact cursor in the output level to split output files
|
||||
output_split_key_ = nullptr;
|
||||
if (immutable_options_.compaction_style == kCompactionStyleLevel &&
|
||||
immutable_options_.compaction_pri == kRoundRobin) {
|
||||
const InternalKey* cursor =
|
||||
&input_vstorage_->GetCompactCursors()[output_level_];
|
||||
if (cursor->size() != 0) {
|
||||
const Slice& cursor_user_key = ExtractUserKey(cursor->Encode());
|
||||
auto ucmp = vstorage->InternalComparator()->user_comparator();
|
||||
// May split output files according to the cursor if it in the user-key
|
||||
// range
|
||||
if (ucmp->CompareWithoutTimestamp(cursor_user_key, smallest_user_key_) >
|
||||
0 &&
|
||||
ucmp->CompareWithoutTimestamp(cursor_user_key, largest_user_key_) <=
|
||||
0) {
|
||||
output_split_key_ = cursor;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
PopulatePenultimateLevelOutputRange();
|
||||
}
|
||||
|
||||
void Compaction::PopulatePenultimateLevelOutputRange() {
|
||||
if (!SupportsPerKeyPlacement()) {
|
||||
return;
|
||||
}
|
||||
|
||||
GetBoundaryKeys(input_vstorage_, inputs_,
|
||||
&penultimate_level_smallest_user_key_,
|
||||
&penultimate_level_largest_user_key_, number_levels_ - 1);
|
||||
}
|
||||
|
||||
Compaction::~Compaction() {
|
||||
@@ -290,6 +328,37 @@ Compaction::~Compaction() {
|
||||
}
|
||||
}
|
||||
|
||||
bool Compaction::SupportsPerKeyPlacement() const {
|
||||
return penultimate_level_ != kInvalidLevel;
|
||||
}
|
||||
|
||||
int Compaction::GetPenultimateLevel() const { return penultimate_level_; }
|
||||
|
||||
bool Compaction::OverlapPenultimateLevelOutputRange(
|
||||
const Slice& smallest_key, const Slice& largest_key) const {
|
||||
if (!SupportsPerKeyPlacement()) {
|
||||
return false;
|
||||
}
|
||||
const Comparator* ucmp =
|
||||
input_vstorage_->InternalComparator()->user_comparator();
|
||||
|
||||
return ucmp->Compare(smallest_key, penultimate_level_largest_user_key_) <=
|
||||
0 &&
|
||||
ucmp->Compare(largest_key, penultimate_level_smallest_user_key_) >= 0;
|
||||
}
|
||||
|
||||
bool Compaction::WithinPenultimateLevelOutputRange(const Slice& key) const {
|
||||
if (!SupportsPerKeyPlacement()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const Comparator* ucmp =
|
||||
input_vstorage_->InternalComparator()->user_comparator();
|
||||
|
||||
return ucmp->Compare(key, penultimate_level_smallest_user_key_) >= 0 &&
|
||||
ucmp->Compare(key, penultimate_level_largest_user_key_) <= 0;
|
||||
}
|
||||
|
||||
bool Compaction::InputCompressionMatchesOutput() const {
|
||||
int base_level = input_vstorage_->base_level();
|
||||
bool matches =
|
||||
@@ -311,8 +380,10 @@ bool Compaction::IsTrivialMove() const {
|
||||
// filter to be applied to that level, and thus cannot be a trivial move.
|
||||
|
||||
// Check if start level have files with overlapping ranges
|
||||
if (start_level_ == 0 && input_vstorage_->level0_non_overlapping() == false) {
|
||||
// We cannot move files from L0 to L1 if the files are overlapping
|
||||
if (start_level_ == 0 && input_vstorage_->level0_non_overlapping() == false &&
|
||||
l0_files_might_overlap_) {
|
||||
// We cannot move files from L0 to L1 if the L0 files in the LSM-tree are
|
||||
// overlapping, unless we are sure that files picked in L0 don't overlap.
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -369,6 +440,11 @@ bool Compaction::IsTrivialMove() const {
|
||||
}
|
||||
}
|
||||
|
||||
// PerKeyPlacement compaction should never be trivial move.
|
||||
if (SupportsPerKeyPlacement()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -651,8 +727,36 @@ uint64_t Compaction::MinInputFileOldestAncesterTime(
|
||||
return min_oldest_ancester_time;
|
||||
}
|
||||
|
||||
int Compaction::GetInputBaseLevel() const {
|
||||
return input_vstorage_->base_level();
|
||||
int Compaction::EvaluatePenultimateLevel(
|
||||
const ImmutableOptions& immutable_options, const int start_level,
|
||||
const int output_level) {
|
||||
// TODO: currently per_key_placement feature only support level and universal
|
||||
// compaction
|
||||
if (immutable_options.compaction_style != kCompactionStyleLevel &&
|
||||
immutable_options.compaction_style != kCompactionStyleUniversal) {
|
||||
return kInvalidLevel;
|
||||
}
|
||||
if (output_level != immutable_options.num_levels - 1) {
|
||||
return kInvalidLevel;
|
||||
}
|
||||
|
||||
int penultimate_level = output_level - 1;
|
||||
assert(penultimate_level < immutable_options.num_levels);
|
||||
if (penultimate_level <= 0 || penultimate_level < start_level) {
|
||||
return kInvalidLevel;
|
||||
}
|
||||
|
||||
bool supports_per_key_placement =
|
||||
immutable_options.preclude_last_level_data_seconds > 0;
|
||||
|
||||
// it could be overridden by unittest
|
||||
TEST_SYNC_POINT_CALLBACK("Compaction::SupportsPerKeyPlacement:Enabled",
|
||||
&supports_per_key_placement);
|
||||
if (!supports_per_key_placement) {
|
||||
return kInvalidLevel;
|
||||
}
|
||||
|
||||
return penultimate_level;
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -81,6 +81,7 @@ class Compaction {
|
||||
std::vector<FileMetaData*> grandparents,
|
||||
bool manual_compaction = false, const std::string& trim_ts = "",
|
||||
double score = -1, bool deletion_compaction = false,
|
||||
bool l0_files_might_overlap = true,
|
||||
CompactionReason compaction_reason = CompactionReason::kUnknown,
|
||||
BlobGarbageCollectionPolicy blob_garbage_collection_policy =
|
||||
BlobGarbageCollectionPolicy::kUseDefault,
|
||||
@@ -177,6 +178,12 @@ class Compaction {
|
||||
// moving a single input file to the next level (no merging or splitting)
|
||||
bool IsTrivialMove() const;
|
||||
|
||||
// The split user key in the output level if this compaction is required to
|
||||
// split the output files according to the existing cursor in the output
|
||||
// level under round-robin compaction policy. Empty indicates no required
|
||||
// splitting key
|
||||
const InternalKey* GetOutputSplitKey() const { return output_split_key_; }
|
||||
|
||||
// If true, then the compaction can be done by simply deleting input files.
|
||||
bool deletion_compaction() const { return deletion_compaction_; }
|
||||
|
||||
@@ -295,7 +302,25 @@ class Compaction {
|
||||
|
||||
Slice GetLargestUserKey() const { return largest_user_key_; }
|
||||
|
||||
int GetInputBaseLevel() const;
|
||||
// Return true if the compaction supports per_key_placement
|
||||
bool SupportsPerKeyPlacement() const;
|
||||
|
||||
// Get per_key_placement penultimate output level, which is `last_level - 1`
|
||||
// if per_key_placement feature is supported. Otherwise, return -1.
|
||||
int GetPenultimateLevel() const;
|
||||
|
||||
// Return true if the given range is overlap with penultimate level output
|
||||
// range.
|
||||
bool OverlapPenultimateLevelOutputRange(const Slice& smallest_key,
|
||||
const Slice& largest_key) const;
|
||||
|
||||
// Return true if the key is within penultimate level output range for
|
||||
// per_key_placement feature, which is safe to place the key to the
|
||||
// penultimate level. different compaction strategy has different rules.
|
||||
// If per_key_placement is not supported, always return false.
|
||||
// TODO: currently it doesn't support moving data from the last level to the
|
||||
// penultimate level
|
||||
bool WithinPenultimateLevelOutputRange(const Slice& key) const;
|
||||
|
||||
CompactionReason compaction_reason() const { return compaction_reason_; }
|
||||
|
||||
@@ -332,6 +357,15 @@ class Compaction {
|
||||
return notify_on_compaction_completion_;
|
||||
}
|
||||
|
||||
static constexpr int kInvalidLevel = -1;
|
||||
// Evaluate penultimate output level. If the compaction supports
|
||||
// per_key_placement feature, it returns the penultimate level number.
|
||||
// Otherwise, it's set to kInvalidLevel (-1), which means
|
||||
// output_to_penultimate_level is not supported.
|
||||
static int EvaluatePenultimateLevel(const ImmutableOptions& immutable_options,
|
||||
const int start_level,
|
||||
const int output_level);
|
||||
|
||||
private:
|
||||
// mark (or clear) all files that are being compacted
|
||||
void MarkFilesBeingCompacted(bool mark_as_compacted);
|
||||
@@ -339,7 +373,18 @@ class Compaction {
|
||||
// get the smallest and largest key present in files to be compacted
|
||||
static void GetBoundaryKeys(VersionStorageInfo* vstorage,
|
||||
const std::vector<CompactionInputFiles>& inputs,
|
||||
Slice* smallest_key, Slice* largest_key);
|
||||
Slice* smallest_key, Slice* largest_key,
|
||||
int exclude_level = -1);
|
||||
|
||||
// populate penultimate level output range, which will be used to determine if
|
||||
// a key is safe to output to the penultimate level (details see
|
||||
// `Compaction::WithinPenultimateLevelOutputRange()`.
|
||||
// TODO: Currently the penultimate level output range is the min/max keys of
|
||||
// non-last-level input files. Which is only good if there's no key moved
|
||||
// from the last level to the penultimate level. For a more complicated per
|
||||
// key placement which may move data from the last level to the penultimate
|
||||
// level, it needs extra check.
|
||||
void PopulatePenultimateLevelOutputRange();
|
||||
|
||||
// Get the atomic file boundaries for all files in the compaction. Necessary
|
||||
// in order to avoid the scenario described in
|
||||
@@ -379,6 +424,13 @@ class Compaction {
|
||||
Temperature output_temperature_;
|
||||
// If true, then the compaction can be done by simply deleting input files.
|
||||
const bool deletion_compaction_;
|
||||
// should it split the output file using the compact cursor?
|
||||
const InternalKey* output_split_key_;
|
||||
|
||||
// L0 files in LSM-tree might be overlapping. But the compaction picking
|
||||
// logic might pick a subset of the files that aren't overlapping. if
|
||||
// that is the case, set the value to false. Otherwise, set it true.
|
||||
bool l0_files_might_overlap_;
|
||||
|
||||
// Compaction input files organized by level. Constant after construction
|
||||
const std::vector<CompactionInputFiles> inputs_;
|
||||
@@ -430,8 +482,36 @@ class Compaction {
|
||||
|
||||
// Blob garbage collection age cutoff.
|
||||
double blob_garbage_collection_age_cutoff_;
|
||||
|
||||
// only set when per_key_placement feature is enabled, -1 (kInvalidLevel)
|
||||
// means not supported.
|
||||
const int penultimate_level_;
|
||||
|
||||
// Key range for penultimate level output
|
||||
Slice penultimate_level_smallest_user_key_;
|
||||
Slice penultimate_level_largest_user_key_;
|
||||
};
|
||||
|
||||
#ifndef NDEBUG
|
||||
// Helper struct only for tests, which contains the data to decide if a key
|
||||
// should be output to the penultimate level.
|
||||
// TODO: remove this when the public feature knob is available
|
||||
struct PerKeyPlacementContext {
|
||||
const int level;
|
||||
const Slice key;
|
||||
const Slice value;
|
||||
const SequenceNumber seq_num;
|
||||
|
||||
bool output_to_penultimate_level;
|
||||
|
||||
PerKeyPlacementContext(int _level, Slice _key, Slice _value,
|
||||
SequenceNumber _seq_num)
|
||||
: level(_level), key(_key), value(_value), seq_num(_seq_num) {
|
||||
output_to_penultimate_level = false;
|
||||
}
|
||||
};
|
||||
#endif /* !NDEBUG */
|
||||
|
||||
// Return sum of sizes of all files in `files`.
|
||||
extern uint64_t TotalFileSize(const std::vector<FileMetaData*>& files);
|
||||
|
||||
|
||||
@@ -33,7 +33,8 @@ CompactionIterator::CompactionIterator(
|
||||
const Compaction* compaction, const CompactionFilter* compaction_filter,
|
||||
const std::atomic<bool>* shutting_down,
|
||||
const std::shared_ptr<Logger> info_log,
|
||||
const std::string* full_history_ts_low)
|
||||
const std::string* full_history_ts_low,
|
||||
const SequenceNumber penultimate_level_cutoff_seqno)
|
||||
: CompactionIterator(
|
||||
input, cmp, merge_helper, last_sequence, snapshots,
|
||||
earliest_write_conflict_snapshot, job_snapshot, snapshot_checker, env,
|
||||
@@ -42,7 +43,8 @@ CompactionIterator::CompactionIterator(
|
||||
manual_compaction_canceled,
|
||||
std::unique_ptr<CompactionProxy>(
|
||||
compaction ? new RealCompaction(compaction) : nullptr),
|
||||
compaction_filter, shutting_down, info_log, full_history_ts_low) {}
|
||||
compaction_filter, shutting_down, info_log, full_history_ts_low,
|
||||
penultimate_level_cutoff_seqno) {}
|
||||
|
||||
CompactionIterator::CompactionIterator(
|
||||
InternalIterator* input, const Comparator* cmp, MergeHelper* merge_helper,
|
||||
@@ -58,7 +60,8 @@ CompactionIterator::CompactionIterator(
|
||||
const CompactionFilter* compaction_filter,
|
||||
const std::atomic<bool>* shutting_down,
|
||||
const std::shared_ptr<Logger> info_log,
|
||||
const std::string* full_history_ts_low)
|
||||
const std::string* full_history_ts_low,
|
||||
const SequenceNumber penultimate_level_cutoff_seqno)
|
||||
: input_(input, cmp,
|
||||
!compaction || compaction->DoesInputReferenceBlobFiles()),
|
||||
cmp_(cmp),
|
||||
@@ -92,7 +95,8 @@ CompactionIterator::CompactionIterator(
|
||||
CreatePrefetchBufferCollectionIfNeeded(compaction_.get())),
|
||||
current_key_committed_(false),
|
||||
cmp_with_history_ts_low_(0),
|
||||
level_(compaction_ == nullptr ? 0 : compaction_->level()) {
|
||||
level_(compaction_ == nullptr ? 0 : compaction_->level()),
|
||||
penultimate_level_cutoff_seqno_(penultimate_level_cutoff_seqno) {
|
||||
assert(snapshots_ != nullptr);
|
||||
bottommost_level_ = compaction_ == nullptr
|
||||
? false
|
||||
@@ -197,6 +201,7 @@ void CompactionIterator::Next() {
|
||||
|
||||
bool CompactionIterator::InvokeFilterIfNeeded(bool* need_skip,
|
||||
Slice* skip_until) {
|
||||
// TODO: support compaction filter for wide-column entities
|
||||
if (!compaction_filter_ ||
|
||||
(ikey_.type != kTypeValue && ikey_.type != kTypeBlobIndex)) {
|
||||
return true;
|
||||
@@ -519,7 +524,8 @@ void CompactionIterator::NextFromInput() {
|
||||
// In the previous iteration we encountered a single delete that we could
|
||||
// not compact out. We will keep this Put, but can drop it's data.
|
||||
// (See Optimization 3, below.)
|
||||
if (ikey_.type != kTypeValue && ikey_.type != kTypeBlobIndex) {
|
||||
if (ikey_.type != kTypeValue && ikey_.type != kTypeBlobIndex &&
|
||||
ikey_.type != kTypeWideColumnEntity) {
|
||||
ROCKS_LOG_FATAL(info_log_, "Unexpected key %s for compaction output",
|
||||
ikey_.DebugString(allow_data_in_errors_, true).c_str());
|
||||
assert(false);
|
||||
@@ -533,7 +539,7 @@ void CompactionIterator::NextFromInput() {
|
||||
assert(false);
|
||||
}
|
||||
|
||||
if (ikey_.type == kTypeBlobIndex) {
|
||||
if (ikey_.type == kTypeBlobIndex || ikey_.type == kTypeWideColumnEntity) {
|
||||
ikey_.type = kTypeValue;
|
||||
current_key_.UpdateInternalKey(ikey_.sequence, ikey_.type);
|
||||
}
|
||||
@@ -689,7 +695,8 @@ void CompactionIterator::NextFromInput() {
|
||||
// either way. We will maintain counts of how many mismatches
|
||||
// happened
|
||||
if (next_ikey.type != kTypeValue &&
|
||||
next_ikey.type != kTypeBlobIndex) {
|
||||
next_ikey.type != kTypeBlobIndex &&
|
||||
next_ikey.type != kTypeWideColumnEntity) {
|
||||
++iter_stats_.num_single_del_mismatch;
|
||||
}
|
||||
|
||||
@@ -1072,6 +1079,52 @@ void CompactionIterator::GarbageCollectBlobIfNeeded() {
|
||||
}
|
||||
}
|
||||
|
||||
void CompactionIterator::DecideOutputLevel() {
|
||||
#ifndef NDEBUG
|
||||
// Could be overridden by unittest
|
||||
PerKeyPlacementContext context(level_, ikey_.user_key, value_,
|
||||
ikey_.sequence);
|
||||
TEST_SYNC_POINT_CALLBACK("CompactionIterator::PrepareOutput.context",
|
||||
&context);
|
||||
output_to_penultimate_level_ = context.output_to_penultimate_level;
|
||||
#endif /* !NDEBUG */
|
||||
|
||||
// if the key is newer than the cutoff sequence or within the earliest
|
||||
// snapshot, it should output to the penultimate level.
|
||||
if (ikey_.sequence > penultimate_level_cutoff_seqno_ ||
|
||||
ikey_.sequence > earliest_snapshot_) {
|
||||
output_to_penultimate_level_ = true;
|
||||
}
|
||||
|
||||
if (output_to_penultimate_level_) {
|
||||
// If it's decided to output to the penultimate level, but unsafe to do so,
|
||||
// still output to the last level. For example, moving the data from a lower
|
||||
// level to a higher level outside of the higher-level input key range is
|
||||
// considered unsafe, because the key may conflict with higher-level SSTs
|
||||
// not from this compaction.
|
||||
// TODO: add statistic for declined output_to_penultimate_level
|
||||
bool safe_to_penultimate_level =
|
||||
compaction_->WithinPenultimateLevelOutputRange(ikey_.user_key);
|
||||
if (!safe_to_penultimate_level) {
|
||||
output_to_penultimate_level_ = false;
|
||||
// It could happen when disable/enable `bottommost_temperature` while
|
||||
// holding a snapshot. When `bottommost_temperature` is not set
|
||||
// (==kUnknown), the data newer than any snapshot is pushed to the last
|
||||
// level, but when the per_key_placement feature is enabled on the fly,
|
||||
// the data later than the snapshot has to be moved to the penultimate
|
||||
// level, which may or may not be safe. So the user needs to make sure all
|
||||
// snapshot is released before enabling `bottommost_temperature` feature
|
||||
// We will migrate the feature to `last_level_temperature` and maybe make
|
||||
// it not dynamically changeable.
|
||||
if (ikey_.sequence > earliest_snapshot_) {
|
||||
status_ = Status::Corruption(
|
||||
"Unsafe to store Seq later than snapshot in the last level if "
|
||||
"per_key_placement is enabled");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void CompactionIterator::PrepareOutput() {
|
||||
if (valid_) {
|
||||
if (ikey_.type == kTypeValue) {
|
||||
@@ -1080,6 +1133,10 @@ void CompactionIterator::PrepareOutput() {
|
||||
GarbageCollectBlobIfNeeded();
|
||||
}
|
||||
|
||||
if (compaction_ != nullptr && compaction_->SupportsPerKeyPlacement()) {
|
||||
DecideOutputLevel();
|
||||
}
|
||||
|
||||
// Zeroing out the sequence number leads to better compression.
|
||||
// If this is the bottommost level (no files in lower levels)
|
||||
// and the earliest snapshot is larger than this seqno
|
||||
@@ -1094,7 +1151,9 @@ void CompactionIterator::PrepareOutput() {
|
||||
if (valid_ && compaction_ != nullptr &&
|
||||
!compaction_->allow_ingest_behind() && bottommost_level_ &&
|
||||
DefinitelyInSnapshot(ikey_.sequence, earliest_snapshot_) &&
|
||||
ikey_.type != kTypeMerge && current_key_committed_) {
|
||||
ikey_.type != kTypeMerge && current_key_committed_ &&
|
||||
!output_to_penultimate_level_ &&
|
||||
ikey_.sequence < penultimate_level_cutoff_seqno_) {
|
||||
if (ikey_.type == kTypeDeletion ||
|
||||
(ikey_.type == kTypeSingleDeletion && timestamp_size_ == 0)) {
|
||||
ROCKS_LOG_FATAL(
|
||||
@@ -1225,7 +1284,10 @@ std::unique_ptr<BlobFetcher> CompactionIterator::CreateBlobFetcherIfNeeded(
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return std::unique_ptr<BlobFetcher>(new BlobFetcher(version, ReadOptions()));
|
||||
ReadOptions read_options;
|
||||
read_options.fill_cache = false;
|
||||
|
||||
return std::unique_ptr<BlobFetcher>(new BlobFetcher(version, read_options));
|
||||
}
|
||||
|
||||
std::unique_ptr<PrefetchBufferCollection>
|
||||
|
||||
@@ -32,7 +32,7 @@ class SequenceIterWrapper : public InternalIterator {
|
||||
public:
|
||||
SequenceIterWrapper(InternalIterator* iter, const Comparator* cmp,
|
||||
bool need_count_entries)
|
||||
: icmp_(cmp, /*named=*/false),
|
||||
: icmp_(cmp),
|
||||
inner_iter_(iter),
|
||||
need_count_entries_(need_count_entries) {}
|
||||
bool Valid() const override { return inner_iter_->Valid(); }
|
||||
@@ -105,6 +105,10 @@ class CompactionIterator {
|
||||
virtual bool DoesInputReferenceBlobFiles() const = 0;
|
||||
|
||||
virtual const Compaction* real_compaction() const = 0;
|
||||
|
||||
virtual bool SupportsPerKeyPlacement() const = 0;
|
||||
|
||||
virtual bool WithinPenultimateLevelOutputRange(const Slice& key) const = 0;
|
||||
};
|
||||
|
||||
class RealCompaction : public CompactionProxy {
|
||||
@@ -163,46 +167,54 @@ class CompactionIterator {
|
||||
|
||||
const Compaction* real_compaction() const override { return compaction_; }
|
||||
|
||||
bool SupportsPerKeyPlacement() const override {
|
||||
return compaction_->SupportsPerKeyPlacement();
|
||||
}
|
||||
|
||||
// Check if key is within penultimate level output range, to see if it's
|
||||
// safe to output to the penultimate level for per_key_placement feature.
|
||||
bool WithinPenultimateLevelOutputRange(const Slice& key) const override {
|
||||
return compaction_->WithinPenultimateLevelOutputRange(key);
|
||||
}
|
||||
|
||||
private:
|
||||
const Compaction* compaction_;
|
||||
};
|
||||
|
||||
CompactionIterator(InternalIterator* input, const Comparator* cmp,
|
||||
MergeHelper* merge_helper, SequenceNumber last_sequence,
|
||||
std::vector<SequenceNumber>* snapshots,
|
||||
SequenceNumber earliest_write_conflict_snapshot,
|
||||
SequenceNumber job_snapshot,
|
||||
const SnapshotChecker* snapshot_checker, Env* env,
|
||||
bool report_detailed_time, bool expect_valid_internal_key,
|
||||
CompactionRangeDelAggregator* range_del_agg,
|
||||
BlobFileBuilder* blob_file_builder,
|
||||
bool allow_data_in_errors,
|
||||
bool enforce_single_del_contracts,
|
||||
const std::atomic<bool>& manual_compaction_canceled,
|
||||
const Compaction* compaction = nullptr,
|
||||
const CompactionFilter* compaction_filter = nullptr,
|
||||
const std::atomic<bool>* shutting_down = nullptr,
|
||||
const std::shared_ptr<Logger> info_log = nullptr,
|
||||
const std::string* full_history_ts_low = nullptr);
|
||||
CompactionIterator(
|
||||
InternalIterator* input, const Comparator* cmp, MergeHelper* merge_helper,
|
||||
SequenceNumber last_sequence, std::vector<SequenceNumber>* snapshots,
|
||||
SequenceNumber earliest_write_conflict_snapshot,
|
||||
SequenceNumber job_snapshot, const SnapshotChecker* snapshot_checker,
|
||||
Env* env, bool report_detailed_time, bool expect_valid_internal_key,
|
||||
CompactionRangeDelAggregator* range_del_agg,
|
||||
BlobFileBuilder* blob_file_builder, bool allow_data_in_errors,
|
||||
bool enforce_single_del_contracts,
|
||||
const std::atomic<bool>& manual_compaction_canceled,
|
||||
const Compaction* compaction = nullptr,
|
||||
const CompactionFilter* compaction_filter = nullptr,
|
||||
const std::atomic<bool>* shutting_down = nullptr,
|
||||
const std::shared_ptr<Logger> info_log = nullptr,
|
||||
const std::string* full_history_ts_low = nullptr,
|
||||
const SequenceNumber penultimate_level_cutoff_seqno = kMaxSequenceNumber);
|
||||
|
||||
// Constructor with custom CompactionProxy, used for tests.
|
||||
CompactionIterator(InternalIterator* input, const Comparator* cmp,
|
||||
MergeHelper* merge_helper, SequenceNumber last_sequence,
|
||||
std::vector<SequenceNumber>* snapshots,
|
||||
SequenceNumber earliest_write_conflict_snapshot,
|
||||
SequenceNumber job_snapshot,
|
||||
const SnapshotChecker* snapshot_checker, Env* env,
|
||||
bool report_detailed_time, bool expect_valid_internal_key,
|
||||
CompactionRangeDelAggregator* range_del_agg,
|
||||
BlobFileBuilder* blob_file_builder,
|
||||
bool allow_data_in_errors,
|
||||
bool enforce_single_del_contracts,
|
||||
const std::atomic<bool>& manual_compaction_canceled,
|
||||
std::unique_ptr<CompactionProxy> compaction,
|
||||
const CompactionFilter* compaction_filter = nullptr,
|
||||
const std::atomic<bool>* shutting_down = nullptr,
|
||||
const std::shared_ptr<Logger> info_log = nullptr,
|
||||
const std::string* full_history_ts_low = nullptr);
|
||||
CompactionIterator(
|
||||
InternalIterator* input, const Comparator* cmp, MergeHelper* merge_helper,
|
||||
SequenceNumber last_sequence, std::vector<SequenceNumber>* snapshots,
|
||||
SequenceNumber earliest_write_conflict_snapshot,
|
||||
SequenceNumber job_snapshot, const SnapshotChecker* snapshot_checker,
|
||||
Env* env, bool report_detailed_time, bool expect_valid_internal_key,
|
||||
CompactionRangeDelAggregator* range_del_agg,
|
||||
BlobFileBuilder* blob_file_builder, bool allow_data_in_errors,
|
||||
bool enforce_single_del_contracts,
|
||||
const std::atomic<bool>& manual_compaction_canceled,
|
||||
std::unique_ptr<CompactionProxy> compaction,
|
||||
const CompactionFilter* compaction_filter = nullptr,
|
||||
const std::atomic<bool>* shutting_down = nullptr,
|
||||
const std::shared_ptr<Logger> info_log = nullptr,
|
||||
const std::string* full_history_ts_low = nullptr,
|
||||
const SequenceNumber penultimate_level_cutoff_seqno = kMaxSequenceNumber);
|
||||
|
||||
~CompactionIterator();
|
||||
|
||||
@@ -227,6 +239,12 @@ class CompactionIterator {
|
||||
const Slice& user_key() const { 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
|
||||
// per_key_placement is supported
|
||||
bool output_to_penultimate_level() const {
|
||||
return output_to_penultimate_level_;
|
||||
}
|
||||
Status InputStatus() const { return input_.status(); }
|
||||
|
||||
private:
|
||||
// Processes the input stream to find the next output
|
||||
@@ -235,6 +253,10 @@ class CompactionIterator {
|
||||
// Do final preparations before presenting the output to the callee.
|
||||
void PrepareOutput();
|
||||
|
||||
// Decide the current key should be output to the last level or penultimate
|
||||
// level, only call for compaction supports per key placement
|
||||
void DecideOutputLevel();
|
||||
|
||||
// Passes the output value to the blob file builder (if any), and replaces it
|
||||
// with the corresponding blob reference if it has been actually written to a
|
||||
// blob file (i.e. if it passed the value size check). Returns true if the
|
||||
@@ -417,6 +439,15 @@ class CompactionIterator {
|
||||
// just been zeroed out during bottommost compaction.
|
||||
bool last_key_seq_zeroed_{false};
|
||||
|
||||
// True if the current key should be output to the penultimate level if
|
||||
// possible, compaction logic makes the final decision on which level to
|
||||
// output to.
|
||||
bool output_to_penultimate_level_{false};
|
||||
|
||||
// any key later than this sequence number should have
|
||||
// output_to_penultimate_level_ set to true
|
||||
const SequenceNumber penultimate_level_cutoff_seqno_ = kMaxSequenceNumber;
|
||||
|
||||
void AdvanceInputIter() { input_.Next(); }
|
||||
|
||||
void SkipUntil(const Slice& skip_until) { input_.Seek(skip_until); }
|
||||
|
||||
@@ -180,11 +180,21 @@ class FakeCompaction : public CompactionIterator::CompactionProxy {
|
||||
|
||||
const Compaction* real_compaction() const override { return nullptr; }
|
||||
|
||||
bool SupportsPerKeyPlacement() const override {
|
||||
return supports_per_key_placement;
|
||||
}
|
||||
|
||||
bool WithinPenultimateLevelOutputRange(const Slice& key) const override {
|
||||
return (!key.starts_with("unsafe_pb"));
|
||||
}
|
||||
|
||||
bool key_not_exists_beyond_output_level = false;
|
||||
|
||||
bool is_bottommost_level = false;
|
||||
|
||||
bool is_allow_ingest_behind = false;
|
||||
|
||||
bool supports_per_key_placement = false;
|
||||
};
|
||||
|
||||
// A simplified snapshot checker which assumes each snapshot has a global
|
||||
@@ -254,6 +264,7 @@ class CompactionIteratorTest : public testing::TestWithParam<bool> {
|
||||
compaction_proxy_->is_allow_ingest_behind = AllowIngestBehind();
|
||||
compaction_proxy_->key_not_exists_beyond_output_level =
|
||||
key_not_exists_beyond_output_level;
|
||||
compaction_proxy_->supports_per_key_placement = SupportsPerKeyPlacement();
|
||||
compaction.reset(compaction_proxy_);
|
||||
}
|
||||
bool use_snapshot_checker = UseSnapshotChecker() || GetParam();
|
||||
@@ -295,6 +306,8 @@ class CompactionIteratorTest : public testing::TestWithParam<bool> {
|
||||
|
||||
virtual bool AllowIngestBehind() const { return false; }
|
||||
|
||||
virtual bool SupportsPerKeyPlacement() const { return false; }
|
||||
|
||||
void RunTest(
|
||||
const std::vector<std::string>& input_keys,
|
||||
const std::vector<std::string>& input_values,
|
||||
@@ -756,6 +769,119 @@ TEST_P(CompactionIteratorTest, ConvertToPutAtBottom) {
|
||||
INSTANTIATE_TEST_CASE_P(CompactionIteratorTestInstance, CompactionIteratorTest,
|
||||
testing::Values(true, false));
|
||||
|
||||
class PerKeyPlacementCompIteratorTest : public CompactionIteratorTest {
|
||||
public:
|
||||
bool SupportsPerKeyPlacement() const override { return true; }
|
||||
};
|
||||
|
||||
TEST_P(PerKeyPlacementCompIteratorTest, SplitLastLevelData) {
|
||||
std::atomic_uint64_t latest_cold_seq = 0;
|
||||
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionIterator::PrepareOutput.context", [&](void* arg) {
|
||||
auto context = static_cast<PerKeyPlacementContext*>(arg);
|
||||
context->output_to_penultimate_level =
|
||||
context->seq_num > latest_cold_seq;
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
latest_cold_seq = 5;
|
||||
|
||||
InitIterators(
|
||||
{test::KeyStr("a", 7, kTypeValue), test::KeyStr("b", 6, kTypeValue),
|
||||
test::KeyStr("c", 5, kTypeValue)},
|
||||
{"vala", "valb", "valc"}, {}, {}, kMaxSequenceNumber, kMaxSequenceNumber,
|
||||
nullptr, nullptr, true);
|
||||
c_iter_->SeekToFirst();
|
||||
ASSERT_TRUE(c_iter_->Valid());
|
||||
|
||||
// the first 2 keys are hot, which should has
|
||||
// `output_to_penultimate_level()==true` and seq num not zeroed out
|
||||
ASSERT_EQ(test::KeyStr("a", 7, kTypeValue), c_iter_->key().ToString());
|
||||
ASSERT_TRUE(c_iter_->output_to_penultimate_level());
|
||||
c_iter_->Next();
|
||||
ASSERT_TRUE(c_iter_->Valid());
|
||||
ASSERT_EQ(test::KeyStr("b", 6, kTypeValue), c_iter_->key().ToString());
|
||||
ASSERT_TRUE(c_iter_->output_to_penultimate_level());
|
||||
c_iter_->Next();
|
||||
ASSERT_TRUE(c_iter_->Valid());
|
||||
// `a` is cold data, which should be output to bottommost
|
||||
ASSERT_EQ(test::KeyStr("c", 0, kTypeValue), c_iter_->key().ToString());
|
||||
ASSERT_FALSE(c_iter_->output_to_penultimate_level());
|
||||
c_iter_->Next();
|
||||
ASSERT_OK(c_iter_->status());
|
||||
ASSERT_FALSE(c_iter_->Valid());
|
||||
}
|
||||
|
||||
TEST_P(PerKeyPlacementCompIteratorTest, SnapshotData) {
|
||||
AddSnapshot(5);
|
||||
|
||||
InitIterators(
|
||||
{test::KeyStr("a", 7, kTypeValue), test::KeyStr("b", 6, kTypeDeletion),
|
||||
test::KeyStr("b", 5, kTypeValue)},
|
||||
{"vala", "", "valb"}, {}, {}, kMaxSequenceNumber, kMaxSequenceNumber,
|
||||
nullptr, nullptr, true);
|
||||
c_iter_->SeekToFirst();
|
||||
ASSERT_TRUE(c_iter_->Valid());
|
||||
|
||||
// The first key and the tombstone are within snapshot, which should output
|
||||
// to the penultimate level (and seq num cannot be zeroed out).
|
||||
ASSERT_EQ(test::KeyStr("a", 7, kTypeValue), c_iter_->key().ToString());
|
||||
ASSERT_TRUE(c_iter_->output_to_penultimate_level());
|
||||
c_iter_->Next();
|
||||
ASSERT_TRUE(c_iter_->Valid());
|
||||
ASSERT_EQ(test::KeyStr("b", 6, kTypeDeletion), c_iter_->key().ToString());
|
||||
ASSERT_TRUE(c_iter_->output_to_penultimate_level());
|
||||
c_iter_->Next();
|
||||
ASSERT_TRUE(c_iter_->Valid());
|
||||
// `a` is not protected by the snapshot, the sequence number is zero out and
|
||||
// should output bottommost
|
||||
ASSERT_EQ(test::KeyStr("b", 0, kTypeValue), c_iter_->key().ToString());
|
||||
ASSERT_FALSE(c_iter_->output_to_penultimate_level());
|
||||
c_iter_->Next();
|
||||
ASSERT_OK(c_iter_->status());
|
||||
ASSERT_FALSE(c_iter_->Valid());
|
||||
}
|
||||
|
||||
TEST_P(PerKeyPlacementCompIteratorTest, ConflictWithSnapshot) {
|
||||
std::atomic_uint64_t latest_cold_seq = 0;
|
||||
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionIterator::PrepareOutput.context", [&](void* arg) {
|
||||
auto context = static_cast<PerKeyPlacementContext*>(arg);
|
||||
context->output_to_penultimate_level =
|
||||
context->seq_num > latest_cold_seq;
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
latest_cold_seq = 6;
|
||||
|
||||
AddSnapshot(5);
|
||||
|
||||
InitIterators({test::KeyStr("a", 7, kTypeValue),
|
||||
test::KeyStr("unsafe_pb", 6, kTypeValue),
|
||||
test::KeyStr("c", 5, kTypeValue)},
|
||||
{"vala", "valb", "valc"}, {}, {}, kMaxSequenceNumber,
|
||||
kMaxSequenceNumber, nullptr, nullptr, true);
|
||||
c_iter_->SeekToFirst();
|
||||
ASSERT_TRUE(c_iter_->Valid());
|
||||
|
||||
ASSERT_EQ(test::KeyStr("a", 7, kTypeValue), c_iter_->key().ToString());
|
||||
ASSERT_TRUE(c_iter_->output_to_penultimate_level());
|
||||
// the 2nd key is unsafe to output_to_penultimate_level, but it's within
|
||||
// snapshot so for per_key_placement feature it has to be outputted to the
|
||||
// penultimate level. which is a corruption. We should never see
|
||||
// such case as the data with seq num (within snapshot) should always come
|
||||
// from higher compaction input level, which makes it safe to
|
||||
// output_to_penultimate_level.
|
||||
c_iter_->Next();
|
||||
ASSERT_TRUE(c_iter_->status().IsCorruption());
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(PerKeyPlacementCompIteratorTest,
|
||||
PerKeyPlacementCompIteratorTest,
|
||||
testing::Values(true, false));
|
||||
|
||||
// Tests how CompactionIterator work together with SnapshotChecker.
|
||||
class CompactionIteratorWithSnapshotCheckerTest
|
||||
: public CompactionIteratorTest {
|
||||
@@ -981,6 +1107,21 @@ TEST_F(CompactionIteratorWithSnapshotCheckerTest,
|
||||
2 /*earliest_write_conflict_snapshot*/);
|
||||
}
|
||||
|
||||
// Same as above but with a wide-column entity. In addition to the value getting
|
||||
// trimmed, the type of the KV is changed to kTypeValue.
|
||||
TEST_F(CompactionIteratorWithSnapshotCheckerTest,
|
||||
KeepSingleDeletionForWriteConflictChecking_WideColumnEntity) {
|
||||
AddSnapshot(2, 0);
|
||||
RunTest({test::KeyStr("a", 2, kTypeSingleDeletion),
|
||||
test::KeyStr("a", 1, kTypeWideColumnEntity)},
|
||||
{"", "fake_entity"},
|
||||
{test::KeyStr("a", 2, kTypeSingleDeletion),
|
||||
test::KeyStr("a", 1, kTypeValue)},
|
||||
{"", ""}, 2 /* last_committed_seq */, nullptr /* merge_operator */,
|
||||
nullptr /* compaction_filter */, false /* bottommost_level */,
|
||||
2 /* earliest_write_conflict_snapshot */);
|
||||
}
|
||||
|
||||
// Compaction filter should keep uncommitted key as-is, and
|
||||
// * Convert the latest value to deletion, and/or
|
||||
// * if latest value is a merge, apply filter to all subsequent merges.
|
||||
|
||||
+256
-1557
File diff suppressed because it is too large.
Load diff
+105
-26
@@ -20,12 +20,14 @@
|
||||
#include "db/blob/blob_file_completion_callback.h"
|
||||
#include "db/column_family.h"
|
||||
#include "db/compaction/compaction_iterator.h"
|
||||
#include "db/compaction/compaction_outputs.h"
|
||||
#include "db/flush_scheduler.h"
|
||||
#include "db/internal_stats.h"
|
||||
#include "db/job_context.h"
|
||||
#include "db/log_writer.h"
|
||||
#include "db/memtable_list.h"
|
||||
#include "db/range_del_aggregator.h"
|
||||
#include "db/seqno_to_time_mapping.h"
|
||||
#include "db/version_edit.h"
|
||||
#include "db/write_controller.h"
|
||||
#include "db/write_thread.h"
|
||||
@@ -47,6 +49,7 @@
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
class Arena;
|
||||
class CompactionState;
|
||||
class ErrorHandler;
|
||||
class MemTable;
|
||||
class SnapshotChecker;
|
||||
@@ -56,11 +59,91 @@ class Version;
|
||||
class VersionEdit;
|
||||
class VersionSet;
|
||||
|
||||
class SubcompactionState;
|
||||
|
||||
// CompactionJob is responsible for executing the compaction. Each (manual or
|
||||
// automated) compaction corresponds to a CompactionJob object, and usually
|
||||
// goes through the stages of `Prepare()`->`Run()`->`Install()`. CompactionJob
|
||||
// will divide the compaction into subcompactions and execute them in parallel
|
||||
// if needed.
|
||||
//
|
||||
// CompactionJob has 2 main stats:
|
||||
// 1. CompactionJobStats compaction_job_stats_
|
||||
// CompactionJobStats is a public data structure which is part of Compaction
|
||||
// event listener that rocksdb share the job stats with the user.
|
||||
// Internally it's an aggregation of all the compaction_job_stats from each
|
||||
// `SubcompactionState`:
|
||||
// +------------------------+
|
||||
// | SubcompactionState |
|
||||
// | |
|
||||
// +--------->| compaction_job_stats |
|
||||
// | | |
|
||||
// | +------------------------+
|
||||
// +------------------------+ |
|
||||
// | CompactionJob | | +------------------------+
|
||||
// | | | | SubcompactionState |
|
||||
// | compaction_job_stats +-----+ | |
|
||||
// | | +--------->| compaction_job_stats |
|
||||
// | | | | |
|
||||
// +------------------------+ | +------------------------+
|
||||
// |
|
||||
// | +------------------------+
|
||||
// | | SubcompactionState |
|
||||
// | | |
|
||||
// +--------->+ compaction_job_stats |
|
||||
// | | |
|
||||
// | +------------------------+
|
||||
// |
|
||||
// | +------------------------+
|
||||
// | | ... |
|
||||
// +--------->+ |
|
||||
// +------------------------+
|
||||
//
|
||||
// 2. CompactionStatsFull compaction_stats_
|
||||
// `CompactionStatsFull` is an internal stats about the compaction, which
|
||||
// is eventually sent to `ColumnFamilyData::internal_stats_` and used for
|
||||
// logging and public metrics.
|
||||
// Internally, it's an aggregation of stats_ from each `SubcompactionState`.
|
||||
// It has 2 parts, normal stats about the main compaction information and
|
||||
// the penultimate level output stats.
|
||||
// `SubcompactionState` maintains the CompactionOutputs for normal output and
|
||||
// the penultimate level output if exists, the per_level stats is
|
||||
// stored with the outputs.
|
||||
// +---------------------------+
|
||||
// | SubcompactionState |
|
||||
// | |
|
||||
// | +----------------------+ |
|
||||
// | | CompactionOutputs | |
|
||||
// | | (normal output) | |
|
||||
// +---->| stats_ | |
|
||||
// | | +----------------------+ |
|
||||
// | | |
|
||||
// | | +----------------------+ |
|
||||
// +--------------------------------+ | | | CompactionOutputs | |
|
||||
// | CompactionJob | | | | (penultimate_level) | |
|
||||
// | | +--------->| stats_ | |
|
||||
// | compaction_stats_ | | | | +----------------------+ |
|
||||
// | +-------------------------+ | | | | |
|
||||
// | |stats (normal) |------|----+ +---------------------------+
|
||||
// | +-------------------------+ | | |
|
||||
// | | | |
|
||||
// | +-------------------------+ | | | +---------------------------+
|
||||
// | |penultimate_level_stats +------+ | | SubcompactionState |
|
||||
// | +-------------------------+ | | | | |
|
||||
// | | | | | +----------------------+ |
|
||||
// | | | | | | CompactionOutputs | |
|
||||
// +--------------------------------+ | | | | (normal output) | |
|
||||
// | +---->| stats_ | |
|
||||
// | | +----------------------+ |
|
||||
// | | |
|
||||
// | | +----------------------+ |
|
||||
// | | | CompactionOutputs | |
|
||||
// | | | (penultimate_level) | |
|
||||
// +--------->| stats_ | |
|
||||
// | +----------------------+ |
|
||||
// | |
|
||||
// +---------------------------+
|
||||
|
||||
class CompactionJob {
|
||||
public:
|
||||
CompactionJob(
|
||||
@@ -107,11 +190,6 @@ class CompactionJob {
|
||||
IOStatus io_status() const { return io_status_; }
|
||||
|
||||
protected:
|
||||
struct SubcompactionState;
|
||||
// CompactionJob state
|
||||
struct CompactionState;
|
||||
|
||||
void AggregateStatistics();
|
||||
void UpdateCompactionStats();
|
||||
void LogCompaction();
|
||||
virtual void RecordCompactionIOStats();
|
||||
@@ -122,7 +200,7 @@ class CompactionJob {
|
||||
void ProcessKeyValueCompaction(SubcompactionState* sub_compact);
|
||||
|
||||
CompactionState* compact_;
|
||||
InternalStats::CompactionStats compaction_stats_;
|
||||
InternalStats::CompactionStatsFull compaction_stats_;
|
||||
const ImmutableDBOptions& db_options_;
|
||||
const MutableDBOptions mutable_db_options_copy_;
|
||||
LogBuffer* log_buffer_;
|
||||
@@ -135,6 +213,8 @@ class CompactionJob {
|
||||
|
||||
IOStatus io_status_;
|
||||
|
||||
CompactionJobStats* compaction_job_stats_;
|
||||
|
||||
private:
|
||||
friend class CompactionJobTestBase;
|
||||
|
||||
@@ -150,15 +230,14 @@ class CompactionJob {
|
||||
|
||||
// update the thread status for starting a compaction.
|
||||
void ReportStartedCompaction(Compaction* compaction);
|
||||
void AllocateCompactionOutputFileNumbers();
|
||||
|
||||
Status FinishCompactionOutputFile(
|
||||
const Status& input_status, SubcompactionState* sub_compact,
|
||||
CompactionRangeDelAggregator* range_del_agg,
|
||||
CompactionIterationStats* range_del_out_stats,
|
||||
const Slice* next_table_min_key = nullptr);
|
||||
Status FinishCompactionOutputFile(const Status& input_status,
|
||||
SubcompactionState* sub_compact,
|
||||
CompactionOutputs& outputs,
|
||||
const Slice& next_table_min_key);
|
||||
Status InstallCompactionResults(const MutableCFOptions& mutable_cf_options);
|
||||
Status OpenCompactionOutputFile(SubcompactionState* sub_compact);
|
||||
Status OpenCompactionOutputFile(SubcompactionState* sub_compact,
|
||||
CompactionOutputs& outputs);
|
||||
void UpdateCompactionJobStats(
|
||||
const InternalStats::CompactionStats& stats) const;
|
||||
void RecordDroppedKeys(const CompactionIterationStats& c_iter_stats,
|
||||
@@ -167,20 +246,12 @@ class CompactionJob {
|
||||
void UpdateCompactionInputStatsHelper(
|
||||
int* num_files, uint64_t* bytes_read, int input_level);
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
void BuildSubcompactionJobInfo(
|
||||
SubcompactionState* sub_compact,
|
||||
SubcompactionJobInfo* subcompaction_job_info) const;
|
||||
#endif // ROCKSDB_LITE
|
||||
|
||||
void NotifyOnSubcompactionBegin(SubcompactionState* sub_compact);
|
||||
|
||||
void NotifyOnSubcompactionCompleted(SubcompactionState* sub_compact);
|
||||
|
||||
uint32_t job_id_;
|
||||
|
||||
CompactionJobStats* compaction_job_stats_;
|
||||
|
||||
// DBImpl state
|
||||
const std::string& dbname_;
|
||||
const std::string db_id_;
|
||||
@@ -222,14 +293,23 @@ class CompactionJob {
|
||||
bool measure_io_stats_;
|
||||
// Stores the Slices that designate the boundaries for each subcompaction
|
||||
std::vector<Slice> boundaries_;
|
||||
// Stores the approx size of keys covered in the range of each subcompaction
|
||||
std::vector<uint64_t> sizes_;
|
||||
Env::Priority thread_pri_;
|
||||
std::string full_history_ts_low_;
|
||||
std::string trim_ts_;
|
||||
BlobFileCompletionCallback* blob_callback_;
|
||||
|
||||
uint64_t GetCompactionId(SubcompactionState* sub_compact);
|
||||
uint64_t GetCompactionId(SubcompactionState* sub_compact) const;
|
||||
|
||||
// Stores the sequence number to time mapping gathered from all input files
|
||||
// it also collects the smallest_seqno -> oldest_ancester_time from the SST.
|
||||
SeqnoToTimeMapping seqno_time_mapping_;
|
||||
|
||||
// cutoff sequence number for penultimate level, only set when
|
||||
// per_key_placement feature is enabled.
|
||||
// If a key with sequence number larger than penultimate_level_cutoff_seqno_,
|
||||
// it will be placed on the penultimate_level and seqnuence number won't be
|
||||
// zeroed out.
|
||||
SequenceNumber penultimate_level_cutoff_seqno_ = kMaxSequenceNumber;
|
||||
|
||||
// Get table file name in where it's outputting to, which should also be in
|
||||
// `output_directory_`.
|
||||
@@ -265,7 +345,6 @@ struct CompactionServiceInput {
|
||||
std::string begin;
|
||||
bool has_end = false;
|
||||
std::string end;
|
||||
uint64_t approx_size = 0;
|
||||
|
||||
// serialization interface to read and write the object
|
||||
static Status Read(const std::string& data_str, CompactionServiceInput* obj);
|
||||
@@ -357,7 +436,7 @@ class CompactionServiceCompactionJob : private CompactionJob {
|
||||
const std::string& dbname, const std::shared_ptr<IOTracer>& io_tracer,
|
||||
const std::atomic<bool>& manual_compaction_canceled,
|
||||
const std::string& db_id, const std::string& db_session_id,
|
||||
const std::string& output_path,
|
||||
std::string output_path,
|
||||
const CompactionServiceInput& compaction_service_input,
|
||||
CompactionServiceResult* compaction_service_result);
|
||||
|
||||
|
||||
@@ -208,11 +208,11 @@ class CompactionJobTestBase : public testing::Test {
|
||||
mutable_db_options_(),
|
||||
table_cache_(NewLRUCache(50000, 16)),
|
||||
write_buffer_manager_(db_options_.db_write_buffer_size),
|
||||
versions_(new VersionSet(dbname_, &db_options_, env_options_,
|
||||
table_cache_.get(), &write_buffer_manager_,
|
||||
&write_controller_,
|
||||
/*block_cache_tracer=*/nullptr,
|
||||
/*io_tracer=*/nullptr, /*db_session_id*/ "")),
|
||||
versions_(new VersionSet(
|
||||
dbname_, &db_options_, env_options_, table_cache_.get(),
|
||||
&write_buffer_manager_, &write_controller_,
|
||||
/*block_cache_tracer=*/nullptr,
|
||||
/*io_tracer=*/nullptr, /*db_id*/ "", /*db_session_id*/ "")),
|
||||
shutting_down_(false),
|
||||
mock_table_factory_(new mock::MockTableFactory()),
|
||||
error_handler_(nullptr, db_options_, &mutex_),
|
||||
@@ -444,7 +444,7 @@ class CompactionJobTestBase : public testing::Test {
|
||||
new VersionSet(dbname_, &db_options_, env_options_, table_cache_.get(),
|
||||
&write_buffer_manager_, &write_controller_,
|
||||
/*block_cache_tracer=*/nullptr, /*io_tracer=*/nullptr,
|
||||
/*db_session_id*/ ""));
|
||||
/*db_id*/ "", /*db_session_id*/ ""));
|
||||
compaction_job_stats_.Reset();
|
||||
ASSERT_OK(SetIdentityFile(env_, dbname_));
|
||||
|
||||
@@ -482,6 +482,17 @@ class CompactionJobTestBase : public testing::Test {
|
||||
cfd_ = versions_->GetColumnFamilySet()->GetDefault();
|
||||
}
|
||||
|
||||
void RunLastLevelCompaction(
|
||||
const std::vector<std::vector<FileMetaData*>>& input_files,
|
||||
std::function<void(Compaction& comp)>&& verify_func,
|
||||
const std::vector<SequenceNumber>& snapshots = {}) {
|
||||
const int kLastLevel = cf_options_.num_levels - 1;
|
||||
verify_per_key_placement_ = std::move(verify_func);
|
||||
mock::KVVector empty_map;
|
||||
RunCompaction(input_files, empty_map, snapshots, kMaxSequenceNumber,
|
||||
kLastLevel, false);
|
||||
}
|
||||
|
||||
void RunCompaction(
|
||||
const std::vector<std::vector<FileMetaData*>>& input_files,
|
||||
const mock::KVVector& expected_results,
|
||||
@@ -571,6 +582,12 @@ class CompactionJobTestBase : public testing::Test {
|
||||
if (check_get_priority) {
|
||||
CheckGetRateLimiterPriority(compaction_job);
|
||||
}
|
||||
|
||||
if (verify_per_key_placement_) {
|
||||
// Verify per_key_placement compaction
|
||||
assert(compaction.SupportsPerKeyPlacement());
|
||||
verify_per_key_placement_(compaction);
|
||||
}
|
||||
}
|
||||
|
||||
void CheckGetRateLimiterPriority(CompactionJob& compaction_job) {
|
||||
@@ -620,6 +637,7 @@ class CompactionJobTestBase : public testing::Test {
|
||||
std::string full_history_ts_low_;
|
||||
const std::function<std::string(uint64_t)> encode_u64_ts_;
|
||||
bool test_io_priority_;
|
||||
std::function<void(Compaction& comp)> verify_per_key_placement_;
|
||||
};
|
||||
|
||||
// TODO(icanadi) Make it simpler once we mock out VersionSet
|
||||
@@ -1311,6 +1329,75 @@ TEST_F(CompactionJobTest, OldestBlobFileNumber) {
|
||||
/* expected_oldest_blob_file_number */ 19);
|
||||
}
|
||||
|
||||
TEST_F(CompactionJobTest, VerifyPenultimateLevelOutput) {
|
||||
cf_options_.bottommost_temperature = Temperature::kCold;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"Compaction::SupportsPerKeyPlacement:Enabled", [&](void* arg) {
|
||||
auto supports_per_key_placement = static_cast<bool*>(arg);
|
||||
*supports_per_key_placement = true;
|
||||
});
|
||||
|
||||
std::atomic_uint64_t latest_cold_seq = 0;
|
||||
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"CompactionIterator::PrepareOutput.context", [&](void* arg) {
|
||||
auto context = static_cast<PerKeyPlacementContext*>(arg);
|
||||
context->output_to_penultimate_level =
|
||||
context->seq_num > latest_cold_seq;
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
NewDB();
|
||||
|
||||
// Add files on different levels that may overlap
|
||||
auto file0_1 = mock::MakeMockFile({{KeyStr("z", 12U, kTypeValue), "val"}});
|
||||
AddMockFile(file0_1);
|
||||
|
||||
auto file1_1 = mock::MakeMockFile({{KeyStr("b", 10U, kTypeValue), "val"},
|
||||
{KeyStr("f", 11U, kTypeValue), "val"}});
|
||||
AddMockFile(file1_1, 1);
|
||||
auto file1_2 = mock::MakeMockFile({{KeyStr("j", 12U, kTypeValue), "val"},
|
||||
{KeyStr("k", 13U, kTypeValue), "val"}});
|
||||
AddMockFile(file1_2, 1);
|
||||
auto file1_3 = mock::MakeMockFile({{KeyStr("p", 14U, kTypeValue), "val"},
|
||||
{KeyStr("u", 15U, kTypeValue), "val"}});
|
||||
AddMockFile(file1_3, 1);
|
||||
|
||||
auto file2_1 = mock::MakeMockFile({{KeyStr("f", 8U, kTypeValue), "val"},
|
||||
{KeyStr("h", 9U, kTypeValue), "val"}});
|
||||
AddMockFile(file2_1, 2);
|
||||
auto file2_2 = mock::MakeMockFile({{KeyStr("m", 6U, kTypeValue), "val"},
|
||||
{KeyStr("p", 7U, kTypeValue), "val"}});
|
||||
AddMockFile(file2_2, 2);
|
||||
|
||||
auto file3_1 = mock::MakeMockFile({{KeyStr("g", 2U, kTypeValue), "val"},
|
||||
{KeyStr("k", 3U, kTypeValue), "val"}});
|
||||
AddMockFile(file3_1, 3);
|
||||
auto file3_2 = mock::MakeMockFile({{KeyStr("v", 4U, kTypeValue), "val"},
|
||||
{KeyStr("x", 5U, kTypeValue), "val"}});
|
||||
AddMockFile(file3_2, 3);
|
||||
|
||||
auto cfd = versions_->GetColumnFamilySet()->GetDefault();
|
||||
auto files0 = cfd->current()->storage_info()->LevelFiles(0);
|
||||
auto files1 = cfd->current()->storage_info()->LevelFiles(1);
|
||||
auto files2 = cfd->current()->storage_info()->LevelFiles(2);
|
||||
auto files3 = cfd->current()->storage_info()->LevelFiles(3);
|
||||
|
||||
RunLastLevelCompaction(
|
||||
{files0, files1, files2, files3}, /*verify_func=*/[&](Compaction& comp) {
|
||||
for (char c = 'a'; c <= 'z'; c++) {
|
||||
std::string c_str;
|
||||
c_str = c;
|
||||
const Slice key(c_str);
|
||||
if (c == 'a') {
|
||||
ASSERT_FALSE(comp.WithinPenultimateLevelOutputRange(key));
|
||||
} else {
|
||||
ASSERT_TRUE(comp.WithinPenultimateLevelOutputRange(key));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
TEST_F(CompactionJobTest, NoEnforceSingleDeleteContract) {
|
||||
db_options_.enforce_single_del_contracts = false;
|
||||
NewDB();
|
||||
@@ -1360,7 +1447,6 @@ TEST_F(CompactionJobTest, InputSerialization) {
|
||||
if (input.has_end) {
|
||||
input.end = rnd.RandomBinaryString(rnd.Uniform(kStrMaxLen));
|
||||
}
|
||||
input.approx_size = rnd64.Uniform(UINT64_MAX);
|
||||
|
||||
std::string output;
|
||||
ASSERT_OK(input.Write(&output));
|
||||
|
||||
@@ -0,0 +1,321 @@
|
||||
// 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).
|
||||
//
|
||||
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
|
||||
// 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 "db/compaction/compaction_outputs.h"
|
||||
|
||||
#include "db/builder.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
void CompactionOutputs::NewBuilder(const TableBuilderOptions& tboptions) {
|
||||
builder_.reset(NewTableBuilder(tboptions, file_writer_.get()));
|
||||
}
|
||||
|
||||
Status CompactionOutputs::Finish(const Status& intput_status,
|
||||
const SeqnoToTimeMapping& seqno_time_mapping) {
|
||||
FileMetaData* meta = GetMetaData();
|
||||
assert(meta != nullptr);
|
||||
Status s = intput_status;
|
||||
if (s.ok()) {
|
||||
std::string seqno_time_mapping_str;
|
||||
seqno_time_mapping.Encode(seqno_time_mapping_str, meta->fd.smallest_seqno,
|
||||
meta->fd.largest_seqno, meta->file_creation_time);
|
||||
builder_->SetSeqnoTimeTableProperties(seqno_time_mapping_str,
|
||||
meta->oldest_ancester_time);
|
||||
s = builder_->Finish();
|
||||
|
||||
} else {
|
||||
builder_->Abandon();
|
||||
}
|
||||
Status io_s = builder_->io_status();
|
||||
if (s.ok()) {
|
||||
s = io_s;
|
||||
} else {
|
||||
io_s.PermitUncheckedError();
|
||||
}
|
||||
const uint64_t current_bytes = builder_->FileSize();
|
||||
if (s.ok()) {
|
||||
meta->fd.file_size = current_bytes;
|
||||
meta->marked_for_compaction = builder_->NeedCompact();
|
||||
}
|
||||
current_output().finished = true;
|
||||
stats_.bytes_written += current_bytes;
|
||||
stats_.num_output_files = outputs_.size();
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
IOStatus CompactionOutputs::WriterSyncClose(const Status& input_status,
|
||||
SystemClock* clock,
|
||||
Statistics* statistics,
|
||||
bool use_fsync) {
|
||||
IOStatus io_s;
|
||||
if (input_status.ok()) {
|
||||
StopWatch sw(clock, statistics, COMPACTION_OUTFILE_SYNC_MICROS);
|
||||
io_s = file_writer_->Sync(use_fsync);
|
||||
}
|
||||
if (input_status.ok() && io_s.ok()) {
|
||||
io_s = file_writer_->Close();
|
||||
}
|
||||
|
||||
if (input_status.ok() && io_s.ok()) {
|
||||
FileMetaData* meta = GetMetaData();
|
||||
meta->file_checksum = file_writer_->GetFileChecksum();
|
||||
meta->file_checksum_func_name = file_writer_->GetFileChecksumFuncName();
|
||||
}
|
||||
|
||||
file_writer_.reset();
|
||||
|
||||
return io_s;
|
||||
}
|
||||
|
||||
Status CompactionOutputs::AddToOutput(
|
||||
const CompactionIterator& c_iter,
|
||||
const CompactionFileOpenFunc& open_file_func,
|
||||
const CompactionFileCloseFunc& close_file_func) {
|
||||
Status s;
|
||||
const Slice& key = c_iter.key();
|
||||
|
||||
if (!pending_close_ && c_iter.Valid() && partitioner_ && HasBuilder() &&
|
||||
partitioner_->ShouldPartition(
|
||||
PartitionerRequest(last_key_for_partitioner_, c_iter.user_key(),
|
||||
current_output_file_size_)) == kRequired) {
|
||||
pending_close_ = true;
|
||||
}
|
||||
|
||||
if (pending_close_) {
|
||||
s = close_file_func(*this, c_iter.InputStatus(), key);
|
||||
pending_close_ = false;
|
||||
}
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
// Open output file if necessary
|
||||
if (!HasBuilder()) {
|
||||
s = open_file_func(*this);
|
||||
}
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
Output& curr = current_output();
|
||||
assert(builder_ != nullptr);
|
||||
const Slice& value = c_iter.value();
|
||||
s = curr.validator.Add(key, value);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
builder_->Add(key, value);
|
||||
|
||||
stats_.num_output_records++;
|
||||
current_output_file_size_ = builder_->EstimatedFileSize();
|
||||
|
||||
if (blob_garbage_meter_) {
|
||||
s = blob_garbage_meter_->ProcessOutFlow(key, value);
|
||||
}
|
||||
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
const ParsedInternalKey& ikey = c_iter.ikey();
|
||||
s = current_output().meta.UpdateBoundaries(key, value, ikey.sequence,
|
||||
ikey.type);
|
||||
|
||||
// Close output file if it is big enough. Two possibilities determine it's
|
||||
// time to close it: (1) the current key should be this file's last key, (2)
|
||||
// the next key should not be in this file.
|
||||
//
|
||||
// TODO(aekmekji): determine if file should be closed earlier than this
|
||||
// during subcompactions (i.e. if output size, estimated by input size, is
|
||||
// going to be 1.2MB and max_output_file_size = 1MB, prefer to have 0.6MB
|
||||
// and 0.6MB instead of 1MB and 0.2MB)
|
||||
if (compaction_->output_level() != 0 &&
|
||||
current_output_file_size_ >= compaction_->max_output_file_size()) {
|
||||
pending_close_ = true;
|
||||
}
|
||||
|
||||
if (partitioner_) {
|
||||
last_key_for_partitioner_.assign(c_iter.user_key().data_,
|
||||
c_iter.user_key().size_);
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
Status CompactionOutputs::AddRangeDels(
|
||||
const Slice* comp_start, const Slice* comp_end,
|
||||
CompactionIterationStats& range_del_out_stats, bool bottommost_level,
|
||||
const InternalKeyComparator& icmp, SequenceNumber earliest_snapshot,
|
||||
const Slice& next_table_min_key) {
|
||||
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;
|
||||
|
||||
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;
|
||||
lower_bound_from_sub_compact = true;
|
||||
} else if (meta.smallest.size() > 0) {
|
||||
// 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 != nullptr &&
|
||||
ucmp->Compare(upper_bound_guard, *comp_end) >= 0) {
|
||||
upper_bound = comp_end;
|
||||
} else {
|
||||
upper_bound = &upper_bound_guard;
|
||||
}
|
||||
} else {
|
||||
// This is the last file in the subcompaction, so extend until the
|
||||
// subcompaction ends.
|
||||
upper_bound = comp_end;
|
||||
}
|
||||
bool has_overlapping_endpoints;
|
||||
if (upper_bound != nullptr && meta.largest.size() > 0) {
|
||||
has_overlapping_endpoints =
|
||||
ucmp->Compare(meta.largest.user_key(), *upper_bound) == 0;
|
||||
} else {
|
||||
has_overlapping_endpoints = false;
|
||||
}
|
||||
|
||||
// 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 == nullptr || upper_bound == nullptr ||
|
||||
ucmp->Compare(*upper_bound, *comp_end) <= 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();
|
||||
}
|
||||
for (; it->Valid(); it->Next()) {
|
||||
auto tombstone = it->Tombstone();
|
||||
if (upper_bound != nullptr) {
|
||||
int cmp = ucmp->Compare(*upper_bound, tombstone.start_key_);
|
||||
if ((has_overlapping_endpoints && cmp < 0) ||
|
||||
(!has_overlapping_endpoints && cmp <= 0)) {
|
||||
// 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.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (bottommost_level && tombstone.seq_ <= earliest_snapshot) {
|
||||
// TODO(andrewkr): tombstones that span multiple output files are
|
||||
// counted for each compaction output file, so lots of double
|
||||
// counting.
|
||||
range_del_out_stats.num_range_del_drop_obsolete++;
|
||||
range_del_out_stats.num_record_drop_obsolete++;
|
||||
continue;
|
||||
}
|
||||
|
||||
auto kv = tombstone.Serialize();
|
||||
assert(lower_bound == nullptr ||
|
||||
ucmp->Compare(*lower_bound, kv.second) < 0);
|
||||
// Range tombstone is not supported by output validator yet.
|
||||
builder_->Add(kv.first.Encode(), kv.second);
|
||||
InternalKey smallest_candidate = std::move(kv.first);
|
||||
if (lower_bound != nullptr &&
|
||||
ucmp->Compare(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.
|
||||
//
|
||||
// When lower_bound is chosen by a subcompaction, 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 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, lower_bound_from_sub_compact ? tombstone.seq_ : 0,
|
||||
kTypeRangeDeletion);
|
||||
}
|
||||
InternalKey largest_candidate = tombstone.SerializeEndKey();
|
||||
if (upper_bound != nullptr &&
|
||||
ucmp->Compare(*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.
|
||||
largest_candidate =
|
||||
InternalKey(*upper_bound, kMaxSequenceNumber, kTypeRangeDeletion);
|
||||
}
|
||||
#ifndef NDEBUG
|
||||
SequenceNumber smallest_ikey_seqnum = kMaxSequenceNumber;
|
||||
if (meta.smallest.size() > 0) {
|
||||
smallest_ikey_seqnum = GetInternalKeySeqno(meta.smallest.Encode());
|
||||
}
|
||||
#endif
|
||||
meta.UpdateBoundariesForRange(smallest_candidate, largest_candidate,
|
||||
tombstone.seq_, icmp);
|
||||
// The smallest key in a file is used for range tombstone truncation, so
|
||||
// it cannot have a seqnum of 0 (unless the smallest data key in a file
|
||||
// has a seqnum of 0). Otherwise, the truncated tombstone may expose
|
||||
// deleted keys at lower levels.
|
||||
assert(smallest_ikey_seqnum == 0 ||
|
||||
ExtractInternalKeyFooter(meta.smallest.Encode()) !=
|
||||
PackSequenceAndType(0, kTypeRangeDeletion));
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -0,0 +1,329 @@
|
||||
// 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).
|
||||
//
|
||||
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
|
||||
// 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.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "db/blob/blob_garbage_meter.h"
|
||||
#include "db/compaction/compaction.h"
|
||||
#include "db/compaction/compaction_iterator.h"
|
||||
#include "db/internal_stats.h"
|
||||
#include "db/output_validator.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
class CompactionOutputs;
|
||||
using CompactionFileOpenFunc = std::function<Status(CompactionOutputs&)>;
|
||||
using CompactionFileCloseFunc =
|
||||
std::function<Status(CompactionOutputs&, const Status&, const Slice&)>;
|
||||
|
||||
// Files produced by subcompaction, most of the functions are used by
|
||||
// compaction_job Open/Close compaction file functions.
|
||||
class CompactionOutputs {
|
||||
public:
|
||||
// compaction output file
|
||||
struct Output {
|
||||
Output(FileMetaData&& _meta, const InternalKeyComparator& _icmp,
|
||||
bool _enable_order_check, bool _enable_hash, bool _finished,
|
||||
uint64_t precalculated_hash)
|
||||
: meta(std::move(_meta)),
|
||||
validator(_icmp, _enable_order_check, _enable_hash,
|
||||
precalculated_hash),
|
||||
finished(_finished) {}
|
||||
FileMetaData meta;
|
||||
OutputValidator validator;
|
||||
bool finished;
|
||||
std::shared_ptr<const TableProperties> table_properties;
|
||||
};
|
||||
|
||||
CompactionOutputs() = delete;
|
||||
|
||||
explicit CompactionOutputs(const Compaction* compaction,
|
||||
const bool is_penultimate_level)
|
||||
: compaction_(compaction), is_penultimate_level_(is_penultimate_level) {
|
||||
partitioner_ = compaction->output_level() == 0
|
||||
? nullptr
|
||||
: compaction->CreateSstPartitioner();
|
||||
}
|
||||
|
||||
// Add generated output to the list
|
||||
void AddOutput(FileMetaData&& meta, const InternalKeyComparator& icmp,
|
||||
bool enable_order_check, bool enable_hash,
|
||||
bool finished = false, uint64_t precalculated_hash = 0) {
|
||||
outputs_.emplace_back(std::move(meta), icmp, enable_order_check,
|
||||
enable_hash, finished, precalculated_hash);
|
||||
}
|
||||
|
||||
// Set new table builder for the current output
|
||||
void NewBuilder(const TableBuilderOptions& tboptions);
|
||||
|
||||
// Assign a new WritableFileWriter to the current output
|
||||
void AssignFileWriter(WritableFileWriter* writer) {
|
||||
file_writer_.reset(writer);
|
||||
}
|
||||
|
||||
// TODO: Remove it when remote compaction support tiered compaction
|
||||
void SetTotalBytes(uint64_t bytes) { stats_.bytes_written += bytes; }
|
||||
void SetNumOutputRecords(uint64_t num) { stats_.num_output_records = num; }
|
||||
|
||||
// TODO: Move the BlobDB builder into CompactionOutputs
|
||||
const std::vector<BlobFileAddition>& GetBlobFileAdditions() const {
|
||||
if (is_penultimate_level_) {
|
||||
assert(blob_file_additions_.empty());
|
||||
}
|
||||
return blob_file_additions_;
|
||||
}
|
||||
|
||||
std::vector<BlobFileAddition>* GetBlobFileAdditionsPtr() {
|
||||
assert(!is_penultimate_level_);
|
||||
return &blob_file_additions_;
|
||||
}
|
||||
|
||||
bool HasBlobFileAdditions() const { return !blob_file_additions_.empty(); }
|
||||
|
||||
BlobGarbageMeter* CreateBlobGarbageMeter() {
|
||||
assert(!is_penultimate_level_);
|
||||
blob_garbage_meter_ = std::make_unique<BlobGarbageMeter>();
|
||||
return blob_garbage_meter_.get();
|
||||
}
|
||||
|
||||
BlobGarbageMeter* GetBlobGarbageMeter() const {
|
||||
if (is_penultimate_level_) {
|
||||
// blobdb doesn't support per_key_placement yet
|
||||
assert(blob_garbage_meter_ == nullptr);
|
||||
return nullptr;
|
||||
}
|
||||
return blob_garbage_meter_.get();
|
||||
}
|
||||
|
||||
void UpdateBlobStats() {
|
||||
assert(!is_penultimate_level_);
|
||||
stats_.num_output_files_blob = blob_file_additions_.size();
|
||||
for (const auto& blob : blob_file_additions_) {
|
||||
stats_.bytes_written_blob += blob.GetTotalBlobBytes();
|
||||
}
|
||||
}
|
||||
|
||||
// Finish the current output file
|
||||
Status Finish(const Status& intput_status,
|
||||
const SeqnoToTimeMapping& seqno_time_mapping);
|
||||
|
||||
// Update output table properties from table builder
|
||||
void UpdateTableProperties() {
|
||||
current_output().table_properties =
|
||||
std::make_shared<TableProperties>(GetTableProperties());
|
||||
}
|
||||
|
||||
IOStatus WriterSyncClose(const Status& intput_status, SystemClock* clock,
|
||||
Statistics* statistics, bool use_fsync);
|
||||
|
||||
TableProperties GetTableProperties() {
|
||||
return builder_->GetTableProperties();
|
||||
}
|
||||
|
||||
Slice SmallestUserKey() const {
|
||||
if (!outputs_.empty() && outputs_[0].finished) {
|
||||
return outputs_[0].meta.smallest.user_key();
|
||||
} else {
|
||||
return Slice{nullptr, 0};
|
||||
}
|
||||
}
|
||||
|
||||
Slice LargestUserKey() const {
|
||||
if (!outputs_.empty() && outputs_.back().finished) {
|
||||
return outputs_.back().meta.largest.user_key();
|
||||
} else {
|
||||
return Slice{nullptr, 0};
|
||||
}
|
||||
}
|
||||
|
||||
// In case the last output file is empty, which doesn't need to keep.
|
||||
void RemoveLastEmptyOutput() {
|
||||
if (!outputs_.empty() && !outputs_.back().meta.fd.file_size) {
|
||||
// An error occurred, so ignore the last output.
|
||||
outputs_.pop_back();
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the last output, for example the last output doesn't have data (no
|
||||
// entry and no range-dels), but file_size might not be 0, as it has SST
|
||||
// metadata.
|
||||
void RemoveLastOutput() {
|
||||
assert(!outputs_.empty());
|
||||
outputs_.pop_back();
|
||||
}
|
||||
|
||||
bool HasBuilder() const { return builder_ != nullptr; }
|
||||
|
||||
FileMetaData* GetMetaData() { return ¤t_output().meta; }
|
||||
|
||||
bool HasOutput() const { return !outputs_.empty(); }
|
||||
|
||||
uint64_t NumEntries() const { return builder_->NumEntries(); }
|
||||
|
||||
void ResetBuilder() {
|
||||
builder_.reset();
|
||||
current_output_file_size_ = 0;
|
||||
}
|
||||
|
||||
// Add range-dels from the aggregator to the current output file
|
||||
Status AddRangeDels(const Slice* comp_start, const Slice* comp_end,
|
||||
CompactionIterationStats& range_del_out_stats,
|
||||
bool bottommost_level, const InternalKeyComparator& icmp,
|
||||
SequenceNumber earliest_snapshot,
|
||||
const Slice& next_table_min_key);
|
||||
|
||||
// Is the current file is already pending for close
|
||||
bool IsPendingClose() const { return pending_close_; }
|
||||
|
||||
// Current file should close before adding a new key
|
||||
void SetPendingClose() { pending_close_ = true; }
|
||||
|
||||
// if the outputs have range delete, range delete is also data
|
||||
bool HasRangeDel() const {
|
||||
return range_del_agg_ && !range_del_agg_->IsEmpty();
|
||||
}
|
||||
|
||||
private:
|
||||
friend class SubcompactionState;
|
||||
|
||||
void Cleanup() {
|
||||
if (builder_ != nullptr) {
|
||||
// May happen if we get a shutdown call in the middle of compaction
|
||||
builder_->Abandon();
|
||||
builder_.reset();
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t GetCurrentOutputFileSize() const {
|
||||
return current_output_file_size_;
|
||||
}
|
||||
|
||||
// Add curent key from compaction_iterator to the output file. If needed
|
||||
// close and open new compaction output with the functions provided.
|
||||
Status AddToOutput(const CompactionIterator& c_iter,
|
||||
const CompactionFileOpenFunc& open_file_func,
|
||||
const CompactionFileCloseFunc& close_file_func);
|
||||
|
||||
// Close the current output. `open_file_func` is needed for creating new file
|
||||
// for range-dels only output file.
|
||||
Status CloseOutput(const Status& curr_status,
|
||||
const CompactionFileOpenFunc& open_file_func,
|
||||
const CompactionFileCloseFunc& close_file_func) {
|
||||
Status status = curr_status;
|
||||
// handle subcompaction containing only range deletions
|
||||
if (status.ok() && !HasBuilder() && !HasOutput() && HasRangeDel()) {
|
||||
status = open_file_func(*this);
|
||||
}
|
||||
if (HasBuilder()) {
|
||||
const Slice empty_key{};
|
||||
Status s = close_file_func(*this, status, empty_key);
|
||||
if (!s.ok() && status.ok()) {
|
||||
status = s;
|
||||
}
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
// This subcompaction's output could be empty if compaction was aborted before
|
||||
// this subcompaction had a chance to generate any output files. When
|
||||
// subcompactions are executed sequentially this is more likely and will be
|
||||
// particularly likely for the later subcompactions to be empty. Once they are
|
||||
// run in parallel however it should be much rarer.
|
||||
// It's caller's responsibility to make sure it's not empty.
|
||||
Output& current_output() {
|
||||
assert(!outputs_.empty());
|
||||
return outputs_.back();
|
||||
}
|
||||
|
||||
// Assign the range_del_agg to the target output level. There's only one
|
||||
// range-del-aggregator per compaction outputs, for
|
||||
// output_to_penultimate_level compaction it is only assigned to the
|
||||
// penultimate level.
|
||||
void AssignRangeDelAggregator(
|
||||
std::unique_ptr<CompactionRangeDelAggregator>&& range_del_agg) {
|
||||
assert(range_del_agg_ == nullptr);
|
||||
range_del_agg_ = std::move(range_del_agg);
|
||||
}
|
||||
|
||||
const Compaction* compaction_;
|
||||
|
||||
// The current file is pending close, which needs to run `close_file_func()`
|
||||
// first to add a new key.
|
||||
bool pending_close_ = false;
|
||||
|
||||
// current output builder and writer
|
||||
std::unique_ptr<TableBuilder> builder_;
|
||||
std::unique_ptr<WritableFileWriter> file_writer_;
|
||||
uint64_t current_output_file_size_ = 0;
|
||||
|
||||
// all the compaction outputs so far
|
||||
std::vector<Output> outputs_;
|
||||
|
||||
// BlobDB info
|
||||
std::vector<BlobFileAddition> blob_file_additions_;
|
||||
std::unique_ptr<BlobGarbageMeter> blob_garbage_meter_;
|
||||
|
||||
// Basic compaction output stats for this level's outputs
|
||||
InternalStats::CompactionOutputsStats stats_;
|
||||
|
||||
// indicate if this CompactionOutputs obj for penultimate_level, should always
|
||||
// be false if per_key_placement feature is not enabled.
|
||||
const bool is_penultimate_level_;
|
||||
std::unique_ptr<CompactionRangeDelAggregator> range_del_agg_ = nullptr;
|
||||
|
||||
// partitioner information
|
||||
std::string last_key_for_partitioner_;
|
||||
std::unique_ptr<SstPartitioner> partitioner_;
|
||||
};
|
||||
|
||||
// helper struct to concatenate the last level and penultimate level outputs
|
||||
// which could be replaced by std::ranges::join_view() in c++20
|
||||
struct OutputIterator {
|
||||
public:
|
||||
explicit OutputIterator(const std::vector<CompactionOutputs::Output>& a,
|
||||
const std::vector<CompactionOutputs::Output>& b)
|
||||
: a_(a), b_(b) {
|
||||
within_a = !a_.empty();
|
||||
idx_ = 0;
|
||||
}
|
||||
|
||||
OutputIterator begin() { return *this; }
|
||||
|
||||
OutputIterator end() { return *this; }
|
||||
|
||||
size_t size() { return a_.size() + b_.size(); }
|
||||
|
||||
const CompactionOutputs::Output& operator*() const {
|
||||
return within_a ? a_[idx_] : b_[idx_];
|
||||
}
|
||||
|
||||
OutputIterator& operator++() {
|
||||
idx_++;
|
||||
if (within_a && idx_ >= a_.size()) {
|
||||
within_a = false;
|
||||
idx_ = 0;
|
||||
}
|
||||
assert(within_a || idx_ <= b_.size());
|
||||
return *this;
|
||||
}
|
||||
|
||||
bool operator!=(const OutputIterator& /*rhs*/) const {
|
||||
return within_a || idx_ < b_.size();
|
||||
}
|
||||
|
||||
private:
|
||||
const std::vector<CompactionOutputs::Output>& a_;
|
||||
const std::vector<CompactionOutputs::Output>& b_;
|
||||
bool within_a;
|
||||
size_t idx_;
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -214,13 +214,13 @@ void CompactionPicker::GetRange(const CompactionInputFiles& inputs1,
|
||||
}
|
||||
|
||||
void CompactionPicker::GetRange(const std::vector<CompactionInputFiles>& inputs,
|
||||
InternalKey* smallest,
|
||||
InternalKey* largest) const {
|
||||
InternalKey* smallest, InternalKey* largest,
|
||||
int exclude_level) const {
|
||||
InternalKey current_smallest;
|
||||
InternalKey current_largest;
|
||||
bool initialized = false;
|
||||
for (const auto& in : inputs) {
|
||||
if (in.empty()) {
|
||||
if (in.empty() || in.level == exclude_level) {
|
||||
continue;
|
||||
}
|
||||
GetRange(in, ¤t_smallest, ¤t_largest);
|
||||
@@ -293,6 +293,12 @@ bool CompactionPicker::RangeOverlapWithCompaction(
|
||||
// Overlap
|
||||
return true;
|
||||
}
|
||||
if (c->SupportsPerKeyPlacement()) {
|
||||
if (c->OverlapPenultimateLevelOutputRange(smallest_user_key,
|
||||
largest_user_key)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
// Did not overlap with any running compaction in level `level`
|
||||
return false;
|
||||
@@ -301,9 +307,11 @@ bool CompactionPicker::RangeOverlapWithCompaction(
|
||||
bool CompactionPicker::FilesRangeOverlapWithCompaction(
|
||||
const std::vector<CompactionInputFiles>& inputs, int level) const {
|
||||
bool is_empty = true;
|
||||
int start_level = -1;
|
||||
for (auto& in : inputs) {
|
||||
if (!in.empty()) {
|
||||
is_empty = false;
|
||||
start_level = in.level; // inputs are sorted by level
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -313,7 +321,19 @@ bool CompactionPicker::FilesRangeOverlapWithCompaction(
|
||||
}
|
||||
|
||||
InternalKey smallest, largest;
|
||||
GetRange(inputs, &smallest, &largest);
|
||||
GetRange(inputs, &smallest, &largest, Compaction::kInvalidLevel);
|
||||
int penultimate_level =
|
||||
Compaction::EvaluatePenultimateLevel(ioptions_, start_level, level);
|
||||
if (penultimate_level != Compaction::kInvalidLevel) {
|
||||
InternalKey penultimate_smallest, penultimate_largest;
|
||||
GetRange(inputs, &penultimate_smallest, &penultimate_largest, level);
|
||||
if (RangeOverlapWithCompaction(penultimate_smallest.user_key(),
|
||||
penultimate_largest.user_key(),
|
||||
penultimate_level)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return RangeOverlapWithCompaction(smallest.user_key(), largest.user_key(),
|
||||
level);
|
||||
}
|
||||
@@ -543,6 +563,10 @@ bool CompactionPicker::SetupOtherInputs(
|
||||
output_level_inputs_size);
|
||||
inputs->files = expanded_inputs.files;
|
||||
}
|
||||
} else {
|
||||
// Likely to be trivial move. Expand files if they are still trivial moves,
|
||||
// but limit to mutable_cf_options.max_compaction_bytes or 8 files so that
|
||||
// we don't create too much compaction pressure for the next level.
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -641,7 +665,8 @@ Compaction* CompactionPicker::CompactRange(
|
||||
GetCompressionOptions(mutable_cf_options, vstorage, output_level),
|
||||
Temperature::kUnknown, compact_range_options.max_subcompactions,
|
||||
/* grandparents */ {}, /* is manual */ true, trim_ts, /* score */ -1,
|
||||
/* deletion_compaction */ false, CompactionReason::kUnknown,
|
||||
/* deletion_compaction */ false, /* l0_files_might_overlap */ true,
|
||||
CompactionReason::kUnknown,
|
||||
compact_range_options.blob_garbage_collection_policy,
|
||||
compact_range_options.blob_garbage_collection_age_cutoff);
|
||||
|
||||
@@ -823,7 +848,8 @@ Compaction* CompactionPicker::CompactRange(
|
||||
GetCompressionOptions(mutable_cf_options, vstorage, output_level),
|
||||
Temperature::kUnknown, compact_range_options.max_subcompactions,
|
||||
std::move(grandparents), /* is manual */ true, trim_ts, /* score */ -1,
|
||||
/* deletion_compaction */ false, CompactionReason::kUnknown,
|
||||
/* deletion_compaction */ false, /* l0_files_might_overlap */ true,
|
||||
CompactionReason::kUnknown,
|
||||
compact_range_options.blob_garbage_collection_policy,
|
||||
compact_range_options.blob_garbage_collection_age_cutoff);
|
||||
|
||||
|
||||
@@ -154,7 +154,8 @@ class CompactionPicker {
|
||||
// in *smallest, *largest.
|
||||
// REQUIRES: inputs is not empty (at least on entry have one file)
|
||||
void GetRange(const std::vector<CompactionInputFiles>& inputs,
|
||||
InternalKey* smallest, InternalKey* largest) const;
|
||||
InternalKey* smallest, InternalKey* largest,
|
||||
int exclude_level) const;
|
||||
|
||||
int NumberLevels() const { return ioptions_.num_levels; }
|
||||
|
||||
@@ -217,6 +218,8 @@ class CompactionPicker {
|
||||
return &compactions_in_progress_;
|
||||
}
|
||||
|
||||
const InternalKeyComparator* icmp() const { return icmp_; }
|
||||
|
||||
protected:
|
||||
const ImmutableOptions& ioptions_;
|
||||
|
||||
|
||||
@@ -116,7 +116,8 @@ Compaction* FIFOCompactionPicker::PickTTLCompaction(
|
||||
mutable_cf_options.compression_opts, Temperature::kUnknown,
|
||||
/* max_subcompactions */ 0, {}, /* is manual */ false,
|
||||
/* trim_ts */ "", vstorage->CompactionScore(0),
|
||||
/* is deletion compaction */ true, CompactionReason::kFIFOTtl);
|
||||
/* is deletion compaction */ true, /* l0_files_might_overlap */ true,
|
||||
CompactionReason::kFIFOTtl);
|
||||
return c;
|
||||
}
|
||||
|
||||
@@ -160,6 +161,7 @@ Compaction* FIFOCompactionPicker::PickSizeCompaction(
|
||||
0 /* max_subcompactions */, {}, /* is manual */ false,
|
||||
/* trim_ts */ "", vstorage->CompactionScore(0),
|
||||
/* is deletion compaction */ false,
|
||||
/* l0_files_might_overlap */ true,
|
||||
CompactionReason::kFIFOReduceNumFiles);
|
||||
return c;
|
||||
}
|
||||
@@ -209,7 +211,8 @@ Compaction* FIFOCompactionPicker::PickSizeCompaction(
|
||||
mutable_cf_options.compression_opts, Temperature::kUnknown,
|
||||
/* max_subcompactions */ 0, {}, /* is manual */ false,
|
||||
/* trim_ts */ "", vstorage->CompactionScore(0),
|
||||
/* is deletion compaction */ true, CompactionReason::kFIFOMaxSize);
|
||||
/* is deletion compaction */ true,
|
||||
/* l0_files_might_overlap */ true, CompactionReason::kFIFOMaxSize);
|
||||
return c;
|
||||
}
|
||||
|
||||
@@ -315,7 +318,8 @@ Compaction* FIFOCompactionPicker::PickCompactionToWarm(
|
||||
Temperature::kWarm,
|
||||
/* max_subcompactions */ 0, {}, /* is manual */ false, /* trim_ts */ "",
|
||||
vstorage->CompactionScore(0),
|
||||
/* is deletion compaction */ false, CompactionReason::kChangeTemperature);
|
||||
/* is deletion compaction */ false, /* l0_files_might_overlap */ true,
|
||||
CompactionReason::kChangeTemperature);
|
||||
return c;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,11 +7,13 @@
|
||||
// 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 "db/compaction/compaction_picker_level.h"
|
||||
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "db/compaction/compaction_picker_level.h"
|
||||
#include "db/version_edit.h"
|
||||
#include "logging/log_buffer.h"
|
||||
#include "test_util/sync_point.h"
|
||||
|
||||
@@ -87,6 +89,9 @@ class LevelCompactionBuilder {
|
||||
// function will return false.
|
||||
bool PickFileToCompact();
|
||||
|
||||
// Return true if a L0 trivial move is picked up.
|
||||
bool TryPickL0TrivialMove();
|
||||
|
||||
// For L0->L0, picks the longest span of files that aren't currently
|
||||
// undergoing compaction for which work-per-deleted-file decreases. The span
|
||||
// always starts from the newest L0 file.
|
||||
@@ -98,6 +103,10 @@ class LevelCompactionBuilder {
|
||||
// otherwise, returns false.
|
||||
bool PickIntraL0Compaction();
|
||||
|
||||
// Return true if TrivialMove is extended. `start_index` is the index of
|
||||
// the intiial file picked, which should already be in `start_level_inputs_`.
|
||||
bool TryExtendNonL0TrivialMove(int start_index);
|
||||
|
||||
// Picks a file from level_files to compact.
|
||||
// level_files is a vector of (level, file metadata) in ascending order of
|
||||
// level. If compact_to_next_level is true, compact the file to the next
|
||||
@@ -117,6 +126,7 @@ class LevelCompactionBuilder {
|
||||
int base_index_ = -1;
|
||||
double start_level_score_ = 0;
|
||||
bool is_manual_ = false;
|
||||
bool is_l0_trivial_move_ = false;
|
||||
CompactionInputFiles start_level_inputs_;
|
||||
std::vector<CompactionInputFiles> compaction_inputs_;
|
||||
CompactionInputFiles output_level_inputs_;
|
||||
@@ -261,7 +271,7 @@ void LevelCompactionBuilder::SetupInitialFiles() {
|
||||
}
|
||||
|
||||
bool LevelCompactionBuilder::SetupOtherL0FilesIfNeeded() {
|
||||
if (start_level_ == 0 && output_level_ != 0) {
|
||||
if (start_level_ == 0 && output_level_ != 0 && !is_l0_trivial_move_) {
|
||||
return compaction_picker_->GetOverlappingL0Files(
|
||||
vstorage_, &start_level_inputs_, output_level_, &parent_index_);
|
||||
}
|
||||
@@ -274,7 +284,8 @@ bool LevelCompactionBuilder::SetupOtherInputsIfNeeded() {
|
||||
// need to consider other levels.
|
||||
if (output_level_ != 0) {
|
||||
output_level_inputs_.level = output_level_;
|
||||
if (!compaction_picker_->SetupOtherInputs(
|
||||
if (!is_l0_trivial_move_ &&
|
||||
!compaction_picker_->SetupOtherInputs(
|
||||
cf_name_, mutable_cf_options_, vstorage_, &start_level_inputs_,
|
||||
&output_level_inputs_, &parent_index_, base_index_)) {
|
||||
return false;
|
||||
@@ -285,20 +296,22 @@ bool LevelCompactionBuilder::SetupOtherInputsIfNeeded() {
|
||||
compaction_inputs_.push_back(output_level_inputs_);
|
||||
}
|
||||
|
||||
// In some edge cases we could pick a compaction that will be compacting
|
||||
// a key range that overlap with another running compaction, and both
|
||||
// of them have the same output level. This could happen if
|
||||
// (1) we are running a non-exclusive manual compaction
|
||||
// (2) AddFile ingest a new file into the LSM tree
|
||||
// We need to disallow this from happening.
|
||||
if (compaction_picker_->FilesRangeOverlapWithCompaction(compaction_inputs_,
|
||||
output_level_)) {
|
||||
// This compaction output could potentially conflict with the output
|
||||
// of a currently running compaction, we cannot run it.
|
||||
return false;
|
||||
if (!is_l0_trivial_move_) {
|
||||
// In some edge cases we could pick a compaction that will be compacting
|
||||
// a key range that overlap with another running compaction, and both
|
||||
// of them have the same output level. This could happen if
|
||||
// (1) we are running a non-exclusive manual compaction
|
||||
// (2) AddFile ingest a new file into the LSM tree
|
||||
// We need to disallow this from happening.
|
||||
if (compaction_picker_->FilesRangeOverlapWithCompaction(
|
||||
compaction_inputs_, output_level_)) {
|
||||
// This compaction output could potentially conflict with the output
|
||||
// of a currently running compaction, we cannot run it.
|
||||
return false;
|
||||
}
|
||||
compaction_picker_->GetGrandparents(vstorage_, start_level_inputs_,
|
||||
output_level_inputs_, &grandparents_);
|
||||
}
|
||||
compaction_picker_->GetGrandparents(vstorage_, start_level_inputs_,
|
||||
output_level_inputs_, &grandparents_);
|
||||
} else {
|
||||
compaction_inputs_.push_back(start_level_inputs_);
|
||||
}
|
||||
@@ -349,6 +362,7 @@ Compaction* LevelCompactionBuilder::GetCompaction() {
|
||||
Temperature::kUnknown,
|
||||
/* max_subcompactions */ 0, std::move(grandparents_), is_manual_,
|
||||
/* trim_ts */ "", start_level_score_, false /* deletion_compaction */,
|
||||
/* l0_files_might_overlap */ start_level_ == 0 && !is_l0_trivial_move_,
|
||||
compaction_reason_);
|
||||
|
||||
// If it's level 0 compaction, make sure we don't execute any other level 0
|
||||
@@ -417,6 +431,124 @@ uint32_t LevelCompactionBuilder::GetPathId(
|
||||
return p;
|
||||
}
|
||||
|
||||
bool LevelCompactionBuilder::TryPickL0TrivialMove() {
|
||||
if (vstorage_->base_level() <= 0) {
|
||||
return false;
|
||||
}
|
||||
if (start_level_ == 0 && mutable_cf_options_.compression_per_level.empty() &&
|
||||
!vstorage_->LevelFiles(output_level_).empty() &&
|
||||
ioptions_.db_paths.size() <= 1) {
|
||||
// Try to pick trivial move from L0 to L1. We start from the oldest
|
||||
// file. We keep expanding to newer files if it would form a
|
||||
// trivial move.
|
||||
// For now we don't support it with
|
||||
// mutable_cf_options_.compression_per_level to prevent the logic
|
||||
// of determining whether L0 can be trivial moved to the next level.
|
||||
// We skip the case where output level is empty, since in this case, at
|
||||
// least the oldest file would qualify for trivial move, and this would
|
||||
// be a surprising behavior with few benefits.
|
||||
|
||||
// We search from the oldest file from the newest. In theory, there are
|
||||
// files in the middle can form trivial move too, but it is probably
|
||||
// uncommon and we ignore these cases for simplicity.
|
||||
const std::vector<FileMetaData*>& level_files =
|
||||
vstorage_->LevelFiles(start_level_);
|
||||
|
||||
InternalKey my_smallest, my_largest;
|
||||
for (auto it = level_files.rbegin(); it != level_files.rend(); ++it) {
|
||||
CompactionInputFiles output_level_inputs;
|
||||
output_level_inputs.level = output_level_;
|
||||
FileMetaData* file = *it;
|
||||
if (it == level_files.rbegin()) {
|
||||
my_smallest = file->smallest;
|
||||
my_largest = file->largest;
|
||||
} else {
|
||||
if (compaction_picker_->icmp()->Compare(file->largest, my_smallest) <
|
||||
0) {
|
||||
my_smallest = file->smallest;
|
||||
} else if (compaction_picker_->icmp()->Compare(file->smallest,
|
||||
my_largest) > 0) {
|
||||
my_largest = file->largest;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
vstorage_->GetOverlappingInputs(output_level_, &my_smallest, &my_largest,
|
||||
&output_level_inputs.files);
|
||||
if (output_level_inputs.empty()) {
|
||||
assert(!file->being_compacted);
|
||||
start_level_inputs_.files.push_back(file);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!start_level_inputs_.empty()) {
|
||||
// Sort files by key range. Not sure it's 100% necessary but it's cleaner
|
||||
// to always keep files sorted by key the key ranges don't overlap.
|
||||
std::sort(start_level_inputs_.files.begin(),
|
||||
start_level_inputs_.files.end(),
|
||||
[icmp = compaction_picker_->icmp()](FileMetaData* f1,
|
||||
FileMetaData* f2) -> bool {
|
||||
return (icmp->Compare(f1->smallest, f2->smallest) < 0);
|
||||
});
|
||||
|
||||
is_l0_trivial_move_ = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool LevelCompactionBuilder::TryExtendNonL0TrivialMove(int start_index) {
|
||||
if (start_level_inputs_.size() == 1 &&
|
||||
(ioptions_.db_paths.empty() || ioptions_.db_paths.size() == 1) &&
|
||||
(mutable_cf_options_.compression_per_level.empty())) {
|
||||
// Only file of `index`, and it is likely a trivial move. Try to
|
||||
// expand if it is still a trivial move, but not beyond
|
||||
// max_compaction_bytes or 4 files, so that we don't create too
|
||||
// much compaction pressure for the next level.
|
||||
// Ignore if there are more than one DB path, as it would be hard
|
||||
// to predict whether it is a trivial move.
|
||||
const std::vector<FileMetaData*>& level_files =
|
||||
vstorage_->LevelFiles(start_level_);
|
||||
const size_t kMaxMultiTrivialMove = 4;
|
||||
FileMetaData* initial_file = start_level_inputs_.files[0];
|
||||
size_t total_size = initial_file->fd.GetFileSize();
|
||||
CompactionInputFiles output_level_inputs;
|
||||
output_level_inputs.level = output_level_;
|
||||
for (int i = start_index + 1;
|
||||
i < static_cast<int>(level_files.size()) &&
|
||||
start_level_inputs_.size() < kMaxMultiTrivialMove;
|
||||
i++) {
|
||||
FileMetaData* next_file = level_files[i];
|
||||
if (next_file->being_compacted) {
|
||||
break;
|
||||
}
|
||||
vstorage_->GetOverlappingInputs(output_level_, &(initial_file->smallest),
|
||||
&(next_file->largest),
|
||||
&output_level_inputs.files);
|
||||
if (!output_level_inputs.empty()) {
|
||||
break;
|
||||
}
|
||||
if (i < static_cast<int>(level_files.size()) - 1 &&
|
||||
compaction_picker_->icmp()->user_comparator()->Compare(
|
||||
next_file->largest.user_key(),
|
||||
level_files[i + 1]->smallest.user_key()) == 0) {
|
||||
// Not a clean up after adding the next file. Skip.
|
||||
break;
|
||||
}
|
||||
total_size += next_file->fd.GetFileSize();
|
||||
if (total_size > mutable_cf_options_.max_compaction_bytes) {
|
||||
break;
|
||||
}
|
||||
start_level_inputs_.files.push_back(next_file);
|
||||
}
|
||||
return start_level_inputs_.size() > 1;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool LevelCompactionBuilder::PickFileToCompact() {
|
||||
// level 0 files are overlapping. So we cannot pick more
|
||||
// than one concurrent compactions at this level. This
|
||||
@@ -429,30 +561,43 @@ bool LevelCompactionBuilder::PickFileToCompact() {
|
||||
}
|
||||
|
||||
start_level_inputs_.clear();
|
||||
start_level_inputs_.level = start_level_;
|
||||
|
||||
assert(start_level_ >= 0);
|
||||
|
||||
// Pick the largest file in this level that is not already
|
||||
// being compacted
|
||||
const std::vector<int>& file_size =
|
||||
vstorage_->FilesByCompactionPri(start_level_);
|
||||
if (TryPickL0TrivialMove()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const std::vector<FileMetaData*>& level_files =
|
||||
vstorage_->LevelFiles(start_level_);
|
||||
|
||||
// Pick the file with the highest score in this level that is not already
|
||||
// being compacted.
|
||||
const std::vector<int>& file_scores =
|
||||
vstorage_->FilesByCompactionPri(start_level_);
|
||||
|
||||
unsigned int cmp_idx;
|
||||
for (cmp_idx = vstorage_->NextCompactionIndex(start_level_);
|
||||
cmp_idx < file_size.size(); cmp_idx++) {
|
||||
int index = file_size[cmp_idx];
|
||||
cmp_idx < file_scores.size(); cmp_idx++) {
|
||||
int index = file_scores[cmp_idx];
|
||||
auto* f = level_files[index];
|
||||
|
||||
// do not pick a file to compact if it is being compacted
|
||||
// from n-1 level.
|
||||
if (f->being_compacted) {
|
||||
if (ioptions_.compaction_pri == kRoundRobin) {
|
||||
// TODO(zichen): this file may be involved in one compaction from
|
||||
// an upper level, cannot advance the cursor for round-robin policy.
|
||||
// Currently, we do not pick any file to compact in this case. We
|
||||
// should fix this later to ensure a compaction is picked but the
|
||||
// cursor shall not be advanced.
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
start_level_inputs_.files.push_back(f);
|
||||
start_level_inputs_.level = start_level_;
|
||||
if (!compaction_picker_->ExpandInputsToCleanCut(cf_name_, vstorage_,
|
||||
&start_level_inputs_) ||
|
||||
compaction_picker_->FilesRangeOverlapWithCompaction(
|
||||
@@ -460,11 +605,18 @@ bool LevelCompactionBuilder::PickFileToCompact() {
|
||||
// A locked (pending compaction) input-level file was pulled in due to
|
||||
// user-key overlap.
|
||||
start_level_inputs_.clear();
|
||||
|
||||
// To ensure every file is selcted in a round-robin manner, we cannot
|
||||
// skip the current file. So we return false and wait for the next time
|
||||
// we can pick this file to compact
|
||||
if (ioptions_.compaction_pri == kRoundRobin) {
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Now that input level is fully expanded, we check whether any output files
|
||||
// are locked due to pending compaction.
|
||||
// Now that input level is fully expanded, we check whether any output
|
||||
// files are locked due to pending compaction.
|
||||
//
|
||||
// Note we rely on ExpandInputsToCleanCut() to tell us whether any output-
|
||||
// level files are locked, not just the extra ones pulled in for user-key
|
||||
@@ -475,19 +627,28 @@ bool LevelCompactionBuilder::PickFileToCompact() {
|
||||
output_level_inputs.level = output_level_;
|
||||
vstorage_->GetOverlappingInputs(output_level_, &smallest, &largest,
|
||||
&output_level_inputs.files);
|
||||
if (!output_level_inputs.empty() &&
|
||||
!compaction_picker_->ExpandInputsToCleanCut(cf_name_, vstorage_,
|
||||
&output_level_inputs)) {
|
||||
start_level_inputs_.clear();
|
||||
continue;
|
||||
if (output_level_inputs.empty()) {
|
||||
if (TryExtendNonL0TrivialMove(index)) {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
if (!compaction_picker_->ExpandInputsToCleanCut(cf_name_, vstorage_,
|
||||
&output_level_inputs)) {
|
||||
start_level_inputs_.clear();
|
||||
if (ioptions_.compaction_pri == kRoundRobin) {
|
||||
return false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
base_index_ = index;
|
||||
break;
|
||||
}
|
||||
|
||||
// store where to start the iteration in the next call to PickCompaction
|
||||
vstorage_->SetNextCompactionIndex(start_level_, cmp_idx);
|
||||
|
||||
if (ioptions_.compaction_pri != kRoundRobin) {
|
||||
vstorage_->SetNextCompactionIndex(start_level_, cmp_idx);
|
||||
}
|
||||
return start_level_inputs_.size() > 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "db/compaction/compaction_picker_level.h"
|
||||
#include "db/compaction/compaction_picker_universal.h"
|
||||
#include "db/compaction/file_pri.h"
|
||||
#include "rocksdb/advanced_options.h"
|
||||
#include "table/unique_id_impl.h"
|
||||
#include "test_util/testharness.h"
|
||||
#include "test_util/testutil.h"
|
||||
@@ -429,8 +430,7 @@ TEST_F(CompactionPickerTest, LevelTriggerDynamic4) {
|
||||
#ifndef ROCKSDB_LITE
|
||||
TEST_F(CompactionPickerTest, NeedsCompactionUniversal) {
|
||||
NewVersionStorage(1, kCompactionStyleUniversal);
|
||||
UniversalCompactionPicker universal_compaction_picker(
|
||||
ioptions_, &icmp_);
|
||||
UniversalCompactionPicker universal_compaction_picker(ioptions_, &icmp_);
|
||||
UpdateVersionStorageInfo();
|
||||
// must return false when there's no files.
|
||||
ASSERT_EQ(universal_compaction_picker.NeedsCompaction(vstorage_.get()),
|
||||
@@ -1311,6 +1311,99 @@ TEST_F(CompactionPickerTest, CompactionPriMinOverlapping4) {
|
||||
ASSERT_EQ(6U, compaction->input(0, 0)->fd.GetNumber());
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerTest, CompactionPriRoundRobin) {
|
||||
std::vector<InternalKey> test_cursors = {InternalKey("249", 100, kTypeValue),
|
||||
InternalKey("600", 100, kTypeValue),
|
||||
InternalKey()};
|
||||
std::vector<uint32_t> selected_files = {8U, 6U, 6U};
|
||||
|
||||
ioptions_.compaction_pri = kRoundRobin;
|
||||
mutable_cf_options_.max_bytes_for_level_base = 10000000;
|
||||
mutable_cf_options_.max_bytes_for_level_multiplier = 10;
|
||||
for (size_t i = 0; i < test_cursors.size(); i++) {
|
||||
// start a brand new version in each test.
|
||||
NewVersionStorage(6, kCompactionStyleLevel);
|
||||
vstorage_->ResizeCompactCursors(6);
|
||||
// Set the cursor
|
||||
vstorage_->AddCursorForOneLevel(2, test_cursors[i]);
|
||||
Add(2, 6U, "150", "199", 50000000U); // Overlap with 26U, 27U
|
||||
Add(2, 7U, "200", "249", 50000000U); // File not overlapping
|
||||
Add(2, 8U, "300", "600", 50000000U); // Overlap with 28U, 29U
|
||||
|
||||
Add(3, 26U, "130", "165", 60000000U);
|
||||
Add(3, 27U, "166", "170", 60000000U);
|
||||
Add(3, 28U, "270", "340", 60000000U);
|
||||
Add(3, 29U, "401", "500", 60000000U);
|
||||
UpdateVersionStorageInfo();
|
||||
LevelCompactionPicker local_level_compaction_picker =
|
||||
LevelCompactionPicker(ioptions_, &icmp_);
|
||||
std::unique_ptr<Compaction> compaction(
|
||||
local_level_compaction_picker.PickCompaction(
|
||||
cf_name_, mutable_cf_options_, mutable_db_options_, vstorage_.get(),
|
||||
&log_buffer_));
|
||||
ASSERT_TRUE(compaction.get() != nullptr);
|
||||
ASSERT_EQ(1U, compaction->num_input_files(0));
|
||||
ASSERT_EQ(selected_files[i], compaction->input(0, 0)->fd.GetNumber());
|
||||
// release the version storage
|
||||
DeleteVersionStorage();
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerTest, CompactionPriMinOverlappingManyFiles) {
|
||||
NewVersionStorage(6, kCompactionStyleLevel);
|
||||
ioptions_.compaction_pri = kMinOverlappingRatio;
|
||||
mutable_cf_options_.max_bytes_for_level_base = 15000000;
|
||||
mutable_cf_options_.max_bytes_for_level_multiplier = 10;
|
||||
|
||||
// file 7 and 8 over lap with the same file, but file 8 is smaller so
|
||||
// it will be picked.
|
||||
Add(2, 13U, "010", "011",
|
||||
6100U); // Overlaps with a large file. Not picked
|
||||
Add(2, 14U, "020", "021",
|
||||
6100U); // Overlaps with a large file. Not picked
|
||||
Add(2, 15U, "030", "031",
|
||||
6100U); // Overlaps with a large file. Not picked
|
||||
Add(2, 16U, "040", "041",
|
||||
6100U); // Overlaps with a large file. Not picked
|
||||
Add(2, 17U, "050", "051",
|
||||
6100U); // Overlaps with a large file. Not picked
|
||||
Add(2, 18U, "060", "061",
|
||||
6100U); // Overlaps with a large file. Not picked
|
||||
Add(2, 19U, "070", "071",
|
||||
6100U); // Overlaps with a large file. Not picked
|
||||
Add(2, 20U, "080", "081",
|
||||
6100U); // Overlaps with a large file. Not picked
|
||||
|
||||
Add(2, 6U, "150", "167", 60000000U); // Overlaps with file 26, 27
|
||||
Add(2, 7U, "168", "169", 60000000U); // Overlaps with file 27
|
||||
Add(2, 8U, "201", "300", 61000000U); // Overlaps with file 28, but the file
|
||||
// itself is larger. Should be picked.
|
||||
Add(2, 9U, "610", "611",
|
||||
6100U); // Overlaps with a large file. Not picked
|
||||
Add(2, 10U, "620", "621",
|
||||
6100U); // Overlaps with a large file. Not picked
|
||||
Add(2, 11U, "630", "631",
|
||||
6100U); // Overlaps with a large file. Not picked
|
||||
Add(2, 12U, "640", "641",
|
||||
6100U); // Overlaps with a large file. Not picked
|
||||
|
||||
Add(3, 31U, "001", "100", 260000000U);
|
||||
Add(3, 26U, "160", "165", 260000000U);
|
||||
Add(3, 27U, "166", "170", 260000000U);
|
||||
Add(3, 28U, "180", "400", 260000000U);
|
||||
Add(3, 29U, "401", "500", 260000000U);
|
||||
Add(3, 30U, "601", "700", 260000000U);
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
std::unique_ptr<Compaction> compaction(level_compaction_picker.PickCompaction(
|
||||
cf_name_, mutable_cf_options_, mutable_db_options_, vstorage_.get(),
|
||||
&log_buffer_));
|
||||
ASSERT_TRUE(compaction.get() != nullptr);
|
||||
ASSERT_EQ(1U, compaction->num_input_files(0));
|
||||
// Picking file 8 because overlapping ratio is the biggest.
|
||||
ASSERT_EQ(8U, compaction->input(0, 0)->fd.GetNumber());
|
||||
}
|
||||
|
||||
// This test exhibits the bug where we don't properly reset parent_index in
|
||||
// PickCompaction()
|
||||
TEST_F(CompactionPickerTest, ParentIndexResetBug) {
|
||||
@@ -2156,6 +2249,92 @@ TEST_F(CompactionPickerTest, IsTrivialMoveOn) {
|
||||
ASSERT_TRUE(compaction->IsTrivialMove());
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerTest, L0TrivialMove1) {
|
||||
mutable_cf_options_.max_bytes_for_level_base = 10000000u;
|
||||
mutable_cf_options_.level0_file_num_compaction_trigger = 4;
|
||||
mutable_cf_options_.max_compaction_bytes = 10000000u;
|
||||
ioptions_.level_compaction_dynamic_level_bytes = false;
|
||||
NewVersionStorage(6, kCompactionStyleLevel);
|
||||
|
||||
Add(0, 1U, "100", "150", 3000U, 0, 710, 800);
|
||||
Add(0, 2U, "151", "200", 3001U, 0, 610, 700);
|
||||
Add(0, 3U, "301", "350", 3000U, 0, 510, 600);
|
||||
Add(0, 4U, "451", "400", 3000U, 0, 410, 500);
|
||||
|
||||
Add(1, 5U, "120", "130", 7000U);
|
||||
Add(1, 6U, "170", "180", 7000U);
|
||||
Add(1, 7U, "220", "230", 7000U);
|
||||
Add(1, 8U, "270", "280", 7000U);
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
std::unique_ptr<Compaction> compaction(level_compaction_picker.PickCompaction(
|
||||
cf_name_, mutable_cf_options_, mutable_db_options_, vstorage_.get(),
|
||||
&log_buffer_));
|
||||
ASSERT_TRUE(compaction.get() != nullptr);
|
||||
ASSERT_EQ(1, compaction->num_input_levels());
|
||||
ASSERT_EQ(2, compaction->num_input_files(0));
|
||||
ASSERT_EQ(3, compaction->input(0, 0)->fd.GetNumber());
|
||||
ASSERT_EQ(4, compaction->input(0, 1)->fd.GetNumber());
|
||||
ASSERT_TRUE(compaction->IsTrivialMove());
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerTest, L0TrivialMoveOneFile) {
|
||||
mutable_cf_options_.max_bytes_for_level_base = 10000000u;
|
||||
mutable_cf_options_.level0_file_num_compaction_trigger = 4;
|
||||
mutable_cf_options_.max_compaction_bytes = 10000000u;
|
||||
ioptions_.level_compaction_dynamic_level_bytes = false;
|
||||
NewVersionStorage(6, kCompactionStyleLevel);
|
||||
|
||||
Add(0, 1U, "100", "150", 3000U, 0, 710, 800);
|
||||
Add(0, 2U, "551", "600", 3001U, 0, 610, 700);
|
||||
Add(0, 3U, "101", "150", 3000U, 0, 510, 600);
|
||||
Add(0, 4U, "451", "400", 3000U, 0, 410, 500);
|
||||
|
||||
Add(1, 5U, "120", "130", 7000U);
|
||||
Add(1, 6U, "170", "180", 7000U);
|
||||
Add(1, 7U, "220", "230", 7000U);
|
||||
Add(1, 8U, "270", "280", 7000U);
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
std::unique_ptr<Compaction> compaction(level_compaction_picker.PickCompaction(
|
||||
cf_name_, mutable_cf_options_, mutable_db_options_, vstorage_.get(),
|
||||
&log_buffer_));
|
||||
ASSERT_TRUE(compaction.get() != nullptr);
|
||||
ASSERT_EQ(1, compaction->num_input_levels());
|
||||
ASSERT_EQ(1, compaction->num_input_files(0));
|
||||
ASSERT_EQ(4, compaction->input(0, 0)->fd.GetNumber());
|
||||
ASSERT_TRUE(compaction->IsTrivialMove());
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerTest, L0TrivialMoveWholeL0) {
|
||||
mutable_cf_options_.max_bytes_for_level_base = 10000000u;
|
||||
mutable_cf_options_.level0_file_num_compaction_trigger = 4;
|
||||
mutable_cf_options_.max_compaction_bytes = 10000000u;
|
||||
ioptions_.level_compaction_dynamic_level_bytes = false;
|
||||
NewVersionStorage(6, kCompactionStyleLevel);
|
||||
|
||||
Add(0, 1U, "300", "350", 3000U, 0, 710, 800);
|
||||
Add(0, 2U, "651", "600", 3001U, 0, 610, 700);
|
||||
Add(0, 3U, "501", "550", 3000U, 0, 510, 600);
|
||||
Add(0, 4U, "451", "400", 3000U, 0, 410, 500);
|
||||
|
||||
Add(1, 5U, "120", "130", 7000U);
|
||||
Add(1, 6U, "970", "980", 7000U);
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
std::unique_ptr<Compaction> compaction(level_compaction_picker.PickCompaction(
|
||||
cf_name_, mutable_cf_options_, mutable_db_options_, vstorage_.get(),
|
||||
&log_buffer_));
|
||||
ASSERT_TRUE(compaction.get() != nullptr);
|
||||
ASSERT_EQ(1, compaction->num_input_levels());
|
||||
ASSERT_EQ(4, compaction->num_input_files(0));
|
||||
ASSERT_EQ(1, compaction->input(0, 0)->fd.GetNumber());
|
||||
ASSERT_EQ(4, compaction->input(0, 1)->fd.GetNumber());
|
||||
ASSERT_EQ(3, compaction->input(0, 2)->fd.GetNumber());
|
||||
ASSERT_EQ(2, compaction->input(0, 3)->fd.GetNumber());
|
||||
ASSERT_TRUE(compaction->IsTrivialMove());
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerTest, IsTrivialMoveOffSstPartitioned) {
|
||||
mutable_cf_options_.max_bytes_for_level_base = 10000u;
|
||||
mutable_cf_options_.max_compaction_bytes = 10001u;
|
||||
@@ -2204,6 +2383,197 @@ TEST_F(CompactionPickerTest, IsTrivialMoveOff) {
|
||||
ASSERT_FALSE(compaction->IsTrivialMove());
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerTest, TrivialMoveMultipleFiles1) {
|
||||
mutable_cf_options_.max_bytes_for_level_base = 1000u;
|
||||
mutable_cf_options_.max_compaction_bytes = 10000001u;
|
||||
ioptions_.level_compaction_dynamic_level_bytes = false;
|
||||
ioptions_.compaction_pri = kMinOverlappingRatio;
|
||||
NewVersionStorage(6, kCompactionStyleLevel);
|
||||
|
||||
Add(2, 1U, "100", "150", 3000U);
|
||||
Add(2, 2U, "151", "200", 3001U);
|
||||
Add(2, 3U, "301", "350", 3000U);
|
||||
Add(2, 4U, "451", "400", 3000U);
|
||||
Add(2, 5U, "551", "500", 3000U);
|
||||
Add(2, 6U, "651", "600", 3000U);
|
||||
Add(2, 7U, "751", "700", 3000U);
|
||||
Add(2, 8U, "851", "900", 3000U);
|
||||
|
||||
Add(3, 15U, "120", "130", 700U);
|
||||
Add(3, 16U, "170", "180", 700U);
|
||||
Add(3, 17U, "220", "230", 700U);
|
||||
Add(3, 18U, "870", "880", 700U);
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
std::unique_ptr<Compaction> compaction(level_compaction_picker.PickCompaction(
|
||||
cf_name_, mutable_cf_options_, mutable_db_options_, vstorage_.get(),
|
||||
&log_buffer_));
|
||||
ASSERT_TRUE(compaction.get() != nullptr);
|
||||
ASSERT_TRUE(compaction->IsTrivialMove());
|
||||
ASSERT_EQ(1, compaction->num_input_levels());
|
||||
ASSERT_EQ(4, compaction->num_input_files(0));
|
||||
ASSERT_EQ(3, compaction->input(0, 0)->fd.GetNumber());
|
||||
ASSERT_EQ(4, compaction->input(0, 1)->fd.GetNumber());
|
||||
ASSERT_EQ(5, compaction->input(0, 2)->fd.GetNumber());
|
||||
ASSERT_EQ(6, compaction->input(0, 3)->fd.GetNumber());
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerTest, TrivialMoveMultipleFiles2) {
|
||||
mutable_cf_options_.max_bytes_for_level_base = 1000u;
|
||||
mutable_cf_options_.max_compaction_bytes = 10000001u;
|
||||
ioptions_.level_compaction_dynamic_level_bytes = false;
|
||||
ioptions_.compaction_pri = kMinOverlappingRatio;
|
||||
NewVersionStorage(6, kCompactionStyleLevel);
|
||||
|
||||
Add(2, 1U, "100", "150", 3000U);
|
||||
Add(2, 2U, "151", "160", 3001U);
|
||||
Add(2, 3U, "161", "179", 3000U);
|
||||
Add(2, 4U, "220", "400", 3000U);
|
||||
Add(2, 5U, "551", "500", 3000U);
|
||||
Add(2, 6U, "651", "600", 3000U);
|
||||
Add(2, 7U, "751", "700", 3000U);
|
||||
Add(2, 8U, "851", "900", 3000U);
|
||||
|
||||
Add(3, 15U, "120", "130", 700U);
|
||||
Add(3, 17U, "220", "230", 700U);
|
||||
Add(3, 18U, "870", "880", 700U);
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
std::unique_ptr<Compaction> compaction(level_compaction_picker.PickCompaction(
|
||||
cf_name_, mutable_cf_options_, mutable_db_options_, vstorage_.get(),
|
||||
&log_buffer_));
|
||||
ASSERT_TRUE(compaction.get() != nullptr);
|
||||
ASSERT_TRUE(compaction->IsTrivialMove());
|
||||
ASSERT_EQ(1, compaction->num_input_levels());
|
||||
ASSERT_EQ(2, compaction->num_input_files(0));
|
||||
ASSERT_EQ(2, compaction->input(0, 0)->fd.GetNumber());
|
||||
ASSERT_EQ(3, compaction->input(0, 1)->fd.GetNumber());
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerTest, TrivialMoveMultipleFiles3) {
|
||||
mutable_cf_options_.max_bytes_for_level_base = 1000u;
|
||||
mutable_cf_options_.max_compaction_bytes = 10000001u;
|
||||
ioptions_.level_compaction_dynamic_level_bytes = false;
|
||||
ioptions_.compaction_pri = kMinOverlappingRatio;
|
||||
NewVersionStorage(6, kCompactionStyleLevel);
|
||||
|
||||
// Even if consecutive files can be trivial moved, we don't pick them
|
||||
// since in case trivial move can't be issued for a reason, we cannot
|
||||
// fall back to normal compactions.
|
||||
Add(2, 1U, "100", "150", 3000U);
|
||||
Add(2, 2U, "151", "160", 3001U);
|
||||
Add(2, 5U, "551", "500", 3000U);
|
||||
Add(2, 6U, "651", "600", 3000U);
|
||||
Add(2, 7U, "751", "700", 3000U);
|
||||
Add(2, 8U, "851", "900", 3000U);
|
||||
|
||||
Add(3, 15U, "120", "130", 700U);
|
||||
Add(3, 17U, "220", "230", 700U);
|
||||
Add(3, 18U, "870", "880", 700U);
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
std::unique_ptr<Compaction> compaction(level_compaction_picker.PickCompaction(
|
||||
cf_name_, mutable_cf_options_, mutable_db_options_, vstorage_.get(),
|
||||
&log_buffer_));
|
||||
ASSERT_TRUE(compaction.get() != nullptr);
|
||||
ASSERT_TRUE(compaction->IsTrivialMove());
|
||||
ASSERT_EQ(1, compaction->num_input_levels());
|
||||
ASSERT_EQ(1, compaction->num_input_files(0));
|
||||
ASSERT_EQ(2, compaction->input(0, 0)->fd.GetNumber());
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerTest, TrivialMoveMultipleFiles4) {
|
||||
mutable_cf_options_.max_bytes_for_level_base = 1000u;
|
||||
mutable_cf_options_.max_compaction_bytes = 10000001u;
|
||||
ioptions_.level_compaction_dynamic_level_bytes = false;
|
||||
ioptions_.compaction_pri = kMinOverlappingRatio;
|
||||
NewVersionStorage(6, kCompactionStyleLevel);
|
||||
|
||||
Add(2, 1U, "100", "150", 4000U);
|
||||
Add(2, 2U, "151", "160", 4001U);
|
||||
Add(2, 3U, "161", "179", 4000U);
|
||||
|
||||
Add(3, 15U, "120", "130", 700U);
|
||||
Add(3, 17U, "220", "230", 700U);
|
||||
Add(3, 18U, "870", "880", 700U);
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
std::unique_ptr<Compaction> compaction(level_compaction_picker.PickCompaction(
|
||||
cf_name_, mutable_cf_options_, mutable_db_options_, vstorage_.get(),
|
||||
&log_buffer_));
|
||||
ASSERT_TRUE(compaction.get() != nullptr);
|
||||
ASSERT_TRUE(compaction->IsTrivialMove());
|
||||
ASSERT_EQ(1, compaction->num_input_levels());
|
||||
ASSERT_EQ(2, compaction->num_input_files(0));
|
||||
ASSERT_EQ(2, compaction->input(0, 0)->fd.GetNumber());
|
||||
ASSERT_EQ(3, compaction->input(0, 1)->fd.GetNumber());
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerTest, TrivialMoveMultipleFiles5) {
|
||||
mutable_cf_options_.max_bytes_for_level_base = 1000u;
|
||||
mutable_cf_options_.max_compaction_bytes = 10000001u;
|
||||
ioptions_.level_compaction_dynamic_level_bytes = false;
|
||||
ioptions_.compaction_pri = kMinOverlappingRatio;
|
||||
NewVersionStorage(6, kCompactionStyleLevel);
|
||||
|
||||
// File 4 and 5 aren't clean cut, so only 2 and 3 are picked.
|
||||
Add(2, 1U, "100", "150", 4000U);
|
||||
Add(2, 2U, "151", "160", 4001U);
|
||||
Add(2, 3U, "161", "179", 4000U);
|
||||
Add(2, 4U, "180", "185", 4000U);
|
||||
Add(2, 5U, "185", "190", 4000U);
|
||||
|
||||
Add(3, 15U, "120", "130", 700U);
|
||||
Add(3, 17U, "220", "230", 700U);
|
||||
Add(3, 18U, "870", "880", 700U);
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
std::unique_ptr<Compaction> compaction(level_compaction_picker.PickCompaction(
|
||||
cf_name_, mutable_cf_options_, mutable_db_options_, vstorage_.get(),
|
||||
&log_buffer_));
|
||||
ASSERT_TRUE(compaction.get() != nullptr);
|
||||
ASSERT_TRUE(compaction->IsTrivialMove());
|
||||
ASSERT_EQ(1, compaction->num_input_levels());
|
||||
ASSERT_EQ(2, compaction->num_input_files(0));
|
||||
ASSERT_EQ(2, compaction->input(0, 0)->fd.GetNumber());
|
||||
ASSERT_EQ(3, compaction->input(0, 1)->fd.GetNumber());
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerTest, TrivialMoveMultipleFiles6) {
|
||||
mutable_cf_options_.max_bytes_for_level_base = 1000u;
|
||||
mutable_cf_options_.max_compaction_bytes = 10000001u;
|
||||
ioptions_.level_compaction_dynamic_level_bytes = false;
|
||||
ioptions_.compaction_pri = kMinOverlappingRatio;
|
||||
NewVersionStorage(6, kCompactionStyleLevel);
|
||||
|
||||
Add(2, 1U, "100", "150", 3000U);
|
||||
Add(2, 2U, "151", "200", 3001U);
|
||||
Add(2, 3U, "301", "350", 3000U);
|
||||
Add(2, 4U, "451", "400", 3000U);
|
||||
Add(2, 5U, "551", "500", 3000U);
|
||||
file_map_[5U].first->being_compacted = true;
|
||||
Add(2, 6U, "651", "600", 3000U);
|
||||
Add(2, 7U, "751", "700", 3000U);
|
||||
Add(2, 8U, "851", "900", 3000U);
|
||||
|
||||
Add(3, 15U, "120", "130", 700U);
|
||||
Add(3, 16U, "170", "180", 700U);
|
||||
Add(3, 17U, "220", "230", 700U);
|
||||
Add(3, 18U, "870", "880", 700U);
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
std::unique_ptr<Compaction> compaction(level_compaction_picker.PickCompaction(
|
||||
cf_name_, mutable_cf_options_, mutable_db_options_, vstorage_.get(),
|
||||
&log_buffer_));
|
||||
ASSERT_TRUE(compaction.get() != nullptr);
|
||||
ASSERT_TRUE(compaction->IsTrivialMove());
|
||||
ASSERT_EQ(1, compaction->num_input_levels());
|
||||
// Since the next file is being compacted. Stopping at 3 and 4.
|
||||
ASSERT_EQ(2, compaction->num_input_files(0));
|
||||
ASSERT_EQ(3, compaction->input(0, 0)->fd.GetNumber());
|
||||
ASSERT_EQ(4, compaction->input(0, 1)->fd.GetNumber());
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerTest, CacheNextCompactionIndex) {
|
||||
NewVersionStorage(6, kCompactionStyleLevel);
|
||||
mutable_cf_options_.max_compaction_bytes = 100000000000u;
|
||||
@@ -2218,7 +2588,11 @@ TEST_F(CompactionPickerTest, CacheNextCompactionIndex) {
|
||||
Add(1 /* level */, 4U /* file_number */, "250" /* smallest */,
|
||||
"299" /* largest */, 700000000U /* file_size */);
|
||||
Add(2 /* level */, 5U /* file_number */, "150" /* smallest */,
|
||||
"199" /* largest */, 1U /* file_size */);
|
||||
"199" /* largest */, 100U /* file_size */);
|
||||
Add(2 /* level */, 6U /* file_number */, "200" /* smallest */,
|
||||
"240" /* largest */, 1U /* file_size */);
|
||||
Add(2 /* level */, 7U /* file_number */, "260" /* smallest */,
|
||||
"270" /* largest */, 1U /* file_size */);
|
||||
file_map_[5U].first->being_compacted = true;
|
||||
|
||||
UpdateVersionStorageInfo();
|
||||
@@ -2227,9 +2601,9 @@ TEST_F(CompactionPickerTest, CacheNextCompactionIndex) {
|
||||
cf_name_, mutable_cf_options_, mutable_db_options_, vstorage_.get(),
|
||||
&log_buffer_));
|
||||
ASSERT_TRUE(compaction.get() != nullptr);
|
||||
ASSERT_EQ(1U, compaction->num_input_levels());
|
||||
ASSERT_EQ(2U, compaction->num_input_levels());
|
||||
ASSERT_EQ(1U, compaction->num_input_files(0));
|
||||
ASSERT_EQ(0U, compaction->num_input_files(1));
|
||||
ASSERT_EQ(1U, compaction->num_input_files(1));
|
||||
ASSERT_EQ(3U, compaction->input(0, 0)->fd.GetNumber());
|
||||
ASSERT_EQ(2, vstorage_->NextCompactionIndex(1 /* level */));
|
||||
|
||||
@@ -2237,9 +2611,9 @@ TEST_F(CompactionPickerTest, CacheNextCompactionIndex) {
|
||||
cf_name_, mutable_cf_options_, mutable_db_options_, vstorage_.get(),
|
||||
&log_buffer_));
|
||||
ASSERT_TRUE(compaction.get() != nullptr);
|
||||
ASSERT_EQ(1U, compaction->num_input_levels());
|
||||
ASSERT_EQ(2U, compaction->num_input_levels());
|
||||
ASSERT_EQ(1U, compaction->num_input_files(0));
|
||||
ASSERT_EQ(0U, compaction->num_input_files(1));
|
||||
ASSERT_EQ(1U, compaction->num_input_files(1));
|
||||
ASSERT_EQ(4U, compaction->input(0, 0)->fd.GetNumber());
|
||||
ASSERT_EQ(3, vstorage_->NextCompactionIndex(1 /* level */));
|
||||
|
||||
@@ -2673,6 +3047,192 @@ TEST_F(CompactionPickerTest, UniversalMarkedManualCompaction) {
|
||||
ASSERT_EQ(0U, vstorage_->FilesMarkedForCompaction().size());
|
||||
}
|
||||
|
||||
class PerKeyPlacementCompactionPickerTest
|
||||
: public CompactionPickerTest,
|
||||
public testing::WithParamInterface<bool> {
|
||||
public:
|
||||
PerKeyPlacementCompactionPickerTest() : CompactionPickerTest() {}
|
||||
|
||||
void SetUp() override { enable_per_key_placement_ = GetParam(); }
|
||||
|
||||
protected:
|
||||
bool enable_per_key_placement_ = false;
|
||||
};
|
||||
|
||||
TEST_P(PerKeyPlacementCompactionPickerTest, OverlapWithNormalCompaction) {
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"Compaction::SupportsPerKeyPlacement:Enabled", [&](void* arg) {
|
||||
auto supports_per_key_placement = static_cast<bool*>(arg);
|
||||
*supports_per_key_placement = enable_per_key_placement_;
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
int num_levels = ioptions_.num_levels;
|
||||
NewVersionStorage(num_levels, kCompactionStyleLevel);
|
||||
|
||||
Add(0, 21U, "100", "150", 60000000U);
|
||||
Add(0, 22U, "300", "350", 60000000U);
|
||||
Add(5, 40U, "200", "250", 60000000U);
|
||||
Add(6, 50U, "101", "351", 60000000U);
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
CompactionOptions comp_options;
|
||||
std::unordered_set<uint64_t> input_set;
|
||||
input_set.insert(40);
|
||||
std::vector<CompactionInputFiles> input_files;
|
||||
ASSERT_OK(level_compaction_picker.GetCompactionInputsFromFileNumbers(
|
||||
&input_files, &input_set, vstorage_.get(), comp_options));
|
||||
|
||||
std::unique_ptr<Compaction> comp1(level_compaction_picker.CompactFiles(
|
||||
comp_options, input_files, 5, vstorage_.get(), mutable_cf_options_,
|
||||
mutable_db_options_, 0));
|
||||
|
||||
input_set.clear();
|
||||
input_files.clear();
|
||||
input_set.insert(21);
|
||||
input_set.insert(22);
|
||||
input_set.insert(50);
|
||||
ASSERT_OK(level_compaction_picker.GetCompactionInputsFromFileNumbers(
|
||||
&input_files, &input_set, vstorage_.get(), comp_options));
|
||||
|
||||
ASSERT_EQ(
|
||||
enable_per_key_placement_,
|
||||
level_compaction_picker.FilesRangeOverlapWithCompaction(input_files, 6));
|
||||
}
|
||||
|
||||
TEST_P(PerKeyPlacementCompactionPickerTest, NormalCompactionOverlap) {
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"Compaction::SupportsPerKeyPlacement:Enabled", [&](void* arg) {
|
||||
auto supports_per_key_placement = static_cast<bool*>(arg);
|
||||
*supports_per_key_placement = enable_per_key_placement_;
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
int num_levels = ioptions_.num_levels;
|
||||
NewVersionStorage(num_levels, kCompactionStyleLevel);
|
||||
|
||||
Add(0, 21U, "100", "150", 60000000U);
|
||||
Add(0, 22U, "300", "350", 60000000U);
|
||||
Add(4, 40U, "200", "220", 60000000U);
|
||||
Add(4, 41U, "230", "250", 60000000U);
|
||||
Add(6, 50U, "101", "351", 60000000U);
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
CompactionOptions comp_options;
|
||||
std::unordered_set<uint64_t> input_set;
|
||||
input_set.insert(21);
|
||||
input_set.insert(22);
|
||||
input_set.insert(50);
|
||||
std::vector<CompactionInputFiles> input_files;
|
||||
ASSERT_OK(level_compaction_picker.GetCompactionInputsFromFileNumbers(
|
||||
&input_files, &input_set, vstorage_.get(), comp_options));
|
||||
|
||||
std::unique_ptr<Compaction> comp1(level_compaction_picker.CompactFiles(
|
||||
comp_options, input_files, 6, vstorage_.get(), mutable_cf_options_,
|
||||
mutable_db_options_, 0));
|
||||
|
||||
input_set.clear();
|
||||
input_files.clear();
|
||||
input_set.insert(40);
|
||||
input_set.insert(41);
|
||||
ASSERT_OK(level_compaction_picker.GetCompactionInputsFromFileNumbers(
|
||||
&input_files, &input_set, vstorage_.get(), comp_options));
|
||||
|
||||
ASSERT_EQ(
|
||||
enable_per_key_placement_,
|
||||
level_compaction_picker.FilesRangeOverlapWithCompaction(input_files, 5));
|
||||
}
|
||||
|
||||
TEST_P(PerKeyPlacementCompactionPickerTest,
|
||||
OverlapWithNormalCompactionUniveral) {
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"Compaction::SupportsPerKeyPlacement:Enabled", [&](void* arg) {
|
||||
auto supports_per_key_placement = static_cast<bool*>(arg);
|
||||
*supports_per_key_placement = enable_per_key_placement_;
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
int num_levels = ioptions_.num_levels;
|
||||
UniversalCompactionPicker universal_compaction_picker(ioptions_, &icmp_);
|
||||
NewVersionStorage(num_levels, kCompactionStyleUniversal);
|
||||
|
||||
Add(0, 21U, "100", "150", 60000000U);
|
||||
Add(0, 22U, "300", "350", 60000000U);
|
||||
Add(5, 40U, "200", "250", 60000000U);
|
||||
Add(6, 50U, "101", "351", 60000000U);
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
CompactionOptions comp_options;
|
||||
std::unordered_set<uint64_t> input_set;
|
||||
input_set.insert(40);
|
||||
std::vector<CompactionInputFiles> input_files;
|
||||
ASSERT_OK(universal_compaction_picker.GetCompactionInputsFromFileNumbers(
|
||||
&input_files, &input_set, vstorage_.get(), comp_options));
|
||||
|
||||
std::unique_ptr<Compaction> comp1(universal_compaction_picker.CompactFiles(
|
||||
comp_options, input_files, 5, vstorage_.get(), mutable_cf_options_,
|
||||
mutable_db_options_, 0));
|
||||
|
||||
input_set.clear();
|
||||
input_files.clear();
|
||||
input_set.insert(21);
|
||||
input_set.insert(22);
|
||||
input_set.insert(50);
|
||||
ASSERT_OK(universal_compaction_picker.GetCompactionInputsFromFileNumbers(
|
||||
&input_files, &input_set, vstorage_.get(), comp_options));
|
||||
|
||||
ASSERT_EQ(enable_per_key_placement_,
|
||||
universal_compaction_picker.FilesRangeOverlapWithCompaction(
|
||||
input_files, 6));
|
||||
}
|
||||
|
||||
TEST_P(PerKeyPlacementCompactionPickerTest, NormalCompactionOverlapUniversal) {
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"Compaction::SupportsPerKeyPlacement:Enabled", [&](void* arg) {
|
||||
auto supports_per_key_placement = static_cast<bool*>(arg);
|
||||
*supports_per_key_placement = enable_per_key_placement_;
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
int num_levels = ioptions_.num_levels;
|
||||
UniversalCompactionPicker universal_compaction_picker(ioptions_, &icmp_);
|
||||
NewVersionStorage(num_levels, kCompactionStyleUniversal);
|
||||
|
||||
Add(0, 21U, "100", "150", 60000000U);
|
||||
Add(0, 22U, "300", "350", 60000000U);
|
||||
Add(4, 40U, "200", "220", 60000000U);
|
||||
Add(4, 41U, "230", "250", 60000000U);
|
||||
Add(6, 50U, "101", "351", 60000000U);
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
CompactionOptions comp_options;
|
||||
std::unordered_set<uint64_t> input_set;
|
||||
input_set.insert(21);
|
||||
input_set.insert(22);
|
||||
input_set.insert(50);
|
||||
std::vector<CompactionInputFiles> input_files;
|
||||
ASSERT_OK(universal_compaction_picker.GetCompactionInputsFromFileNumbers(
|
||||
&input_files, &input_set, vstorage_.get(), comp_options));
|
||||
|
||||
std::unique_ptr<Compaction> comp1(universal_compaction_picker.CompactFiles(
|
||||
comp_options, input_files, 6, vstorage_.get(), mutable_cf_options_,
|
||||
mutable_db_options_, 0));
|
||||
|
||||
input_set.clear();
|
||||
input_files.clear();
|
||||
input_set.insert(40);
|
||||
input_set.insert(41);
|
||||
ASSERT_OK(universal_compaction_picker.GetCompactionInputsFromFileNumbers(
|
||||
&input_files, &input_set, vstorage_.get(), comp_options));
|
||||
|
||||
ASSERT_EQ(enable_per_key_placement_,
|
||||
universal_compaction_picker.FilesRangeOverlapWithCompaction(
|
||||
input_files, 5));
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(PerKeyPlacementCompactionPickerTest,
|
||||
PerKeyPlacementCompactionPickerTest, ::testing::Bool());
|
||||
|
||||
#endif // ROCKSDB_LITE
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
@@ -758,7 +758,8 @@ Compaction* UniversalCompactionBuilder::PickCompactionToReduceSortedRuns(
|
||||
Temperature::kUnknown,
|
||||
/* max_subcompactions */ 0, grandparents,
|
||||
/* is manual */ false, /* trim_ts */ "", score_,
|
||||
false /* deletion_compaction */, compaction_reason);
|
||||
false /* deletion_compaction */,
|
||||
/* l0_files_might_overlap */ true, compaction_reason);
|
||||
}
|
||||
|
||||
// Look at overall size amplification. If size amplification
|
||||
@@ -1083,6 +1084,7 @@ Compaction* UniversalCompactionBuilder::PickIncrementalForReduceSizeAmp(
|
||||
Temperature::kUnknown,
|
||||
/* max_subcompactions */ 0, /* grandparents */ {}, /* is manual */ false,
|
||||
/* trim_ts */ "", score_, false /* deletion_compaction */,
|
||||
/* l0_files_might_overlap */ true,
|
||||
CompactionReason::kUniversalSizeAmplification);
|
||||
}
|
||||
|
||||
@@ -1225,6 +1227,7 @@ Compaction* UniversalCompactionBuilder::PickDeleteTriggeredCompaction() {
|
||||
Temperature::kUnknown,
|
||||
/* max_subcompactions */ 0, grandparents, /* is manual */ false,
|
||||
/* trim_ts */ "", score_, false /* deletion_compaction */,
|
||||
/* l0_files_might_overlap */ true,
|
||||
CompactionReason::kFilesMarkedForCompaction);
|
||||
}
|
||||
|
||||
@@ -1300,7 +1303,7 @@ Compaction* UniversalCompactionBuilder::PickCompactionToOldest(
|
||||
Temperature::kUnknown,
|
||||
/* max_subcompactions */ 0, /* grandparents */ {}, /* is manual */ false,
|
||||
/* trim_ts */ "", score_, false /* deletion_compaction */,
|
||||
compaction_reason);
|
||||
/* l0_files_might_overlap */ true, compaction_reason);
|
||||
}
|
||||
|
||||
Compaction* UniversalCompactionBuilder::PickPeriodicCompaction() {
|
||||
|
||||
@@ -0,0 +1,825 @@
|
||||
// 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).
|
||||
//
|
||||
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
|
||||
// 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 "db/compaction/compaction_job.h"
|
||||
#include "db/compaction/compaction_state.h"
|
||||
#include "logging/logging.h"
|
||||
#include "monitoring/iostats_context_imp.h"
|
||||
#include "monitoring/thread_status_util.h"
|
||||
#include "options/options_helper.h"
|
||||
#include "rocksdb/utilities/options_type.h"
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
class SubcompactionState;
|
||||
|
||||
CompactionServiceJobStatus
|
||||
CompactionJob::ProcessKeyValueCompactionWithCompactionService(
|
||||
SubcompactionState* sub_compact) {
|
||||
assert(sub_compact);
|
||||
assert(sub_compact->compaction);
|
||||
assert(db_options_.compaction_service);
|
||||
|
||||
const Compaction* compaction = sub_compact->compaction;
|
||||
CompactionServiceInput compaction_input;
|
||||
compaction_input.output_level = compaction->output_level();
|
||||
compaction_input.db_id = db_id_;
|
||||
|
||||
const std::vector<CompactionInputFiles>& inputs =
|
||||
*(compact_->compaction->inputs());
|
||||
for (const auto& files_per_level : inputs) {
|
||||
for (const auto& file : files_per_level.files) {
|
||||
compaction_input.input_files.emplace_back(
|
||||
MakeTableFileName(file->fd.GetNumber()));
|
||||
}
|
||||
}
|
||||
compaction_input.column_family.name =
|
||||
compaction->column_family_data()->GetName();
|
||||
compaction_input.column_family.options =
|
||||
compaction->column_family_data()->GetLatestCFOptions();
|
||||
compaction_input.db_options =
|
||||
BuildDBOptions(db_options_, mutable_db_options_copy_);
|
||||
compaction_input.snapshots = existing_snapshots_;
|
||||
compaction_input.has_begin = sub_compact->start;
|
||||
compaction_input.begin =
|
||||
compaction_input.has_begin ? sub_compact->start->ToString() : "";
|
||||
compaction_input.has_end = sub_compact->end;
|
||||
compaction_input.end =
|
||||
compaction_input.has_end ? sub_compact->end->ToString() : "";
|
||||
|
||||
std::string compaction_input_binary;
|
||||
Status s = compaction_input.Write(&compaction_input_binary);
|
||||
if (!s.ok()) {
|
||||
sub_compact->status = s;
|
||||
return CompactionServiceJobStatus::kFailure;
|
||||
}
|
||||
|
||||
std::ostringstream input_files_oss;
|
||||
bool is_first_one = true;
|
||||
for (const auto& file : compaction_input.input_files) {
|
||||
input_files_oss << (is_first_one ? "" : ", ") << file;
|
||||
is_first_one = false;
|
||||
}
|
||||
|
||||
ROCKS_LOG_INFO(
|
||||
db_options_.info_log,
|
||||
"[%s] [JOB %d] Starting remote compaction (output level: %d): %s",
|
||||
compaction_input.column_family.name.c_str(), job_id_,
|
||||
compaction_input.output_level, input_files_oss.str().c_str());
|
||||
CompactionServiceJobInfo info(dbname_, db_id_, db_session_id_,
|
||||
GetCompactionId(sub_compact), thread_pri_);
|
||||
CompactionServiceJobStatus compaction_status =
|
||||
db_options_.compaction_service->StartV2(info, compaction_input_binary);
|
||||
switch (compaction_status) {
|
||||
case CompactionServiceJobStatus::kSuccess:
|
||||
break;
|
||||
case CompactionServiceJobStatus::kFailure:
|
||||
sub_compact->status = Status::Incomplete(
|
||||
"CompactionService failed to start compaction job.");
|
||||
ROCKS_LOG_WARN(db_options_.info_log,
|
||||
"[%s] [JOB %d] Remote compaction failed to start.",
|
||||
compaction_input.column_family.name.c_str(), job_id_);
|
||||
return compaction_status;
|
||||
case CompactionServiceJobStatus::kUseLocal:
|
||||
ROCKS_LOG_INFO(
|
||||
db_options_.info_log,
|
||||
"[%s] [JOB %d] Remote compaction fallback to local by API Start.",
|
||||
compaction_input.column_family.name.c_str(), job_id_);
|
||||
return compaction_status;
|
||||
default:
|
||||
assert(false); // unknown status
|
||||
break;
|
||||
}
|
||||
|
||||
ROCKS_LOG_INFO(db_options_.info_log,
|
||||
"[%s] [JOB %d] Waiting for remote compaction...",
|
||||
compaction_input.column_family.name.c_str(), job_id_);
|
||||
std::string compaction_result_binary;
|
||||
compaction_status = db_options_.compaction_service->WaitForCompleteV2(
|
||||
info, &compaction_result_binary);
|
||||
|
||||
if (compaction_status == CompactionServiceJobStatus::kUseLocal) {
|
||||
ROCKS_LOG_INFO(db_options_.info_log,
|
||||
"[%s] [JOB %d] Remote compaction fallback to local by API "
|
||||
"WaitForComplete.",
|
||||
compaction_input.column_family.name.c_str(), job_id_);
|
||||
return compaction_status;
|
||||
}
|
||||
|
||||
CompactionServiceResult compaction_result;
|
||||
s = CompactionServiceResult::Read(compaction_result_binary,
|
||||
&compaction_result);
|
||||
|
||||
if (compaction_status == CompactionServiceJobStatus::kFailure) {
|
||||
if (s.ok()) {
|
||||
if (compaction_result.status.ok()) {
|
||||
sub_compact->status = Status::Incomplete(
|
||||
"CompactionService failed to run the compaction job (even though "
|
||||
"the internal status is okay).");
|
||||
} else {
|
||||
// set the current sub compaction status with the status returned from
|
||||
// remote
|
||||
sub_compact->status = compaction_result.status;
|
||||
}
|
||||
} else {
|
||||
sub_compact->status = Status::Incomplete(
|
||||
"CompactionService failed to run the compaction job (and no valid "
|
||||
"result is returned).");
|
||||
compaction_result.status.PermitUncheckedError();
|
||||
}
|
||||
ROCKS_LOG_WARN(db_options_.info_log,
|
||||
"[%s] [JOB %d] Remote compaction failed.",
|
||||
compaction_input.column_family.name.c_str(), job_id_);
|
||||
return compaction_status;
|
||||
}
|
||||
|
||||
if (!s.ok()) {
|
||||
sub_compact->status = s;
|
||||
compaction_result.status.PermitUncheckedError();
|
||||
return CompactionServiceJobStatus::kFailure;
|
||||
}
|
||||
sub_compact->status = compaction_result.status;
|
||||
|
||||
std::ostringstream output_files_oss;
|
||||
is_first_one = true;
|
||||
for (const auto& file : compaction_result.output_files) {
|
||||
output_files_oss << (is_first_one ? "" : ", ") << file.file_name;
|
||||
is_first_one = false;
|
||||
}
|
||||
|
||||
ROCKS_LOG_INFO(db_options_.info_log,
|
||||
"[%s] [JOB %d] Receive remote compaction result, output path: "
|
||||
"%s, files: %s",
|
||||
compaction_input.column_family.name.c_str(), job_id_,
|
||||
compaction_result.output_path.c_str(),
|
||||
output_files_oss.str().c_str());
|
||||
|
||||
if (!s.ok()) {
|
||||
sub_compact->status = s;
|
||||
return CompactionServiceJobStatus::kFailure;
|
||||
}
|
||||
|
||||
for (const auto& file : compaction_result.output_files) {
|
||||
uint64_t file_num = versions_->NewFileNumber();
|
||||
auto src_file = compaction_result.output_path + "/" + file.file_name;
|
||||
auto tgt_file = TableFileName(compaction->immutable_options()->cf_paths,
|
||||
file_num, compaction->output_path_id());
|
||||
s = fs_->RenameFile(src_file, tgt_file, IOOptions(), nullptr);
|
||||
if (!s.ok()) {
|
||||
sub_compact->status = s;
|
||||
return CompactionServiceJobStatus::kFailure;
|
||||
}
|
||||
|
||||
FileMetaData meta;
|
||||
uint64_t file_size;
|
||||
s = fs_->GetFileSize(tgt_file, IOOptions(), &file_size, nullptr);
|
||||
if (!s.ok()) {
|
||||
sub_compact->status = s;
|
||||
return CompactionServiceJobStatus::kFailure;
|
||||
}
|
||||
meta.fd = FileDescriptor(file_num, compaction->output_path_id(), file_size,
|
||||
file.smallest_seqno, file.largest_seqno);
|
||||
meta.smallest.DecodeFrom(file.smallest_internal_key);
|
||||
meta.largest.DecodeFrom(file.largest_internal_key);
|
||||
meta.oldest_ancester_time = file.oldest_ancester_time;
|
||||
meta.file_creation_time = file.file_creation_time;
|
||||
meta.marked_for_compaction = file.marked_for_compaction;
|
||||
meta.unique_id = file.unique_id;
|
||||
|
||||
auto cfd = compaction->column_family_data();
|
||||
sub_compact->Current().AddOutput(std::move(meta),
|
||||
cfd->internal_comparator(), false, false,
|
||||
true, file.paranoid_hash);
|
||||
}
|
||||
sub_compact->compaction_job_stats = compaction_result.stats;
|
||||
sub_compact->Current().SetNumOutputRecords(
|
||||
compaction_result.num_output_records);
|
||||
sub_compact->Current().SetTotalBytes(compaction_result.total_bytes);
|
||||
RecordTick(stats_, REMOTE_COMPACT_READ_BYTES, compaction_result.bytes_read);
|
||||
RecordTick(stats_, REMOTE_COMPACT_WRITE_BYTES,
|
||||
compaction_result.bytes_written);
|
||||
return CompactionServiceJobStatus::kSuccess;
|
||||
}
|
||||
|
||||
std::string CompactionServiceCompactionJob::GetTableFileName(
|
||||
uint64_t file_number) {
|
||||
return MakeTableFileName(output_path_, file_number);
|
||||
}
|
||||
|
||||
void CompactionServiceCompactionJob::RecordCompactionIOStats() {
|
||||
compaction_result_->bytes_read += IOSTATS(bytes_read);
|
||||
compaction_result_->bytes_written += IOSTATS(bytes_written);
|
||||
CompactionJob::RecordCompactionIOStats();
|
||||
}
|
||||
|
||||
CompactionServiceCompactionJob::CompactionServiceCompactionJob(
|
||||
int job_id, Compaction* compaction, const ImmutableDBOptions& db_options,
|
||||
const MutableDBOptions& mutable_db_options, const FileOptions& file_options,
|
||||
VersionSet* versions, const std::atomic<bool>* shutting_down,
|
||||
LogBuffer* log_buffer, FSDirectory* output_directory, Statistics* stats,
|
||||
InstrumentedMutex* db_mutex, ErrorHandler* db_error_handler,
|
||||
std::vector<SequenceNumber> existing_snapshots,
|
||||
std::shared_ptr<Cache> table_cache, EventLogger* event_logger,
|
||||
const std::string& dbname, const std::shared_ptr<IOTracer>& io_tracer,
|
||||
const std::atomic<bool>& manual_compaction_canceled,
|
||||
const std::string& db_id, const std::string& db_session_id,
|
||||
std::string output_path,
|
||||
const CompactionServiceInput& compaction_service_input,
|
||||
CompactionServiceResult* compaction_service_result)
|
||||
: CompactionJob(
|
||||
job_id, compaction, db_options, mutable_db_options, file_options,
|
||||
versions, shutting_down, log_buffer, nullptr, output_directory,
|
||||
nullptr, stats, db_mutex, db_error_handler,
|
||||
std::move(existing_snapshots), kMaxSequenceNumber, nullptr, nullptr,
|
||||
std::move(table_cache), event_logger,
|
||||
compaction->mutable_cf_options()->paranoid_file_checks,
|
||||
compaction->mutable_cf_options()->report_bg_io_stats, dbname,
|
||||
&(compaction_service_result->stats), Env::Priority::USER, io_tracer,
|
||||
manual_compaction_canceled, db_id, db_session_id,
|
||||
compaction->column_family_data()->GetFullHistoryTsLow()),
|
||||
output_path_(std::move(output_path)),
|
||||
compaction_input_(compaction_service_input),
|
||||
compaction_result_(compaction_service_result) {}
|
||||
|
||||
Status CompactionServiceCompactionJob::Run() {
|
||||
AutoThreadOperationStageUpdater stage_updater(
|
||||
ThreadStatus::STAGE_COMPACTION_RUN);
|
||||
|
||||
auto* c = compact_->compaction;
|
||||
assert(c->column_family_data() != nullptr);
|
||||
assert(c->column_family_data()->current()->storage_info()->NumLevelFiles(
|
||||
compact_->compaction->level()) > 0);
|
||||
|
||||
write_hint_ =
|
||||
c->column_family_data()->CalculateSSTWriteHint(c->output_level());
|
||||
bottommost_level_ = c->bottommost_level();
|
||||
|
||||
Slice begin = compaction_input_.begin;
|
||||
Slice end = compaction_input_.end;
|
||||
compact_->sub_compact_states.emplace_back(
|
||||
c, compaction_input_.has_begin ? &begin : nullptr,
|
||||
compaction_input_.has_end ? &end : nullptr, /*sub_job_id*/ 0);
|
||||
|
||||
log_buffer_->FlushBufferToLog();
|
||||
LogCompaction();
|
||||
const uint64_t start_micros = db_options_.clock->NowMicros();
|
||||
// Pick the only sub-compaction we should have
|
||||
assert(compact_->sub_compact_states.size() == 1);
|
||||
SubcompactionState* sub_compact = compact_->sub_compact_states.data();
|
||||
|
||||
ProcessKeyValueCompaction(sub_compact);
|
||||
|
||||
compaction_stats_.stats.micros =
|
||||
db_options_.clock->NowMicros() - start_micros;
|
||||
compaction_stats_.stats.cpu_micros =
|
||||
sub_compact->compaction_job_stats.cpu_micros;
|
||||
|
||||
RecordTimeToHistogram(stats_, COMPACTION_TIME,
|
||||
compaction_stats_.stats.micros);
|
||||
RecordTimeToHistogram(stats_, COMPACTION_CPU_TIME,
|
||||
compaction_stats_.stats.cpu_micros);
|
||||
|
||||
Status status = sub_compact->status;
|
||||
IOStatus io_s = sub_compact->io_status;
|
||||
|
||||
if (io_status_.ok()) {
|
||||
io_status_ = io_s;
|
||||
}
|
||||
|
||||
if (status.ok()) {
|
||||
constexpr IODebugContext* dbg = nullptr;
|
||||
|
||||
if (output_directory_) {
|
||||
io_s = output_directory_->FsyncWithDirOptions(IOOptions(), dbg,
|
||||
DirFsyncOptions());
|
||||
}
|
||||
}
|
||||
if (io_status_.ok()) {
|
||||
io_status_ = io_s;
|
||||
}
|
||||
if (status.ok()) {
|
||||
status = io_s;
|
||||
}
|
||||
if (status.ok()) {
|
||||
// TODO: Add verify_table()
|
||||
}
|
||||
|
||||
// Finish up all book-keeping to unify the subcompaction results
|
||||
compact_->AggregateCompactionStats(compaction_stats_, *compaction_job_stats_);
|
||||
UpdateCompactionStats();
|
||||
RecordCompactionIOStats();
|
||||
|
||||
LogFlush(db_options_.info_log);
|
||||
compact_->status = status;
|
||||
compact_->status.PermitUncheckedError();
|
||||
|
||||
// Build compaction result
|
||||
compaction_result_->output_level = compact_->compaction->output_level();
|
||||
compaction_result_->output_path = output_path_;
|
||||
for (const auto& output_file : sub_compact->GetOutputs()) {
|
||||
auto& meta = output_file.meta;
|
||||
compaction_result_->output_files.emplace_back(
|
||||
MakeTableFileName(meta.fd.GetNumber()), meta.fd.smallest_seqno,
|
||||
meta.fd.largest_seqno, meta.smallest.Encode().ToString(),
|
||||
meta.largest.Encode().ToString(), meta.oldest_ancester_time,
|
||||
meta.file_creation_time, output_file.validator.GetHash(),
|
||||
meta.marked_for_compaction, meta.unique_id);
|
||||
}
|
||||
InternalStats::CompactionStatsFull compaction_stats;
|
||||
sub_compact->AggregateCompactionStats(compaction_stats);
|
||||
compaction_result_->num_output_records =
|
||||
compaction_stats.stats.num_output_records;
|
||||
compaction_result_->total_bytes = compaction_stats.TotalBytesWritten();
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
void CompactionServiceCompactionJob::CleanupCompaction() {
|
||||
CompactionJob::CleanupCompaction();
|
||||
}
|
||||
|
||||
// Internal binary format for the input and result data
|
||||
enum BinaryFormatVersion : uint32_t {
|
||||
kOptionsString = 1, // Use string format similar to Option string format
|
||||
};
|
||||
|
||||
static std::unordered_map<std::string, OptionTypeInfo> cfd_type_info = {
|
||||
{"name",
|
||||
{offsetof(struct ColumnFamilyDescriptor, name), OptionType::kEncodedString,
|
||||
OptionVerificationType::kNormal, OptionTypeFlags::kNone}},
|
||||
{"options",
|
||||
{offsetof(struct ColumnFamilyDescriptor, options),
|
||||
OptionType::kConfigurable, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone,
|
||||
[](const ConfigOptions& opts, const std::string& /*name*/,
|
||||
const std::string& value, void* addr) {
|
||||
auto cf_options = static_cast<ColumnFamilyOptions*>(addr);
|
||||
return GetColumnFamilyOptionsFromString(opts, ColumnFamilyOptions(),
|
||||
value, cf_options);
|
||||
},
|
||||
[](const ConfigOptions& opts, const std::string& /*name*/,
|
||||
const void* addr, std::string* value) {
|
||||
const auto cf_options = static_cast<const ColumnFamilyOptions*>(addr);
|
||||
std::string result;
|
||||
auto status =
|
||||
GetStringFromColumnFamilyOptions(opts, *cf_options, &result);
|
||||
*value = "{" + result + "}";
|
||||
return status;
|
||||
},
|
||||
[](const ConfigOptions& opts, const std::string& name, const void* addr1,
|
||||
const void* addr2, std::string* mismatch) {
|
||||
const auto this_one = static_cast<const ColumnFamilyOptions*>(addr1);
|
||||
const auto that_one = static_cast<const ColumnFamilyOptions*>(addr2);
|
||||
auto this_conf = CFOptionsAsConfigurable(*this_one);
|
||||
auto that_conf = CFOptionsAsConfigurable(*that_one);
|
||||
std::string mismatch_opt;
|
||||
bool result =
|
||||
this_conf->AreEquivalent(opts, that_conf.get(), &mismatch_opt);
|
||||
if (!result) {
|
||||
*mismatch = name + "." + mismatch_opt;
|
||||
}
|
||||
return result;
|
||||
}}},
|
||||
};
|
||||
|
||||
static std::unordered_map<std::string, OptionTypeInfo> cs_input_type_info = {
|
||||
{"column_family",
|
||||
OptionTypeInfo::Struct(
|
||||
"column_family", &cfd_type_info,
|
||||
offsetof(struct CompactionServiceInput, column_family),
|
||||
OptionVerificationType::kNormal, OptionTypeFlags::kNone)},
|
||||
{"db_options",
|
||||
{offsetof(struct CompactionServiceInput, db_options),
|
||||
OptionType::kConfigurable, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone,
|
||||
[](const ConfigOptions& opts, const std::string& /*name*/,
|
||||
const std::string& value, void* addr) {
|
||||
auto options = static_cast<DBOptions*>(addr);
|
||||
return GetDBOptionsFromString(opts, DBOptions(), value, options);
|
||||
},
|
||||
[](const ConfigOptions& opts, const std::string& /*name*/,
|
||||
const void* addr, std::string* value) {
|
||||
const auto options = static_cast<const DBOptions*>(addr);
|
||||
std::string result;
|
||||
auto status = GetStringFromDBOptions(opts, *options, &result);
|
||||
*value = "{" + result + "}";
|
||||
return status;
|
||||
},
|
||||
[](const ConfigOptions& opts, const std::string& name, const void* addr1,
|
||||
const void* addr2, std::string* mismatch) {
|
||||
const auto this_one = static_cast<const DBOptions*>(addr1);
|
||||
const auto that_one = static_cast<const DBOptions*>(addr2);
|
||||
auto this_conf = DBOptionsAsConfigurable(*this_one);
|
||||
auto that_conf = DBOptionsAsConfigurable(*that_one);
|
||||
std::string mismatch_opt;
|
||||
bool result =
|
||||
this_conf->AreEquivalent(opts, that_conf.get(), &mismatch_opt);
|
||||
if (!result) {
|
||||
*mismatch = name + "." + mismatch_opt;
|
||||
}
|
||||
return result;
|
||||
}}},
|
||||
{"snapshots", OptionTypeInfo::Vector<uint64_t>(
|
||||
offsetof(struct CompactionServiceInput, snapshots),
|
||||
OptionVerificationType::kNormal, OptionTypeFlags::kNone,
|
||||
{0, OptionType::kUInt64T})},
|
||||
{"input_files", OptionTypeInfo::Vector<std::string>(
|
||||
offsetof(struct CompactionServiceInput, input_files),
|
||||
OptionVerificationType::kNormal, OptionTypeFlags::kNone,
|
||||
{0, OptionType::kEncodedString})},
|
||||
{"output_level",
|
||||
{offsetof(struct CompactionServiceInput, output_level), OptionType::kInt,
|
||||
OptionVerificationType::kNormal, OptionTypeFlags::kNone}},
|
||||
{"db_id",
|
||||
{offsetof(struct CompactionServiceInput, db_id),
|
||||
OptionType::kEncodedString}},
|
||||
{"has_begin",
|
||||
{offsetof(struct CompactionServiceInput, has_begin), OptionType::kBoolean,
|
||||
OptionVerificationType::kNormal, OptionTypeFlags::kNone}},
|
||||
{"begin",
|
||||
{offsetof(struct CompactionServiceInput, begin),
|
||||
OptionType::kEncodedString, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"has_end",
|
||||
{offsetof(struct CompactionServiceInput, has_end), OptionType::kBoolean,
|
||||
OptionVerificationType::kNormal, OptionTypeFlags::kNone}},
|
||||
{"end",
|
||||
{offsetof(struct CompactionServiceInput, end), OptionType::kEncodedString,
|
||||
OptionVerificationType::kNormal, OptionTypeFlags::kNone}},
|
||||
};
|
||||
|
||||
static std::unordered_map<std::string, OptionTypeInfo>
|
||||
cs_output_file_type_info = {
|
||||
{"file_name",
|
||||
{offsetof(struct CompactionServiceOutputFile, file_name),
|
||||
OptionType::kEncodedString, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"smallest_seqno",
|
||||
{offsetof(struct CompactionServiceOutputFile, smallest_seqno),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"largest_seqno",
|
||||
{offsetof(struct CompactionServiceOutputFile, largest_seqno),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"smallest_internal_key",
|
||||
{offsetof(struct CompactionServiceOutputFile, smallest_internal_key),
|
||||
OptionType::kEncodedString, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"largest_internal_key",
|
||||
{offsetof(struct CompactionServiceOutputFile, largest_internal_key),
|
||||
OptionType::kEncodedString, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"oldest_ancester_time",
|
||||
{offsetof(struct CompactionServiceOutputFile, oldest_ancester_time),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"file_creation_time",
|
||||
{offsetof(struct CompactionServiceOutputFile, file_creation_time),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"paranoid_hash",
|
||||
{offsetof(struct CompactionServiceOutputFile, paranoid_hash),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"marked_for_compaction",
|
||||
{offsetof(struct CompactionServiceOutputFile, marked_for_compaction),
|
||||
OptionType::kBoolean, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"unique_id",
|
||||
OptionTypeInfo::Array<uint64_t, 2>(
|
||||
offsetof(struct CompactionServiceOutputFile, unique_id),
|
||||
OptionVerificationType::kNormal, OptionTypeFlags::kNone,
|
||||
{0, OptionType::kUInt64T})},
|
||||
};
|
||||
|
||||
static std::unordered_map<std::string, OptionTypeInfo>
|
||||
compaction_job_stats_type_info = {
|
||||
{"elapsed_micros",
|
||||
{offsetof(struct CompactionJobStats, elapsed_micros),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"cpu_micros",
|
||||
{offsetof(struct CompactionJobStats, cpu_micros), OptionType::kUInt64T,
|
||||
OptionVerificationType::kNormal, OptionTypeFlags::kNone}},
|
||||
{"num_input_records",
|
||||
{offsetof(struct CompactionJobStats, num_input_records),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"num_blobs_read",
|
||||
{offsetof(struct CompactionJobStats, num_blobs_read),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"num_input_files",
|
||||
{offsetof(struct CompactionJobStats, num_input_files),
|
||||
OptionType::kSizeT, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"num_input_files_at_output_level",
|
||||
{offsetof(struct CompactionJobStats, num_input_files_at_output_level),
|
||||
OptionType::kSizeT, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"num_output_records",
|
||||
{offsetof(struct CompactionJobStats, num_output_records),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"num_output_files",
|
||||
{offsetof(struct CompactionJobStats, num_output_files),
|
||||
OptionType::kSizeT, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"num_output_files_blob",
|
||||
{offsetof(struct CompactionJobStats, num_output_files_blob),
|
||||
OptionType::kSizeT, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"is_full_compaction",
|
||||
{offsetof(struct CompactionJobStats, is_full_compaction),
|
||||
OptionType::kBoolean, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"is_manual_compaction",
|
||||
{offsetof(struct CompactionJobStats, is_manual_compaction),
|
||||
OptionType::kBoolean, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"total_input_bytes",
|
||||
{offsetof(struct CompactionJobStats, total_input_bytes),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"total_blob_bytes_read",
|
||||
{offsetof(struct CompactionJobStats, total_blob_bytes_read),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"total_output_bytes",
|
||||
{offsetof(struct CompactionJobStats, total_output_bytes),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"total_output_bytes_blob",
|
||||
{offsetof(struct CompactionJobStats, total_output_bytes_blob),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"num_records_replaced",
|
||||
{offsetof(struct CompactionJobStats, num_records_replaced),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"total_input_raw_key_bytes",
|
||||
{offsetof(struct CompactionJobStats, total_input_raw_key_bytes),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"total_input_raw_value_bytes",
|
||||
{offsetof(struct CompactionJobStats, total_input_raw_value_bytes),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"num_input_deletion_records",
|
||||
{offsetof(struct CompactionJobStats, num_input_deletion_records),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"num_expired_deletion_records",
|
||||
{offsetof(struct CompactionJobStats, num_expired_deletion_records),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"num_corrupt_keys",
|
||||
{offsetof(struct CompactionJobStats, num_corrupt_keys),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"file_write_nanos",
|
||||
{offsetof(struct CompactionJobStats, file_write_nanos),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"file_range_sync_nanos",
|
||||
{offsetof(struct CompactionJobStats, file_range_sync_nanos),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"file_fsync_nanos",
|
||||
{offsetof(struct CompactionJobStats, file_fsync_nanos),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"file_prepare_write_nanos",
|
||||
{offsetof(struct CompactionJobStats, file_prepare_write_nanos),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"smallest_output_key_prefix",
|
||||
{offsetof(struct CompactionJobStats, smallest_output_key_prefix),
|
||||
OptionType::kEncodedString, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"largest_output_key_prefix",
|
||||
{offsetof(struct CompactionJobStats, largest_output_key_prefix),
|
||||
OptionType::kEncodedString, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"num_single_del_fallthru",
|
||||
{offsetof(struct CompactionJobStats, num_single_del_fallthru),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"num_single_del_mismatch",
|
||||
{offsetof(struct CompactionJobStats, num_single_del_mismatch),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
};
|
||||
|
||||
namespace {
|
||||
// this is a helper struct to serialize and deserialize class Status, because
|
||||
// Status's members are not public.
|
||||
struct StatusSerializationAdapter {
|
||||
uint8_t code;
|
||||
uint8_t subcode;
|
||||
uint8_t severity;
|
||||
std::string message;
|
||||
|
||||
StatusSerializationAdapter() = default;
|
||||
explicit StatusSerializationAdapter(const Status& s) {
|
||||
code = s.code();
|
||||
subcode = s.subcode();
|
||||
severity = s.severity();
|
||||
auto msg = s.getState();
|
||||
message = msg ? msg : "";
|
||||
}
|
||||
|
||||
Status GetStatus() const {
|
||||
return Status{static_cast<Status::Code>(code),
|
||||
static_cast<Status::SubCode>(subcode),
|
||||
static_cast<Status::Severity>(severity), message};
|
||||
}
|
||||
};
|
||||
} // namespace
|
||||
|
||||
static std::unordered_map<std::string, OptionTypeInfo>
|
||||
status_adapter_type_info = {
|
||||
{"code",
|
||||
{offsetof(struct StatusSerializationAdapter, code),
|
||||
OptionType::kUInt8T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"subcode",
|
||||
{offsetof(struct StatusSerializationAdapter, subcode),
|
||||
OptionType::kUInt8T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"severity",
|
||||
{offsetof(struct StatusSerializationAdapter, severity),
|
||||
OptionType::kUInt8T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"message",
|
||||
{offsetof(struct StatusSerializationAdapter, message),
|
||||
OptionType::kEncodedString, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
};
|
||||
|
||||
static std::unordered_map<std::string, OptionTypeInfo> cs_result_type_info = {
|
||||
{"status",
|
||||
{offsetof(struct CompactionServiceResult, status),
|
||||
OptionType::kCustomizable, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone,
|
||||
[](const ConfigOptions& opts, const std::string& /*name*/,
|
||||
const std::string& value, void* addr) {
|
||||
auto status_obj = static_cast<Status*>(addr);
|
||||
StatusSerializationAdapter adapter;
|
||||
Status s = OptionTypeInfo::ParseType(
|
||||
opts, value, status_adapter_type_info, &adapter);
|
||||
*status_obj = adapter.GetStatus();
|
||||
return s;
|
||||
},
|
||||
[](const ConfigOptions& opts, const std::string& /*name*/,
|
||||
const void* addr, std::string* value) {
|
||||
const auto status_obj = static_cast<const Status*>(addr);
|
||||
StatusSerializationAdapter adapter(*status_obj);
|
||||
std::string result;
|
||||
Status s = OptionTypeInfo::SerializeType(opts, status_adapter_type_info,
|
||||
&adapter, &result);
|
||||
*value = "{" + result + "}";
|
||||
return s;
|
||||
},
|
||||
[](const ConfigOptions& opts, const std::string& /*name*/,
|
||||
const void* addr1, const void* addr2, std::string* mismatch) {
|
||||
const auto status1 = static_cast<const Status*>(addr1);
|
||||
const auto status2 = static_cast<const Status*>(addr2);
|
||||
|
||||
StatusSerializationAdapter adatper1(*status1);
|
||||
StatusSerializationAdapter adapter2(*status2);
|
||||
return OptionTypeInfo::TypesAreEqual(opts, status_adapter_type_info,
|
||||
&adatper1, &adapter2, mismatch);
|
||||
}}},
|
||||
{"output_files",
|
||||
OptionTypeInfo::Vector<CompactionServiceOutputFile>(
|
||||
offsetof(struct CompactionServiceResult, output_files),
|
||||
OptionVerificationType::kNormal, OptionTypeFlags::kNone,
|
||||
OptionTypeInfo::Struct("output_files", &cs_output_file_type_info, 0,
|
||||
OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone))},
|
||||
{"output_level",
|
||||
{offsetof(struct CompactionServiceResult, output_level), OptionType::kInt,
|
||||
OptionVerificationType::kNormal, OptionTypeFlags::kNone}},
|
||||
{"output_path",
|
||||
{offsetof(struct CompactionServiceResult, output_path),
|
||||
OptionType::kEncodedString, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"num_output_records",
|
||||
{offsetof(struct CompactionServiceResult, num_output_records),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"total_bytes",
|
||||
{offsetof(struct CompactionServiceResult, total_bytes),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"bytes_read",
|
||||
{offsetof(struct CompactionServiceResult, bytes_read),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"bytes_written",
|
||||
{offsetof(struct CompactionServiceResult, bytes_written),
|
||||
OptionType::kUInt64T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kNone}},
|
||||
{"stats", OptionTypeInfo::Struct(
|
||||
"stats", &compaction_job_stats_type_info,
|
||||
offsetof(struct CompactionServiceResult, stats),
|
||||
OptionVerificationType::kNormal, OptionTypeFlags::kNone)},
|
||||
};
|
||||
|
||||
Status CompactionServiceInput::Read(const std::string& data_str,
|
||||
CompactionServiceInput* obj) {
|
||||
if (data_str.size() <= sizeof(BinaryFormatVersion)) {
|
||||
return Status::InvalidArgument("Invalid CompactionServiceInput string");
|
||||
}
|
||||
auto format_version = DecodeFixed32(data_str.data());
|
||||
if (format_version == kOptionsString) {
|
||||
ConfigOptions cf;
|
||||
cf.invoke_prepare_options = false;
|
||||
cf.ignore_unknown_options = true;
|
||||
return OptionTypeInfo::ParseType(
|
||||
cf, data_str.substr(sizeof(BinaryFormatVersion)), cs_input_type_info,
|
||||
obj);
|
||||
} else {
|
||||
return Status::NotSupported(
|
||||
"Compaction Service Input data version not supported: " +
|
||||
std::to_string(format_version));
|
||||
}
|
||||
}
|
||||
|
||||
Status CompactionServiceInput::Write(std::string* output) {
|
||||
char buf[sizeof(BinaryFormatVersion)];
|
||||
EncodeFixed32(buf, kOptionsString);
|
||||
output->append(buf, sizeof(BinaryFormatVersion));
|
||||
ConfigOptions cf;
|
||||
cf.invoke_prepare_options = false;
|
||||
return OptionTypeInfo::SerializeType(cf, cs_input_type_info, this, output);
|
||||
}
|
||||
|
||||
Status CompactionServiceResult::Read(const std::string& data_str,
|
||||
CompactionServiceResult* obj) {
|
||||
if (data_str.size() <= sizeof(BinaryFormatVersion)) {
|
||||
return Status::InvalidArgument("Invalid CompactionServiceResult string");
|
||||
}
|
||||
auto format_version = DecodeFixed32(data_str.data());
|
||||
if (format_version == kOptionsString) {
|
||||
ConfigOptions cf;
|
||||
cf.invoke_prepare_options = false;
|
||||
cf.ignore_unknown_options = true;
|
||||
return OptionTypeInfo::ParseType(
|
||||
cf, data_str.substr(sizeof(BinaryFormatVersion)), cs_result_type_info,
|
||||
obj);
|
||||
} else {
|
||||
return Status::NotSupported(
|
||||
"Compaction Service Result data version not supported: " +
|
||||
std::to_string(format_version));
|
||||
}
|
||||
}
|
||||
|
||||
Status CompactionServiceResult::Write(std::string* output) {
|
||||
char buf[sizeof(BinaryFormatVersion)];
|
||||
EncodeFixed32(buf, kOptionsString);
|
||||
output->append(buf, sizeof(BinaryFormatVersion));
|
||||
ConfigOptions cf;
|
||||
cf.invoke_prepare_options = false;
|
||||
return OptionTypeInfo::SerializeType(cf, cs_result_type_info, this, output);
|
||||
}
|
||||
|
||||
#ifndef NDEBUG
|
||||
bool CompactionServiceResult::TEST_Equals(CompactionServiceResult* other) {
|
||||
std::string mismatch;
|
||||
return TEST_Equals(other, &mismatch);
|
||||
}
|
||||
|
||||
bool CompactionServiceResult::TEST_Equals(CompactionServiceResult* other,
|
||||
std::string* mismatch) {
|
||||
ConfigOptions cf;
|
||||
cf.invoke_prepare_options = false;
|
||||
return OptionTypeInfo::TypesAreEqual(cf, cs_result_type_info, this, other,
|
||||
mismatch);
|
||||
}
|
||||
|
||||
bool CompactionServiceInput::TEST_Equals(CompactionServiceInput* other) {
|
||||
std::string mismatch;
|
||||
return TEST_Equals(other, &mismatch);
|
||||
}
|
||||
|
||||
bool CompactionServiceInput::TEST_Equals(CompactionServiceInput* other,
|
||||
std::string* mismatch) {
|
||||
ConfigOptions cf;
|
||||
cf.invoke_prepare_options = false;
|
||||
return OptionTypeInfo::TypesAreEqual(cf, cs_input_type_info, this, other,
|
||||
mismatch);
|
||||
}
|
||||
#endif // NDEBUG
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
#endif // !ROCKSDB_LITE
|
||||
@@ -0,0 +1,46 @@
|
||||
// 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).
|
||||
//
|
||||
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
|
||||
// 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 "db/compaction/compaction_state.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
Slice CompactionState::SmallestUserKey() {
|
||||
for (const auto& sub_compact_state : sub_compact_states) {
|
||||
Slice smallest = sub_compact_state.SmallestUserKey();
|
||||
if (!smallest.empty()) {
|
||||
return smallest;
|
||||
}
|
||||
}
|
||||
// If there is no finished output, return an empty slice.
|
||||
return Slice{nullptr, 0};
|
||||
}
|
||||
|
||||
Slice CompactionState::LargestUserKey() {
|
||||
for (auto it = sub_compact_states.rbegin(); it < sub_compact_states.rend();
|
||||
++it) {
|
||||
Slice largest = it->LargestUserKey();
|
||||
if (!largest.empty()) {
|
||||
return largest;
|
||||
}
|
||||
}
|
||||
// If there is no finished output, return an empty slice.
|
||||
return Slice{nullptr, 0};
|
||||
}
|
||||
|
||||
void CompactionState::AggregateCompactionStats(
|
||||
InternalStats::CompactionStatsFull& compaction_stats,
|
||||
CompactionJobStats& compaction_job_stats) {
|
||||
for (const auto& sc : sub_compact_states) {
|
||||
sc.AggregateCompactionStats(compaction_stats);
|
||||
compaction_job_stats.Add(sc.compaction_job_stats);
|
||||
}
|
||||
}
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -0,0 +1,42 @@
|
||||
// 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).
|
||||
//
|
||||
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
|
||||
// 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.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "db/compaction/compaction.h"
|
||||
#include "db/compaction/subcompaction_state.h"
|
||||
#include "db/internal_stats.h"
|
||||
|
||||
// Data structures used for compaction_job and compaction_service_job which has
|
||||
// the list of sub_compact_states and the aggregated information for the
|
||||
// compaction.
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
// Maintains state for the entire compaction
|
||||
class CompactionState {
|
||||
public:
|
||||
Compaction* const compaction;
|
||||
|
||||
// REQUIRED: subcompaction states are stored in order of increasing key-range
|
||||
std::vector<SubcompactionState> sub_compact_states;
|
||||
Status status;
|
||||
|
||||
void AggregateCompactionStats(
|
||||
InternalStats::CompactionStatsFull& compaction_stats,
|
||||
CompactionJobStats& compaction_job_stats);
|
||||
|
||||
explicit CompactionState(Compaction* c) : compaction(c) {}
|
||||
|
||||
Slice SmallestUserKey();
|
||||
|
||||
Slice LargestUserKey();
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -0,0 +1,223 @@
|
||||
// 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).
|
||||
//
|
||||
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
|
||||
// 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 "db/compaction/subcompaction_state.h"
|
||||
|
||||
#include "rocksdb/sst_partitioner.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
void SubcompactionState::AggregateCompactionStats(
|
||||
InternalStats::CompactionStatsFull& compaction_stats) const {
|
||||
compaction_stats.stats.Add(compaction_outputs_.stats_);
|
||||
if (HasPenultimateLevelOutputs()) {
|
||||
compaction_stats.has_penultimate_level_output = true;
|
||||
compaction_stats.penultimate_level_stats.Add(
|
||||
penultimate_level_outputs_.stats_);
|
||||
}
|
||||
}
|
||||
|
||||
void SubcompactionState::FillFilesToCutForTtl() {
|
||||
if (compaction->immutable_options()->compaction_style !=
|
||||
CompactionStyle::kCompactionStyleLevel ||
|
||||
compaction->immutable_options()->compaction_pri !=
|
||||
CompactionPri::kMinOverlappingRatio ||
|
||||
compaction->mutable_cf_options()->ttl == 0 ||
|
||||
compaction->num_input_levels() < 2 || compaction->bottommost_level()) {
|
||||
return;
|
||||
}
|
||||
|
||||
// We define new file with the oldest ancestor time to be younger than 1/4
|
||||
// TTL, and an old one to be older than 1/2 TTL time.
|
||||
int64_t temp_current_time;
|
||||
auto get_time_status = compaction->immutable_options()->clock->GetCurrentTime(
|
||||
&temp_current_time);
|
||||
if (!get_time_status.ok()) {
|
||||
return;
|
||||
}
|
||||
auto current_time = static_cast<uint64_t>(temp_current_time);
|
||||
if (current_time < compaction->mutable_cf_options()->ttl) {
|
||||
return;
|
||||
}
|
||||
uint64_t old_age_thres =
|
||||
current_time - compaction->mutable_cf_options()->ttl / 2;
|
||||
|
||||
const std::vector<FileMetaData*>& olevel =
|
||||
*(compaction->inputs(compaction->num_input_levels() - 1));
|
||||
for (FileMetaData* file : olevel) {
|
||||
// Worth filtering out by start and end?
|
||||
uint64_t oldest_ancester_time = file->TryGetOldestAncesterTime();
|
||||
// We put old files if they are not too small to prevent a flood
|
||||
// of small files.
|
||||
if (oldest_ancester_time < old_age_thres &&
|
||||
file->fd.GetFileSize() >
|
||||
compaction->mutable_cf_options()->target_file_size_base / 2) {
|
||||
files_to_cut_for_ttl_.push_back(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
OutputIterator SubcompactionState::GetOutputs() const {
|
||||
return OutputIterator(penultimate_level_outputs_.outputs_,
|
||||
compaction_outputs_.outputs_);
|
||||
}
|
||||
|
||||
void SubcompactionState::Cleanup(Cache* cache) {
|
||||
penultimate_level_outputs_.Cleanup();
|
||||
compaction_outputs_.Cleanup();
|
||||
|
||||
if (!status.ok()) {
|
||||
for (const auto& out : GetOutputs()) {
|
||||
// If this file was inserted into the table cache then remove
|
||||
// them here because this compaction was not committed.
|
||||
TableCache::Evict(cache, out.meta.fd.GetNumber());
|
||||
}
|
||||
}
|
||||
// TODO: sub_compact.io_status is not checked like status. Not sure if thats
|
||||
// intentional. So ignoring the io_status as of now.
|
||||
io_status.PermitUncheckedError();
|
||||
}
|
||||
|
||||
Slice SubcompactionState::SmallestUserKey() const {
|
||||
if (has_penultimate_level_outputs_) {
|
||||
Slice a = compaction_outputs_.SmallestUserKey();
|
||||
Slice b = penultimate_level_outputs_.SmallestUserKey();
|
||||
if (a.empty()) {
|
||||
return b;
|
||||
}
|
||||
if (b.empty()) {
|
||||
return a;
|
||||
}
|
||||
const Comparator* user_cmp =
|
||||
compaction->column_family_data()->user_comparator();
|
||||
if (user_cmp->Compare(a, b) > 0) {
|
||||
return b;
|
||||
} else {
|
||||
return a;
|
||||
}
|
||||
} else {
|
||||
return compaction_outputs_.SmallestUserKey();
|
||||
}
|
||||
}
|
||||
|
||||
Slice SubcompactionState::LargestUserKey() const {
|
||||
if (has_penultimate_level_outputs_) {
|
||||
Slice a = compaction_outputs_.LargestUserKey();
|
||||
Slice b = penultimate_level_outputs_.LargestUserKey();
|
||||
if (a.empty()) {
|
||||
return b;
|
||||
}
|
||||
if (b.empty()) {
|
||||
return a;
|
||||
}
|
||||
const Comparator* user_cmp =
|
||||
compaction->column_family_data()->user_comparator();
|
||||
if (user_cmp->Compare(a, b) < 0) {
|
||||
return b;
|
||||
} else {
|
||||
return a;
|
||||
}
|
||||
} else {
|
||||
return compaction_outputs_.LargestUserKey();
|
||||
}
|
||||
}
|
||||
|
||||
bool SubcompactionState::ShouldStopBefore(const Slice& internal_key) {
|
||||
uint64_t curr_file_size = Current().GetCurrentOutputFileSize();
|
||||
const InternalKeyComparator* icmp =
|
||||
&compaction->column_family_data()->internal_comparator();
|
||||
|
||||
// Invalid local_output_split_key indicates that we do not need to split
|
||||
if (local_output_split_key_ != nullptr && !is_split_) {
|
||||
// Split occurs when the next key is larger than/equal to the cursor
|
||||
if (icmp->Compare(internal_key, local_output_split_key_->Encode()) >= 0) {
|
||||
is_split_ = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
const std::vector<FileMetaData*>& grandparents = compaction->grandparents();
|
||||
bool grandparant_file_switched = false;
|
||||
// Scan to find the earliest grandparent file that contains key.
|
||||
while (grandparent_index_ < grandparents.size() &&
|
||||
icmp->Compare(internal_key,
|
||||
grandparents[grandparent_index_]->largest.Encode()) >
|
||||
0) {
|
||||
if (seen_key_) {
|
||||
overlapped_bytes_ += grandparents[grandparent_index_]->fd.GetFileSize();
|
||||
grandparant_file_switched = true;
|
||||
}
|
||||
assert(grandparent_index_ + 1 >= grandparents.size() ||
|
||||
icmp->Compare(
|
||||
grandparents[grandparent_index_]->largest.Encode(),
|
||||
grandparents[grandparent_index_ + 1]->smallest.Encode()) <= 0);
|
||||
grandparent_index_++;
|
||||
}
|
||||
seen_key_ = true;
|
||||
|
||||
if (grandparant_file_switched &&
|
||||
overlapped_bytes_ + curr_file_size > compaction->max_compaction_bytes()) {
|
||||
// Too much overlap for current output; start new output
|
||||
overlapped_bytes_ = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!files_to_cut_for_ttl_.empty()) {
|
||||
if (cur_files_to_cut_for_ttl_ != -1) {
|
||||
// Previous key is inside the range of a file
|
||||
if (icmp->Compare(internal_key,
|
||||
files_to_cut_for_ttl_[cur_files_to_cut_for_ttl_]
|
||||
->largest.Encode()) > 0) {
|
||||
next_files_to_cut_for_ttl_ = cur_files_to_cut_for_ttl_ + 1;
|
||||
cur_files_to_cut_for_ttl_ = -1;
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
// Look for the key position
|
||||
while (next_files_to_cut_for_ttl_ <
|
||||
static_cast<int>(files_to_cut_for_ttl_.size())) {
|
||||
if (icmp->Compare(internal_key,
|
||||
files_to_cut_for_ttl_[next_files_to_cut_for_ttl_]
|
||||
->smallest.Encode()) >= 0) {
|
||||
if (icmp->Compare(internal_key,
|
||||
files_to_cut_for_ttl_[next_files_to_cut_for_ttl_]
|
||||
->largest.Encode()) <= 0) {
|
||||
// With in the current file
|
||||
cur_files_to_cut_for_ttl_ = next_files_to_cut_for_ttl_;
|
||||
return true;
|
||||
}
|
||||
// Beyond the current file
|
||||
next_files_to_cut_for_ttl_++;
|
||||
} else {
|
||||
// Still fall into the gap
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Status SubcompactionState::AddToOutput(
|
||||
const CompactionIterator& iter,
|
||||
const CompactionFileOpenFunc& open_file_func,
|
||||
const CompactionFileCloseFunc& close_file_func) {
|
||||
// update target output first
|
||||
is_current_penultimate_level_ = iter.output_to_penultimate_level();
|
||||
current_outputs_ = is_current_penultimate_level_ ? &penultimate_level_outputs_
|
||||
: &compaction_outputs_;
|
||||
if (is_current_penultimate_level_) {
|
||||
has_penultimate_level_outputs_ = true;
|
||||
}
|
||||
|
||||
return Current().AddToOutput(iter, open_file_func, close_file_func);
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -0,0 +1,255 @@
|
||||
// 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).
|
||||
//
|
||||
// Copyright (c) 2011 The LevelDB Authors. All rights reserved.
|
||||
// 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.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "db/blob/blob_file_addition.h"
|
||||
#include "db/blob/blob_garbage_meter.h"
|
||||
#include "db/compaction/compaction.h"
|
||||
#include "db/compaction/compaction_iterator.h"
|
||||
#include "db/compaction/compaction_outputs.h"
|
||||
#include "db/internal_stats.h"
|
||||
#include "db/output_validator.h"
|
||||
#include "db/range_del_aggregator.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
// Maintains state and outputs for each sub-compaction
|
||||
// It contains 2 `CompactionOutputs`:
|
||||
// 1. one for the normal output files
|
||||
// 2. another for the penultimate level outputs
|
||||
// a `current` pointer maintains the current output group, when calling
|
||||
// `AddToOutput()`, it checks the output of the current compaction_iterator key
|
||||
// and point `current` to the target output group. By default, it just points to
|
||||
// normal compaction_outputs, if the compaction_iterator key should be placed on
|
||||
// the penultimate level, `current` is changed to point to
|
||||
// `penultimate_level_outputs`.
|
||||
// The later operations uses `Current()` to get the target group.
|
||||
//
|
||||
// +----------+ +-----------------------------+ +---------+
|
||||
// | *current |--------> | compaction_outputs |----->| output |
|
||||
// +----------+ +-----------------------------+ +---------+
|
||||
// | | output |
|
||||
// | +---------+
|
||||
// | | ... |
|
||||
// |
|
||||
// | +-----------------------------+ +---------+
|
||||
// +-------------> | penultimate_level_outputs |----->| output |
|
||||
// +-----------------------------+ +---------+
|
||||
// | ... |
|
||||
|
||||
class SubcompactionState {
|
||||
public:
|
||||
const Compaction* compaction;
|
||||
|
||||
// The boundaries of the key-range this compaction is interested in. No two
|
||||
// sub-compactions may have overlapping key-ranges.
|
||||
// 'start' is inclusive, 'end' is exclusive, and nullptr means unbounded
|
||||
const Slice *start, *end;
|
||||
|
||||
// The return status of this sub-compaction
|
||||
Status status;
|
||||
|
||||
// The return IO Status of this sub-compaction
|
||||
IOStatus io_status;
|
||||
|
||||
// Notify on sub-compaction completion only if listener was notified on
|
||||
// sub-compaction begin.
|
||||
bool notify_on_subcompaction_completion = false;
|
||||
|
||||
// compaction job stats for this sub-compaction
|
||||
CompactionJobStats compaction_job_stats;
|
||||
|
||||
// sub-compaction job id, which is used to identify different sub-compaction
|
||||
// within the same compaction job.
|
||||
const uint32_t sub_job_id;
|
||||
|
||||
Slice SmallestUserKey() const;
|
||||
|
||||
Slice LargestUserKey() const;
|
||||
|
||||
// Get all outputs from the subcompaction. For per_key_placement compaction,
|
||||
// it returns both the last level outputs and penultimate level outputs.
|
||||
OutputIterator GetOutputs() const;
|
||||
|
||||
// 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.
|
||||
void AssignRangeDelAggregator(
|
||||
std::unique_ptr<CompactionRangeDelAggregator>&& range_del_agg) {
|
||||
if (compaction->SupportsPerKeyPlacement()) {
|
||||
penultimate_level_outputs_.AssignRangeDelAggregator(
|
||||
std::move(range_del_agg));
|
||||
} else {
|
||||
compaction_outputs_.AssignRangeDelAggregator(std::move(range_del_agg));
|
||||
}
|
||||
}
|
||||
|
||||
void RemoveLastEmptyOutput() {
|
||||
compaction_outputs_.RemoveLastEmptyOutput();
|
||||
penultimate_level_outputs_.RemoveLastEmptyOutput();
|
||||
}
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
void BuildSubcompactionJobInfo(
|
||||
SubcompactionJobInfo& subcompaction_job_info) const {
|
||||
const Compaction* c = compaction;
|
||||
const ColumnFamilyData* cfd = c->column_family_data();
|
||||
|
||||
subcompaction_job_info.cf_id = cfd->GetID();
|
||||
subcompaction_job_info.cf_name = cfd->GetName();
|
||||
subcompaction_job_info.status = status;
|
||||
subcompaction_job_info.subcompaction_job_id = static_cast<int>(sub_job_id);
|
||||
subcompaction_job_info.base_input_level = c->start_level();
|
||||
subcompaction_job_info.output_level = c->output_level();
|
||||
subcompaction_job_info.stats = compaction_job_stats;
|
||||
}
|
||||
#endif // !ROCKSDB_LITE
|
||||
|
||||
SubcompactionState() = delete;
|
||||
SubcompactionState(const SubcompactionState&) = delete;
|
||||
SubcompactionState& operator=(const SubcompactionState&) = delete;
|
||||
|
||||
SubcompactionState(Compaction* c, Slice* _start, Slice* _end,
|
||||
uint32_t _sub_job_id)
|
||||
: compaction(c),
|
||||
start(_start),
|
||||
end(_end),
|
||||
sub_job_id(_sub_job_id),
|
||||
compaction_outputs_(c, /*is_penultimate_level=*/false),
|
||||
penultimate_level_outputs_(c, /*is_penultimate_level=*/true) {
|
||||
assert(compaction != nullptr);
|
||||
const InternalKeyComparator* icmp =
|
||||
&compaction->column_family_data()->internal_comparator();
|
||||
const InternalKey* output_split_key = compaction->GetOutputSplitKey();
|
||||
// Invalid output_split_key indicates that we do not need to split
|
||||
if (output_split_key != nullptr) {
|
||||
// We may only split the output when the cursor is in the range. Split
|
||||
if ((end == nullptr || icmp->user_comparator()->Compare(
|
||||
ExtractUserKey(output_split_key->Encode()),
|
||||
ExtractUserKey(*end)) < 0) &&
|
||||
(start == nullptr || icmp->user_comparator()->Compare(
|
||||
ExtractUserKey(output_split_key->Encode()),
|
||||
ExtractUserKey(*start)) > 0)) {
|
||||
local_output_split_key_ = output_split_key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
SubcompactionState(SubcompactionState&& state) noexcept
|
||||
: compaction(state.compaction),
|
||||
start(state.start),
|
||||
end(state.end),
|
||||
status(std::move(state.status)),
|
||||
io_status(std::move(state.io_status)),
|
||||
notify_on_subcompaction_completion(
|
||||
state.notify_on_subcompaction_completion),
|
||||
compaction_job_stats(std::move(state.compaction_job_stats)),
|
||||
sub_job_id(state.sub_job_id),
|
||||
files_to_cut_for_ttl_(std::move(state.files_to_cut_for_ttl_)),
|
||||
cur_files_to_cut_for_ttl_(state.cur_files_to_cut_for_ttl_),
|
||||
next_files_to_cut_for_ttl_(state.next_files_to_cut_for_ttl_),
|
||||
grandparent_index_(state.grandparent_index_),
|
||||
overlapped_bytes_(state.overlapped_bytes_),
|
||||
seen_key_(state.seen_key_),
|
||||
compaction_outputs_(std::move(state.compaction_outputs_)),
|
||||
penultimate_level_outputs_(std::move(state.penultimate_level_outputs_)),
|
||||
is_current_penultimate_level_(state.is_current_penultimate_level_),
|
||||
has_penultimate_level_outputs_(state.has_penultimate_level_outputs_) {
|
||||
current_outputs_ = is_current_penultimate_level_
|
||||
? &penultimate_level_outputs_
|
||||
: &compaction_outputs_;
|
||||
}
|
||||
|
||||
bool HasPenultimateLevelOutputs() const {
|
||||
return has_penultimate_level_outputs_ ||
|
||||
penultimate_level_outputs_.HasRangeDel();
|
||||
}
|
||||
|
||||
void FillFilesToCutForTtl();
|
||||
|
||||
// Returns true iff we should stop building the current output
|
||||
// before processing "internal_key".
|
||||
bool ShouldStopBefore(const Slice& internal_key);
|
||||
|
||||
bool IsCurrentPenultimateLevel() const {
|
||||
return is_current_penultimate_level_;
|
||||
}
|
||||
|
||||
// Add all the new files from this compaction to version_edit
|
||||
void AddOutputsEdit(VersionEdit* out_edit) const {
|
||||
for (const auto& file : penultimate_level_outputs_.outputs_) {
|
||||
out_edit->AddFile(compaction->GetPenultimateLevel(), file.meta);
|
||||
}
|
||||
for (const auto& file : compaction_outputs_.outputs_) {
|
||||
out_edit->AddFile(compaction->output_level(), file.meta);
|
||||
}
|
||||
}
|
||||
|
||||
void Cleanup(Cache* cache);
|
||||
|
||||
void AggregateCompactionStats(
|
||||
InternalStats::CompactionStatsFull& compaction_stats) const;
|
||||
|
||||
CompactionOutputs& Current() const {
|
||||
assert(current_outputs_);
|
||||
return *current_outputs_;
|
||||
}
|
||||
|
||||
// Add compaction_iterator key/value to the `Current` output group.
|
||||
Status AddToOutput(const CompactionIterator& iter,
|
||||
const CompactionFileOpenFunc& open_file_func,
|
||||
const CompactionFileCloseFunc& close_file_func);
|
||||
|
||||
// Close all compaction output files, both output_to_penultimate_level outputs
|
||||
// and normal outputs.
|
||||
Status CloseCompactionFiles(const Status& curr_status,
|
||||
const CompactionFileOpenFunc& open_file_func,
|
||||
const CompactionFileCloseFunc& close_file_func) {
|
||||
// Call FinishCompactionOutputFile() even if status is not ok: it needs to
|
||||
// close the output file.
|
||||
Status s = penultimate_level_outputs_.CloseOutput(
|
||||
curr_status, open_file_func, close_file_func);
|
||||
s = compaction_outputs_.CloseOutput(s, open_file_func, close_file_func);
|
||||
return s;
|
||||
}
|
||||
|
||||
private:
|
||||
// Some identified files with old oldest ancester time and the range should be
|
||||
// isolated out so that the output file(s) in that range can be merged down
|
||||
// for TTL and clear the timestamps for the range.
|
||||
std::vector<FileMetaData*> files_to_cut_for_ttl_;
|
||||
int cur_files_to_cut_for_ttl_ = -1;
|
||||
int next_files_to_cut_for_ttl_ = 0;
|
||||
|
||||
// An index that used to speed up ShouldStopBefore().
|
||||
size_t grandparent_index_ = 0;
|
||||
// The number of bytes overlapping between the current output and
|
||||
// grandparent files used in ShouldStopBefore().
|
||||
uint64_t overlapped_bytes_ = 0;
|
||||
// A flag determines whether the key has been seen in ShouldStopBefore()
|
||||
bool seen_key_ = false;
|
||||
|
||||
// A flag determines if this subcompaction has been split by the cursor
|
||||
bool is_split_ = false;
|
||||
|
||||
// We also maintain the output split key for each subcompaction to avoid
|
||||
// repetitive comparison in ShouldStopBefore()
|
||||
const InternalKey* local_output_split_key_ = nullptr;
|
||||
|
||||
// State kept for output being generated
|
||||
CompactionOutputs compaction_outputs_;
|
||||
CompactionOutputs penultimate_level_outputs_;
|
||||
CompactionOutputs* current_outputs_ = &compaction_outputs_;
|
||||
bool is_current_penultimate_level_ = false;
|
||||
bool has_penultimate_level_outputs_ = false;
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
File diff suppressed because it is too large.
Load diff
+32
-8
@@ -7,6 +7,7 @@
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file. See the AUTHORS file for names of contributors.
|
||||
|
||||
#include "rocksdb/options.h"
|
||||
#ifndef ROCKSDB_LITE
|
||||
|
||||
#include <fcntl.h>
|
||||
@@ -553,7 +554,10 @@ TEST_F(CorruptionTest, CorruptedDescriptor) {
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "foo", "hello"));
|
||||
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
|
||||
ASSERT_OK(dbi->TEST_FlushMemTable());
|
||||
ASSERT_OK(dbi->TEST_CompactRange(0, nullptr, nullptr));
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
|
||||
ASSERT_OK(
|
||||
dbi->CompactRange(cro, dbi->DefaultColumnFamily(), nullptr, nullptr));
|
||||
|
||||
Corrupt(kDescriptorFile, 0, 1000);
|
||||
Status s = TryReopen();
|
||||
@@ -770,7 +774,9 @@ TEST_F(CorruptionTest, ParanoidFileChecksOnCompact) {
|
||||
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
|
||||
ASSERT_OK(dbi->TEST_FlushMemTable());
|
||||
mock->SetCorruptionMode(mode);
|
||||
s = dbi->TEST_CompactRange(0, nullptr, nullptr, nullptr, true);
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
|
||||
s = dbi->CompactRange(cro, dbi->DefaultColumnFamily(), nullptr, nullptr);
|
||||
if (mode == mock::MockTableFactory::kCorruptNone) {
|
||||
ASSERT_OK(s);
|
||||
} else {
|
||||
@@ -806,7 +812,10 @@ TEST_F(CorruptionTest, ParanoidFileChecksWithDeleteRangeFirst) {
|
||||
} else {
|
||||
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
|
||||
ASSERT_OK(dbi->TEST_FlushMemTable());
|
||||
ASSERT_OK(dbi->TEST_CompactRange(0, nullptr, nullptr, nullptr, true));
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
|
||||
ASSERT_OK(
|
||||
dbi->CompactRange(cro, dbi->DefaultColumnFamily(), nullptr, nullptr));
|
||||
}
|
||||
db_->ReleaseSnapshot(snap);
|
||||
}
|
||||
@@ -842,7 +851,10 @@ TEST_F(CorruptionTest, ParanoidFileChecksWithDeleteRange) {
|
||||
} else {
|
||||
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
|
||||
ASSERT_OK(dbi->TEST_FlushMemTable());
|
||||
ASSERT_OK(dbi->TEST_CompactRange(0, nullptr, nullptr, nullptr, true));
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
|
||||
ASSERT_OK(
|
||||
dbi->CompactRange(cro, dbi->DefaultColumnFamily(), nullptr, nullptr));
|
||||
}
|
||||
db_->ReleaseSnapshot(snap);
|
||||
}
|
||||
@@ -875,7 +887,10 @@ TEST_F(CorruptionTest, ParanoidFileChecksWithDeleteRangeLast) {
|
||||
} else {
|
||||
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
|
||||
ASSERT_OK(dbi->TEST_FlushMemTable());
|
||||
ASSERT_OK(dbi->TEST_CompactRange(0, nullptr, nullptr, nullptr, true));
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
|
||||
ASSERT_OK(
|
||||
dbi->CompactRange(cro, dbi->DefaultColumnFamily(), nullptr, nullptr));
|
||||
}
|
||||
db_->ReleaseSnapshot(snap);
|
||||
}
|
||||
@@ -902,7 +917,10 @@ TEST_F(CorruptionTest, LogCorruptionErrorsInCompactionIterator) {
|
||||
|
||||
DBImpl* dbi = static_cast_with_check<DBImpl>(db_);
|
||||
ASSERT_OK(dbi->TEST_FlushMemTable());
|
||||
Status s = dbi->TEST_CompactRange(0, nullptr, nullptr, nullptr, true);
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
|
||||
Status s =
|
||||
dbi->CompactRange(cro, dbi->DefaultColumnFamily(), nullptr, nullptr);
|
||||
ASSERT_NOK(s);
|
||||
ASSERT_TRUE(s.IsCorruption());
|
||||
}
|
||||
@@ -928,7 +946,10 @@ TEST_F(CorruptionTest, CompactionKeyOrderCheck) {
|
||||
|
||||
mock->SetCorruptionMode(mock::MockTableFactory::kCorruptNone);
|
||||
ASSERT_OK(db_->SetOptions({{"check_flush_compaction_key_order", "true"}}));
|
||||
ASSERT_NOK(dbi->TEST_CompactRange(0, nullptr, nullptr, nullptr, true));
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
|
||||
ASSERT_NOK(
|
||||
dbi->CompactRange(cro, dbi->DefaultColumnFamily(), nullptr, nullptr));
|
||||
}
|
||||
|
||||
TEST_F(CorruptionTest, FlushKeyOrderCheck) {
|
||||
@@ -975,7 +996,10 @@ TEST_F(CorruptionTest, DisableKeyOrderCheck) {
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "foo2", "v1"));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "foo4", "v1"));
|
||||
ASSERT_OK(dbi->TEST_FlushMemTable());
|
||||
ASSERT_OK(dbi->TEST_CompactRange(0, nullptr, nullptr, nullptr, true));
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
|
||||
ASSERT_OK(
|
||||
dbi->CompactRange(cro, dbi->DefaultColumnFamily(), nullptr, nullptr));
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
+53
-32
@@ -1209,9 +1209,9 @@ TEST_F(DBBasicTest, DBCloseAllDirectoryFDs) {
|
||||
s = db->Close();
|
||||
auto* counted_fs =
|
||||
options.env->GetFileSystem()->CheckedCast<CountedFileSystem>();
|
||||
assert(counted_fs);
|
||||
ASSERT_TRUE(counted_fs->counters()->dir_opens ==
|
||||
counted_fs->counters()->dir_closes);
|
||||
ASSERT_TRUE(counted_fs != nullptr);
|
||||
ASSERT_EQ(counted_fs->counters()->dir_opens,
|
||||
counted_fs->counters()->dir_closes);
|
||||
ASSERT_OK(s);
|
||||
delete db;
|
||||
}
|
||||
@@ -2467,35 +2467,37 @@ TEST_P(MultiGetPrefixExtractorTest, Batched) {
|
||||
SetPerfLevel(kEnableCount);
|
||||
get_perf_context()->Reset();
|
||||
|
||||
// First key is not in the prefix_extractor domain
|
||||
ASSERT_OK(Put("k", "v0"));
|
||||
ASSERT_OK(Put("kk1", "v1"));
|
||||
ASSERT_OK(Put("kk2", "v2"));
|
||||
ASSERT_OK(Put("kk3", "v3"));
|
||||
ASSERT_OK(Put("kk4", "v4"));
|
||||
std::vector<std::string> mem_keys(
|
||||
std::vector<std::string> keys(
|
||||
{"k", "kk1", "kk2", "kk3", "kk4", "rofl", "lmho"});
|
||||
std::vector<std::string> inmem_values;
|
||||
inmem_values = MultiGet(mem_keys, nullptr);
|
||||
ASSERT_EQ(inmem_values[0], "v0");
|
||||
ASSERT_EQ(inmem_values[1], "v1");
|
||||
ASSERT_EQ(inmem_values[2], "v2");
|
||||
ASSERT_EQ(inmem_values[3], "v3");
|
||||
ASSERT_EQ(inmem_values[4], "v4");
|
||||
std::vector<std::string> expected(
|
||||
{"v0", "v1", "v2", "v3", "v4", "NOT_FOUND", "NOT_FOUND"});
|
||||
std::vector<std::string> values;
|
||||
values = MultiGet(keys, nullptr);
|
||||
ASSERT_EQ(values, expected);
|
||||
// One key ("k") is not queried against the filter because it is outside
|
||||
// the prefix_extractor domain, leaving 6 keys with queried prefixes.
|
||||
ASSERT_EQ(get_perf_context()->bloom_memtable_miss_count, 2);
|
||||
ASSERT_EQ(get_perf_context()->bloom_memtable_hit_count, 5);
|
||||
ASSERT_EQ(get_perf_context()->bloom_memtable_hit_count, 4);
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
std::vector<std::string> keys({"k", "kk1", "kk2", "kk3", "kk4"});
|
||||
std::vector<std::string> values;
|
||||
get_perf_context()->Reset();
|
||||
values = MultiGet(keys, nullptr);
|
||||
ASSERT_EQ(values[0], "v0");
|
||||
ASSERT_EQ(values[1], "v1");
|
||||
ASSERT_EQ(values[2], "v2");
|
||||
ASSERT_EQ(values[3], "v3");
|
||||
ASSERT_EQ(values[4], "v4");
|
||||
// Filter hits for 4 in-domain keys
|
||||
ASSERT_EQ(values, expected);
|
||||
ASSERT_EQ(get_perf_context()->bloom_sst_miss_count, 2);
|
||||
ASSERT_EQ(get_perf_context()->bloom_sst_hit_count, 4);
|
||||
|
||||
// Also check Get stat
|
||||
get_perf_context()->Reset();
|
||||
for (size_t i = 0; i < keys.size(); ++i) {
|
||||
values[i] = Get(keys[i]);
|
||||
}
|
||||
ASSERT_EQ(values, expected);
|
||||
ASSERT_EQ(get_perf_context()->bloom_sst_miss_count, 2);
|
||||
ASSERT_EQ(get_perf_context()->bloom_sst_hit_count, 4);
|
||||
}
|
||||
|
||||
@@ -2616,13 +2618,20 @@ TEST_F(DBBasicTest, GetAllKeyVersions) {
|
||||
ASSERT_OK(Delete(std::to_string(i)));
|
||||
}
|
||||
std::vector<KeyVersion> key_versions;
|
||||
ASSERT_OK(ROCKSDB_NAMESPACE::GetAllKeyVersions(
|
||||
db_, Slice(), Slice(), std::numeric_limits<size_t>::max(),
|
||||
&key_versions));
|
||||
ASSERT_OK(GetAllKeyVersions(db_, Slice(), Slice(),
|
||||
std::numeric_limits<size_t>::max(),
|
||||
&key_versions));
|
||||
ASSERT_EQ(kNumInserts + kNumDeletes + kNumUpdates, key_versions.size());
|
||||
ASSERT_OK(ROCKSDB_NAMESPACE::GetAllKeyVersions(
|
||||
db_, handles_[0], Slice(), Slice(), std::numeric_limits<size_t>::max(),
|
||||
&key_versions));
|
||||
for (size_t i = 0; i < kNumInserts + kNumDeletes + kNumUpdates; i++) {
|
||||
if (i % 3 == 0) {
|
||||
ASSERT_EQ(key_versions[i].GetTypeName(), "TypeDeletion");
|
||||
} else {
|
||||
ASSERT_EQ(key_versions[i].GetTypeName(), "TypeValue");
|
||||
}
|
||||
}
|
||||
ASSERT_OK(GetAllKeyVersions(db_, handles_[0], Slice(), Slice(),
|
||||
std::numeric_limits<size_t>::max(),
|
||||
&key_versions));
|
||||
ASSERT_EQ(kNumInserts + kNumDeletes + kNumUpdates, key_versions.size());
|
||||
|
||||
// Check non-default column family
|
||||
@@ -2635,11 +2644,21 @@ TEST_F(DBBasicTest, GetAllKeyVersions) {
|
||||
for (size_t i = 0; i + 1 != kNumDeletes; ++i) {
|
||||
ASSERT_OK(Delete(1, std::to_string(i)));
|
||||
}
|
||||
ASSERT_OK(ROCKSDB_NAMESPACE::GetAllKeyVersions(
|
||||
db_, handles_[1], Slice(), Slice(), std::numeric_limits<size_t>::max(),
|
||||
&key_versions));
|
||||
ASSERT_OK(GetAllKeyVersions(db_, handles_[1], Slice(), Slice(),
|
||||
std::numeric_limits<size_t>::max(),
|
||||
&key_versions));
|
||||
ASSERT_EQ(kNumInserts + kNumDeletes + kNumUpdates - 3, key_versions.size());
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, ValueTypeString) {
|
||||
KeyVersion key_version;
|
||||
// when adding new type, please also update `value_type_string_map`
|
||||
for (unsigned char i = ValueType::kTypeDeletion; i < ValueType::kTypeMaxValid;
|
||||
i++) {
|
||||
key_version.type = i;
|
||||
ASSERT_TRUE(key_version.GetTypeName() != "Invalid");
|
||||
}
|
||||
}
|
||||
#endif // !ROCKSDB_LITE
|
||||
|
||||
TEST_F(DBBasicTest, MultiGetIOBufferOverrun) {
|
||||
@@ -4143,7 +4162,7 @@ TEST_F(DBBasicTest, FailOpenIfLoggerCreationFail) {
|
||||
|
||||
Status s = TryReopen(options);
|
||||
ASSERT_EQ(nullptr, options.info_log);
|
||||
ASSERT_TRUE(s.IsAborted());
|
||||
ASSERT_TRUE(s.IsIOError());
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
@@ -4193,7 +4212,9 @@ TEST_F(DBBasicTest, VerifyFileChecksums) {
|
||||
ASSERT_TRUE(db_->VerifyFileChecksums(ReadOptions()).IsInvalidArgument());
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, ManualWalSync) {
|
||||
// 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) {
|
||||
Options options = CurrentOptions();
|
||||
options.track_and_verify_wals_in_manifest = true;
|
||||
options.wal_recovery_mode = WALRecoveryMode::kAbsoluteConsistency;
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
#include "cache/cache_entry_roles.h"
|
||||
#include "cache/cache_key.h"
|
||||
#include "cache/clock_cache.h"
|
||||
#include "cache/fast_lru_cache.h"
|
||||
#include "cache/lru_cache.h"
|
||||
#include "db/column_family.h"
|
||||
@@ -250,7 +251,7 @@ TEST_F(DBBlockCacheTest, TestWithoutCompressedBlockCache) {
|
||||
cache->SetStrictCapacityLimit(true);
|
||||
iter = db_->NewIterator(read_options);
|
||||
iter->Seek(std::to_string(kNumBlocks - 1));
|
||||
ASSERT_TRUE(iter->status().IsIncomplete());
|
||||
ASSERT_TRUE(iter->status().IsMemoryLimit());
|
||||
CheckCacheCounters(options, 1, 0, 0, 1);
|
||||
delete iter;
|
||||
iter = nullptr;
|
||||
@@ -333,8 +334,10 @@ TEST_F(DBBlockCacheTest, TestWithCompressedBlockCache) {
|
||||
ASSERT_EQ(usage, cache->GetPinnedUsage());
|
||||
|
||||
// Load last key block.
|
||||
ASSERT_EQ("Result incomplete: Insert failed due to LRU cache being full.",
|
||||
Get(std::to_string(kNumBlocks - 1)));
|
||||
ASSERT_EQ(
|
||||
"Operation aborted: Memory limit reached: Insert failed due to LRU cache "
|
||||
"being full.",
|
||||
Get(std::to_string(kNumBlocks - 1)));
|
||||
// Failure will also record the miss counter.
|
||||
CheckCacheCounters(options, 1, 0, 0, 1);
|
||||
CheckCompressedCacheCounters(options, 1, 0, 1, 0);
|
||||
@@ -676,6 +679,7 @@ TEST_P(DBBlockCacheTest1, WarmCacheWithBlocksDuringFlush) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.statistics = ROCKSDB_NAMESPACE::CreateDBStatistics();
|
||||
options.max_compaction_bytes = 2000;
|
||||
|
||||
BlockBasedTableOptions table_options;
|
||||
table_options.block_cache = NewLRUCache(1 << 25, 0, false);
|
||||
@@ -932,7 +936,9 @@ TEST_F(DBBlockCacheTest, AddRedundantStats) {
|
||||
int iterations_tested = 0;
|
||||
for (std::shared_ptr<Cache> base_cache :
|
||||
{NewLRUCache(capacity, num_shard_bits),
|
||||
NewClockCache(capacity, num_shard_bits),
|
||||
ExperimentalNewClockCache(
|
||||
capacity, 1 /*estimated_value_size*/, num_shard_bits,
|
||||
false /*strict_capacity_limit*/, kDefaultCacheMetadataChargePolicy),
|
||||
NewFastLRUCache(capacity, 1 /*estimated_value_size*/, num_shard_bits,
|
||||
false /*strict_capacity_limit*/,
|
||||
kDefaultCacheMetadataChargePolicy)}) {
|
||||
@@ -1288,11 +1294,10 @@ TEST_F(DBBlockCacheTest, CacheEntryRoleStats) {
|
||||
const size_t capacity = size_t{1} << 25;
|
||||
int iterations_tested = 0;
|
||||
for (bool partition : {false, true}) {
|
||||
for (std::shared_ptr<Cache> cache :
|
||||
{NewLRUCache(capacity), NewClockCache(capacity)}) {
|
||||
// This test doesn't support FastLRUCache because the
|
||||
for (std::shared_ptr<Cache> cache : {NewLRUCache(capacity)}) {
|
||||
// This test doesn't support FastLRUCache nor ClockCache because the
|
||||
// keys used are not 16B long.
|
||||
// TODO(guido) Add support for FastLRUCache.
|
||||
// TODO(guido) Add support for FastLRUCache and ClockCache.
|
||||
if (!cache) {
|
||||
// Skip clock cache when not supported
|
||||
continue;
|
||||
|
||||
@@ -3113,6 +3113,382 @@ TEST_F(DBBloomFilterTest, SeekForPrevWithPartitionedFilters) {
|
||||
it.reset();
|
||||
}
|
||||
|
||||
namespace {
|
||||
class BackwardBytewiseComparator : public Comparator {
|
||||
public:
|
||||
const char* Name() const override { return "BackwardBytewiseComparator"; }
|
||||
|
||||
int Compare(const Slice& a, const Slice& b) const override {
|
||||
int min_size_neg = -static_cast<int>(std::min(a.size(), b.size()));
|
||||
const char* a_end = a.data() + a.size();
|
||||
const char* b_end = b.data() + b.size();
|
||||
for (int i = -1; i >= min_size_neg; --i) {
|
||||
if (a_end[i] != b_end[i]) {
|
||||
if (static_cast<unsigned char>(a_end[i]) <
|
||||
static_cast<unsigned char>(b_end[i])) {
|
||||
return -1;
|
||||
} else {
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
return static_cast<int>(a.size()) - static_cast<int>(b.size());
|
||||
}
|
||||
|
||||
void FindShortestSeparator(std::string* /*start*/,
|
||||
const Slice& /*limit*/) const override {}
|
||||
|
||||
void FindShortSuccessor(std::string* /*key*/) const override {}
|
||||
};
|
||||
|
||||
const BackwardBytewiseComparator kBackwardBytewiseComparator{};
|
||||
|
||||
class FixedSuffix4Transform : public SliceTransform {
|
||||
const char* Name() const override { return "FixedSuffixTransform"; }
|
||||
|
||||
Slice Transform(const Slice& src) const override {
|
||||
return Slice(src.data() + src.size() - 4, 4);
|
||||
}
|
||||
|
||||
bool InDomain(const Slice& src) const override { return src.size() >= 4; }
|
||||
};
|
||||
|
||||
std::pair<uint64_t, uint64_t> GetBloomStat(const Options& options, bool sst) {
|
||||
if (sst) {
|
||||
return {
|
||||
options.statistics->getAndResetTickerCount(BLOOM_FILTER_PREFIX_CHECKED),
|
||||
options.statistics->getAndResetTickerCount(BLOOM_FILTER_PREFIX_USEFUL)};
|
||||
} else {
|
||||
auto hit = std::exchange(get_perf_context()->bloom_memtable_hit_count, 0);
|
||||
auto miss = std::exchange(get_perf_context()->bloom_memtable_miss_count, 0);
|
||||
return {hit + miss, miss};
|
||||
}
|
||||
}
|
||||
|
||||
std::pair<uint64_t, uint64_t> CheckedAndUseful(uint64_t checked,
|
||||
uint64_t useful) {
|
||||
return {checked, useful};
|
||||
}
|
||||
} // namespace
|
||||
|
||||
// This uses a prefix_extractor + comparator combination that violates
|
||||
// one of the old obsolete, unnecessary axioms of prefix extraction:
|
||||
// * key.starts_with(prefix(key))
|
||||
// This axiom is not really needed, and we validate that here.
|
||||
TEST_F(DBBloomFilterTest, WeirdPrefixExtractorWithFilter1) {
|
||||
BlockBasedTableOptions bbto;
|
||||
bbto.filter_policy.reset(ROCKSDB_NAMESPACE::NewBloomFilterPolicy(10));
|
||||
bbto.whole_key_filtering = false;
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.comparator = &kBackwardBytewiseComparator;
|
||||
options.prefix_extractor = std::make_shared<FixedSuffix4Transform>();
|
||||
options.table_factory.reset(NewBlockBasedTableFactory(bbto));
|
||||
options.memtable_prefix_bloom_size_ratio = 0.1;
|
||||
options.statistics = CreateDBStatistics();
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(Put("321aaaa", "val1"));
|
||||
ASSERT_OK(Put("112aaaa", "val2"));
|
||||
ASSERT_OK(Put("009aaaa", "val3"));
|
||||
ASSERT_OK(Put("baa", "val4")); // out of domain
|
||||
ASSERT_OK(Put("321abaa", "val5"));
|
||||
ASSERT_OK(Put("zzz", "val6")); // out of domain
|
||||
|
||||
for (auto flushed : {false, true}) {
|
||||
SCOPED_TRACE("flushed=" + std::to_string(flushed));
|
||||
if (flushed) {
|
||||
ASSERT_OK(Flush());
|
||||
}
|
||||
ReadOptions read_options;
|
||||
if (flushed) { // TODO: support auto_prefix_mode in memtable?
|
||||
read_options.auto_prefix_mode = true;
|
||||
}
|
||||
EXPECT_EQ(GetBloomStat(options, flushed), CheckedAndUseful(0, 0));
|
||||
{
|
||||
Slice ub("999aaaa");
|
||||
read_options.iterate_upper_bound = &ub;
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
EXPECT_EQ(CountIter(iter, "aaaa"), 3);
|
||||
EXPECT_EQ(GetBloomStat(options, flushed), CheckedAndUseful(1, 0));
|
||||
}
|
||||
{
|
||||
Slice ub("999abaa");
|
||||
read_options.iterate_upper_bound = &ub;
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
EXPECT_EQ(CountIter(iter, "abaa"), 1);
|
||||
EXPECT_EQ(GetBloomStat(options, flushed), CheckedAndUseful(1, 0));
|
||||
}
|
||||
{
|
||||
Slice ub("999acaa");
|
||||
read_options.iterate_upper_bound = &ub;
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
EXPECT_EQ(CountIter(iter, "acaa"), 0);
|
||||
EXPECT_EQ(GetBloomStat(options, flushed), CheckedAndUseful(1, 1));
|
||||
}
|
||||
{
|
||||
Slice ub("zzzz");
|
||||
read_options.iterate_upper_bound = &ub;
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
EXPECT_EQ(CountIter(iter, "baa"), 3);
|
||||
if (flushed) { // TODO: fix memtable case
|
||||
EXPECT_EQ(GetBloomStat(options, flushed), CheckedAndUseful(0, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// This uses a prefix_extractor + comparator combination that violates
|
||||
// one of the old obsolete, unnecessary axioms of prefix extraction:
|
||||
// * Compare(prefix(key), key) <= 0
|
||||
// This axiom is not really needed, and we validate that here.
|
||||
TEST_F(DBBloomFilterTest, WeirdPrefixExtractorWithFilter2) {
|
||||
BlockBasedTableOptions bbto;
|
||||
bbto.filter_policy.reset(ROCKSDB_NAMESPACE::NewBloomFilterPolicy(10));
|
||||
bbto.whole_key_filtering = false;
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.comparator = ReverseBytewiseComparator();
|
||||
options.prefix_extractor.reset(NewFixedPrefixTransform(4));
|
||||
options.table_factory.reset(NewBlockBasedTableFactory(bbto));
|
||||
options.memtable_prefix_bloom_size_ratio = 0.1;
|
||||
options.statistics = CreateDBStatistics();
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(Put("aaaa123", "val1"));
|
||||
ASSERT_OK(Put("aaaa211", "val2"));
|
||||
ASSERT_OK(Put("aaaa900", "val3"));
|
||||
ASSERT_OK(Put("aab", "val4")); // out of domain
|
||||
ASSERT_OK(Put("aaba123", "val5"));
|
||||
ASSERT_OK(Put("qqqq123", "val7"));
|
||||
ASSERT_OK(Put("qqqq", "val8"));
|
||||
ASSERT_OK(Put("zzz", "val8")); // out of domain
|
||||
|
||||
for (auto flushed : {false, true}) {
|
||||
SCOPED_TRACE("flushed=" + std::to_string(flushed));
|
||||
if (flushed) {
|
||||
ASSERT_OK(Flush());
|
||||
}
|
||||
ReadOptions read_options;
|
||||
if (flushed) { // TODO: support auto_prefix_mode in memtable?
|
||||
read_options.auto_prefix_mode = true;
|
||||
} else {
|
||||
// TODO: why needed?
|
||||
get_perf_context()->bloom_memtable_hit_count = 0;
|
||||
get_perf_context()->bloom_memtable_miss_count = 0;
|
||||
}
|
||||
EXPECT_EQ(GetBloomStat(options, flushed), CheckedAndUseful(0, 0));
|
||||
{
|
||||
Slice ub("aaaa000");
|
||||
read_options.iterate_upper_bound = &ub;
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
EXPECT_EQ(CountIter(iter, "aaaa999"), 3);
|
||||
EXPECT_EQ(GetBloomStat(options, flushed), CheckedAndUseful(1, 0));
|
||||
}
|
||||
{
|
||||
// Note: prefix does work as upper bound
|
||||
Slice ub("aaaa");
|
||||
read_options.iterate_upper_bound = &ub;
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
EXPECT_EQ(CountIter(iter, "aaaa999"), 3);
|
||||
EXPECT_EQ(GetBloomStat(options, flushed), CheckedAndUseful(1, 0));
|
||||
}
|
||||
{
|
||||
// Note: prefix does not work here as seek key
|
||||
Slice ub("aaaa500");
|
||||
read_options.iterate_upper_bound = &ub;
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
EXPECT_EQ(CountIter(iter, "aaaa"), 0);
|
||||
EXPECT_EQ(GetBloomStat(options, flushed), CheckedAndUseful(1, 0));
|
||||
}
|
||||
{
|
||||
Slice ub("aaba000");
|
||||
read_options.iterate_upper_bound = &ub;
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
EXPECT_EQ(CountIter(iter, "aaba999"), 1);
|
||||
EXPECT_EQ(GetBloomStat(options, flushed), CheckedAndUseful(1, 0));
|
||||
}
|
||||
{
|
||||
Slice ub("aaca000");
|
||||
read_options.iterate_upper_bound = &ub;
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
EXPECT_EQ(CountIter(iter, "aaca999"), 0);
|
||||
EXPECT_EQ(GetBloomStat(options, flushed), CheckedAndUseful(1, 1));
|
||||
}
|
||||
{
|
||||
Slice ub("aaaz");
|
||||
read_options.iterate_upper_bound = &ub;
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
EXPECT_EQ(CountIter(iter, "zzz"), 5);
|
||||
EXPECT_EQ(GetBloomStat(options, flushed), CheckedAndUseful(0, 0));
|
||||
}
|
||||
{
|
||||
// Note: prefix does work here as seek key, but only finds key equal
|
||||
// to prefix (others with same prefix are less)
|
||||
read_options.auto_prefix_mode = false;
|
||||
read_options.iterate_upper_bound = nullptr;
|
||||
read_options.prefix_same_as_start = true;
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
EXPECT_EQ(CountIter(iter, "qqqq"), 1);
|
||||
EXPECT_EQ(GetBloomStat(options, flushed), CheckedAndUseful(1, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
// A weird comparator that in combination with NonIdempotentFixed4Transform
|
||||
// breaks an old axiom of prefix filtering.
|
||||
class WeirdComparator : public Comparator {
|
||||
public:
|
||||
const char* Name() const override { return "WeirdComparator"; }
|
||||
|
||||
int Compare(const Slice& a, const Slice& b) const override {
|
||||
bool a_in = a.size() >= 5;
|
||||
bool b_in = b.size() >= 5;
|
||||
if (a_in != b_in) {
|
||||
// Order keys after prefixes
|
||||
return a_in - b_in;
|
||||
}
|
||||
if (a_in) {
|
||||
return BytewiseComparator()->Compare(a, b);
|
||||
} else {
|
||||
// Different ordering on the prefixes
|
||||
return ReverseBytewiseComparator()->Compare(a, b);
|
||||
}
|
||||
}
|
||||
|
||||
void FindShortestSeparator(std::string* /*start*/,
|
||||
const Slice& /*limit*/) const override {}
|
||||
|
||||
void FindShortSuccessor(std::string* /*key*/) const override {}
|
||||
};
|
||||
const WeirdComparator kWeirdComparator{};
|
||||
|
||||
// Non-idempotentent because prefix is always 4 bytes, but this is
|
||||
// out-of-domain for keys to be assigned prefixes (>= 5 bytes)
|
||||
class NonIdempotentFixed4Transform : public SliceTransform {
|
||||
const char* Name() const override { return "NonIdempotentFixed4Transform"; }
|
||||
|
||||
Slice Transform(const Slice& src) const override {
|
||||
return Slice(src.data(), 4);
|
||||
}
|
||||
|
||||
bool InDomain(const Slice& src) const override { return src.size() >= 5; }
|
||||
};
|
||||
} // namespace
|
||||
|
||||
// This uses a prefix_extractor + comparator combination that violates
|
||||
// two of the old obsolete, unnecessary axioms of prefix extraction:
|
||||
// * prefix(prefix(key)) == prefix(key)
|
||||
// * If Compare(k1, k2) <= 0, then Compare(prefix(k1), prefix(k2)) <= 0
|
||||
// This axiom is not really needed, and we validate that here.
|
||||
TEST_F(DBBloomFilterTest, WeirdPrefixExtractorWithFilter3) {
|
||||
BlockBasedTableOptions bbto;
|
||||
bbto.filter_policy.reset(ROCKSDB_NAMESPACE::NewBloomFilterPolicy(10));
|
||||
bbto.whole_key_filtering = false;
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.prefix_extractor = std::make_shared<NonIdempotentFixed4Transform>();
|
||||
options.table_factory.reset(NewBlockBasedTableFactory(bbto));
|
||||
options.memtable_prefix_bloom_size_ratio = 0.1;
|
||||
options.statistics = CreateDBStatistics();
|
||||
|
||||
for (auto weird_comparator : {false, true}) {
|
||||
if (weird_comparator) {
|
||||
options.comparator = &kWeirdComparator;
|
||||
}
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(Put("aaaa123", "val1"));
|
||||
ASSERT_OK(Put("aaaa211", "val2"));
|
||||
ASSERT_OK(Put("aaaa900", "val3"));
|
||||
ASSERT_OK(Put("aab", "val4")); // out of domain
|
||||
ASSERT_OK(Put("aaba123", "val5"));
|
||||
ASSERT_OK(Put("qqqq123", "val7"));
|
||||
ASSERT_OK(Put("qqqq", "val8")); // out of domain
|
||||
ASSERT_OK(Put("zzzz", "val8")); // out of domain
|
||||
|
||||
for (auto flushed : {false, true}) {
|
||||
SCOPED_TRACE("flushed=" + std::to_string(flushed));
|
||||
if (flushed) {
|
||||
ASSERT_OK(Flush());
|
||||
}
|
||||
ReadOptions read_options;
|
||||
if (flushed) { // TODO: support auto_prefix_mode in memtable?
|
||||
read_options.auto_prefix_mode = true;
|
||||
} else {
|
||||
// TODO: why needed?
|
||||
get_perf_context()->bloom_memtable_hit_count = 0;
|
||||
get_perf_context()->bloom_memtable_miss_count = 0;
|
||||
}
|
||||
EXPECT_EQ(GetBloomStat(options, flushed), CheckedAndUseful(0, 0));
|
||||
{
|
||||
Slice ub("aaaa999");
|
||||
read_options.iterate_upper_bound = &ub;
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
EXPECT_EQ(CountIter(iter, "aaaa000"), 3);
|
||||
EXPECT_EQ(GetBloomStat(options, flushed), CheckedAndUseful(1, 0));
|
||||
}
|
||||
{
|
||||
// Note: prefix as seek key is not bloom-optimized
|
||||
// Note: the count works with weird_comparator because "aaaa" is
|
||||
// ordered as the last of the prefixes
|
||||
Slice ub("aaaa999");
|
||||
read_options.iterate_upper_bound = &ub;
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
EXPECT_EQ(CountIter(iter, "aaaa"), 3);
|
||||
EXPECT_EQ(GetBloomStat(options, flushed), CheckedAndUseful(0, 0));
|
||||
}
|
||||
{
|
||||
Slice ub("aaba9");
|
||||
read_options.iterate_upper_bound = &ub;
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
EXPECT_EQ(CountIter(iter, "aaba0"), 1);
|
||||
EXPECT_EQ(GetBloomStat(options, flushed), CheckedAndUseful(1, 0));
|
||||
}
|
||||
{
|
||||
Slice ub("aaca9");
|
||||
read_options.iterate_upper_bound = &ub;
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
EXPECT_EQ(CountIter(iter, "aaca0"), 0);
|
||||
EXPECT_EQ(GetBloomStat(options, flushed), CheckedAndUseful(1, 1));
|
||||
}
|
||||
{
|
||||
Slice ub("qqqq9");
|
||||
read_options.iterate_upper_bound = &ub;
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
EXPECT_EQ(CountIter(iter, "qqqq0"), 1);
|
||||
EXPECT_EQ(GetBloomStat(options, flushed), CheckedAndUseful(1, 0));
|
||||
}
|
||||
{
|
||||
// Note: prefix as seek key is not bloom-optimized
|
||||
Slice ub("qqqq9");
|
||||
read_options.iterate_upper_bound = &ub;
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
EXPECT_EQ(CountIter(iter, "qqqq"), weird_comparator ? 7 : 2);
|
||||
EXPECT_EQ(GetBloomStat(options, flushed), CheckedAndUseful(0, 0));
|
||||
}
|
||||
{
|
||||
// Note: prefix as seek key is not bloom-optimized
|
||||
Slice ub("zzzz9");
|
||||
read_options.iterate_upper_bound = &ub;
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
EXPECT_EQ(CountIter(iter, "zzzz"), weird_comparator ? 8 : 1);
|
||||
EXPECT_EQ(GetBloomStat(options, flushed), CheckedAndUseful(0, 0));
|
||||
}
|
||||
{
|
||||
Slice ub("zzzz9");
|
||||
read_options.iterate_upper_bound = &ub;
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
EXPECT_EQ(CountIter(iter, "aab"), weird_comparator ? 6 : 5);
|
||||
EXPECT_EQ(GetBloomStat(options, flushed), CheckedAndUseful(0, 0));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // ROCKSDB_LITE
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
+221
-79
@@ -30,10 +30,89 @@ namespace ROCKSDB_NAMESPACE {
|
||||
// SYNC_POINT is not supported in released Windows mode.
|
||||
#if !defined(ROCKSDB_LITE)
|
||||
|
||||
class CompactionStatsCollector : public EventListener {
|
||||
public:
|
||||
CompactionStatsCollector()
|
||||
: compaction_completed_(
|
||||
static_cast<int>(CompactionReason::kNumOfReasons)) {
|
||||
for (auto& v : compaction_completed_) {
|
||||
v.store(0);
|
||||
}
|
||||
}
|
||||
|
||||
~CompactionStatsCollector() override {}
|
||||
|
||||
void OnCompactionCompleted(DB* /* db */,
|
||||
const CompactionJobInfo& info) override {
|
||||
int k = static_cast<int>(info.compaction_reason);
|
||||
int num_of_reasons = static_cast<int>(CompactionReason::kNumOfReasons);
|
||||
assert(k >= 0 && k < num_of_reasons);
|
||||
compaction_completed_[k]++;
|
||||
}
|
||||
|
||||
void OnExternalFileIngested(
|
||||
DB* /* db */, const ExternalFileIngestionInfo& /* info */) override {
|
||||
int k = static_cast<int>(CompactionReason::kExternalSstIngestion);
|
||||
compaction_completed_[k]++;
|
||||
}
|
||||
|
||||
void OnFlushCompleted(DB* /* db */, const FlushJobInfo& /* info */) override {
|
||||
int k = static_cast<int>(CompactionReason::kFlush);
|
||||
compaction_completed_[k]++;
|
||||
}
|
||||
|
||||
int NumberOfCompactions(CompactionReason reason) const {
|
||||
int num_of_reasons = static_cast<int>(CompactionReason::kNumOfReasons);
|
||||
int k = static_cast<int>(reason);
|
||||
assert(k >= 0 && k < num_of_reasons);
|
||||
return compaction_completed_.at(k).load();
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<std::atomic<int>> compaction_completed_;
|
||||
};
|
||||
|
||||
class DBCompactionTest : public DBTestBase {
|
||||
public:
|
||||
DBCompactionTest()
|
||||
: DBTestBase("db_compaction_test", /*env_do_fsync=*/true) {}
|
||||
|
||||
protected:
|
||||
/*
|
||||
* Verifies compaction stats of cfd are valid.
|
||||
*
|
||||
* For each level of cfd, its compaction stats are valid if
|
||||
* 1) sum(stat.counts) == stat.count, and
|
||||
* 2) stat.counts[i] == collector.NumberOfCompactions(i)
|
||||
*/
|
||||
void VerifyCompactionStats(ColumnFamilyData& cfd,
|
||||
const CompactionStatsCollector& collector) {
|
||||
#ifndef NDEBUG
|
||||
InternalStats* internal_stats_ptr = cfd.internal_stats();
|
||||
ASSERT_NE(internal_stats_ptr, nullptr);
|
||||
const std::vector<InternalStats::CompactionStats>& comp_stats =
|
||||
internal_stats_ptr->TEST_GetCompactionStats();
|
||||
const int num_of_reasons =
|
||||
static_cast<int>(CompactionReason::kNumOfReasons);
|
||||
std::vector<int> counts(num_of_reasons, 0);
|
||||
// Count the number of compactions caused by each CompactionReason across
|
||||
// all levels.
|
||||
for (const auto& stat : comp_stats) {
|
||||
int sum = 0;
|
||||
for (int i = 0; i < num_of_reasons; i++) {
|
||||
counts[i] += stat.counts[i];
|
||||
sum += stat.counts[i];
|
||||
}
|
||||
ASSERT_EQ(sum, stat.count);
|
||||
}
|
||||
// Verify InternalStats bookkeeping matches that of
|
||||
// CompactionStatsCollector, assuming that all compactions complete.
|
||||
for (int i = 0; i < num_of_reasons; i++) {
|
||||
ASSERT_EQ(collector.NumberOfCompactions(static_cast<CompactionReason>(i)),
|
||||
counts[i]);
|
||||
}
|
||||
#endif /* NDEBUG */
|
||||
}
|
||||
};
|
||||
|
||||
class DBCompactionTestWithParam
|
||||
@@ -110,47 +189,6 @@ class FlushedFileCollector : public EventListener {
|
||||
std::mutex mutex_;
|
||||
};
|
||||
|
||||
class CompactionStatsCollector : public EventListener {
|
||||
public:
|
||||
CompactionStatsCollector()
|
||||
: compaction_completed_(static_cast<int>(CompactionReason::kNumOfReasons)) {
|
||||
for (auto& v : compaction_completed_) {
|
||||
v.store(0);
|
||||
}
|
||||
}
|
||||
|
||||
~CompactionStatsCollector() override {}
|
||||
|
||||
void OnCompactionCompleted(DB* /* db */,
|
||||
const CompactionJobInfo& info) override {
|
||||
int k = static_cast<int>(info.compaction_reason);
|
||||
int num_of_reasons = static_cast<int>(CompactionReason::kNumOfReasons);
|
||||
assert(k >= 0 && k < num_of_reasons);
|
||||
compaction_completed_[k]++;
|
||||
}
|
||||
|
||||
void OnExternalFileIngested(
|
||||
DB* /* db */, const ExternalFileIngestionInfo& /* info */) override {
|
||||
int k = static_cast<int>(CompactionReason::kExternalSstIngestion);
|
||||
compaction_completed_[k]++;
|
||||
}
|
||||
|
||||
void OnFlushCompleted(DB* /* db */, const FlushJobInfo& /* info */) override {
|
||||
int k = static_cast<int>(CompactionReason::kFlush);
|
||||
compaction_completed_[k]++;
|
||||
}
|
||||
|
||||
int NumberOfCompactions(CompactionReason reason) const {
|
||||
int num_of_reasons = static_cast<int>(CompactionReason::kNumOfReasons);
|
||||
int k = static_cast<int>(reason);
|
||||
assert(k >= 0 && k < num_of_reasons);
|
||||
return compaction_completed_.at(k).load();
|
||||
}
|
||||
|
||||
private:
|
||||
std::vector<std::atomic<int>> compaction_completed_;
|
||||
};
|
||||
|
||||
class SstStatsCollector : public EventListener {
|
||||
public:
|
||||
SstStatsCollector() : num_ssts_creation_started_(0) {}
|
||||
@@ -247,40 +285,6 @@ void VerifyCompactionResult(
|
||||
#endif
|
||||
}
|
||||
|
||||
/*
|
||||
* Verifies compaction stats of cfd are valid.
|
||||
*
|
||||
* For each level of cfd, its compaction stats are valid if
|
||||
* 1) sum(stat.counts) == stat.count, and
|
||||
* 2) stat.counts[i] == collector.NumberOfCompactions(i)
|
||||
*/
|
||||
void VerifyCompactionStats(ColumnFamilyData& cfd,
|
||||
const CompactionStatsCollector& collector) {
|
||||
#ifndef NDEBUG
|
||||
InternalStats* internal_stats_ptr = cfd.internal_stats();
|
||||
ASSERT_NE(internal_stats_ptr, nullptr);
|
||||
const std::vector<InternalStats::CompactionStats>& comp_stats =
|
||||
internal_stats_ptr->TEST_GetCompactionStats();
|
||||
const int num_of_reasons = static_cast<int>(CompactionReason::kNumOfReasons);
|
||||
std::vector<int> counts(num_of_reasons, 0);
|
||||
// Count the number of compactions caused by each CompactionReason across
|
||||
// all levels.
|
||||
for (const auto& stat : comp_stats) {
|
||||
int sum = 0;
|
||||
for (int i = 0; i < num_of_reasons; i++) {
|
||||
counts[i] += stat.counts[i];
|
||||
sum += stat.counts[i];
|
||||
}
|
||||
ASSERT_EQ(sum, stat.count);
|
||||
}
|
||||
// Verify InternalStats bookkeeping matches that of CompactionStatsCollector,
|
||||
// assuming that all compactions complete.
|
||||
for (int i = 0; i < num_of_reasons; i++) {
|
||||
ASSERT_EQ(collector.NumberOfCompactions(static_cast<CompactionReason>(i)), counts[i]);
|
||||
}
|
||||
#endif /* NDEBUG */
|
||||
}
|
||||
|
||||
const SstFileMetaData* PickFileRandomly(
|
||||
const ColumnFamilyMetaData& cf_meta,
|
||||
Random* rand,
|
||||
@@ -1093,16 +1097,20 @@ TEST_F(DBCompactionTest, ManualCompactionUnknownOutputSize) {
|
||||
// create two files in l1 that we can compact
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
for (int j = 0; j < options.level0_file_num_compaction_trigger; j++) {
|
||||
// make l0 files' ranges overlap to avoid trivial move
|
||||
ASSERT_OK(Put(std::to_string(2 * i), std::string(1, 'A')));
|
||||
ASSERT_OK(Put(std::to_string(2 * i + 1), std::string(1, 'A')));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(dbfull()->TEST_WaitForFlushMemTable());
|
||||
}
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
ASSERT_EQ(NumTableFilesAtLevel(0, 0), 0);
|
||||
ASSERT_EQ(NumTableFilesAtLevel(1, 0), i + 1);
|
||||
}
|
||||
ASSERT_OK(
|
||||
dbfull()->SetOptions({{"level0_file_num_compaction_trigger", "2"}}));
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
ASSERT_EQ(NumTableFilesAtLevel(0, 0), 0);
|
||||
ASSERT_EQ(NumTableFilesAtLevel(1, 0), 2);
|
||||
ASSERT_OK(
|
||||
dbfull()->SetOptions({{"level0_file_num_compaction_trigger", "3"}}));
|
||||
|
||||
ColumnFamilyMetaData cf_meta;
|
||||
dbfull()->GetColumnFamilyMetaData(dbfull()->DefaultColumnFamily(), &cf_meta);
|
||||
@@ -4366,7 +4374,13 @@ TEST_F(DBCompactionTest, LevelTtlBooster) {
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
}
|
||||
// Force files to be compacted to L1
|
||||
ASSERT_OK(
|
||||
dbfull()->SetOptions({{"level0_file_num_compaction_trigger", "1"}}));
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
ASSERT_EQ("0,1,2", FilesPerLevel());
|
||||
ASSERT_OK(
|
||||
dbfull()->SetOptions({{"level0_file_num_compaction_trigger", "2"}}));
|
||||
|
||||
ASSERT_GT(SizeAtLevel(1), kNumKeysPerFile * 4 * kValueSize);
|
||||
}
|
||||
@@ -5219,7 +5233,135 @@ INSTANTIATE_TEST_CASE_P(
|
||||
::testing::Values(CompactionPri::kByCompensatedSize,
|
||||
CompactionPri::kOldestLargestSeqFirst,
|
||||
CompactionPri::kOldestSmallestSeqFirst,
|
||||
CompactionPri::kMinOverlappingRatio));
|
||||
CompactionPri::kMinOverlappingRatio,
|
||||
CompactionPri::kRoundRobin));
|
||||
|
||||
TEST_F(DBCompactionTest, PersistRoundRobinCompactCursor) {
|
||||
Options options = CurrentOptions();
|
||||
options.write_buffer_size = 16 * 1024;
|
||||
options.max_bytes_for_level_base = 128 * 1024;
|
||||
options.target_file_size_base = 64 * 1024;
|
||||
options.level0_file_num_compaction_trigger = 4;
|
||||
options.compaction_pri = CompactionPri::kRoundRobin;
|
||||
options.max_bytes_for_level_multiplier = 4;
|
||||
options.num_levels = 3;
|
||||
options.compression = kNoCompression;
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
Random rnd(301);
|
||||
|
||||
// 30 Files in L0 to trigger compactions between L1 and L2
|
||||
for (int i = 0; i < 30; i++) {
|
||||
for (int j = 0; j < 16; j++) {
|
||||
ASSERT_OK(Put(rnd.RandomString(24), rnd.RandomString(1000)));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
}
|
||||
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
|
||||
VersionSet* const versions = dbfull()->GetVersionSet();
|
||||
assert(versions);
|
||||
|
||||
ColumnFamilyData* const cfd = versions->GetColumnFamilySet()->GetDefault();
|
||||
ASSERT_NE(cfd, nullptr);
|
||||
|
||||
Version* const current = cfd->current();
|
||||
ASSERT_NE(current, nullptr);
|
||||
|
||||
const VersionStorageInfo* const storage_info = current->storage_info();
|
||||
ASSERT_NE(storage_info, nullptr);
|
||||
|
||||
const std::vector<InternalKey> compact_cursors =
|
||||
storage_info->GetCompactCursors();
|
||||
|
||||
Reopen(options);
|
||||
|
||||
VersionSet* const reopened_versions = dbfull()->GetVersionSet();
|
||||
assert(reopened_versions);
|
||||
|
||||
ColumnFamilyData* const reopened_cfd =
|
||||
reopened_versions->GetColumnFamilySet()->GetDefault();
|
||||
ASSERT_NE(reopened_cfd, nullptr);
|
||||
|
||||
Version* const reopened_current = reopened_cfd->current();
|
||||
ASSERT_NE(reopened_current, nullptr);
|
||||
|
||||
const VersionStorageInfo* const reopened_storage_info =
|
||||
reopened_current->storage_info();
|
||||
ASSERT_NE(reopened_storage_info, nullptr);
|
||||
|
||||
const std::vector<InternalKey> reopened_compact_cursors =
|
||||
reopened_storage_info->GetCompactCursors();
|
||||
const auto icmp = reopened_storage_info->InternalComparator();
|
||||
ASSERT_EQ(compact_cursors.size(), reopened_compact_cursors.size());
|
||||
for (size_t i = 0; i < compact_cursors.size(); i++) {
|
||||
if (compact_cursors[i].Valid()) {
|
||||
ASSERT_EQ(0,
|
||||
icmp->Compare(compact_cursors[i], reopened_compact_cursors[i]));
|
||||
} else {
|
||||
ASSERT_TRUE(!reopened_compact_cursors[i].Valid());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionTest, RoundRobinCutOutputAtCompactCursor) {
|
||||
Options options = CurrentOptions();
|
||||
options.num_levels = 3;
|
||||
options.compression = kNoCompression;
|
||||
options.write_buffer_size = 4 * 1024;
|
||||
options.max_bytes_for_level_base = 64 * 1024;
|
||||
options.max_bytes_for_level_multiplier = 4;
|
||||
options.level0_file_num_compaction_trigger = 4;
|
||||
options.compaction_pri = CompactionPri::kRoundRobin;
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
VersionSet* const versions = dbfull()->GetVersionSet();
|
||||
assert(versions);
|
||||
|
||||
ColumnFamilyData* const cfd = versions->GetColumnFamilySet()->GetDefault();
|
||||
ASSERT_NE(cfd, nullptr);
|
||||
|
||||
Version* const current = cfd->current();
|
||||
ASSERT_NE(current, nullptr);
|
||||
|
||||
VersionStorageInfo* storage_info = current->storage_info();
|
||||
ASSERT_NE(storage_info, nullptr);
|
||||
|
||||
const InternalKey split_cursor = InternalKey(Key(600), 100, kTypeValue);
|
||||
storage_info->AddCursorForOneLevel(2, split_cursor);
|
||||
|
||||
Random rnd(301);
|
||||
|
||||
for (int i = 0; i < 50; i++) {
|
||||
for (int j = 0; j < 50; j++) {
|
||||
ASSERT_OK(Put(Key(j * 2 + i * 100), rnd.RandomString(102)));
|
||||
}
|
||||
}
|
||||
// Add more overlapping files (avoid trivial move) to trigger compaction that
|
||||
// output files in L2. Note that trivial move does not trigger compaction and
|
||||
// in that case the cursor is not necessarily the boundary of file.
|
||||
for (int i = 0; i < 50; i++) {
|
||||
for (int j = 0; j < 50; j++) {
|
||||
ASSERT_OK(Put(Key(j * 2 + 1 + i * 100), rnd.RandomString(1014)));
|
||||
}
|
||||
}
|
||||
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
|
||||
std::vector<std::vector<FileMetaData>> level_to_files;
|
||||
dbfull()->TEST_GetFilesMetaData(dbfull()->DefaultColumnFamily(),
|
||||
&level_to_files);
|
||||
const auto icmp = cfd->current()->storage_info()->InternalComparator();
|
||||
// Files in level 2 should be split by the cursor
|
||||
for (const auto& file : level_to_files[2]) {
|
||||
ASSERT_TRUE(
|
||||
icmp->Compare(file.smallest.Encode(), split_cursor.Encode()) >= 0 ||
|
||||
icmp->Compare(file.largest.Encode(), split_cursor.Encode()) < 0);
|
||||
}
|
||||
}
|
||||
|
||||
class NoopMergeOperator : public MergeOperator {
|
||||
public:
|
||||
|
||||
+247
-6
@@ -777,13 +777,25 @@ TEST_F(DBFlushTest, MemPurgeBasic) {
|
||||
|
||||
// Enforce size of a single MemTable to 64MB (64MB = 67108864 bytes).
|
||||
options.write_buffer_size = 1 << 20;
|
||||
// Activate the MemPurge prototype.
|
||||
options.experimental_mempurge_threshold = 1.0;
|
||||
#ifndef ROCKSDB_LITE
|
||||
// Initially deactivate the MemPurge prototype.
|
||||
options.experimental_mempurge_threshold = 0.0;
|
||||
TestFlushListener* listener = new TestFlushListener(options.env, this);
|
||||
options.listeners.emplace_back(listener);
|
||||
#else
|
||||
// Activate directly the MemPurge prototype.
|
||||
// (RocksDB lite does not support dynamic options)
|
||||
options.experimental_mempurge_threshold = 1.0;
|
||||
#endif // !ROCKSDB_LITE
|
||||
ASSERT_OK(TryReopen(options));
|
||||
|
||||
// RocksDB lite does not support dynamic options
|
||||
#ifndef ROCKSDB_LITE
|
||||
// Dynamically activate the MemPurge prototype without restarting the DB.
|
||||
ColumnFamilyHandle* cfh = db_->DefaultColumnFamily();
|
||||
ASSERT_OK(db_->SetOptions(cfh, {{"experimental_mempurge_threshold", "1.0"}}));
|
||||
#endif
|
||||
|
||||
std::atomic<uint32_t> mempurge_count{0};
|
||||
std::atomic<uint32_t> sst_count{0};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
@@ -914,6 +926,234 @@ TEST_F(DBFlushTest, MemPurgeBasic) {
|
||||
Close();
|
||||
}
|
||||
|
||||
// RocksDB lite does not support dynamic options
|
||||
#ifndef ROCKSDB_LITE
|
||||
TEST_F(DBFlushTest, MemPurgeBasicToggle) {
|
||||
Options options = CurrentOptions();
|
||||
|
||||
// The following options are used to enforce several values that
|
||||
// may already exist as default values to make this test resilient
|
||||
// to default value updates in the future.
|
||||
options.statistics = CreateDBStatistics();
|
||||
|
||||
// Record all statistics.
|
||||
options.statistics->set_stats_level(StatsLevel::kAll);
|
||||
|
||||
// create the DB if it's not already present
|
||||
options.create_if_missing = true;
|
||||
|
||||
// Useful for now as we are trying to compare uncompressed data savings on
|
||||
// flush().
|
||||
options.compression = kNoCompression;
|
||||
|
||||
// Prevent memtable in place updates. Should already be disabled
|
||||
// (from Wiki:
|
||||
// In place updates can be enabled by toggling on the bool
|
||||
// inplace_update_support flag. However, this flag is by default set to
|
||||
// false
|
||||
// because this thread-safe in-place update support is not compatible
|
||||
// with concurrent memtable writes. Note that the bool
|
||||
// allow_concurrent_memtable_write is set to true by default )
|
||||
options.inplace_update_support = false;
|
||||
options.allow_concurrent_memtable_write = true;
|
||||
|
||||
// Enforce size of a single MemTable to 64MB (64MB = 67108864 bytes).
|
||||
options.write_buffer_size = 1 << 20;
|
||||
// Initially deactivate the MemPurge prototype.
|
||||
// (negative values are equivalent to 0.0).
|
||||
options.experimental_mempurge_threshold = -25.3;
|
||||
TestFlushListener* listener = new TestFlushListener(options.env, this);
|
||||
options.listeners.emplace_back(listener);
|
||||
|
||||
ASSERT_OK(TryReopen(options));
|
||||
// Dynamically activate the MemPurge prototype without restarting the DB.
|
||||
ColumnFamilyHandle* cfh = db_->DefaultColumnFamily();
|
||||
// Values greater than 1.0 are equivalent to 1.0
|
||||
ASSERT_OK(
|
||||
db_->SetOptions(cfh, {{"experimental_mempurge_threshold", "3.7898"}}));
|
||||
std::atomic<uint32_t> mempurge_count{0};
|
||||
std::atomic<uint32_t> sst_count{0};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::FlushJob:MemPurgeSuccessful",
|
||||
[&](void* /*arg*/) { mempurge_count++; });
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::FlushJob:SSTFileCreated", [&](void* /*arg*/) { sst_count++; });
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
const size_t KVSIZE = 3;
|
||||
std::vector<std::string> KEYS(KVSIZE);
|
||||
for (size_t k = 0; k < KVSIZE; k++) {
|
||||
KEYS[k] = "IamKey" + std::to_string(k);
|
||||
}
|
||||
|
||||
std::vector<std::string> RNDVALS(KVSIZE);
|
||||
const std::string NOT_FOUND = "NOT_FOUND";
|
||||
|
||||
// Heavy overwrite workload,
|
||||
// more than would fit in maximum allowed memtables.
|
||||
Random rnd(719);
|
||||
const size_t NUM_REPEAT = 100;
|
||||
const size_t RAND_VALUES_LENGTH = 10240;
|
||||
|
||||
// Insertion of of K-V pairs, multiple times (overwrites).
|
||||
for (size_t i = 0; i < NUM_REPEAT; i++) {
|
||||
for (size_t j = 0; j < KEYS.size(); j++) {
|
||||
RNDVALS[j] = rnd.RandomString(RAND_VALUES_LENGTH);
|
||||
ASSERT_OK(Put(KEYS[j], RNDVALS[j]));
|
||||
ASSERT_EQ(Get(KEYS[j]), RNDVALS[j]);
|
||||
}
|
||||
for (size_t j = 0; j < KEYS.size(); j++) {
|
||||
ASSERT_EQ(Get(KEYS[j]), RNDVALS[j]);
|
||||
}
|
||||
}
|
||||
|
||||
// Check that there was at least one mempurge
|
||||
const uint32_t EXPECTED_MIN_MEMPURGE_COUNT = 1;
|
||||
// Check that there was no SST files created during flush.
|
||||
const uint32_t EXPECTED_SST_COUNT = 0;
|
||||
|
||||
EXPECT_GE(mempurge_count.exchange(0), EXPECTED_MIN_MEMPURGE_COUNT);
|
||||
EXPECT_EQ(sst_count.exchange(0), EXPECTED_SST_COUNT);
|
||||
|
||||
// Dynamically deactivate MemPurge.
|
||||
ASSERT_OK(
|
||||
db_->SetOptions(cfh, {{"experimental_mempurge_threshold", "-1023.0"}}));
|
||||
|
||||
// Insertion of of K-V pairs, multiple times (overwrites).
|
||||
for (size_t i = 0; i < NUM_REPEAT; i++) {
|
||||
for (size_t j = 0; j < KEYS.size(); j++) {
|
||||
RNDVALS[j] = rnd.RandomString(RAND_VALUES_LENGTH);
|
||||
ASSERT_OK(Put(KEYS[j], RNDVALS[j]));
|
||||
ASSERT_EQ(Get(KEYS[j]), RNDVALS[j]);
|
||||
}
|
||||
for (size_t j = 0; j < KEYS.size(); j++) {
|
||||
ASSERT_EQ(Get(KEYS[j]), RNDVALS[j]);
|
||||
}
|
||||
}
|
||||
|
||||
// Check that there was at least one mempurge
|
||||
const uint32_t ZERO = 0;
|
||||
// Assert that at least one flush to storage has been performed
|
||||
EXPECT_GT(sst_count.exchange(0), EXPECTED_SST_COUNT);
|
||||
// The mempurge count is expected to be set to 0 when the options are updated.
|
||||
// We expect no mempurge at all.
|
||||
EXPECT_EQ(mempurge_count.exchange(0), ZERO);
|
||||
|
||||
Close();
|
||||
}
|
||||
// Closes the "#ifndef ROCKSDB_LITE"
|
||||
// End of MemPurgeBasicToggle, which is not
|
||||
// supported with RocksDB LITE because it
|
||||
// relies on dynamically changing the option
|
||||
// flag experimental_mempurge_threshold.
|
||||
#endif
|
||||
|
||||
// At the moment, MemPurge feature is deactivated
|
||||
// when atomic_flush is enabled. This is because the level
|
||||
// of garbage between Column Families is not guaranteed to
|
||||
// be consistent, therefore a CF could hypothetically
|
||||
// trigger a MemPurge while another CF would trigger
|
||||
// a regular Flush.
|
||||
TEST_F(DBFlushTest, MemPurgeWithAtomicFlush) {
|
||||
Options options = CurrentOptions();
|
||||
|
||||
// The following options are used to enforce several values that
|
||||
// may already exist as default values to make this test resilient
|
||||
// to default value updates in the future.
|
||||
options.statistics = CreateDBStatistics();
|
||||
|
||||
// Record all statistics.
|
||||
options.statistics->set_stats_level(StatsLevel::kAll);
|
||||
|
||||
// create the DB if it's not already present
|
||||
options.create_if_missing = true;
|
||||
|
||||
// Useful for now as we are trying to compare uncompressed data savings on
|
||||
// flush().
|
||||
options.compression = kNoCompression;
|
||||
|
||||
// Prevent memtable in place updates. Should already be disabled
|
||||
// (from Wiki:
|
||||
// In place updates can be enabled by toggling on the bool
|
||||
// inplace_update_support flag. However, this flag is by default set to
|
||||
// false
|
||||
// because this thread-safe in-place update support is not compatible
|
||||
// with concurrent memtable writes. Note that the bool
|
||||
// allow_concurrent_memtable_write is set to true by default )
|
||||
options.inplace_update_support = false;
|
||||
options.allow_concurrent_memtable_write = true;
|
||||
|
||||
// Enforce size of a single MemTable to 64KB (64KB = 65,536 bytes).
|
||||
options.write_buffer_size = 1 << 20;
|
||||
// Activate the MemPurge prototype.
|
||||
options.experimental_mempurge_threshold = 153.245;
|
||||
// Activate atomic_flush.
|
||||
options.atomic_flush = true;
|
||||
|
||||
const std::vector<std::string> new_cf_names = {"pikachu", "eevie"};
|
||||
CreateColumnFamilies(new_cf_names, options);
|
||||
|
||||
Close();
|
||||
|
||||
// 3 CFs: default will be filled with overwrites (would normally trigger
|
||||
// mempurge)
|
||||
// new_cf_names[1] will be filled with random values (would trigger
|
||||
// flush) new_cf_names[2] not filled with anything.
|
||||
ReopenWithColumnFamilies(
|
||||
{kDefaultColumnFamilyName, new_cf_names[0], new_cf_names[1]}, options);
|
||||
size_t num_cfs = handles_.size();
|
||||
ASSERT_EQ(3, num_cfs);
|
||||
ASSERT_OK(Put(1, "foo", "bar"));
|
||||
ASSERT_OK(Put(2, "bar", "baz"));
|
||||
|
||||
std::atomic<uint32_t> mempurge_count{0};
|
||||
std::atomic<uint32_t> sst_count{0};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::FlushJob:MemPurgeSuccessful",
|
||||
[&](void* /*arg*/) { mempurge_count++; });
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::FlushJob:SSTFileCreated", [&](void* /*arg*/) { sst_count++; });
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
const size_t KVSIZE = 3;
|
||||
std::vector<std::string> KEYS(KVSIZE);
|
||||
for (size_t k = 0; k < KVSIZE; k++) {
|
||||
KEYS[k] = "IamKey" + std::to_string(k);
|
||||
}
|
||||
|
||||
std::string RNDKEY;
|
||||
std::vector<std::string> RNDVALS(KVSIZE);
|
||||
const std::string NOT_FOUND = "NOT_FOUND";
|
||||
|
||||
// Heavy overwrite workload,
|
||||
// more than would fit in maximum allowed memtables.
|
||||
Random rnd(106);
|
||||
const size_t NUM_REPEAT = 100;
|
||||
const size_t RAND_KEY_LENGTH = 128;
|
||||
const size_t RAND_VALUES_LENGTH = 10240;
|
||||
|
||||
// Insertion of of K-V pairs, multiple times (overwrites).
|
||||
for (size_t i = 0; i < NUM_REPEAT; i++) {
|
||||
for (size_t j = 0; j < KEYS.size(); j++) {
|
||||
RNDKEY = rnd.RandomString(RAND_KEY_LENGTH);
|
||||
RNDVALS[j] = rnd.RandomString(RAND_VALUES_LENGTH);
|
||||
ASSERT_OK(Put(KEYS[j], RNDVALS[j]));
|
||||
ASSERT_OK(Put(1, RNDKEY, RNDVALS[j]));
|
||||
ASSERT_EQ(Get(KEYS[j]), RNDVALS[j]);
|
||||
ASSERT_EQ(Get(1, RNDKEY), RNDVALS[j]);
|
||||
}
|
||||
}
|
||||
|
||||
// Check that there was no mempurge because atomic_flush option is true.
|
||||
const uint32_t EXPECTED_MIN_MEMPURGE_COUNT = 0;
|
||||
// Check that there was at least one SST files created during flush.
|
||||
const uint32_t EXPECTED_SST_COUNT = 1;
|
||||
|
||||
EXPECT_EQ(mempurge_count.exchange(0), EXPECTED_MIN_MEMPURGE_COUNT);
|
||||
EXPECT_GE(sst_count.exchange(0), EXPECTED_SST_COUNT);
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(DBFlushTest, MemPurgeDeleteAndDeleteRange) {
|
||||
Options options = CurrentOptions();
|
||||
|
||||
@@ -930,7 +1170,7 @@ TEST_F(DBFlushTest, MemPurgeDeleteAndDeleteRange) {
|
||||
// Enforce size of a single MemTable to 64MB (64MB = 67108864 bytes).
|
||||
options.write_buffer_size = 1 << 20;
|
||||
// Activate the MemPurge prototype.
|
||||
options.experimental_mempurge_threshold = 1.0;
|
||||
options.experimental_mempurge_threshold = 15.0;
|
||||
|
||||
ASSERT_OK(TryReopen(options));
|
||||
|
||||
@@ -1137,7 +1377,7 @@ TEST_F(DBFlushTest, MemPurgeAndCompactionFilter) {
|
||||
// Enforce size of a single MemTable to 64MB (64MB = 67108864 bytes).
|
||||
options.write_buffer_size = 1 << 20;
|
||||
// Activate the MemPurge prototype.
|
||||
options.experimental_mempurge_threshold = 1.0;
|
||||
options.experimental_mempurge_threshold = 26.55;
|
||||
|
||||
ASSERT_OK(TryReopen(options));
|
||||
|
||||
@@ -1212,8 +1452,9 @@ TEST_F(DBFlushTest, DISABLED_MemPurgeWALSupport) {
|
||||
|
||||
// Enforce size of a single MemTable to 128KB.
|
||||
options.write_buffer_size = 128 << 10;
|
||||
// Activate the MemPurge prototype.
|
||||
options.experimental_mempurge_threshold = 1.0;
|
||||
// Activate the MemPurge prototype
|
||||
// (values >1.0 are equivalent to 1.0).
|
||||
options.experimental_mempurge_threshold = 2.5;
|
||||
|
||||
ASSERT_OK(TryReopen(options));
|
||||
|
||||
|
||||
@@ -54,12 +54,22 @@ class CompactedDBImpl : public DBImpl {
|
||||
const Slice& /*key*/, const Slice& /*value*/) override {
|
||||
return Status::NotSupported("Not supported in compacted db mode.");
|
||||
}
|
||||
|
||||
using DBImpl::PutEntity;
|
||||
Status PutEntity(const WriteOptions& /* options */,
|
||||
ColumnFamilyHandle* /* column_family */,
|
||||
const Slice& /* key */,
|
||||
const WideColumns& /* columns */) override {
|
||||
return Status::NotSupported("Not supported in compacted db mode.");
|
||||
}
|
||||
|
||||
using DBImpl::Merge;
|
||||
virtual Status Merge(const WriteOptions& /*options*/,
|
||||
ColumnFamilyHandle* /*column_family*/,
|
||||
const Slice& /*key*/, const Slice& /*value*/) override {
|
||||
return Status::NotSupported("Not supported in compacted db mode.");
|
||||
}
|
||||
|
||||
using DBImpl::Delete;
|
||||
virtual Status Delete(const WriteOptions& /*options*/,
|
||||
ColumnFamilyHandle* /*column_family*/,
|
||||
|
||||
+104
-40
@@ -156,7 +156,9 @@ DBImpl::DBImpl(const DBOptions& options, const std::string& dbname,
|
||||
bool read_only)
|
||||
: dbname_(dbname),
|
||||
own_info_log_(options.info_log == nullptr),
|
||||
initial_db_options_(SanitizeOptions(dbname, options, read_only)),
|
||||
init_logger_creation_s_(),
|
||||
initial_db_options_(SanitizeOptions(dbname, options, read_only,
|
||||
&init_logger_creation_s_)),
|
||||
env_(initial_db_options_.env),
|
||||
io_tracer_(std::make_shared<IOTracer>()),
|
||||
immutable_db_options_(initial_db_options_),
|
||||
@@ -244,8 +246,6 @@ DBImpl::DBImpl(const DBOptions& options, const std::string& dbname,
|
||||
// !batch_per_trx_ implies seq_per_batch_ because it is only unset for
|
||||
// WriteUnprepared, which should use seq_per_batch_.
|
||||
assert(batch_per_txn_ || seq_per_batch_);
|
||||
// TODO: Check for an error here
|
||||
env_->GetAbsolutePath(dbname, &db_absolute_path_).PermitUncheckedError();
|
||||
|
||||
// Reserve ten files or so for other uses and give the rest to TableCache.
|
||||
// Give a large number for setting of "infinite" open files.
|
||||
@@ -263,7 +263,7 @@ DBImpl::DBImpl(const DBOptions& options, const std::string& dbname,
|
||||
versions_.reset(new VersionSet(dbname_, &immutable_db_options_, file_options_,
|
||||
table_cache_.get(), write_buffer_manager_,
|
||||
&write_controller_, &block_cache_tracer_,
|
||||
io_tracer_, db_session_id_));
|
||||
io_tracer_, db_id_, db_session_id_));
|
||||
column_family_memtables_.reset(
|
||||
new ColumnFamilyMemTablesImpl(versions_->GetColumnFamilySet()));
|
||||
|
||||
@@ -480,6 +480,7 @@ void DBImpl::CancelAllBackgroundWork(bool wait) {
|
||||
#ifndef ROCKSDB_LITE
|
||||
if (periodic_work_scheduler_ != nullptr) {
|
||||
periodic_work_scheduler_->Unregister(this);
|
||||
periodic_work_scheduler_->UnregisterRecordSeqnoTimeWorker(this);
|
||||
}
|
||||
#endif // !ROCKSDB_LITE
|
||||
|
||||
@@ -576,38 +577,6 @@ Status DBImpl::CloseHelper() {
|
||||
flush_scheduler_.Clear();
|
||||
trim_history_scheduler_.Clear();
|
||||
|
||||
// For now, simply trigger a manual flush at close time
|
||||
// on all the column families.
|
||||
// TODO(bjlemaire): Check if this is needed. Also, in the
|
||||
// future we can contemplate doing a more fine-grained
|
||||
// flushing by first checking if there is a need for
|
||||
// flushing (but need to implement something
|
||||
// else than imm()->IsFlushPending() because the output
|
||||
// memtables added to imm() don't trigger flushes).
|
||||
if (immutable_db_options_.experimental_mempurge_threshold > 0.0) {
|
||||
Status flush_ret;
|
||||
mutex_.Unlock();
|
||||
for (ColumnFamilyData* cf : *versions_->GetColumnFamilySet()) {
|
||||
if (immutable_db_options_.atomic_flush) {
|
||||
flush_ret = AtomicFlushMemTables({cf}, FlushOptions(),
|
||||
FlushReason::kManualFlush);
|
||||
if (!flush_ret.ok()) {
|
||||
ROCKS_LOG_INFO(
|
||||
immutable_db_options_.info_log,
|
||||
"Atomic flush memtables failed upon closing (mempurge).");
|
||||
}
|
||||
} else {
|
||||
flush_ret =
|
||||
FlushMemTable(cf, FlushOptions(), FlushReason::kManualFlush);
|
||||
if (!flush_ret.ok()) {
|
||||
ROCKS_LOG_INFO(immutable_db_options_.info_log,
|
||||
"Flush memtables failed upon closing (mempurge).");
|
||||
}
|
||||
}
|
||||
}
|
||||
mutex_.Lock();
|
||||
}
|
||||
|
||||
while (!flush_queue_.empty()) {
|
||||
const FlushRequest& flush_req = PopFirstFromFlushQueue();
|
||||
for (const auto& iter : flush_req) {
|
||||
@@ -747,6 +716,9 @@ Status DBImpl::CloseHelper() {
|
||||
Status DBImpl::CloseImpl() { return CloseHelper(); }
|
||||
|
||||
DBImpl::~DBImpl() {
|
||||
// TODO: remove this.
|
||||
init_logger_creation_s_.PermitUncheckedError();
|
||||
|
||||
InstrumentedMutexLock closing_lock_guard(&closing_mutex_);
|
||||
if (closed_) {
|
||||
return;
|
||||
@@ -820,6 +792,66 @@ Status DBImpl::StartPeriodicWorkScheduler() {
|
||||
#endif // !ROCKSDB_LITE
|
||||
}
|
||||
|
||||
Status DBImpl::RegisterRecordSeqnoTimeWorker() {
|
||||
#ifndef ROCKSDB_LITE
|
||||
if (!periodic_work_scheduler_) {
|
||||
return Status::OK();
|
||||
}
|
||||
uint64_t min_time_duration = std::numeric_limits<uint64_t>::max();
|
||||
uint64_t max_time_duration = std::numeric_limits<uint64_t>::min();
|
||||
{
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
|
||||
for (auto cfd : *versions_->GetColumnFamilySet()) {
|
||||
uint64_t preclude_last_option =
|
||||
cfd->ioptions()->preclude_last_level_data_seconds;
|
||||
if (!cfd->IsDropped() && preclude_last_option > 0) {
|
||||
min_time_duration = std::min(preclude_last_option, min_time_duration);
|
||||
max_time_duration = std::max(preclude_last_option, max_time_duration);
|
||||
}
|
||||
}
|
||||
if (min_time_duration == std::numeric_limits<uint64_t>::max()) {
|
||||
seqno_time_mapping_.Resize(0, 0);
|
||||
} else {
|
||||
seqno_time_mapping_.Resize(min_time_duration, max_time_duration);
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t seqno_time_cadence = 0;
|
||||
if (min_time_duration != std::numeric_limits<uint64_t>::max()) {
|
||||
seqno_time_cadence =
|
||||
min_time_duration / SeqnoToTimeMapping::kMaxSeqnoTimePairsPerCF;
|
||||
}
|
||||
|
||||
Status s;
|
||||
if (seqno_time_cadence != record_seqno_time_cadence_) {
|
||||
if (seqno_time_cadence == 0) {
|
||||
periodic_work_scheduler_->UnregisterRecordSeqnoTimeWorker(this);
|
||||
} else {
|
||||
s = periodic_work_scheduler_->RegisterRecordSeqnoTimeWorker(
|
||||
this, seqno_time_cadence);
|
||||
}
|
||||
|
||||
if (s.ok()) {
|
||||
record_seqno_time_cadence_ = seqno_time_cadence;
|
||||
}
|
||||
|
||||
if (s.IsNotSupported()) {
|
||||
// TODO: Fix the timer cannot cancel and re-add the same task
|
||||
ROCKS_LOG_WARN(
|
||||
immutable_db_options_.info_log,
|
||||
"Updating seqno to time worker cadence is not supported yet, to make "
|
||||
"the change effective, please reopen the DB instance.");
|
||||
s = Status::OK();
|
||||
}
|
||||
}
|
||||
|
||||
return s;
|
||||
#else
|
||||
return Status::OK();
|
||||
#endif // !ROCKSDB_LITE
|
||||
}
|
||||
|
||||
// esitmate the total size of stats_history_
|
||||
size_t DBImpl::EstimateInMemoryStatsHistorySize() const {
|
||||
size_t size_total =
|
||||
@@ -1471,12 +1503,12 @@ Status DBImpl::MarkLogsSynced(uint64_t up_to, bool synced_dir) {
|
||||
for (auto it = logs_.begin(); it != logs_.end() && it->number <= up_to;) {
|
||||
auto& wal = *it;
|
||||
assert(wal.IsSyncing());
|
||||
if (immutable_db_options_.track_and_verify_wals_in_manifest &&
|
||||
wal.GetPreSyncSize() > 0) {
|
||||
synced_wals.AddWal(wal.number, WalMetadata(wal.GetPreSyncSize()));
|
||||
}
|
||||
|
||||
if (logs_.size() > 1) {
|
||||
if (immutable_db_options_.track_and_verify_wals_in_manifest &&
|
||||
wal.GetPreSyncSize() > 0) {
|
||||
synced_wals.AddWal(wal.number, WalMetadata(wal.GetPreSyncSize()));
|
||||
}
|
||||
logs_to_free_.push_back(wal.ReleaseWriter());
|
||||
// To modify logs_ both mutex_ and log_write_mutex_ must be held
|
||||
InstrumentedMutexLock l(&log_write_mutex_);
|
||||
@@ -2834,6 +2866,14 @@ Status DBImpl::CreateColumnFamilyImpl(const ColumnFamilyOptions& cf_options,
|
||||
}
|
||||
} // InstrumentedMutexLock l(&mutex_)
|
||||
|
||||
if (cf_options.preclude_last_level_data_seconds > 0) {
|
||||
// TODO(zjay): Fix the timer issue and re-enable this.
|
||||
ROCKS_LOG_ERROR(
|
||||
immutable_db_options_.info_log,
|
||||
"Creating column family with `preclude_last_level_data_seconds` needs "
|
||||
"to restart DB to take effect");
|
||||
// s = RegisterRecordSeqnoTimeWorker();
|
||||
}
|
||||
sv_context.Clean();
|
||||
// this is outside the mutex
|
||||
if (s.ok()) {
|
||||
@@ -2922,6 +2962,10 @@ Status DBImpl::DropColumnFamilyImpl(ColumnFamilyHandle* column_family) {
|
||||
bg_cv_.SignalAll();
|
||||
}
|
||||
|
||||
if (cfd->ioptions()->preclude_last_level_data_seconds > 0) {
|
||||
s = RegisterRecordSeqnoTimeWorker();
|
||||
}
|
||||
|
||||
if (s.ok()) {
|
||||
// Note that here we erase the associated cf_info of the to-be-dropped
|
||||
// cfd before its ref-count goes to zero to avoid having to erase cf_info
|
||||
@@ -5565,6 +5609,26 @@ Status DBImpl::GetCreationTimeOfOldestFile(uint64_t* creation_time) {
|
||||
return Status::NotSupported("This API only works if max_open_files = -1");
|
||||
}
|
||||
}
|
||||
|
||||
void DBImpl::RecordSeqnoToTimeMapping() {
|
||||
// Get time first then sequence number, so the actual time of seqno is <=
|
||||
// unix_time recorded
|
||||
int64_t unix_time = 0;
|
||||
immutable_db_options_.clock->GetCurrentTime(&unix_time)
|
||||
.PermitUncheckedError(); // Ignore error
|
||||
SequenceNumber seqno = GetLatestSequenceNumber();
|
||||
bool appended = false;
|
||||
{
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
appended = seqno_time_mapping_.Append(seqno, unix_time);
|
||||
}
|
||||
if (!appended) {
|
||||
ROCKS_LOG_WARN(immutable_db_options_.info_log,
|
||||
"Failed to insert sequence number to time entry: %" PRIu64
|
||||
" -> %" PRIu64,
|
||||
seqno, unix_time);
|
||||
}
|
||||
}
|
||||
#endif // ROCKSDB_LITE
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
+56
-50
@@ -36,6 +36,7 @@
|
||||
#include "db/pre_release_callback.h"
|
||||
#include "db/range_del_aggregator.h"
|
||||
#include "db/read_callback.h"
|
||||
#include "db/seqno_to_time_mapping.h"
|
||||
#include "db/snapshot_checker.h"
|
||||
#include "db/snapshot_impl.h"
|
||||
#include "db/trim_history_scheduler.h"
|
||||
@@ -117,7 +118,6 @@ class Directories {
|
||||
IOStatus Close(const IOOptions& options, IODebugContext* dbg) {
|
||||
// close all directories for all database paths
|
||||
IOStatus s = IOStatus::OK();
|
||||
IOStatus temp_s = IOStatus::OK();
|
||||
|
||||
// The default implementation for Close() in Directory/FSDirectory class
|
||||
// "NotSupported" status, the upper level interface should be able to
|
||||
@@ -126,53 +126,35 @@ class Directories {
|
||||
// `FSDirectory::Close()` yet
|
||||
|
||||
if (db_dir_) {
|
||||
temp_s = db_dir_->Close(options, dbg);
|
||||
if (!temp_s.ok()) {
|
||||
if (temp_s.IsNotSupported()) {
|
||||
temp_s.PermitUncheckedError();
|
||||
} else {
|
||||
s = temp_s;
|
||||
}
|
||||
IOStatus temp_s = db_dir_->Close(options, dbg);
|
||||
if (!temp_s.ok() && !temp_s.IsNotSupported() && s.ok()) {
|
||||
s = std::move(temp_s);
|
||||
}
|
||||
}
|
||||
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
// Attempt to close everything even if one fails
|
||||
s.PermitUncheckedError();
|
||||
|
||||
if (wal_dir_) {
|
||||
s = wal_dir_->Close(options, dbg);
|
||||
if (!temp_s.ok()) {
|
||||
if (temp_s.IsNotSupported()) {
|
||||
temp_s.PermitUncheckedError();
|
||||
} else {
|
||||
s = temp_s;
|
||||
IOStatus temp_s = wal_dir_->Close(options, dbg);
|
||||
if (!temp_s.ok() && !temp_s.IsNotSupported() && s.ok()) {
|
||||
s = std::move(temp_s);
|
||||
}
|
||||
}
|
||||
|
||||
s.PermitUncheckedError();
|
||||
|
||||
for (auto& data_dir_ptr : data_dirs_) {
|
||||
if (data_dir_ptr) {
|
||||
IOStatus temp_s = data_dir_ptr->Close(options, dbg);
|
||||
if (!temp_s.ok() && !temp_s.IsNotSupported() && s.ok()) {
|
||||
s = std::move(temp_s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
if (data_dirs_.size() > 0 && s.ok()) {
|
||||
for (auto& data_dir_ptr : data_dirs_) {
|
||||
if (data_dir_ptr) {
|
||||
temp_s = data_dir_ptr->Close(options, dbg);
|
||||
if (!temp_s.ok()) {
|
||||
if (temp_s.IsNotSupported()) {
|
||||
temp_s.PermitUncheckedError();
|
||||
} else {
|
||||
return temp_s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Mark temp_s as checked when temp_s is still the initial status
|
||||
// (IOStatus::OK(), not checked yet)
|
||||
temp_s.PermitUncheckedError();
|
||||
// Ready for caller
|
||||
s.MustCheck();
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -217,6 +199,11 @@ class DBImpl : public DB {
|
||||
Status Put(const WriteOptions& options, ColumnFamilyHandle* column_family,
|
||||
const Slice& key, const Slice& ts, const Slice& value) override;
|
||||
|
||||
using DB::PutEntity;
|
||||
Status PutEntity(const WriteOptions& options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
const WideColumns& columns) override;
|
||||
|
||||
using DB::Merge;
|
||||
Status Merge(const WriteOptions& options, ColumnFamilyHandle* column_family,
|
||||
const Slice& key, const Slice& value) override;
|
||||
@@ -1153,7 +1140,8 @@ class DBImpl : public DB {
|
||||
int TEST_BGCompactionsAllowed() const;
|
||||
int TEST_BGFlushesAllowed() const;
|
||||
size_t TEST_GetWalPreallocateBlockSize(uint64_t write_buffer_size) const;
|
||||
void TEST_WaitForStatsDumpRun(std::function<void()> callback) const;
|
||||
void TEST_WaitForPeridicWorkerRun(std::function<void()> callback) const;
|
||||
SeqnoToTimeMapping TEST_GetSeqnoToTimeMapping() const;
|
||||
size_t TEST_EstimateInMemoryStatsHistorySize() const;
|
||||
|
||||
uint64_t TEST_GetCurrentLogNumber() const {
|
||||
@@ -1181,6 +1169,9 @@ class DBImpl : public DB {
|
||||
// flush LOG out of application buffer
|
||||
void FlushInfoLog();
|
||||
|
||||
// record current sequence number to time mapping
|
||||
void RecordSeqnoToTimeMapping();
|
||||
|
||||
// Interface to block and signal the DB in case of stalling writes by
|
||||
// WriteBufferManager. Each DBImpl object contains ptr to WBMStallInterface.
|
||||
// When DB needs to be blocked or signalled by WriteBufferManager,
|
||||
@@ -1248,6 +1239,7 @@ class DBImpl : public DB {
|
||||
std::unique_ptr<VersionSet> versions_;
|
||||
// Flag to check whether we allocated and own the info log file
|
||||
bool own_info_log_;
|
||||
Status init_logger_creation_s_;
|
||||
const DBOptions initial_db_options_;
|
||||
Env* const env_;
|
||||
std::shared_ptr<IOTracer> io_tracer_;
|
||||
@@ -1517,6 +1509,16 @@ class DBImpl : public DB {
|
||||
// recovery.
|
||||
Status LogAndApplyForRecovery(const RecoveryContext& recovery_ctx);
|
||||
|
||||
void InvokeWalFilterIfNeededOnColumnFamilyToWalNumberMap();
|
||||
|
||||
// Return true to proceed with current WAL record whose content is stored in
|
||||
// `batch`. Return false to skip current WAL record.
|
||||
bool InvokeWalFilterIfNeededOnWalRecord(uint64_t wal_number,
|
||||
const std::string& wal_fname,
|
||||
log::Reader::Reporter& reporter,
|
||||
Status& status, bool& stop_replay,
|
||||
WriteBatch& batch);
|
||||
|
||||
private:
|
||||
friend class DB;
|
||||
friend class ErrorHandler;
|
||||
@@ -1983,7 +1985,6 @@ class DBImpl : public DB {
|
||||
void MemTableInsertStatusCheck(const Status& memtable_insert_status);
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
|
||||
Status CompactFilesImpl(const CompactionOptions& compact_options,
|
||||
ColumnFamilyData* cfd, Version* version,
|
||||
const std::vector<std::string>& input_file_names,
|
||||
@@ -1995,7 +1996,6 @@ class DBImpl : public DB {
|
||||
// Wait for current IngestExternalFile() calls to finish.
|
||||
// REQUIRES: mutex_ held
|
||||
void WaitForIngestFile();
|
||||
|
||||
#else
|
||||
// IngestExternalFile is not supported in ROCKSDB_LITE so this function
|
||||
// will be no-op
|
||||
@@ -2055,6 +2055,8 @@ class DBImpl : public DB {
|
||||
// Schedule background tasks
|
||||
Status StartPeriodicWorkScheduler();
|
||||
|
||||
Status RegisterRecordSeqnoTimeWorker();
|
||||
|
||||
void PrintStatistics();
|
||||
|
||||
size_t EstimateInMemoryStatsHistorySize() const;
|
||||
@@ -2482,12 +2484,6 @@ class DBImpl : public DB {
|
||||
// log is fully commited.
|
||||
bool unable_to_release_oldest_log_;
|
||||
|
||||
static const int KEEP_LOG_FILE_NUM = 1000;
|
||||
// MSVC version 1800 still does not have constexpr for ::max()
|
||||
static const uint64_t kNoTimeOut = std::numeric_limits<uint64_t>::max();
|
||||
|
||||
std::string db_absolute_path_;
|
||||
|
||||
// Number of running IngestExternalFile() or CreateColumnFamilyWithImport()
|
||||
// calls.
|
||||
// REQUIRES: mutex held
|
||||
@@ -2529,6 +2525,10 @@ class DBImpl : public DB {
|
||||
// PeriodicWorkScheduler::Default(). Only in unittest, it can be overrided by
|
||||
// PeriodicWorkTestScheduler.
|
||||
PeriodicWorkScheduler* periodic_work_scheduler_;
|
||||
|
||||
// Current cadence of the periodic worker for recording sequence number to
|
||||
// time.
|
||||
uint64_t record_seqno_time_cadence_ = 0;
|
||||
#endif
|
||||
|
||||
// When set, we use a separate queue for writes that don't write to memtable.
|
||||
@@ -2578,6 +2578,10 @@ class DBImpl : public DB {
|
||||
|
||||
// Pointer to WriteBufferManager stalling interface.
|
||||
std::unique_ptr<StallInterface> wbm_stall_;
|
||||
|
||||
// seqno_time_mapping_ stores the sequence number to time mapping, it's not
|
||||
// thread safe, both read and write need db mutex hold.
|
||||
SeqnoToTimeMapping seqno_time_mapping_;
|
||||
};
|
||||
|
||||
class GetWithTimestampReadCallback : public ReadCallback {
|
||||
@@ -2590,10 +2594,12 @@ class GetWithTimestampReadCallback : public ReadCallback {
|
||||
};
|
||||
|
||||
extern Options SanitizeOptions(const std::string& db, const Options& src,
|
||||
bool read_only = false);
|
||||
bool read_only = false,
|
||||
Status* logger_creation_s = nullptr);
|
||||
|
||||
extern DBOptions SanitizeOptions(const std::string& db, const DBOptions& src,
|
||||
bool read_only = false);
|
||||
bool read_only = false,
|
||||
Status* logger_creation_s = nullptr);
|
||||
|
||||
extern CompressionType GetCompressionFlush(
|
||||
const ImmutableCFOptions& ioptions,
|
||||
|
||||
@@ -212,8 +212,8 @@ Status DBImpl::FlushMemTableToOutputFile(
|
||||
GetCompressionFlush(*cfd->ioptions(), mutable_cf_options), stats_,
|
||||
&event_logger_, mutable_cf_options.report_bg_io_stats,
|
||||
true /* sync_output_directory */, true /* write_manifest */, thread_pri,
|
||||
io_tracer_, db_id_, db_session_id_, cfd->GetFullHistoryTsLow(),
|
||||
&blob_callback_);
|
||||
io_tracer_, seqno_time_mapping_, db_id_, db_session_id_,
|
||||
cfd->GetFullHistoryTsLow(), &blob_callback_);
|
||||
FileMetaData file_meta;
|
||||
|
||||
Status s;
|
||||
@@ -450,7 +450,7 @@ Status DBImpl::AtomicFlushMemTablesToOutputFiles(
|
||||
data_dir, GetCompressionFlush(*cfd->ioptions(), mutable_cf_options),
|
||||
stats_, &event_logger_, mutable_cf_options.report_bg_io_stats,
|
||||
false /* sync_output_directory */, false /* write_manifest */,
|
||||
thread_pri, io_tracer_, db_id_, db_session_id_,
|
||||
thread_pri, io_tracer_, seqno_time_mapping_, db_id_, db_session_id_,
|
||||
cfd->GetFullHistoryTsLow(), &blob_callback_));
|
||||
}
|
||||
|
||||
@@ -2237,7 +2237,7 @@ Status DBImpl::AtomicFlushMemTables(
|
||||
// This method tries to avoid write stall (similar to CompactRange() behavior)
|
||||
// it emulates how the SuperVersion / LSM would change if flush happens, checks
|
||||
// it against various constrains and delays flush if it'd cause write stall.
|
||||
// Called should check status and flush_needed to see if flush already happened.
|
||||
// Caller should check status and flush_needed to see if flush already happened.
|
||||
Status DBImpl::WaitUntilFlushWouldNotStallWrites(ColumnFamilyData* cfd,
|
||||
bool* flush_needed) {
|
||||
{
|
||||
@@ -2616,17 +2616,7 @@ void DBImpl::SchedulePendingFlush(const FlushRequest& flush_req,
|
||||
assert(flush_req.size() == 1);
|
||||
ColumnFamilyData* cfd = flush_req[0].first;
|
||||
assert(cfd);
|
||||
// Note: SchedulePendingFlush is always preceded
|
||||
// with an imm()->FlushRequested() call. However,
|
||||
// we want to make this code snipper more resilient to
|
||||
// future changes. Therefore, we add the following if
|
||||
// statement - note that calling it twice (or more)
|
||||
// doesn't break anything.
|
||||
if (immutable_db_options_.experimental_mempurge_threshold > 0.0) {
|
||||
// If imm() contains silent memtables,
|
||||
// requesting a flush will mark the imm_needed as true.
|
||||
cfd->imm()->FlushRequested();
|
||||
}
|
||||
|
||||
if (!cfd->queued_for_flush() && cfd->imm()->IsFlushPending()) {
|
||||
cfd->Ref();
|
||||
cfd->set_queued_for_flush(true);
|
||||
@@ -2775,11 +2765,13 @@ Status DBImpl::BackgroundFlush(bool* made_progress, JobContext* job_context,
|
||||
|
||||
for (const auto& iter : flush_req) {
|
||||
ColumnFamilyData* cfd = iter.first;
|
||||
if (immutable_db_options_.experimental_mempurge_threshold > 0.0) {
|
||||
// If imm() contains silent memtables,
|
||||
// requesting a flush will mark the imm_needed as true.
|
||||
if (cfd->GetMempurgeUsed()) {
|
||||
// If imm() contains silent memtables (e.g.: because
|
||||
// MemPurge was activated), requesting a flush will
|
||||
// mark the imm_needed as true.
|
||||
cfd->imm()->FlushRequested();
|
||||
}
|
||||
|
||||
if (cfd->IsDropped() || !cfd->imm()->IsFlushPending()) {
|
||||
// can't flush this CF, try next one
|
||||
column_families_not_to_flush.push_back(cfd);
|
||||
@@ -3316,7 +3308,15 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
|
||||
moved_bytes += f->fd.GetFileSize();
|
||||
}
|
||||
}
|
||||
|
||||
if (c->compaction_reason() == CompactionReason::kLevelMaxLevelSize &&
|
||||
c->immutable_options()->compaction_pri == kRoundRobin) {
|
||||
int start_level = c->start_level();
|
||||
if (start_level > 0) {
|
||||
auto vstorage = c->input_version()->storage_info();
|
||||
c->edit()->AddCompactCursor(
|
||||
start_level, vstorage->GetNextCompactCursor(start_level));
|
||||
}
|
||||
}
|
||||
status = versions_->LogAndApply(c->column_family_data(),
|
||||
*c->mutable_cf_options(), c->edit(),
|
||||
&mutex_, directories_.GetDbDir());
|
||||
@@ -3802,11 +3802,6 @@ void DBImpl::GetSnapshotContext(
|
||||
|
||||
Status DBImpl::WaitForCompact(bool wait_unscheduled) {
|
||||
// Wait until the compaction completes
|
||||
|
||||
// TODO: a bug here. This function actually does not necessarily
|
||||
// wait for compact. It actually waits for scheduled compaction
|
||||
// OR flush to finish.
|
||||
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
while ((bg_bottom_compaction_scheduled_ || bg_compaction_scheduled_ ||
|
||||
bg_flush_scheduled_ ||
|
||||
|
||||
@@ -302,7 +302,8 @@ size_t DBImpl::TEST_GetWalPreallocateBlockSize(
|
||||
}
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
void DBImpl::TEST_WaitForStatsDumpRun(std::function<void()> callback) const {
|
||||
void DBImpl::TEST_WaitForPeridicWorkerRun(
|
||||
std::function<void()> callback) const {
|
||||
if (periodic_work_scheduler_ != nullptr) {
|
||||
static_cast<PeriodicWorkTestScheduler*>(periodic_work_scheduler_)
|
||||
->TEST_WaitForRun(callback);
|
||||
@@ -312,6 +313,12 @@ void DBImpl::TEST_WaitForStatsDumpRun(std::function<void()> callback) const {
|
||||
PeriodicWorkTestScheduler* DBImpl::TEST_GetPeriodicWorkScheduler() const {
|
||||
return static_cast<PeriodicWorkTestScheduler*>(periodic_work_scheduler_);
|
||||
}
|
||||
|
||||
SeqnoToTimeMapping DBImpl::TEST_GetSeqnoToTimeMapping() const {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
return seqno_time_mapping_;
|
||||
}
|
||||
|
||||
#endif // !ROCKSDB_LITE
|
||||
|
||||
size_t DBImpl::TEST_EstimateInMemoryStatsHistorySize() const {
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
#include "logging/logging.h"
|
||||
#include "port/port.h"
|
||||
#include "util/autovector.h"
|
||||
#include "util/defer.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
@@ -252,6 +253,22 @@ void DBImpl::FindObsoleteFiles(JobContext* job_context, bool force,
|
||||
job_context->blob_delete_files);
|
||||
}
|
||||
|
||||
// Before potentially releasing mutex and waiting on condvar, increment
|
||||
// pending_purge_obsolete_files_ so that another thread executing
|
||||
// `GetSortedWals` will wait until this thread finishes execution since the
|
||||
// other thread will be waiting for `pending_purge_obsolete_files_`.
|
||||
// pending_purge_obsolete_files_ MUST be decremented if there is nothing to
|
||||
// delete.
|
||||
++pending_purge_obsolete_files_;
|
||||
|
||||
Defer cleanup([job_context, this]() {
|
||||
assert(job_context != nullptr);
|
||||
if (!job_context->HaveSomethingToDelete()) {
|
||||
mutex_.AssertHeld();
|
||||
--pending_purge_obsolete_files_;
|
||||
}
|
||||
});
|
||||
|
||||
// logs_ is empty when called during recovery, in which case there can't yet
|
||||
// be any tracked obsolete logs
|
||||
if (!alive_log_files_.empty() && !logs_.empty()) {
|
||||
@@ -308,9 +325,6 @@ void DBImpl::FindObsoleteFiles(JobContext* job_context, bool force,
|
||||
job_context->logs_to_free = logs_to_free_;
|
||||
job_context->log_recycle_files.assign(log_recycle_files_.begin(),
|
||||
log_recycle_files_.end());
|
||||
if (job_context->HaveSomethingToDelete()) {
|
||||
++pending_purge_obsolete_files_;
|
||||
}
|
||||
logs_to_free_.clear();
|
||||
}
|
||||
|
||||
|
||||
+171
-102
@@ -27,8 +27,9 @@
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
Options SanitizeOptions(const std::string& dbname, const Options& src,
|
||||
bool read_only) {
|
||||
auto db_options = SanitizeOptions(dbname, DBOptions(src), read_only);
|
||||
bool read_only, Status* logger_creation_s) {
|
||||
auto db_options =
|
||||
SanitizeOptions(dbname, DBOptions(src), read_only, logger_creation_s);
|
||||
ImmutableDBOptions immutable_db_options(db_options);
|
||||
auto cf_options =
|
||||
SanitizeOptions(immutable_db_options, ColumnFamilyOptions(src));
|
||||
@@ -36,7 +37,7 @@ Options SanitizeOptions(const std::string& dbname, const Options& src,
|
||||
}
|
||||
|
||||
DBOptions SanitizeOptions(const std::string& dbname, const DBOptions& src,
|
||||
bool read_only) {
|
||||
bool read_only, Status* logger_creation_s) {
|
||||
DBOptions result(src);
|
||||
|
||||
if (result.env == nullptr) {
|
||||
@@ -59,6 +60,9 @@ DBOptions SanitizeOptions(const std::string& dbname, const DBOptions& src,
|
||||
if (!s.ok()) {
|
||||
// No place suitable for logging
|
||||
result.info_log = nullptr;
|
||||
if (logger_creation_s) {
|
||||
*logger_creation_s = s;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -534,6 +538,7 @@ Status DBImpl::Recover(
|
||||
s = CheckConsistency();
|
||||
}
|
||||
if (s.ok() && !read_only) {
|
||||
// TODO: share file descriptors (FSDirectory) with SetDirectories above
|
||||
std::map<std::string, std::shared_ptr<FSDirectory>> created_dirs;
|
||||
for (auto cfd : *versions_->GetColumnFamilySet()) {
|
||||
s = cfd->AddDirectories(&created_dirs);
|
||||
@@ -714,6 +719,12 @@ Status DBImpl::VerifySstUniqueIdInManifest() {
|
||||
status = version->VerifySstUniqueIds();
|
||||
mutex_.Lock();
|
||||
version->Unref();
|
||||
if (!status.ok()) {
|
||||
ROCKS_LOG_WARN(immutable_db_options_.info_log,
|
||||
"SST unique id mismatch in column family \"%s\": %s",
|
||||
cfd->GetName().c_str(), status.ToString().c_str());
|
||||
return status;
|
||||
}
|
||||
}
|
||||
}
|
||||
return status;
|
||||
@@ -845,6 +856,131 @@ Status DBImpl::LogAndApplyForRecovery(const RecoveryContext& recovery_ctx) {
|
||||
return s;
|
||||
}
|
||||
|
||||
void DBImpl::InvokeWalFilterIfNeededOnColumnFamilyToWalNumberMap() {
|
||||
#ifndef ROCKSDB_LITE
|
||||
if (immutable_db_options_.wal_filter == nullptr) {
|
||||
return;
|
||||
}
|
||||
assert(immutable_db_options_.wal_filter != nullptr);
|
||||
WalFilter& wal_filter = *(immutable_db_options_.wal_filter);
|
||||
|
||||
std::map<std::string, uint32_t> cf_name_id_map;
|
||||
std::map<uint32_t, uint64_t> cf_lognumber_map;
|
||||
assert(versions_);
|
||||
assert(versions_->GetColumnFamilySet());
|
||||
for (auto cfd : *versions_->GetColumnFamilySet()) {
|
||||
assert(cfd);
|
||||
cf_name_id_map.insert(std::make_pair(cfd->GetName(), cfd->GetID()));
|
||||
cf_lognumber_map.insert(std::make_pair(cfd->GetID(), cfd->GetLogNumber()));
|
||||
}
|
||||
|
||||
wal_filter.ColumnFamilyLogNumberMap(cf_lognumber_map, cf_name_id_map);
|
||||
#endif // !ROCKSDB_LITE
|
||||
}
|
||||
|
||||
bool DBImpl::InvokeWalFilterIfNeededOnWalRecord(uint64_t wal_number,
|
||||
const std::string& wal_fname,
|
||||
log::Reader::Reporter& reporter,
|
||||
Status& status,
|
||||
bool& stop_replay,
|
||||
WriteBatch& batch) {
|
||||
#ifndef ROCKSDB_LITE
|
||||
if (immutable_db_options_.wal_filter == nullptr) {
|
||||
return true;
|
||||
}
|
||||
assert(immutable_db_options_.wal_filter != nullptr);
|
||||
WalFilter& wal_filter = *(immutable_db_options_.wal_filter);
|
||||
|
||||
WriteBatch new_batch;
|
||||
bool batch_changed = false;
|
||||
|
||||
bool process_current_record = true;
|
||||
|
||||
WalFilter::WalProcessingOption wal_processing_option =
|
||||
wal_filter.LogRecordFound(wal_number, wal_fname, batch, &new_batch,
|
||||
&batch_changed);
|
||||
|
||||
switch (wal_processing_option) {
|
||||
case WalFilter::WalProcessingOption::kContinueProcessing:
|
||||
// do nothing, proceeed normally
|
||||
break;
|
||||
case WalFilter::WalProcessingOption::kIgnoreCurrentRecord:
|
||||
// skip current record
|
||||
process_current_record = false;
|
||||
break;
|
||||
case WalFilter::WalProcessingOption::kStopReplay:
|
||||
// skip current record and stop replay
|
||||
process_current_record = false;
|
||||
stop_replay = true;
|
||||
break;
|
||||
case WalFilter::WalProcessingOption::kCorruptedRecord: {
|
||||
status = Status::Corruption("Corruption reported by Wal Filter ",
|
||||
wal_filter.Name());
|
||||
MaybeIgnoreError(&status);
|
||||
if (!status.ok()) {
|
||||
process_current_record = false;
|
||||
reporter.Corruption(batch.GetDataSize(), status);
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
// logical error which should not happen. If RocksDB throws, we would
|
||||
// just do `throw std::logic_error`.
|
||||
assert(false);
|
||||
status = Status::NotSupported(
|
||||
"Unknown WalProcessingOption returned by Wal Filter ",
|
||||
wal_filter.Name());
|
||||
MaybeIgnoreError(&status);
|
||||
if (!status.ok()) {
|
||||
// Ignore the error with current record processing.
|
||||
stop_replay = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!process_current_record) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (batch_changed) {
|
||||
// Make sure that the count in the new batch is
|
||||
// within the orignal count.
|
||||
int new_count = WriteBatchInternal::Count(&new_batch);
|
||||
int original_count = WriteBatchInternal::Count(&batch);
|
||||
if (new_count > original_count) {
|
||||
ROCKS_LOG_FATAL(
|
||||
immutable_db_options_.info_log,
|
||||
"Recovering log #%" PRIu64
|
||||
" mode %d log filter %s returned "
|
||||
"more records (%d) than original (%d) which is not allowed. "
|
||||
"Aborting recovery.",
|
||||
wal_number, static_cast<int>(immutable_db_options_.wal_recovery_mode),
|
||||
wal_filter.Name(), new_count, original_count);
|
||||
status = Status::NotSupported(
|
||||
"More than original # of records "
|
||||
"returned by Wal Filter ",
|
||||
wal_filter.Name());
|
||||
return false;
|
||||
}
|
||||
// Set the same sequence number in the new_batch
|
||||
// as the original batch.
|
||||
WriteBatchInternal::SetSequence(&new_batch,
|
||||
WriteBatchInternal::Sequence(&batch));
|
||||
batch = new_batch;
|
||||
}
|
||||
return true;
|
||||
#else // !ROCKSDB_LITE
|
||||
(void)wal_number;
|
||||
(void)wal_fname;
|
||||
(void)reporter;
|
||||
(void)status;
|
||||
(void)stop_replay;
|
||||
(void)batch;
|
||||
return true;
|
||||
#endif // ROCKSDB_LITE
|
||||
}
|
||||
|
||||
// REQUIRES: wal_numbers are sorted in ascending order
|
||||
Status DBImpl::RecoverLogFiles(const std::vector<uint64_t>& wal_numbers,
|
||||
SequenceNumber* next_sequence, bool read_only,
|
||||
@@ -887,20 +1023,8 @@ Status DBImpl::RecoverLogFiles(const std::vector<uint64_t>& wal_numbers,
|
||||
stream.EndArray();
|
||||
}
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
if (immutable_db_options_.wal_filter != nullptr) {
|
||||
std::map<std::string, uint32_t> cf_name_id_map;
|
||||
std::map<uint32_t, uint64_t> cf_lognumber_map;
|
||||
for (auto cfd : *versions_->GetColumnFamilySet()) {
|
||||
cf_name_id_map.insert(std::make_pair(cfd->GetName(), cfd->GetID()));
|
||||
cf_lognumber_map.insert(
|
||||
std::make_pair(cfd->GetID(), cfd->GetLogNumber()));
|
||||
}
|
||||
|
||||
immutable_db_options_.wal_filter->ColumnFamilyLogNumberMap(cf_lognumber_map,
|
||||
cf_name_id_map);
|
||||
}
|
||||
#endif
|
||||
// No-op for immutable_db_options_.wal_filter == nullptr.
|
||||
InvokeWalFilterIfNeededOnColumnFamilyToWalNumberMap();
|
||||
|
||||
bool stop_replay_by_wal_filter = false;
|
||||
bool stop_replay_for_corruption = false;
|
||||
@@ -991,9 +1115,11 @@ Status DBImpl::RecoverLogFiles(const std::vector<uint64_t>& wal_numbers,
|
||||
|
||||
TEST_SYNC_POINT_CALLBACK("DBImpl::RecoverLogFiles:BeforeReadWal",
|
||||
/*arg=*/nullptr);
|
||||
uint64_t record_checksum;
|
||||
while (!stop_replay_by_wal_filter &&
|
||||
reader.ReadRecord(&record, &scratch,
|
||||
immutable_db_options_.wal_recovery_mode) &&
|
||||
immutable_db_options_.wal_recovery_mode,
|
||||
&record_checksum) &&
|
||||
status.ok()) {
|
||||
if (record.size() < WriteBatchInternal::kHeader) {
|
||||
reporter.Corruption(record.size(),
|
||||
@@ -1009,8 +1135,13 @@ Status DBImpl::RecoverLogFiles(const std::vector<uint64_t>& wal_numbers,
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
status = WriteBatchInternal::UpdateProtectionInfo(&batch,
|
||||
8 /* bytes_per_key */);
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"DBImpl::RecoverLogFiles:BeforeUpdateProtectionInfo:batch", &batch);
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"DBImpl::RecoverLogFiles:BeforeUpdateProtectionInfo:checksum",
|
||||
&record_checksum);
|
||||
status = WriteBatchInternal::UpdateProtectionInfo(
|
||||
&batch, 8 /* bytes_per_key */, &record_checksum);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
@@ -1032,83 +1163,13 @@ Status DBImpl::RecoverLogFiles(const std::vector<uint64_t>& wal_numbers,
|
||||
}
|
||||
}
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
if (immutable_db_options_.wal_filter != nullptr) {
|
||||
WriteBatch new_batch;
|
||||
bool batch_changed = false;
|
||||
|
||||
WalFilter::WalProcessingOption wal_processing_option =
|
||||
immutable_db_options_.wal_filter->LogRecordFound(
|
||||
wal_number, fname, batch, &new_batch, &batch_changed);
|
||||
|
||||
switch (wal_processing_option) {
|
||||
case WalFilter::WalProcessingOption::kContinueProcessing:
|
||||
// do nothing, proceeed normally
|
||||
break;
|
||||
case WalFilter::WalProcessingOption::kIgnoreCurrentRecord:
|
||||
// skip current record
|
||||
continue;
|
||||
case WalFilter::WalProcessingOption::kStopReplay:
|
||||
// skip current record and stop replay
|
||||
stop_replay_by_wal_filter = true;
|
||||
continue;
|
||||
case WalFilter::WalProcessingOption::kCorruptedRecord: {
|
||||
status =
|
||||
Status::Corruption("Corruption reported by Wal Filter ",
|
||||
immutable_db_options_.wal_filter->Name());
|
||||
MaybeIgnoreError(&status);
|
||||
if (!status.ok()) {
|
||||
reporter.Corruption(record.size(), status);
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assert(false); // unhandled case
|
||||
status = Status::NotSupported(
|
||||
"Unknown WalProcessingOption returned"
|
||||
" by Wal Filter ",
|
||||
immutable_db_options_.wal_filter->Name());
|
||||
MaybeIgnoreError(&status);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
} else {
|
||||
// Ignore the error with current record processing.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (batch_changed) {
|
||||
// Make sure that the count in the new batch is
|
||||
// within the orignal count.
|
||||
int new_count = WriteBatchInternal::Count(&new_batch);
|
||||
int original_count = WriteBatchInternal::Count(&batch);
|
||||
if (new_count > original_count) {
|
||||
ROCKS_LOG_FATAL(
|
||||
immutable_db_options_.info_log,
|
||||
"Recovering log #%" PRIu64
|
||||
" mode %d log filter %s returned "
|
||||
"more records (%d) than original (%d) which is not allowed. "
|
||||
"Aborting recovery.",
|
||||
wal_number,
|
||||
static_cast<int>(immutable_db_options_.wal_recovery_mode),
|
||||
immutable_db_options_.wal_filter->Name(), new_count,
|
||||
original_count);
|
||||
status = Status::NotSupported(
|
||||
"More than original # of records "
|
||||
"returned by Wal Filter ",
|
||||
immutable_db_options_.wal_filter->Name());
|
||||
return status;
|
||||
}
|
||||
// Set the same sequence number in the new_batch
|
||||
// as the original batch.
|
||||
WriteBatchInternal::SetSequence(&new_batch,
|
||||
WriteBatchInternal::Sequence(&batch));
|
||||
batch = new_batch;
|
||||
}
|
||||
// For the default case of wal_filter == nullptr, always performs no-op
|
||||
// and returns true.
|
||||
if (!InvokeWalFilterIfNeededOnWalRecord(wal_number, fname, reporter,
|
||||
status, stop_replay_by_wal_filter,
|
||||
batch)) {
|
||||
continue;
|
||||
}
|
||||
#endif // ROCKSDB_LITE
|
||||
|
||||
// If column family was not found, it might mean that the WAL write
|
||||
// batch references to the column family that was dropped after the
|
||||
@@ -1500,17 +1561,19 @@ Status DBImpl::WriteLevel0TableForRecovery(int job_id, ColumnFamilyData* cfd,
|
||||
GetCompressionFlush(*cfd->ioptions(), mutable_cf_options),
|
||||
mutable_cf_options.compression_opts, cfd->GetID(), cfd->GetName(),
|
||||
0 /* level */, false /* is_bottommost */,
|
||||
TableFileCreationReason::kRecovery, current_time,
|
||||
0 /* oldest_key_time */, 0 /* file_creation_time */, db_id_,
|
||||
db_session_id_, 0 /* target_file_size */, meta.fd.GetNumber());
|
||||
TableFileCreationReason::kRecovery, 0 /* oldest_key_time */,
|
||||
0 /* file_creation_time */, db_id_, db_session_id_,
|
||||
0 /* target_file_size */, meta.fd.GetNumber());
|
||||
SeqnoToTimeMapping empty_seqno_time_mapping;
|
||||
s = BuildTable(
|
||||
dbname_, versions_.get(), immutable_db_options_, tboptions,
|
||||
file_options_for_compaction_, cfd->table_cache(), iter.get(),
|
||||
std::move(range_del_iters), &meta, &blob_file_additions,
|
||||
snapshot_seqs, earliest_write_conflict_snapshot, kMaxSequenceNumber,
|
||||
snapshot_checker, paranoid_file_checks, cfd->internal_stats(), &io_s,
|
||||
io_tracer_, BlobFileCreationReason::kRecovery, &event_logger_, job_id,
|
||||
Env::IO_HIGH, nullptr /* table_properties */, write_hint,
|
||||
io_tracer_, BlobFileCreationReason::kRecovery,
|
||||
empty_seqno_time_mapping, &event_logger_, job_id, Env::IO_HIGH,
|
||||
nullptr /* table_properties */, write_hint,
|
||||
nullptr /*full_history_ts_low*/, &blob_callback_);
|
||||
LogFlush(immutable_db_options_.info_log);
|
||||
ROCKS_LOG_DEBUG(immutable_db_options_.info_log,
|
||||
@@ -1747,9 +1810,11 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname,
|
||||
|
||||
DBImpl* impl = new DBImpl(db_options, dbname, seq_per_batch, batch_per_txn);
|
||||
if (!impl->immutable_db_options_.info_log) {
|
||||
s = Status::Aborted("Failed to create logger");
|
||||
s = impl->init_logger_creation_s_;
|
||||
delete impl;
|
||||
return s;
|
||||
} else {
|
||||
assert(impl->init_logger_creation_s_.ok());
|
||||
}
|
||||
s = impl->env_->CreateDirIfMissing(impl->immutable_db_options_.GetWalDir());
|
||||
if (s.ok()) {
|
||||
@@ -2050,6 +2115,10 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname,
|
||||
if (s.ok()) {
|
||||
s = impl->StartPeriodicWorkScheduler();
|
||||
}
|
||||
|
||||
if (s.ok()) {
|
||||
s = impl->RegisterRecordSeqnoTimeWorker();
|
||||
}
|
||||
if (!s.ok()) {
|
||||
for (auto* h : *handles) {
|
||||
delete h;
|
||||
|
||||
@@ -48,6 +48,15 @@ class DBImplReadOnly : public DBImpl {
|
||||
const Slice& /*key*/, const Slice& /*value*/) override {
|
||||
return Status::NotSupported("Not supported operation in read only mode.");
|
||||
}
|
||||
|
||||
using DBImpl::PutEntity;
|
||||
Status PutEntity(const WriteOptions& /* options */,
|
||||
ColumnFamilyHandle* /* column_family */,
|
||||
const Slice& /* key */,
|
||||
const WideColumns& /* columns */) override {
|
||||
return Status::NotSupported("Not supported operation in read only mode.");
|
||||
}
|
||||
|
||||
using DBImpl::Merge;
|
||||
virtual Status Merge(const WriteOptions& /*options*/,
|
||||
ColumnFamilyHandle* /*column_family*/,
|
||||
|
||||
@@ -682,12 +682,6 @@ Status DB::OpenAsSecondary(
|
||||
const std::vector<ColumnFamilyDescriptor>& column_families,
|
||||
std::vector<ColumnFamilyHandle*>* handles, DB** dbptr) {
|
||||
*dbptr = nullptr;
|
||||
if (db_options.max_open_files != -1) {
|
||||
// TODO (yanqin) maybe support max_open_files != -1 by creating hard links
|
||||
// on SST files so that db secondary can still have access to old SSTs
|
||||
// while primary instance may delete original.
|
||||
return Status::InvalidArgument("require max_open_files to be -1");
|
||||
}
|
||||
|
||||
DBOptions tmp_opts(db_options);
|
||||
Status s;
|
||||
@@ -699,6 +693,27 @@ Status DB::OpenAsSecondary(
|
||||
}
|
||||
}
|
||||
|
||||
assert(tmp_opts.info_log != nullptr);
|
||||
if (db_options.max_open_files != -1) {
|
||||
std::ostringstream oss;
|
||||
oss << "The primary instance may delete all types of files after they "
|
||||
"become obsolete. The application can coordinate the primary and "
|
||||
"secondary so that primary does not delete/rename files that are "
|
||||
"currently being used by the secondary. Alternatively, a custom "
|
||||
"Env/FS can be provided such that files become inaccessible only "
|
||||
"after all primary and secondaries indicate that they are obsolete "
|
||||
"and deleted. If the above two are not possible, you can open the "
|
||||
"secondary instance with `max_open_files==-1` so that secondary "
|
||||
"will eagerly keep all table files open. Even if a file is deleted, "
|
||||
"its content can still be accessed via a prior open file "
|
||||
"descriptor. This is a hacky workaround for only table files. If "
|
||||
"none of the above is done, then point lookup or "
|
||||
"range scan via the secondary instance can result in IOError: file "
|
||||
"not found. This can be resolved by retrying "
|
||||
"TryCatchUpWithPrimary().";
|
||||
ROCKS_LOG_WARN(tmp_opts.info_log, "%s", oss.str().c_str());
|
||||
}
|
||||
|
||||
handles->clear();
|
||||
DBImplSecondary* impl = new DBImplSecondary(tmp_opts, dbname, secondary_path);
|
||||
impl->versions_.reset(new ReactiveVersionSet(
|
||||
|
||||
@@ -84,8 +84,17 @@ class DBImplSecondary : public DBImpl {
|
||||
bool error_if_data_exists_in_wals, uint64_t* = nullptr,
|
||||
RecoveryContext* recovery_ctx = nullptr) override;
|
||||
|
||||
// Implementations of the DB interface
|
||||
// Implementations of the DB interface.
|
||||
using DB::Get;
|
||||
// Can return IOError due to files being deleted by the primary. To avoid
|
||||
// IOError in this case, application can coordinate between primary and
|
||||
// secondaries so that primary will not delete files that are currently being
|
||||
// used by the secondaries. The application can also provide a custom FS/Env
|
||||
// implementation so that files will remain present until all primary and
|
||||
// secondaries indicate that they can be deleted. As a partial hacky
|
||||
// workaround, the secondaries can be opened with `max_open_files=-1` so that
|
||||
// it eagerly keeps all talbe files open and is able to access the contents of
|
||||
// deleted files via prior open fd.
|
||||
Status Get(const ReadOptions& options, ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableSlice* value) override;
|
||||
|
||||
@@ -98,6 +107,15 @@ class DBImplSecondary : public DBImpl {
|
||||
std::string* timestamp);
|
||||
|
||||
using DBImpl::NewIterator;
|
||||
// Operations on the created iterators can return IOError due to files being
|
||||
// deleted by the primary. To avoid IOError in this case, application can
|
||||
// coordinate between primary and secondaries so that primary will not delete
|
||||
// files that are currently being used by the secondaries. The application can
|
||||
// also provide a custom FS/Env implementation so that files will remain
|
||||
// present until all primary and secondaries indicate that they can be
|
||||
// deleted. As a partial hacky workaround, the secondaries can be opened with
|
||||
// `max_open_files=-1` so that it eagerly keeps all talbe files open and is
|
||||
// able to access the contents of deleted files via prior open fd.
|
||||
Iterator* NewIterator(const ReadOptions&,
|
||||
ColumnFamilyHandle* column_family) override;
|
||||
|
||||
@@ -119,6 +137,14 @@ class DBImplSecondary : public DBImpl {
|
||||
return Status::NotSupported("Not supported operation in secondary mode.");
|
||||
}
|
||||
|
||||
using DBImpl::PutEntity;
|
||||
Status PutEntity(const WriteOptions& /* options */,
|
||||
ColumnFamilyHandle* /* column_family */,
|
||||
const Slice& /* key */,
|
||||
const WideColumns& /* columns */) override {
|
||||
return Status::NotSupported("Not supported operation in secondary mode.");
|
||||
}
|
||||
|
||||
using DBImpl::Merge;
|
||||
Status Merge(const WriteOptions& /*options*/,
|
||||
ColumnFamilyHandle* /*column_family*/, const Slice& /*key*/,
|
||||
|
||||
@@ -37,6 +37,17 @@ Status DBImpl::Put(const WriteOptions& o, ColumnFamilyHandle* column_family,
|
||||
return DB::Put(o, column_family, key, ts, val);
|
||||
}
|
||||
|
||||
Status DBImpl::PutEntity(const WriteOptions& options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
const WideColumns& columns) {
|
||||
const Status s = FailIfCfHasTs(column_family);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
return DB::PutEntity(options, column_family, key, columns);
|
||||
}
|
||||
|
||||
Status DBImpl::Merge(const WriteOptions& o, ColumnFamilyHandle* column_family,
|
||||
const Slice& key, const Slice& val) {
|
||||
const Status s = FailIfCfHasTs(column_family);
|
||||
@@ -2236,6 +2247,27 @@ Status DB::Put(const WriteOptions& opt, ColumnFamilyHandle* column_family,
|
||||
return Write(opt, &batch);
|
||||
}
|
||||
|
||||
Status DB::PutEntity(const WriteOptions& options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
const WideColumns& columns) {
|
||||
const ColumnFamilyHandle* const default_cf = DefaultColumnFamily();
|
||||
assert(default_cf);
|
||||
|
||||
const Comparator* const default_cf_ucmp = default_cf->GetComparator();
|
||||
assert(default_cf_ucmp);
|
||||
|
||||
WriteBatch batch(/* reserved_bytes */ 0, /* max_bytes */ 0,
|
||||
options.protection_bytes_per_key,
|
||||
default_cf_ucmp->timestamp_size());
|
||||
|
||||
const Status s = batch.PutEntity(column_family, key, columns);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
return Write(options, &batch);
|
||||
}
|
||||
|
||||
Status DB::Delete(const WriteOptions& opt, ColumnFamilyHandle* column_family,
|
||||
const Slice& key) {
|
||||
WriteBatch batch(0 /* reserved_bytes */, 0 /* max_bytes */,
|
||||
|
||||
@@ -65,6 +65,60 @@ TEST_F(DBTestInPlaceUpdate, InPlaceUpdateLargeNewValue) {
|
||||
} while (ChangeCompactOptions());
|
||||
}
|
||||
|
||||
TEST_F(DBTestInPlaceUpdate, InPlaceUpdateEntitySmallerNewValue) {
|
||||
do {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.inplace_update_support = true;
|
||||
options.env = env_;
|
||||
options.allow_concurrent_memtable_write = false;
|
||||
|
||||
Reopen(options);
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
// Update key with values of smaller size
|
||||
constexpr int num_values = 10;
|
||||
for (int i = num_values; i > 0; --i) {
|
||||
constexpr char key[] = "key";
|
||||
const std::string value = DummyString(i, 'a');
|
||||
WideColumns wide_columns{{"attr", value}};
|
||||
|
||||
ASSERT_OK(db_->PutEntity(WriteOptions(), handles_[1], key, wide_columns));
|
||||
// TODO: use Get to check entity once it's supported
|
||||
}
|
||||
|
||||
// Only 1 instance for that key.
|
||||
validateNumberOfEntries(1, 1);
|
||||
} while (ChangeCompactOptions());
|
||||
}
|
||||
|
||||
TEST_F(DBTestInPlaceUpdate, InPlaceUpdateEntityLargerNewValue) {
|
||||
do {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.inplace_update_support = true;
|
||||
options.env = env_;
|
||||
options.allow_concurrent_memtable_write = false;
|
||||
|
||||
Reopen(options);
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
|
||||
// Update key with values of larger size
|
||||
constexpr int num_values = 10;
|
||||
for (int i = 0; i < num_values; ++i) {
|
||||
constexpr char key[] = "key";
|
||||
const std::string value = DummyString(i, 'a');
|
||||
WideColumns wide_columns{{"attr", value}};
|
||||
|
||||
ASSERT_OK(db_->PutEntity(WriteOptions(), handles_[1], key, wide_columns));
|
||||
// TODO: use Get to check entity once it's supported
|
||||
}
|
||||
|
||||
// All 10 updates exist in the internal iterator
|
||||
validateNumberOfEntries(num_values, 1);
|
||||
} while (ChangeCompactOptions());
|
||||
}
|
||||
|
||||
TEST_F(DBTestInPlaceUpdate, InPlaceUpdateCallbackSmallerSize) {
|
||||
do {
|
||||
Options options = CurrentOptions();
|
||||
|
||||
+141
-16
@@ -207,6 +207,15 @@ bool DBIter::SetBlobValueIfNeeded(const Slice& user_key,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DBIter::SetWideColumnValueIfNeeded(const Slice& /* wide_columns_slice */) {
|
||||
assert(!is_blob_);
|
||||
|
||||
// TODO: support wide-column entities
|
||||
status_ = Status::NotSupported("Encountered unexpected wide-column entity");
|
||||
valid_ = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
// PRE: saved_key_ has the current user key if skipping_saved_key
|
||||
// POST: saved_key_ should have the next user key if valid_,
|
||||
// if the current entry is a result of merge
|
||||
@@ -341,6 +350,7 @@ bool DBIter::FindNextUserEntryInternal(bool skipping_saved_key,
|
||||
break;
|
||||
case kTypeValue:
|
||||
case kTypeBlobIndex:
|
||||
case kTypeWideColumnEntity:
|
||||
if (timestamp_lb_) {
|
||||
saved_key_.SetInternalKey(ikey_);
|
||||
|
||||
@@ -348,6 +358,10 @@ bool DBIter::FindNextUserEntryInternal(bool skipping_saved_key,
|
||||
if (!SetBlobValueIfNeeded(ikey_.user_key, iter_.value())) {
|
||||
return false;
|
||||
}
|
||||
} else if (ikey_.type == kTypeWideColumnEntity) {
|
||||
if (!SetWideColumnValueIfNeeded(iter_.value())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
valid_ = true;
|
||||
@@ -369,6 +383,10 @@ bool DBIter::FindNextUserEntryInternal(bool skipping_saved_key,
|
||||
if (!SetBlobValueIfNeeded(ikey_.user_key, iter_.value())) {
|
||||
return false;
|
||||
}
|
||||
} else if (ikey_.type == kTypeWideColumnEntity) {
|
||||
if (!SetWideColumnValueIfNeeded(iter_.value())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
valid_ = true;
|
||||
@@ -580,6 +598,12 @@ bool DBIter::MergeValuesNewToOld() {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} else if (kTypeWideColumnEntity == ikey.type) {
|
||||
// TODO: support wide-column entities
|
||||
status_ = Status::NotSupported(
|
||||
"Merge currently not supported for wide-column entities");
|
||||
valid_ = false;
|
||||
return false;
|
||||
} else {
|
||||
valid_ = false;
|
||||
status_ = Status::Corruption(
|
||||
@@ -783,10 +807,15 @@ bool DBIter::FindValueForCurrentKey() {
|
||||
merge_context_.Clear();
|
||||
current_entry_is_merged_ = false;
|
||||
// last entry before merge (could be kTypeDeletion,
|
||||
// kTypeDeletionWithTimestamp, kTypeSingleDeletion or kTypeValue)
|
||||
// kTypeDeletionWithTimestamp, kTypeSingleDeletion, kTypeValue,
|
||||
// kTypeBlobIndex, or kTypeWideColumnEntity)
|
||||
ValueType last_not_merge_type = kTypeDeletion;
|
||||
ValueType last_key_entry_type = kTypeDeletion;
|
||||
|
||||
// If false, it indicates that we have not seen any valid entry, even though
|
||||
// last_key_entry_type is initialized to kTypeDeletion.
|
||||
bool valid_entry_seen = false;
|
||||
|
||||
// Temporarily pin blocks that hold (merge operands / the value)
|
||||
ReleaseTempPinnedData();
|
||||
TempPinData();
|
||||
@@ -797,20 +826,33 @@ bool DBIter::FindValueForCurrentKey() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!user_comparator_.EqualWithoutTimestamp(ikey.user_key,
|
||||
saved_key_.GetUserKey())) {
|
||||
// Found a smaller user key, thus we are done with current user key.
|
||||
break;
|
||||
}
|
||||
|
||||
assert(ikey.user_key.size() >= timestamp_size_);
|
||||
Slice ts;
|
||||
if (timestamp_size_ > 0) {
|
||||
ts = Slice(ikey.user_key.data() + ikey.user_key.size() - timestamp_size_,
|
||||
timestamp_size_);
|
||||
}
|
||||
if (!IsVisible(ikey.sequence, ts) ||
|
||||
!user_comparator_.EqualWithoutTimestamp(ikey.user_key,
|
||||
saved_key_.GetUserKey())) {
|
||||
|
||||
bool visible = IsVisible(ikey.sequence, ts);
|
||||
if (!visible &&
|
||||
(timestamp_lb_ == nullptr ||
|
||||
user_comparator_.CompareTimestamp(ts, *timestamp_ub_) > 0)) {
|
||||
// Found an invisible version of the current user key, and it must have
|
||||
// a higher sequence number or timestamp. Therefore, we are done with the
|
||||
// current user key.
|
||||
break;
|
||||
}
|
||||
|
||||
if (!ts.empty()) {
|
||||
saved_timestamp_.assign(ts.data(), ts.size());
|
||||
}
|
||||
|
||||
if (TooManyInternalKeysSkipped()) {
|
||||
return false;
|
||||
}
|
||||
@@ -827,10 +869,20 @@ bool DBIter::FindValueForCurrentKey() {
|
||||
return false;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
valid_entry_seen = true;
|
||||
last_key_entry_type = ikey.type;
|
||||
switch (last_key_entry_type) {
|
||||
case kTypeValue:
|
||||
case kTypeBlobIndex:
|
||||
case kTypeWideColumnEntity:
|
||||
if (range_del_agg_.ShouldDelete(
|
||||
ikey, RangeDelPositioningMode::kBackwardTraversal)) {
|
||||
last_key_entry_type = kTypeRangeDeletion;
|
||||
@@ -882,6 +934,14 @@ bool DBIter::FindValueForCurrentKey() {
|
||||
PERF_COUNTER_ADD(internal_key_skipped_count, 1);
|
||||
iter_.Prev();
|
||||
++num_skipped;
|
||||
|
||||
if (visible && timestamp_lb_ != nullptr) {
|
||||
// If timestamp_lb_ is not nullptr, we do not have to look further for
|
||||
// another internal key. We can return this current internal key. Yet we
|
||||
// still keep the invariant that iter_ is positioned before the returned
|
||||
// key.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!iter_.status().ok()) {
|
||||
@@ -889,6 +949,20 @@ bool DBIter::FindValueForCurrentKey() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!valid_entry_seen) {
|
||||
// Since we haven't seen any valid entry, last_key_entry_type remains
|
||||
// unchanged and the same as its initial value.
|
||||
assert(last_key_entry_type == kTypeDeletion);
|
||||
assert(last_not_merge_type == kTypeDeletion);
|
||||
valid_ = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (timestamp_lb_ != nullptr) {
|
||||
assert(last_key_entry_type == ikey_.type ||
|
||||
last_key_entry_type == kTypeRangeDeletion);
|
||||
}
|
||||
|
||||
Status s;
|
||||
s.PermitUncheckedError();
|
||||
is_blob_ = false;
|
||||
@@ -897,7 +971,12 @@ bool DBIter::FindValueForCurrentKey() {
|
||||
case kTypeDeletionWithTimestamp:
|
||||
case kTypeSingleDeletion:
|
||||
case kTypeRangeDeletion:
|
||||
valid_ = false;
|
||||
if (timestamp_lb_ == nullptr) {
|
||||
valid_ = false;
|
||||
} else {
|
||||
saved_key_.SetInternalKey(saved_ikey_);
|
||||
valid_ = true;
|
||||
}
|
||||
return true;
|
||||
case kTypeMerge:
|
||||
current_entry_is_merged_ = true;
|
||||
@@ -927,6 +1006,12 @@ bool DBIter::FindValueForCurrentKey() {
|
||||
}
|
||||
is_blob_ = false;
|
||||
return true;
|
||||
} else if (last_not_merge_type == kTypeWideColumnEntity) {
|
||||
// TODO: support wide-column entities
|
||||
status_ = Status::NotSupported(
|
||||
"Merge currently not supported for wide-column entities");
|
||||
valid_ = false;
|
||||
return false;
|
||||
} else {
|
||||
assert(last_not_merge_type == kTypeValue);
|
||||
s = Merge(&pinned_value_, saved_key_.GetUserKey());
|
||||
@@ -938,12 +1023,20 @@ bool DBIter::FindValueForCurrentKey() {
|
||||
break;
|
||||
case kTypeValue:
|
||||
// do nothing - we've already has value in pinned_value_
|
||||
if (timestamp_lb_ != nullptr) {
|
||||
saved_key_.SetInternalKey(saved_ikey_);
|
||||
}
|
||||
break;
|
||||
case kTypeBlobIndex:
|
||||
if (!SetBlobValueIfNeeded(saved_key_.GetUserKey(), pinned_value_)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
case kTypeWideColumnEntity:
|
||||
if (!SetWideColumnValueIfNeeded(pinned_value_)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
valid_ = false;
|
||||
status_ = Status::Corruption(
|
||||
@@ -978,7 +1071,7 @@ bool DBIter::FindValueForCurrentKeyUsingSeek() {
|
||||
&last_key,
|
||||
ParsedInternalKey(saved_key_.GetUserKey(), sequence_,
|
||||
kValueTypeForSeek),
|
||||
*timestamp_ub_);
|
||||
timestamp_lb_ == nullptr ? *timestamp_ub_ : *timestamp_lb_);
|
||||
}
|
||||
iter_.Seek(last_key);
|
||||
RecordTick(statistics_, NUMBER_OF_RESEEKS_IN_ITERATION);
|
||||
@@ -1023,7 +1116,12 @@ bool DBIter::FindValueForCurrentKeyUsingSeek() {
|
||||
range_del_agg_.ShouldDelete(
|
||||
ikey, RangeDelPositioningMode::kBackwardTraversal) ||
|
||||
kTypeDeletionWithTimestamp == ikey.type) {
|
||||
valid_ = false;
|
||||
if (timestamp_lb_ == nullptr) {
|
||||
valid_ = false;
|
||||
} else {
|
||||
valid_ = true;
|
||||
saved_key_.SetInternalKey(ikey);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (!iter_.PrepareValue()) {
|
||||
@@ -1034,13 +1132,22 @@ bool DBIter::FindValueForCurrentKeyUsingSeek() {
|
||||
Slice ts = ExtractTimestampFromUserKey(ikey.user_key, timestamp_size_);
|
||||
saved_timestamp_.assign(ts.data(), ts.size());
|
||||
}
|
||||
if (ikey.type == kTypeValue || ikey.type == kTypeBlobIndex) {
|
||||
if (ikey.type == kTypeValue || ikey.type == kTypeBlobIndex ||
|
||||
ikey.type == kTypeWideColumnEntity) {
|
||||
assert(iter_.iter()->IsValuePinned());
|
||||
pinned_value_ = iter_.value();
|
||||
if (ikey.type == kTypeBlobIndex) {
|
||||
if (!SetBlobValueIfNeeded(ikey.user_key, pinned_value_)) {
|
||||
return false;
|
||||
}
|
||||
} else if (ikey_.type == kTypeWideColumnEntity) {
|
||||
if (!SetWideColumnValueIfNeeded(pinned_value_)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (timestamp_lb_ != nullptr) {
|
||||
saved_key_.SetInternalKey(ikey);
|
||||
}
|
||||
|
||||
valid_ = true;
|
||||
@@ -1109,6 +1216,12 @@ bool DBIter::FindValueForCurrentKeyUsingSeek() {
|
||||
}
|
||||
is_blob_ = false;
|
||||
return true;
|
||||
} else if (ikey.type == kTypeWideColumnEntity) {
|
||||
// TODO: support wide-column entities
|
||||
status_ = Status::NotSupported(
|
||||
"Merge currently not supported for wide-column entities");
|
||||
valid_ = false;
|
||||
return false;
|
||||
} else {
|
||||
valid_ = false;
|
||||
status_ = Status::Corruption(
|
||||
@@ -1166,8 +1279,7 @@ bool DBIter::FindUserKeyBeforeSavedKey() {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (user_comparator_.CompareWithoutTimestamp(ikey.user_key,
|
||||
saved_key_.GetUserKey()) < 0) {
|
||||
if (CompareKeyForSkip(ikey.user_key, saved_key_.GetUserKey()) < 0) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1280,7 +1392,9 @@ void DBIter::SetSavedKeyToSeekForPrevTarget(const Slice& target) {
|
||||
if (timestamp_size_ > 0) {
|
||||
const std::string kTsMin(timestamp_size_, '\0');
|
||||
Slice ts = kTsMin;
|
||||
saved_key_.UpdateInternalKey(/*seq=*/0, kValueTypeForSeekForPrev, &ts);
|
||||
saved_key_.UpdateInternalKey(
|
||||
/*seq=*/0, kValueTypeForSeekForPrev,
|
||||
timestamp_lb_ == nullptr ? &ts : timestamp_lb_);
|
||||
}
|
||||
|
||||
if (iterate_upper_bound_ != nullptr &&
|
||||
@@ -1293,8 +1407,9 @@ 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,
|
||||
&ts);
|
||||
saved_key_.UpdateInternalKey(
|
||||
kMaxSequenceNumber, kValueTypeForSeekForPrev,
|
||||
timestamp_lb_ != nullptr ? timestamp_lb_ : &ts);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1495,11 +1610,21 @@ void DBIter::SeekToLast() {
|
||||
if (iterate_upper_bound_ != nullptr) {
|
||||
// Seek to last key strictly less than ReadOptions.iterate_upper_bound.
|
||||
SeekForPrev(*iterate_upper_bound_);
|
||||
if (Valid() && 0 == user_comparator_.CompareWithoutTimestamp(
|
||||
*iterate_upper_bound_, /*a_has_ts=*/false, key(),
|
||||
/*b_has_ts=*/false)) {
|
||||
const bool is_ikey = (timestamp_size_ > 0 && timestamp_lb_ != nullptr);
|
||||
Slice k = Valid() ? key() : Slice();
|
||||
if (is_ikey && Valid()) {
|
||||
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();
|
||||
PrevInternal(nullptr);
|
||||
|
||||
k = key();
|
||||
if (is_ikey) {
|
||||
k.remove_suffix(kNumInternalBytes + timestamp_size_);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -224,9 +224,11 @@ class DBIter final : public Iterator {
|
||||
bool ReverseToBackward();
|
||||
// Set saved_key_ to the seek key to target, with proper sequence number set.
|
||||
// It might get adjusted if the seek key is smaller than iterator lower bound.
|
||||
// target does not have timestamp.
|
||||
void SetSavedKeyToSeekTarget(const Slice& target);
|
||||
// Set saved_key_ to the seek key to target, with proper sequence number set.
|
||||
// It might get adjusted if the seek key is larger than iterator upper bound.
|
||||
// target does not have timestamp.
|
||||
void SetSavedKeyToSeekForPrevTarget(const Slice& target);
|
||||
bool FindValueForCurrentKey();
|
||||
bool FindValueForCurrentKeyUsingSeek();
|
||||
@@ -298,6 +300,8 @@ class DBIter final : public Iterator {
|
||||
// index when using the integrated BlobDB implementation.
|
||||
bool SetBlobValueIfNeeded(const Slice& user_key, const Slice& blob_index);
|
||||
|
||||
bool SetWideColumnValueIfNeeded(const Slice& wide_columns_slice);
|
||||
|
||||
Status Merge(const Slice* val, const Slice& user_key);
|
||||
|
||||
const SliceTransform* prefix_extractor_;
|
||||
@@ -375,6 +379,9 @@ 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
|
||||
|
||||
+89
-25
@@ -15,6 +15,7 @@ enum class WriteBatchOpType {
|
||||
kSingleDelete,
|
||||
kDeleteRange,
|
||||
kMerge,
|
||||
kPutEntity,
|
||||
kNum,
|
||||
};
|
||||
|
||||
@@ -62,18 +63,38 @@ std::pair<WriteBatch, Status> GetWriteBatch(ColumnFamilyHandle* cf_handle,
|
||||
case WriteBatchOpType::kMerge:
|
||||
s = wb.Merge(cf_handle, "key", "val");
|
||||
break;
|
||||
case WriteBatchOpType::kPutEntity:
|
||||
s = wb.PutEntity(cf_handle, "key",
|
||||
{{"attr_name1", "foo"}, {"attr_name2", "bar"}});
|
||||
break;
|
||||
case WriteBatchOpType::kNum:
|
||||
assert(false);
|
||||
}
|
||||
return {std::move(wb), std::move(s)};
|
||||
}
|
||||
|
||||
class DbKvChecksumTest : public DBTestBase,
|
||||
class DbKvChecksumTestBase : public DBTestBase {
|
||||
public:
|
||||
DbKvChecksumTestBase(const std::string& path, bool env_do_fsync)
|
||||
: DBTestBase(path, env_do_fsync) {}
|
||||
|
||||
ColumnFamilyHandle* GetCFHandleToUse(ColumnFamilyHandle* column_family,
|
||||
WriteBatchOpType op_type) const {
|
||||
// Note: PutEntity cannot be called without column family
|
||||
if (op_type == WriteBatchOpType::kPutEntity && !column_family) {
|
||||
return db_->DefaultColumnFamily();
|
||||
}
|
||||
|
||||
return column_family;
|
||||
}
|
||||
};
|
||||
|
||||
class DbKvChecksumTest : public DbKvChecksumTestBase,
|
||||
public ::testing::WithParamInterface<
|
||||
std::tuple<WriteBatchOpType, char, WriteMode>> {
|
||||
public:
|
||||
DbKvChecksumTest()
|
||||
: DBTestBase("db_kv_checksum_test", /*env_do_fsync=*/false) {
|
||||
: DbKvChecksumTestBase("db_kv_checksum_test", /*env_do_fsync=*/false) {
|
||||
op_type_ = std::get<0>(GetParam());
|
||||
corrupt_byte_addend_ = std::get<1>(GetParam());
|
||||
write_mode_ = std::get<2>(GetParam());
|
||||
@@ -82,14 +103,16 @@ class DbKvChecksumTest : public DBTestBase,
|
||||
Status ExecuteWrite(ColumnFamilyHandle* cf_handle) {
|
||||
switch (write_mode_) {
|
||||
case WriteMode::kWriteProtectedBatch: {
|
||||
auto batch_and_status = GetWriteBatch(
|
||||
cf_handle, 8 /* protection_bytes_per_key */, op_type_);
|
||||
auto batch_and_status =
|
||||
GetWriteBatch(GetCFHandleToUse(cf_handle, op_type_),
|
||||
8 /* protection_bytes_per_key */, op_type_);
|
||||
assert(batch_and_status.second.ok());
|
||||
return db_->Write(WriteOptions(), &batch_and_status.first);
|
||||
}
|
||||
case WriteMode::kWriteUnprotectedBatch: {
|
||||
auto batch_and_status = GetWriteBatch(
|
||||
cf_handle, 0 /* protection_bytes_per_key */, op_type_);
|
||||
auto batch_and_status =
|
||||
GetWriteBatch(GetCFHandleToUse(cf_handle, op_type_),
|
||||
0 /* protection_bytes_per_key */, op_type_);
|
||||
assert(batch_and_status.second.ok());
|
||||
WriteOptions write_opts;
|
||||
write_opts.protection_bytes_per_key = 8;
|
||||
@@ -135,10 +158,10 @@ std::string GetOpTypeString(const WriteBatchOpType& op_type) {
|
||||
return "SingleDelete";
|
||||
case WriteBatchOpType::kDeleteRange:
|
||||
return "DeleteRange";
|
||||
break;
|
||||
case WriteBatchOpType::kMerge:
|
||||
return "Merge";
|
||||
break;
|
||||
case WriteBatchOpType::kPutEntity:
|
||||
return "PutEntity";
|
||||
case WriteBatchOpType::kNum:
|
||||
assert(false);
|
||||
}
|
||||
@@ -242,7 +265,8 @@ TEST_P(DbKvChecksumTest, MemTableAddWithColumnFamilyCorrupted) {
|
||||
TEST_P(DbKvChecksumTest, NoCorruptionCase) {
|
||||
// If this test fails, we may have found a piece of malfunctioned hardware
|
||||
auto batch_and_status =
|
||||
GetWriteBatch(nullptr, 8 /* protection_bytes_per_key */, op_type_);
|
||||
GetWriteBatch(GetCFHandleToUse(nullptr, op_type_),
|
||||
8 /* protection_bytes_per_key */, op_type_);
|
||||
ASSERT_OK(batch_and_status.second);
|
||||
ASSERT_OK(batch_and_status.first.VerifyChecksum());
|
||||
}
|
||||
@@ -323,12 +347,12 @@ TEST_P(DbKvChecksumTest, WriteToWALWithColumnFamilyCorrupted) {
|
||||
}
|
||||
|
||||
class DbKvChecksumTestMergedBatch
|
||||
: public DBTestBase,
|
||||
: public DbKvChecksumTestBase,
|
||||
public ::testing::WithParamInterface<
|
||||
std::tuple<WriteBatchOpType, WriteBatchOpType, char>> {
|
||||
public:
|
||||
DbKvChecksumTestMergedBatch()
|
||||
: DBTestBase("db_kv_checksum_test", /*env_do_fsync=*/false) {
|
||||
: DbKvChecksumTestBase("db_kv_checksum_test", /*env_do_fsync=*/false) {
|
||||
op_type1_ = std::get<0>(GetParam());
|
||||
op_type2_ = std::get<1>(GetParam());
|
||||
corrupt_byte_addend_ = std::get<2>(GetParam());
|
||||
@@ -349,10 +373,10 @@ void CorruptWriteBatch(Slice* content, size_t offset,
|
||||
|
||||
TEST_P(DbKvChecksumTestMergedBatch, NoCorruptionCase) {
|
||||
// Veirfy write batch checksum after write batch append
|
||||
auto batch1 = GetWriteBatch(nullptr /* cf_handle */,
|
||||
auto batch1 = GetWriteBatch(GetCFHandleToUse(nullptr, op_type1_),
|
||||
8 /* protection_bytes_per_key */, op_type1_);
|
||||
ASSERT_OK(batch1.second);
|
||||
auto batch2 = GetWriteBatch(nullptr /* cf_handle */,
|
||||
auto batch2 = GetWriteBatch(GetCFHandleToUse(nullptr, op_type2_),
|
||||
8 /* protection_bytes_per_key */, op_type2_);
|
||||
ASSERT_OK(batch2.second);
|
||||
ASSERT_OK(WriteBatchInternal::Append(&batch1.first, &batch2.first));
|
||||
@@ -374,11 +398,13 @@ TEST_P(DbKvChecksumTestMergedBatch, WriteToWALCorrupted) {
|
||||
options.merge_operator = MergeOperators::CreateStringAppendOperator();
|
||||
}
|
||||
|
||||
auto leader_batch_and_status = GetWriteBatch(
|
||||
nullptr /* cf_handle */, 8 /* protection_bytes_per_key */, op_type1_);
|
||||
auto leader_batch_and_status =
|
||||
GetWriteBatch(GetCFHandleToUse(nullptr, op_type1_),
|
||||
8 /* protection_bytes_per_key */, op_type1_);
|
||||
ASSERT_OK(leader_batch_and_status.second);
|
||||
auto follower_batch_and_status = GetWriteBatch(
|
||||
nullptr /* cf_handle */, 8 /* protection_bytes_per_key */, op_type2_);
|
||||
auto follower_batch_and_status =
|
||||
GetWriteBatch(GetCFHandleToUse(nullptr, op_type2_),
|
||||
8 /* protection_bytes_per_key */, op_type2_);
|
||||
size_t leader_batch_size = leader_batch_and_status.first.GetDataSize();
|
||||
size_t total_bytes =
|
||||
leader_batch_size + follower_batch_and_status.first.GetDataSize();
|
||||
@@ -419,7 +445,7 @@ TEST_P(DbKvChecksumTestMergedBatch, WriteToWALCorrupted) {
|
||||
// follower
|
||||
follower_thread = port::Thread([&]() {
|
||||
follower_batch_and_status =
|
||||
GetWriteBatch(nullptr /* cf_handle */,
|
||||
GetWriteBatch(GetCFHandleToUse(nullptr, op_type2_),
|
||||
8 /* protection_bytes_per_key */, op_type2_);
|
||||
ASSERT_OK(follower_batch_and_status.second);
|
||||
ASSERT_TRUE(
|
||||
@@ -443,8 +469,9 @@ TEST_P(DbKvChecksumTestMergedBatch, WriteToWALCorrupted) {
|
||||
Reopen(options);
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
auto log_size_pre_write = dbfull()->TEST_total_log_size();
|
||||
leader_batch_and_status = GetWriteBatch(
|
||||
nullptr /* cf_handle */, 8 /* protection_bytes_per_key */, op_type1_);
|
||||
leader_batch_and_status =
|
||||
GetWriteBatch(GetCFHandleToUse(nullptr, op_type1_),
|
||||
8 /* protection_bytes_per_key */, op_type1_);
|
||||
ASSERT_OK(leader_batch_and_status.second);
|
||||
ASSERT_TRUE(db_->Write(WriteOptions(), &leader_batch_and_status.first)
|
||||
.IsCorruption());
|
||||
@@ -484,10 +511,12 @@ TEST_P(DbKvChecksumTestMergedBatch, WriteToWALWithColumnFamilyCorrupted) {
|
||||
CreateAndReopenWithCF({"ramen"}, options);
|
||||
|
||||
auto leader_batch_and_status =
|
||||
GetWriteBatch(handles_[1], 8 /* protection_bytes_per_key */, op_type1_);
|
||||
GetWriteBatch(GetCFHandleToUse(handles_[1], op_type1_),
|
||||
8 /* protection_bytes_per_key */, op_type1_);
|
||||
ASSERT_OK(leader_batch_and_status.second);
|
||||
auto follower_batch_and_status =
|
||||
GetWriteBatch(handles_[1], 8 /* protection_bytes_per_key */, op_type2_);
|
||||
GetWriteBatch(GetCFHandleToUse(handles_[1], op_type2_),
|
||||
8 /* protection_bytes_per_key */, op_type2_);
|
||||
size_t leader_batch_size = leader_batch_and_status.first.GetDataSize();
|
||||
size_t total_bytes =
|
||||
leader_batch_size + follower_batch_and_status.first.GetDataSize();
|
||||
@@ -527,8 +556,9 @@ TEST_P(DbKvChecksumTestMergedBatch, WriteToWALWithColumnFamilyCorrupted) {
|
||||
// Start the other writer thread which will join the write group as
|
||||
// follower
|
||||
follower_thread = port::Thread([&]() {
|
||||
follower_batch_and_status = GetWriteBatch(
|
||||
handles_[1], 8 /* protection_bytes_per_key */, op_type2_);
|
||||
follower_batch_and_status =
|
||||
GetWriteBatch(GetCFHandleToUse(handles_[1], op_type2_),
|
||||
8 /* protection_bytes_per_key */, op_type2_);
|
||||
ASSERT_OK(follower_batch_and_status.second);
|
||||
ASSERT_TRUE(
|
||||
db_->Write(WriteOptions(), &follower_batch_and_status.first)
|
||||
@@ -553,7 +583,8 @@ TEST_P(DbKvChecksumTestMergedBatch, WriteToWALWithColumnFamilyCorrupted) {
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
auto log_size_pre_write = dbfull()->TEST_total_log_size();
|
||||
leader_batch_and_status =
|
||||
GetWriteBatch(handles_[1], 8 /* protection_bytes_per_key */, op_type1_);
|
||||
GetWriteBatch(GetCFHandleToUse(handles_[1], op_type1_),
|
||||
8 /* protection_bytes_per_key */, op_type1_);
|
||||
ASSERT_OK(leader_batch_and_status.second);
|
||||
ASSERT_TRUE(db_->Write(WriteOptions(), &leader_batch_and_status.first)
|
||||
.IsCorruption());
|
||||
@@ -596,6 +627,39 @@ INSTANTIATE_TEST_CASE_P(
|
||||
|
||||
// TODO: add test for transactions
|
||||
// TODO: add test for corrupted write batch with WAL disabled
|
||||
|
||||
class DbKVChecksumWALToWriteBatchTest : public DBTestBase {
|
||||
public:
|
||||
DbKVChecksumWALToWriteBatchTest()
|
||||
: DBTestBase("db_kv_checksum_test", /*env_do_fsync=*/false) {}
|
||||
};
|
||||
|
||||
TEST_F(DbKVChecksumWALToWriteBatchTest, WriteBatchChecksumHandoff) {
|
||||
Options options = CurrentOptions();
|
||||
Reopen(options);
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "key", "val"));
|
||||
std::string content = "";
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::RecoverLogFiles:BeforeUpdateProtectionInfo:batch",
|
||||
[&](void* batch_ptr) {
|
||||
WriteBatch* batch = reinterpret_cast<WriteBatch*>(batch_ptr);
|
||||
content.assign(batch->Data().data(), batch->GetDataSize());
|
||||
Slice batch_content = batch->Data();
|
||||
// Corrupt first bit
|
||||
CorruptWriteBatch(&batch_content, 0, 1);
|
||||
});
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::RecoverLogFiles:BeforeUpdateProtectionInfo:checksum",
|
||||
[&](void* checksum_ptr) {
|
||||
// Verify that checksum is produced on the batch content
|
||||
uint64_t checksum = *reinterpret_cast<uint64_t*>(checksum_ptr);
|
||||
ASSERT_EQ(checksum, XXH3_64bits(content.data(), content.size()));
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
ASSERT_TRUE(TryReopen(options).IsCorruption());
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
@@ -453,7 +453,7 @@ TEST_F(DBOptionsTest, WritableFileMaxBufferSize) {
|
||||
dbfull()->SetDBOptions({{"writable_file_max_buffer_size", "524288"}}));
|
||||
buffer_size = 512 * 1024;
|
||||
match_cnt = 0;
|
||||
unmatch_cnt = 0; // SetDBOptions() will create a WriteableFileWriter
|
||||
unmatch_cnt = 0; // SetDBOptions() will create a WritableFileWriter
|
||||
|
||||
ASSERT_EQ(buffer_size,
|
||||
dbfull()->GetDBOptions().writable_file_max_buffer_size);
|
||||
|
||||
@@ -1818,6 +1818,85 @@ TEST_F(DBPropertiesTest, MinObsoleteSstNumberToKeep) {
|
||||
ASSERT_TRUE(listener->Validated());
|
||||
}
|
||||
|
||||
TEST_F(DBPropertiesTest, BlobCacheProperties) {
|
||||
Options options;
|
||||
uint64_t value;
|
||||
|
||||
options.env = CurrentOptions().env;
|
||||
|
||||
// Test with empty blob cache.
|
||||
constexpr size_t kCapacity = 100;
|
||||
LRUCacheOptions co;
|
||||
co.capacity = kCapacity;
|
||||
co.num_shard_bits = 0;
|
||||
co.metadata_charge_policy = kDontChargeCacheMetadata;
|
||||
auto blob_cache = NewLRUCache(co);
|
||||
options.blob_cache = blob_cache;
|
||||
|
||||
Reopen(options);
|
||||
|
||||
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kBlobCacheCapacity, &value));
|
||||
ASSERT_EQ(kCapacity, value);
|
||||
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kBlobCacheUsage, &value));
|
||||
ASSERT_EQ(0, value);
|
||||
ASSERT_TRUE(
|
||||
db_->GetIntProperty(DB::Properties::kBlobCachePinnedUsage, &value));
|
||||
ASSERT_EQ(0, value);
|
||||
|
||||
// Insert unpinned blob to the cache and check size.
|
||||
constexpr size_t kSize1 = 70;
|
||||
ASSERT_OK(blob_cache->Insert("blob1", nullptr /*value*/, kSize1,
|
||||
nullptr /*deleter*/));
|
||||
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kBlobCacheCapacity, &value));
|
||||
ASSERT_EQ(kCapacity, value);
|
||||
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kBlobCacheUsage, &value));
|
||||
ASSERT_EQ(kSize1, value);
|
||||
ASSERT_TRUE(
|
||||
db_->GetIntProperty(DB::Properties::kBlobCachePinnedUsage, &value));
|
||||
ASSERT_EQ(0, value);
|
||||
|
||||
// Insert pinned blob to the cache and check size.
|
||||
constexpr size_t kSize2 = 60;
|
||||
Cache::Handle* blob2 = nullptr;
|
||||
ASSERT_OK(blob_cache->Insert("blob2", nullptr /*value*/, kSize2,
|
||||
nullptr /*deleter*/, &blob2));
|
||||
ASSERT_NE(nullptr, blob2);
|
||||
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kBlobCacheCapacity, &value));
|
||||
ASSERT_EQ(kCapacity, value);
|
||||
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kBlobCacheUsage, &value));
|
||||
// blob1 is evicted.
|
||||
ASSERT_EQ(kSize2, value);
|
||||
ASSERT_TRUE(
|
||||
db_->GetIntProperty(DB::Properties::kBlobCachePinnedUsage, &value));
|
||||
ASSERT_EQ(kSize2, value);
|
||||
|
||||
// Insert another pinned blob to make the cache over-sized.
|
||||
constexpr size_t kSize3 = 80;
|
||||
Cache::Handle* blob3 = nullptr;
|
||||
ASSERT_OK(blob_cache->Insert("blob3", nullptr /*value*/, kSize3,
|
||||
nullptr /*deleter*/, &blob3));
|
||||
ASSERT_NE(nullptr, blob3);
|
||||
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kBlobCacheCapacity, &value));
|
||||
ASSERT_EQ(kCapacity, value);
|
||||
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kBlobCacheUsage, &value));
|
||||
ASSERT_EQ(kSize2 + kSize3, value);
|
||||
ASSERT_TRUE(
|
||||
db_->GetIntProperty(DB::Properties::kBlobCachePinnedUsage, &value));
|
||||
ASSERT_EQ(kSize2 + kSize3, value);
|
||||
|
||||
// Check size after release.
|
||||
blob_cache->Release(blob2);
|
||||
blob_cache->Release(blob3);
|
||||
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kBlobCacheCapacity, &value));
|
||||
ASSERT_EQ(kCapacity, value);
|
||||
ASSERT_TRUE(db_->GetIntProperty(DB::Properties::kBlobCacheUsage, &value));
|
||||
// blob2 will be evicted, while blob3 remain in cache after release.
|
||||
ASSERT_EQ(kSize3, value);
|
||||
ASSERT_TRUE(
|
||||
db_->GetIntProperty(DB::Properties::kBlobCachePinnedUsage, &value));
|
||||
ASSERT_EQ(0, value);
|
||||
}
|
||||
|
||||
TEST_F(DBPropertiesTest, BlockCacheProperties) {
|
||||
Options options;
|
||||
uint64_t value;
|
||||
|
||||
@@ -401,6 +401,7 @@ TEST_F(DBRangeDelTest, ValidLevelSubcompactionBoundaries) {
|
||||
options.num_levels = 3;
|
||||
options.target_file_size_base = kFileBytes;
|
||||
options.target_file_size_multiplier = 1;
|
||||
options.max_compaction_bytes = 1500;
|
||||
Reopen(options);
|
||||
|
||||
Random rnd(301);
|
||||
@@ -1028,6 +1029,7 @@ TEST_F(DBRangeDelTest, CompactionTreatsSplitInputLevelDeletionAtomically) {
|
||||
options.memtable_factory.reset(
|
||||
test::NewSpecialSkipListFactory(2 /* num_entries_flush */));
|
||||
options.target_file_size_base = kValueBytes;
|
||||
options.max_compaction_bytes = 1500;
|
||||
// i == 0: CompactFiles
|
||||
// i == 1: CompactRange
|
||||
// i == 2: automatic compaction
|
||||
|
||||
+9
-3
@@ -2897,6 +2897,15 @@ class ModelDB : public DB {
|
||||
const Slice& /*v*/) override {
|
||||
return Status::NotSupported();
|
||||
}
|
||||
|
||||
using DB::PutEntity;
|
||||
Status PutEntity(const WriteOptions& /* options */,
|
||||
ColumnFamilyHandle* /* column_family */,
|
||||
const Slice& /* key */,
|
||||
const WideColumns& /* columns */) override {
|
||||
return Status::NotSupported();
|
||||
}
|
||||
|
||||
using DB::Close;
|
||||
Status Close() override { return Status::OK(); }
|
||||
using DB::Delete;
|
||||
@@ -4092,9 +4101,6 @@ class MockedRateLimiterWithNoOptionalAPIImpl : public RateLimiter {
|
||||
|
||||
~MockedRateLimiterWithNoOptionalAPIImpl() override {}
|
||||
|
||||
const char* Name() const override {
|
||||
return "MockedRateLimiterWithNoOptionalAPI";
|
||||
}
|
||||
void SetBytesPerSecond(int64_t bytes_per_second) override {
|
||||
(void)bytes_per_second;
|
||||
}
|
||||
|
||||
+40
-13
@@ -33,18 +33,6 @@ namespace ROCKSDB_NAMESPACE {
|
||||
class DBTest2 : public DBTestBase {
|
||||
public:
|
||||
DBTest2() : DBTestBase("db_test2", /*env_do_fsync=*/true) {}
|
||||
|
||||
protected:
|
||||
#ifndef ROCKSDB_LITE
|
||||
uint64_t GetSstSizeHelper(Temperature temperature) {
|
||||
std::string prop;
|
||||
EXPECT_TRUE(dbfull()->GetProperty(
|
||||
DB::Properties::kLiveSstFilesSizeAtTemperature +
|
||||
std::to_string(static_cast<uint8_t>(temperature)),
|
||||
&prop));
|
||||
return static_cast<uint64_t>(std::atoi(prop.c_str()));
|
||||
}
|
||||
#endif // ROCKSDB_LITE
|
||||
};
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
@@ -2371,7 +2359,7 @@ TEST_F(DBTest2, MaxCompactionBytesTest) {
|
||||
GenerateNewRandomFile(&rnd);
|
||||
}
|
||||
CompactRangeOptions cro;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForceOptimized;
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForce;
|
||||
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
|
||||
ASSERT_EQ("0,0,8", FilesPerLevel(0));
|
||||
|
||||
@@ -7436,6 +7424,45 @@ TEST_F(DBTest2, SstUniqueIdVerify) {
|
||||
ASSERT_TRUE(s.IsCorruption());
|
||||
}
|
||||
|
||||
TEST_F(DBTest2, SstUniqueIdVerifyMultiCFs) {
|
||||
const int kNumSst = 3;
|
||||
const int kLevel0Trigger = 4;
|
||||
auto options = CurrentOptions();
|
||||
options.level0_file_num_compaction_trigger = kLevel0Trigger;
|
||||
|
||||
CreateAndReopenWithCF({"one", "two"}, options);
|
||||
|
||||
// generate good SSTs
|
||||
for (int cf_num : {0, 2}) {
|
||||
for (int i = 0; i < kNumSst; i++) {
|
||||
for (int j = 0; j < 100; j++) {
|
||||
ASSERT_OK(Put(cf_num, Key(i * 10 + j), "value"));
|
||||
}
|
||||
ASSERT_OK(Flush(cf_num));
|
||||
}
|
||||
}
|
||||
|
||||
// generate SSTs with bad unique id
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"PropertyBlockBuilder::AddTableProperty:Start", [&](void* props_vs) {
|
||||
auto props = static_cast<TableProperties*>(props_vs);
|
||||
// update table property session_id to a different one
|
||||
props->db_session_id = DBImpl::GenerateDbSessionId(nullptr);
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
for (int i = 0; i < kNumSst; i++) {
|
||||
for (int j = 0; j < 100; j++) {
|
||||
ASSERT_OK(Put(1, Key(i * 10 + j), "value"));
|
||||
}
|
||||
ASSERT_OK(Flush(1));
|
||||
}
|
||||
|
||||
// Reopen with verification should report corruption
|
||||
options.verify_sst_unique_id_in_manifest = true;
|
||||
auto s = TryReopenWithColumnFamilies({"default", "one", "two"}, options);
|
||||
ASSERT_TRUE(s.IsCorruption());
|
||||
}
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
TEST_F(DBTest2, GetLatestSeqAndTsForKey) {
|
||||
Destroy(last_options_);
|
||||
|
||||
@@ -1676,6 +1676,15 @@ uint64_t DBTestBase::GetNumberOfSstFilesForColumnFamily(
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
uint64_t DBTestBase::GetSstSizeHelper(Temperature temperature) {
|
||||
std::string prop;
|
||||
EXPECT_TRUE(dbfull()->GetProperty(
|
||||
DB::Properties::kLiveSstFilesSizeAtTemperature +
|
||||
std::to_string(static_cast<uint8_t>(temperature)),
|
||||
&prop));
|
||||
return static_cast<uint64_t>(std::atoi(prop.c_str()));
|
||||
}
|
||||
#endif // ROCKSDB_LITE
|
||||
|
||||
void VerifySstUniqueIds(const TablePropertiesCollection& props) {
|
||||
|
||||
@@ -1345,6 +1345,8 @@ class DBTestBase : public testing::Test {
|
||||
#ifndef ROCKSDB_LITE
|
||||
uint64_t GetNumberOfSstFilesForColumnFamily(DB* db,
|
||||
std::string column_family_name);
|
||||
|
||||
uint64_t GetSstSizeHelper(Temperature temperature);
|
||||
#endif // ROCKSDB_LITE
|
||||
|
||||
uint64_t TestGetTickerCount(const Options& options, Tickers ticker_type) {
|
||||
|
||||
+17
-9
@@ -1261,11 +1261,11 @@ class RecoveryTestHelper {
|
||||
std::unique_ptr<WalManager> wal_manager;
|
||||
WriteController write_controller;
|
||||
|
||||
versions.reset(new VersionSet(test->dbname_, &db_options, file_options,
|
||||
table_cache.get(), &write_buffer_manager,
|
||||
&write_controller,
|
||||
/*block_cache_tracer=*/nullptr,
|
||||
/*io_tracer=*/nullptr, /*db_session_id*/ ""));
|
||||
versions.reset(new VersionSet(
|
||||
test->dbname_, &db_options, file_options, table_cache.get(),
|
||||
&write_buffer_manager, &write_controller,
|
||||
/*block_cache_tracer=*/nullptr,
|
||||
/*io_tracer=*/nullptr, /*db_id*/ "", /*db_session_id*/ ""));
|
||||
|
||||
wal_manager.reset(
|
||||
new WalManager(db_options, file_options, /*io_tracer=*/nullptr));
|
||||
@@ -1449,7 +1449,7 @@ TEST_P(DBWALTestWithParams, kAbsoluteConsistency) {
|
||||
// fill with new date
|
||||
RecoveryTestHelper::FillData(this, &options);
|
||||
// corrupt the wal
|
||||
RecoveryTestHelper::CorruptWAL(this, options, corrupt_offset * .3,
|
||||
RecoveryTestHelper::CorruptWAL(this, options, corrupt_offset * .33,
|
||||
/*len%=*/.1, wal_file_id, trunc);
|
||||
// verify
|
||||
options.wal_recovery_mode = WALRecoveryMode::kAbsoluteConsistency;
|
||||
@@ -1497,6 +1497,8 @@ TEST_F(DBWALTest, RaceInstallFlushResultsWithWalObsoletion) {
|
||||
// The following make sure there are two bg flush threads.
|
||||
options.max_background_jobs = 8;
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
const std::string cf1_name("cf1");
|
||||
CreateAndReopenWithCF({cf1_name}, options);
|
||||
assert(handles_.size() == 2);
|
||||
@@ -1512,10 +1514,13 @@ TEST_F(DBWALTest, RaceInstallFlushResultsWithWalObsoletion) {
|
||||
ASSERT_OK(db_->Put(WriteOptions(), handles_[1], "foo", "value"));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "foo", "value"));
|
||||
|
||||
ASSERT_OK(dbfull()->TEST_FlushMemTable(false, true, handles_[1]));
|
||||
ASSERT_OK(dbfull()->TEST_FlushMemTable(
|
||||
/*wait=*/false, /*allow_write_stall=*/true, handles_[1]));
|
||||
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "foo", "value"));
|
||||
ASSERT_OK(dbfull()->TEST_FlushMemTable(false, true, handles_[0]));
|
||||
|
||||
ASSERT_OK(dbfull()->TEST_FlushMemTable(
|
||||
/*wait=*/false, /*allow_write_stall=*/true, handles_[0]));
|
||||
|
||||
bool called = false;
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
@@ -1597,7 +1602,10 @@ TEST_P(DBWALTestWithParams, kPointInTimeRecovery) {
|
||||
const size_t row_count = RecoveryTestHelper::FillData(this, &options);
|
||||
|
||||
// Corrupt the wal
|
||||
RecoveryTestHelper::CorruptWAL(this, options, corrupt_offset * .3,
|
||||
// The offset here was 0.3 which cuts off right at the end of a
|
||||
// valid fragment after wal zstd compression checksum is enabled,
|
||||
// so changed the value to 0.33.
|
||||
RecoveryTestHelper::CorruptWAL(this, options, corrupt_offset * .33,
|
||||
/*len%=*/.1, wal_file_id, trunc);
|
||||
|
||||
// Verify
|
||||
|
||||
@@ -1065,6 +1065,191 @@ TEST_F(DBBasicTestWithTimestamp, SimpleForwardIterateLowerTsBound) {
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTestWithTimestamp, BackwardIterateLowerTsBound) {
|
||||
constexpr int kNumKeysPerFile = 128;
|
||||
constexpr uint64_t kMaxKey = 1024;
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
options.create_if_missing = true;
|
||||
const size_t kTimestampSize = Timestamp(0, 0).size();
|
||||
TestComparator test_cmp(kTimestampSize);
|
||||
options.comparator = &test_cmp;
|
||||
options.memtable_factory.reset(
|
||||
test::NewSpecialSkipListFactory(kNumKeysPerFile));
|
||||
DestroyAndReopen(options);
|
||||
const std::vector<std::string> write_timestamps = {Timestamp(1, 0),
|
||||
Timestamp(3, 0)};
|
||||
const std::vector<std::string> read_timestamps = {Timestamp(2, 0),
|
||||
Timestamp(4, 0)};
|
||||
const std::vector<std::string> read_timestamps_lb = {Timestamp(1, 0),
|
||||
Timestamp(1, 0)};
|
||||
for (size_t i = 0; i < write_timestamps.size(); ++i) {
|
||||
WriteOptions write_opts;
|
||||
for (uint64_t key = 0; key <= kMaxKey; ++key) {
|
||||
Status s = db_->Put(write_opts, Key1(key), write_timestamps[i],
|
||||
"value" + std::to_string(i));
|
||||
ASSERT_OK(s);
|
||||
}
|
||||
}
|
||||
for (size_t i = 0; i < read_timestamps.size(); ++i) {
|
||||
ReadOptions read_opts;
|
||||
Slice read_ts = read_timestamps[i];
|
||||
Slice read_ts_lb = read_timestamps_lb[i];
|
||||
read_opts.timestamp = &read_ts;
|
||||
read_opts.iter_start_ts = &read_ts_lb;
|
||||
std::unique_ptr<Iterator> it(db_->NewIterator(read_opts));
|
||||
int count = 0;
|
||||
uint64_t key = 0;
|
||||
for (it->SeekForPrev(Key1(kMaxKey)), key = kMaxKey; it->Valid();
|
||||
it->Prev(), ++count, --key) {
|
||||
CheckIterEntry(it.get(), Key1(key), kTypeValue, "value0",
|
||||
write_timestamps[0]);
|
||||
if (i > 0) {
|
||||
it->Prev();
|
||||
CheckIterEntry(it.get(), Key1(key), kTypeValue, "value1",
|
||||
write_timestamps[1]);
|
||||
}
|
||||
}
|
||||
size_t expected_count = kMaxKey + 1;
|
||||
ASSERT_EQ(expected_count, count);
|
||||
}
|
||||
// Delete all keys@ts=5 and check iteration result with start ts set
|
||||
{
|
||||
std::string write_timestamp = Timestamp(5, 0);
|
||||
WriteOptions write_opts;
|
||||
for (uint64_t key = 0; key < kMaxKey + 1; ++key) {
|
||||
Status s = db_->Delete(write_opts, Key1(key), write_timestamp);
|
||||
ASSERT_OK(s);
|
||||
}
|
||||
|
||||
std::string read_timestamp = Timestamp(6, 0);
|
||||
ReadOptions read_opts;
|
||||
Slice read_ts = read_timestamp;
|
||||
read_opts.timestamp = &read_ts;
|
||||
std::string read_timestamp_lb = Timestamp(2, 0);
|
||||
Slice read_ts_lb = read_timestamp_lb;
|
||||
read_opts.iter_start_ts = &read_ts_lb;
|
||||
std::unique_ptr<Iterator> it(db_->NewIterator(read_opts));
|
||||
int count = 0;
|
||||
uint64_t key = kMaxKey;
|
||||
for (it->SeekForPrev(Key1(key)), key = kMaxKey; it->Valid();
|
||||
it->Prev(), ++count, --key) {
|
||||
CheckIterEntry(it.get(), Key1(key), kTypeValue, "value1",
|
||||
Timestamp(3, 0));
|
||||
it->Prev();
|
||||
CheckIterEntry(it.get(), Key1(key), kTypeDeletionWithTimestamp, Slice(),
|
||||
write_timestamp);
|
||||
}
|
||||
ASSERT_EQ(kMaxKey + 1, count);
|
||||
}
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTestWithTimestamp, SimpleBackwardIterateLowerTsBound) {
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
options.create_if_missing = true;
|
||||
const size_t kTimestampSize = Timestamp(0, 0).size();
|
||||
TestComparator test_cmp(kTimestampSize);
|
||||
options.comparator = &test_cmp;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
std::string ts_ub_buf = Timestamp(5, 0);
|
||||
Slice ts_ub = ts_ub_buf;
|
||||
std::string ts_lb_buf = Timestamp(1, 0);
|
||||
Slice ts_lb = ts_lb_buf;
|
||||
|
||||
{
|
||||
ReadOptions read_opts;
|
||||
read_opts.timestamp = &ts_ub;
|
||||
read_opts.iter_start_ts = &ts_lb;
|
||||
std::unique_ptr<Iterator> it(db_->NewIterator(read_opts));
|
||||
it->SeekToLast();
|
||||
ASSERT_FALSE(it->Valid());
|
||||
ASSERT_OK(it->status());
|
||||
|
||||
it->SeekForPrev("foo");
|
||||
ASSERT_FALSE(it->Valid());
|
||||
ASSERT_OK(it->status());
|
||||
}
|
||||
|
||||
// Test iterate_upper_bound
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "a", Timestamp(0, 0), "v0"));
|
||||
ASSERT_OK(db_->SingleDelete(WriteOptions(), "a", Timestamp(1, 0)));
|
||||
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "b", Timestamp(i, 0),
|
||||
"v" + std::to_string(i)));
|
||||
}
|
||||
|
||||
{
|
||||
ReadOptions read_opts;
|
||||
read_opts.timestamp = &ts_ub;
|
||||
read_opts.iter_start_ts = &ts_lb;
|
||||
std::string key_ub_str = "b"; // exclusive
|
||||
Slice key_ub = key_ub_str;
|
||||
read_opts.iterate_upper_bound = &key_ub;
|
||||
std::unique_ptr<Iterator> it(db_->NewIterator(read_opts));
|
||||
it->SeekToLast();
|
||||
CheckIterEntry(it.get(), "a", kTypeSingleDeletion, Slice(),
|
||||
Timestamp(1, 0));
|
||||
|
||||
key_ub_str = "a"; // exclusive
|
||||
key_ub = key_ub_str;
|
||||
read_opts.iterate_upper_bound = &key_ub;
|
||||
it.reset(db_->NewIterator(read_opts));
|
||||
it->SeekToLast();
|
||||
ASSERT_FALSE(it->Valid());
|
||||
ASSERT_OK(it->status());
|
||||
}
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTestWithTimestamp, BackwardIterateLowerTsBound_Reseek) {
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
options.create_if_missing = true;
|
||||
options.max_sequential_skip_in_iterations = 2;
|
||||
const size_t kTimestampSize = Timestamp(0, 0).size();
|
||||
TestComparator test_cmp(kTimestampSize);
|
||||
options.comparator = &test_cmp;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "a", Timestamp(i, 0),
|
||||
"v" + std::to_string(i)));
|
||||
}
|
||||
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "b", Timestamp(i, 0),
|
||||
"v" + std::to_string(i)));
|
||||
}
|
||||
|
||||
{
|
||||
std::string ts_ub_buf = Timestamp(6, 0);
|
||||
Slice ts_ub = ts_ub_buf;
|
||||
std::string ts_lb_buf = Timestamp(4, 0);
|
||||
Slice ts_lb = ts_lb_buf;
|
||||
|
||||
ReadOptions read_opts;
|
||||
read_opts.timestamp = &ts_ub;
|
||||
read_opts.iter_start_ts = &ts_lb;
|
||||
std::unique_ptr<Iterator> it(db_->NewIterator(read_opts));
|
||||
it->SeekToLast();
|
||||
for (int i = 0; i < 3 && it->Valid(); it->Prev(), ++i) {
|
||||
CheckIterEntry(it.get(), "b", kTypeValue, "v" + std::to_string(4 + i),
|
||||
Timestamp(4 + i, 0));
|
||||
}
|
||||
for (int i = 0; i < 3 && it->Valid(); it->Prev(), ++i) {
|
||||
CheckIterEntry(it.get(), "a", kTypeValue, "v" + std::to_string(4 + i),
|
||||
Timestamp(4 + i, 0));
|
||||
}
|
||||
}
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTestWithTimestamp, ReseekToTargetTimestamp) {
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
|
||||
+3
-42
@@ -26,7 +26,7 @@ namespace ROCKSDB_NAMESPACE {
|
||||
// and the value type is embedded as the low 8 bits in the sequence
|
||||
// number in internal keys, we need to use the highest-numbered
|
||||
// ValueType, not the lowest).
|
||||
const ValueType kValueTypeForSeek = kTypeDeletionWithTimestamp;
|
||||
const ValueType kValueTypeForSeek = kTypeWideColumnEntity;
|
||||
const ValueType kValueTypeForSeekForPrev = kTypeDeletion;
|
||||
const std::string kDisableUserTimestamp("");
|
||||
|
||||
@@ -46,6 +46,8 @@ EntryType GetEntryType(ValueType value_type) {
|
||||
return kEntryRangeDeletion;
|
||||
case kTypeBlobIndex:
|
||||
return kEntryBlobIndex;
|
||||
case kTypeWideColumnEntity:
|
||||
return kEntryWideColumnEntity;
|
||||
default:
|
||||
return kEntryOther;
|
||||
}
|
||||
@@ -114,13 +116,6 @@ std::string InternalKey::DebugString(bool hex) const {
|
||||
return result;
|
||||
}
|
||||
|
||||
const char* InternalKeyComparator::Name() const {
|
||||
if (name_.empty()) {
|
||||
return "rocksdb.anonymous.InternalKeyComparator";
|
||||
}
|
||||
return name_.c_str();
|
||||
}
|
||||
|
||||
int InternalKeyComparator::Compare(const ParsedInternalKey& a,
|
||||
const ParsedInternalKey& b) const {
|
||||
// Order by:
|
||||
@@ -142,40 +137,6 @@ int InternalKeyComparator::Compare(const ParsedInternalKey& a,
|
||||
return r;
|
||||
}
|
||||
|
||||
void InternalKeyComparator::FindShortestSeparator(std::string* start,
|
||||
const Slice& limit) const {
|
||||
// Attempt to shorten the user portion of the key
|
||||
Slice user_start = ExtractUserKey(*start);
|
||||
Slice user_limit = ExtractUserKey(limit);
|
||||
std::string tmp(user_start.data(), user_start.size());
|
||||
user_comparator_.FindShortestSeparator(&tmp, user_limit);
|
||||
if (tmp.size() <= user_start.size() &&
|
||||
user_comparator_.Compare(user_start, tmp) < 0) {
|
||||
// User key has become shorter physically, but larger logically.
|
||||
// Tack on the earliest possible number to the shortened user key.
|
||||
PutFixed64(&tmp,
|
||||
PackSequenceAndType(kMaxSequenceNumber, kValueTypeForSeek));
|
||||
assert(this->Compare(*start, tmp) < 0);
|
||||
assert(this->Compare(tmp, limit) < 0);
|
||||
start->swap(tmp);
|
||||
}
|
||||
}
|
||||
|
||||
void InternalKeyComparator::FindShortSuccessor(std::string* key) const {
|
||||
Slice user_key = ExtractUserKey(*key);
|
||||
std::string tmp(user_key.data(), user_key.size());
|
||||
user_comparator_.FindShortSuccessor(&tmp);
|
||||
if (tmp.size() <= user_key.size() &&
|
||||
user_comparator_.Compare(user_key, tmp) < 0) {
|
||||
// User key has become shorter physically, but larger logically.
|
||||
// Tack on the earliest possible number to the shortened user key.
|
||||
PutFixed64(&tmp,
|
||||
PackSequenceAndType(kMaxSequenceNumber, kValueTypeForSeek));
|
||||
assert(this->Compare(*key, tmp) < 0);
|
||||
key->swap(tmp);
|
||||
}
|
||||
}
|
||||
|
||||
LookupKey::LookupKey(const Slice& _user_key, SequenceNumber s,
|
||||
const Slice* ts) {
|
||||
size_t usize = _user_key.size();
|
||||
|
||||
+18
-21
@@ -66,7 +66,11 @@ enum ValueType : unsigned char {
|
||||
kTypeBeginUnprepareXID = 0x13, // WAL only.
|
||||
kTypeDeletionWithTimestamp = 0x14,
|
||||
kTypeCommitXIDAndTimestamp = 0x15, // WAL only
|
||||
kMaxValue = 0x7F // Not used for storing records.
|
||||
kTypeWideColumnEntity = 0x16,
|
||||
kTypeColumnFamilyWideColumnEntity = 0x17, // WAL only
|
||||
kTypeMaxValid, // Should be after the last valid type, only used for
|
||||
// validation
|
||||
kMaxValue = 0x7F // Not used for storing records.
|
||||
};
|
||||
|
||||
// Defined in dbformat.cc
|
||||
@@ -76,8 +80,8 @@ extern const ValueType kValueTypeForSeekForPrev;
|
||||
// Checks whether a type is an inline value type
|
||||
// (i.e. a type used in memtable skiplist and sst file datablock).
|
||||
inline bool IsValueType(ValueType t) {
|
||||
return t <= kTypeMerge || t == kTypeSingleDeletion || t == kTypeBlobIndex ||
|
||||
kTypeDeletionWithTimestamp == t;
|
||||
return t <= kTypeMerge || kTypeSingleDeletion == t || kTypeBlobIndex == t ||
|
||||
kTypeDeletionWithTimestamp == t || kTypeWideColumnEntity == t;
|
||||
}
|
||||
|
||||
// Checks whether a type is from user operation
|
||||
@@ -233,10 +237,9 @@ class InternalKeyComparator
|
||||
#ifdef NDEBUG
|
||||
final
|
||||
#endif
|
||||
: public Comparator {
|
||||
: public CompareInterface {
|
||||
private:
|
||||
UserComparatorWrapper user_comparator_;
|
||||
std::string name_;
|
||||
|
||||
public:
|
||||
// `InternalKeyComparator`s constructed with the default constructor are not
|
||||
@@ -248,22 +251,19 @@ class InternalKeyComparator
|
||||
// this constructor to precompute the result of `Name()`. To avoid this
|
||||
// overhead, set `named` to false. In that case, `Name()` will return a
|
||||
// generic name that is non-specific to the underlying comparator.
|
||||
explicit InternalKeyComparator(const Comparator* c, bool named = true)
|
||||
: Comparator(c->timestamp_size()), user_comparator_(c) {
|
||||
if (named) {
|
||||
name_ = "rocksdb.InternalKeyComparator:" +
|
||||
std::string(user_comparator_.Name());
|
||||
}
|
||||
}
|
||||
explicit InternalKeyComparator(const Comparator* c) : user_comparator_(c) {}
|
||||
virtual ~InternalKeyComparator() {}
|
||||
|
||||
virtual const char* Name() const override;
|
||||
virtual int Compare(const Slice& a, const Slice& b) const override;
|
||||
int Compare(const Slice& a, const Slice& b) const override;
|
||||
|
||||
bool Equal(const Slice& a, const Slice& b) const {
|
||||
// TODO Use user_comparator_.Equal(). Perhaps compare seqno before
|
||||
// comparing the user key too.
|
||||
return Compare(a, b) == 0;
|
||||
}
|
||||
|
||||
// Same as Compare except that it excludes the value type from comparison
|
||||
virtual int CompareKeySeq(const Slice& a, const Slice& b) const;
|
||||
virtual void FindShortestSeparator(std::string* start,
|
||||
const Slice& limit) const override;
|
||||
virtual void FindShortSuccessor(std::string* key) const override;
|
||||
int CompareKeySeq(const Slice& a, const Slice& b) const;
|
||||
|
||||
const Comparator* user_comparator() const {
|
||||
return user_comparator_.user_comparator();
|
||||
@@ -277,9 +277,6 @@ class InternalKeyComparator
|
||||
// value `kDisableGlobalSequenceNumber`.
|
||||
int Compare(const Slice& a, SequenceNumber a_global_seqno, const Slice& b,
|
||||
SequenceNumber b_global_seqno) const;
|
||||
virtual const Comparator* GetRootComparator() const override {
|
||||
return user_comparator_.GetRootComparator();
|
||||
}
|
||||
};
|
||||
|
||||
// The class represent the internal key in encoded form.
|
||||
|
||||
+5
-2
@@ -9,6 +9,7 @@
|
||||
|
||||
#include "db/dbformat.h"
|
||||
|
||||
#include "table/block_based/index_builder.h"
|
||||
#include "test_util/testharness.h"
|
||||
#include "test_util/testutil.h"
|
||||
|
||||
@@ -24,13 +25,15 @@ static std::string IKey(const std::string& user_key,
|
||||
|
||||
static std::string Shorten(const std::string& s, const std::string& l) {
|
||||
std::string result = s;
|
||||
InternalKeyComparator(BytewiseComparator()).FindShortestSeparator(&result, l);
|
||||
ShortenedIndexBuilder::FindShortestInternalKeySeparator(*BytewiseComparator(),
|
||||
&result, l);
|
||||
return result;
|
||||
}
|
||||
|
||||
static std::string ShortSuccessor(const std::string& s) {
|
||||
std::string result = s;
|
||||
InternalKeyComparator(BytewiseComparator()).FindShortSuccessor(&result);
|
||||
ShortenedIndexBuilder::FindShortInternalKeySuccessor(*BytewiseComparator(),
|
||||
&result);
|
||||
return result;
|
||||
}
|
||||
|
||||
|
||||
+13
-1
@@ -148,7 +148,19 @@ void EventHelpers::LogAndNotifyTableFileCreationFinished(
|
||||
<< table_properties.fast_compression_estimated_data_size
|
||||
<< "db_id" << table_properties.db_id << "db_session_id"
|
||||
<< table_properties.db_session_id << "orig_file_number"
|
||||
<< table_properties.orig_file_number;
|
||||
<< table_properties.orig_file_number << "seqno_to_time_mapping";
|
||||
|
||||
if (table_properties.seqno_to_time_mapping.empty()) {
|
||||
jwriter << "N/A";
|
||||
} else {
|
||||
SeqnoToTimeMapping tmp;
|
||||
Status status = tmp.Add(table_properties.seqno_to_time_mapping);
|
||||
if (status.ok()) {
|
||||
jwriter << tmp.ToHumanString();
|
||||
} else {
|
||||
jwriter << "Invalid";
|
||||
}
|
||||
}
|
||||
|
||||
// user collected properties
|
||||
for (const auto& prop : table_properties.readable_properties) {
|
||||
|
||||
@@ -187,16 +187,6 @@ class ExternalSSTFileBasicTest
|
||||
std::string sst_files_dir_;
|
||||
std::unique_ptr<FaultInjectionTestEnv> fault_injection_test_env_;
|
||||
bool random_rwfile_supported_;
|
||||
#ifndef ROCKSDB_LITE
|
||||
uint64_t GetSstSizeHelper(Temperature temperature) {
|
||||
std::string prop;
|
||||
EXPECT_TRUE(dbfull()->GetProperty(
|
||||
DB::Properties::kLiveSstFilesSizeAtTemperature +
|
||||
std::to_string(static_cast<uint8_t>(temperature)),
|
||||
&prop));
|
||||
return static_cast<uint64_t>(std::atoi(prop.c_str()));
|
||||
}
|
||||
#endif // ROCKSDB_LITE
|
||||
};
|
||||
|
||||
TEST_F(ExternalSSTFileBasicTest, Basic) {
|
||||
|
||||
+36
-23
@@ -95,8 +95,9 @@ FlushJob::FlushJob(
|
||||
Statistics* stats, EventLogger* event_logger, bool measure_io_stats,
|
||||
const bool sync_output_directory, const bool write_manifest,
|
||||
Env::Priority thread_pri, const std::shared_ptr<IOTracer>& io_tracer,
|
||||
const std::string& db_id, const std::string& db_session_id,
|
||||
std::string full_history_ts_low, BlobFileCompletionCallback* blob_callback)
|
||||
const SeqnoToTimeMapping& seqno_time_mapping, const std::string& db_id,
|
||||
const std::string& db_session_id, std::string full_history_ts_low,
|
||||
BlobFileCompletionCallback* blob_callback)
|
||||
: dbname_(dbname),
|
||||
db_id_(db_id),
|
||||
db_session_id_(db_session_id),
|
||||
@@ -128,7 +129,8 @@ FlushJob::FlushJob(
|
||||
io_tracer_(io_tracer),
|
||||
clock_(db_options_.clock),
|
||||
full_history_ts_low_(std::move(full_history_ts_low)),
|
||||
blob_callback_(blob_callback) {
|
||||
blob_callback_(blob_callback),
|
||||
db_impl_seqno_time_mapping_(seqno_time_mapping) {
|
||||
// Update the thread status to indicate flush.
|
||||
ReportStartedFlush();
|
||||
TEST_SYNC_POINT("FlushJob::FlushJob()");
|
||||
@@ -211,6 +213,13 @@ Status FlushJob::Run(LogsWithPrepTracker* prep_tracker, FileMetaData* file_meta,
|
||||
TEST_SYNC_POINT("FlushJob::Start");
|
||||
db_mutex_->AssertHeld();
|
||||
assert(pick_memtable_called);
|
||||
// Mempurge threshold can be dynamically changed.
|
||||
// For sake of consistency, mempurge_threshold is
|
||||
// saved locally to maintain consistency in each
|
||||
// FlushJob::Run call.
|
||||
double mempurge_threshold =
|
||||
mutable_cf_options_.experimental_mempurge_threshold;
|
||||
|
||||
AutoThreadOperationStageUpdater stage_run(
|
||||
ThreadStatus::STAGE_FLUSH_RUN);
|
||||
if (mems_.empty()) {
|
||||
@@ -238,9 +247,11 @@ Status FlushJob::Run(LogsWithPrepTracker* prep_tracker, FileMetaData* file_meta,
|
||||
prev_cpu_read_nanos = IOSTATS(cpu_read_nanos);
|
||||
}
|
||||
Status mempurge_s = Status::NotFound("No MemPurge.");
|
||||
if ((db_options_.experimental_mempurge_threshold > 0.0) &&
|
||||
if ((mempurge_threshold > 0.0) &&
|
||||
(cfd_->GetFlushReason() == FlushReason::kWriteBufferFull) &&
|
||||
(!mems_.empty()) && MemPurgeDecider()) {
|
||||
(!mems_.empty()) && MemPurgeDecider(mempurge_threshold) &&
|
||||
!(db_options_.atomic_flush)) {
|
||||
cfd_->SetMempurgeUsed();
|
||||
mempurge_s = MemPurge();
|
||||
if (!mempurge_s.ok()) {
|
||||
// Mempurge is typically aborted when the output
|
||||
@@ -628,8 +639,7 @@ Status FlushJob::MemPurge() {
|
||||
return s;
|
||||
}
|
||||
|
||||
bool FlushJob::MemPurgeDecider() {
|
||||
double threshold = db_options_.experimental_mempurge_threshold;
|
||||
bool FlushJob::MemPurgeDecider(double threshold) {
|
||||
// Never trigger mempurge if threshold is not a strictly positive value.
|
||||
if (!(threshold > 0.0)) {
|
||||
return false;
|
||||
@@ -779,10 +789,11 @@ bool FlushJob::MemPurgeDecider() {
|
||||
estimated_useful_payload +=
|
||||
(mt->ApproximateMemoryUsage()) * (useful_payload * 1.0 / payload);
|
||||
|
||||
ROCKS_LOG_INFO(
|
||||
db_options_.info_log,
|
||||
"Mempurge sampling - found garbage ratio from sampling: %f.\n",
|
||||
(payload - useful_payload) * 1.0 / payload);
|
||||
ROCKS_LOG_INFO(db_options_.info_log,
|
||||
"Mempurge sampling [CF %s] - found garbage ratio from "
|
||||
"sampling: %f. Threshold is %f\n",
|
||||
cfd_->GetName().c_str(),
|
||||
(payload - useful_payload) * 1.0 / payload, threshold);
|
||||
} else {
|
||||
ROCKS_LOG_WARN(db_options_.info_log,
|
||||
"Mempurge sampling: null payload measured, and collected "
|
||||
@@ -805,6 +816,13 @@ Status FlushJob::WriteLevel0Table() {
|
||||
const uint64_t start_cpu_micros = clock_->CPUMicros();
|
||||
Status s;
|
||||
|
||||
SequenceNumber smallest_seqno = mems_.front()->GetEarliestSequenceNumber();
|
||||
if (!db_impl_seqno_time_mapping_.Empty()) {
|
||||
// make a local copy, as the seqno_time_mapping from db_impl is not thread
|
||||
// safe, which will be used while not holding the db_mutex.
|
||||
seqno_to_time_mapping_ = db_impl_seqno_time_mapping_.Copy(smallest_seqno);
|
||||
}
|
||||
|
||||
std::vector<BlobFileAddition> blob_file_additions;
|
||||
|
||||
{
|
||||
@@ -893,18 +911,13 @@ Status FlushJob::WriteLevel0Table() {
|
||||
"FlushJob::WriteLevel0Table:oldest_ancester_time",
|
||||
&oldest_ancester_time);
|
||||
meta_.oldest_ancester_time = oldest_ancester_time;
|
||||
|
||||
meta_.file_creation_time = current_time;
|
||||
|
||||
uint64_t creation_time = (cfd_->ioptions()->compaction_style ==
|
||||
CompactionStyle::kCompactionStyleFIFO)
|
||||
? current_time
|
||||
: meta_.oldest_ancester_time;
|
||||
|
||||
uint64_t num_input_entries = 0;
|
||||
uint64_t memtable_payload_bytes = 0;
|
||||
uint64_t memtable_garbage_bytes = 0;
|
||||
IOStatus io_s;
|
||||
|
||||
const std::string* const full_history_ts_low =
|
||||
(full_history_ts_low_.empty()) ? nullptr : &full_history_ts_low_;
|
||||
TableBuilderOptions tboptions(
|
||||
@@ -912,8 +925,8 @@ Status FlushJob::WriteLevel0Table() {
|
||||
cfd_->int_tbl_prop_collector_factories(), output_compression_,
|
||||
mutable_cf_options_.compression_opts, cfd_->GetID(), cfd_->GetName(),
|
||||
0 /* level */, false /* is_bottommost */,
|
||||
TableFileCreationReason::kFlush, creation_time, oldest_key_time,
|
||||
current_time, db_id_, db_session_id_, 0 /* target_file_size */,
|
||||
TableFileCreationReason::kFlush, oldest_key_time, current_time,
|
||||
db_id_, db_session_id_, 0 /* target_file_size */,
|
||||
meta_.fd.GetNumber());
|
||||
const SequenceNumber job_snapshot_seq =
|
||||
job_context_->GetJobSnapshotSequence();
|
||||
@@ -924,10 +937,10 @@ Status FlushJob::WriteLevel0Table() {
|
||||
earliest_write_conflict_snapshot_, job_snapshot_seq,
|
||||
snapshot_checker_, mutable_cf_options_.paranoid_file_checks,
|
||||
cfd_->internal_stats(), &io_s, io_tracer_,
|
||||
BlobFileCreationReason::kFlush, event_logger_, job_context_->job_id,
|
||||
io_priority, &table_properties_, write_hint, full_history_ts_low,
|
||||
blob_callback_, &num_input_entries, &memtable_payload_bytes,
|
||||
&memtable_garbage_bytes);
|
||||
BlobFileCreationReason::kFlush, seqno_to_time_mapping_, event_logger_,
|
||||
job_context_->job_id, io_priority, &table_properties_, write_hint,
|
||||
full_history_ts_low, blob_callback_, &num_input_entries,
|
||||
&memtable_payload_bytes, &memtable_garbage_bytes);
|
||||
// TODO: Cleanup io_status in BuildTable and table builders
|
||||
assert(!s.ok() || io_s.ok());
|
||||
io_s.PermitUncheckedError();
|
||||
|
||||
+8
-1
@@ -25,6 +25,7 @@
|
||||
#include "db/log_writer.h"
|
||||
#include "db/logs_with_prep_tracker.h"
|
||||
#include "db/memtable_list.h"
|
||||
#include "db/seqno_to_time_mapping.h"
|
||||
#include "db/snapshot_impl.h"
|
||||
#include "db/version_edit.h"
|
||||
#include "db/write_controller.h"
|
||||
@@ -72,6 +73,7 @@ class FlushJob {
|
||||
EventLogger* event_logger, bool measure_io_stats,
|
||||
const bool sync_output_directory, const bool write_manifest,
|
||||
Env::Priority thread_pri, const std::shared_ptr<IOTracer>& io_tracer,
|
||||
const SeqnoToTimeMapping& seq_time_mapping,
|
||||
const std::string& db_id = "", const std::string& db_session_id = "",
|
||||
std::string full_history_ts_low = "",
|
||||
BlobFileCompletionCallback* blob_callback = nullptr);
|
||||
@@ -122,7 +124,7 @@ class FlushJob {
|
||||
// recommend all users not to set this flag as true given that the MemPurge
|
||||
// process has not matured yet.
|
||||
Status MemPurge();
|
||||
bool MemPurgeDecider();
|
||||
bool MemPurgeDecider(double threshold);
|
||||
// The rate limiter priority (io_priority) is determined dynamically here.
|
||||
Env::IOPriority GetRateLimiterPriorityForWrite();
|
||||
#ifndef ROCKSDB_LITE
|
||||
@@ -191,6 +193,11 @@ class FlushJob {
|
||||
|
||||
const std::string full_history_ts_low_;
|
||||
BlobFileCompletionCallback* blob_callback_;
|
||||
|
||||
// reference to the seqno_time_mapping_ in db_impl.h, not safe to read without
|
||||
// db mutex
|
||||
const SeqnoToTimeMapping& db_impl_seqno_time_mapping_;
|
||||
SeqnoToTimeMapping seqno_to_time_mapping_;
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
+22
-16
@@ -128,7 +128,7 @@ class FlushJobTestBase : public testing::Test {
|
||||
new VersionSet(dbname_, &db_options_, env_options_, table_cache_.get(),
|
||||
&write_buffer_manager_, &write_controller_,
|
||||
/*block_cache_tracer=*/nullptr, /*io_tracer=*/nullptr,
|
||||
/*db_session_id*/ ""));
|
||||
/*db_id*/ "", /*db_session_id*/ ""));
|
||||
EXPECT_OK(versions_->Recover(column_families, false));
|
||||
}
|
||||
|
||||
@@ -148,6 +148,8 @@ class FlushJobTestBase : public testing::Test {
|
||||
InstrumentedMutex mutex_;
|
||||
std::atomic<bool> shutting_down_;
|
||||
std::shared_ptr<mock::MockTableFactory> mock_table_factory_;
|
||||
|
||||
SeqnoToTimeMapping empty_seqno_to_time_mapping_;
|
||||
};
|
||||
|
||||
class FlushJobTest : public FlushJobTestBase {
|
||||
@@ -162,14 +164,15 @@ TEST_F(FlushJobTest, Empty) {
|
||||
auto cfd = versions_->GetColumnFamilySet()->GetDefault();
|
||||
EventLogger event_logger(db_options_.info_log.get());
|
||||
SnapshotChecker* snapshot_checker = nullptr; // not relavant
|
||||
FlushJob flush_job(
|
||||
dbname_, versions_->GetColumnFamilySet()->GetDefault(), db_options_,
|
||||
*cfd->GetLatestMutableCFOptions(),
|
||||
std::numeric_limits<uint64_t>::max() /* memtable_id */, env_options_,
|
||||
versions_.get(), &mutex_, &shutting_down_, {}, kMaxSequenceNumber,
|
||||
snapshot_checker, &job_context, nullptr, nullptr, nullptr, kNoCompression,
|
||||
nullptr, &event_logger, false, true /* sync_output_directory */,
|
||||
true /* write_manifest */, Env::Priority::USER, nullptr /*IOTracer*/);
|
||||
FlushJob flush_job(dbname_, versions_->GetColumnFamilySet()->GetDefault(),
|
||||
db_options_, *cfd->GetLatestMutableCFOptions(),
|
||||
std::numeric_limits<uint64_t>::max() /* memtable_id */,
|
||||
env_options_, versions_.get(), &mutex_, &shutting_down_,
|
||||
{}, kMaxSequenceNumber, snapshot_checker, &job_context,
|
||||
nullptr, nullptr, nullptr, kNoCompression, nullptr,
|
||||
&event_logger, false, true /* sync_output_directory */,
|
||||
true /* write_manifest */, Env::Priority::USER,
|
||||
nullptr /*IOTracer*/, empty_seqno_to_time_mapping_);
|
||||
{
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
flush_job.PickMemTable();
|
||||
@@ -254,7 +257,7 @@ TEST_F(FlushJobTest, NonEmpty) {
|
||||
snapshot_checker, &job_context, nullptr, nullptr, nullptr, kNoCompression,
|
||||
db_options_.statistics.get(), &event_logger, true,
|
||||
true /* sync_output_directory */, true /* write_manifest */,
|
||||
Env::Priority::USER, nullptr /*IOTracer*/);
|
||||
Env::Priority::USER, nullptr /*IOTracer*/, empty_seqno_to_time_mapping_);
|
||||
|
||||
HistogramData hist;
|
||||
FileMetaData file_meta;
|
||||
@@ -316,7 +319,7 @@ TEST_F(FlushJobTest, FlushMemTablesSingleColumnFamily) {
|
||||
snapshot_checker, &job_context, nullptr, nullptr, nullptr, kNoCompression,
|
||||
db_options_.statistics.get(), &event_logger, true,
|
||||
true /* sync_output_directory */, true /* write_manifest */,
|
||||
Env::Priority::USER, nullptr /*IOTracer*/);
|
||||
Env::Priority::USER, nullptr /*IOTracer*/, empty_seqno_to_time_mapping_);
|
||||
HistogramData hist;
|
||||
FileMetaData file_meta;
|
||||
mutex_.Lock();
|
||||
@@ -389,7 +392,8 @@ TEST_F(FlushJobTest, FlushMemtablesMultipleColumnFamilies) {
|
||||
&job_context, nullptr, nullptr, nullptr, kNoCompression,
|
||||
db_options_.statistics.get(), &event_logger, true,
|
||||
false /* sync_output_directory */, false /* write_manifest */,
|
||||
Env::Priority::USER, nullptr /*IOTracer*/));
|
||||
Env::Priority::USER, nullptr /*IOTracer*/,
|
||||
empty_seqno_to_time_mapping_));
|
||||
k++;
|
||||
}
|
||||
HistogramData hist;
|
||||
@@ -516,7 +520,7 @@ TEST_F(FlushJobTest, Snapshots) {
|
||||
snapshot_checker, &job_context, nullptr, nullptr, nullptr, kNoCompression,
|
||||
db_options_.statistics.get(), &event_logger, true,
|
||||
true /* sync_output_directory */, true /* write_manifest */,
|
||||
Env::Priority::USER, nullptr /*IOTracer*/);
|
||||
Env::Priority::USER, nullptr /*IOTracer*/, empty_seqno_to_time_mapping_);
|
||||
mutex_.Lock();
|
||||
flush_job.PickMemTable();
|
||||
ASSERT_OK(flush_job.Run());
|
||||
@@ -571,7 +575,7 @@ TEST_F(FlushJobTest, GetRateLimiterPriorityForWrite) {
|
||||
snapshot_checker, &job_context, nullptr, nullptr, nullptr, kNoCompression,
|
||||
db_options_.statistics.get(), &event_logger, true,
|
||||
true /* sync_output_directory */, true /* write_manifest */,
|
||||
Env::Priority::USER, nullptr /*IOTracer*/);
|
||||
Env::Priority::USER, nullptr /*IOTracer*/, empty_seqno_to_time_mapping_);
|
||||
|
||||
// When the state from WriteController is normal.
|
||||
ASSERT_EQ(flush_job.GetRateLimiterPriorityForWrite(), Env::IO_HIGH);
|
||||
@@ -650,7 +654,8 @@ TEST_F(FlushJobTimestampTest, AllKeysExpired) {
|
||||
snapshot_checker, &job_context, nullptr, nullptr, nullptr, kNoCompression,
|
||||
db_options_.statistics.get(), &event_logger, true,
|
||||
true /* sync_output_directory */, true /* write_manifest */,
|
||||
Env::Priority::USER, nullptr /*IOTracer*/, /*db_id=*/"",
|
||||
Env::Priority::USER, nullptr /*IOTracer*/, empty_seqno_to_time_mapping_,
|
||||
/*db_id=*/"",
|
||||
/*db_session_id=*/"", full_history_ts_low);
|
||||
|
||||
FileMetaData fmeta;
|
||||
@@ -701,7 +706,8 @@ TEST_F(FlushJobTimestampTest, NoKeyExpired) {
|
||||
snapshot_checker, &job_context, nullptr, nullptr, nullptr, kNoCompression,
|
||||
db_options_.statistics.get(), &event_logger, true,
|
||||
true /* sync_output_directory */, true /* write_manifest */,
|
||||
Env::Priority::USER, nullptr /*IOTracer*/, /*db_id=*/"",
|
||||
Env::Priority::USER, nullptr /*IOTracer*/, empty_seqno_to_time_mapping_,
|
||||
/*db_id=*/"",
|
||||
/*db_session_id=*/"", full_history_ts_low);
|
||||
|
||||
FileMetaData fmeta;
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
#pragma once
|
||||
|
||||
#include "rocksdb/comparator.h"
|
||||
#ifndef ROCKSDB_LITE
|
||||
|
||||
#include <string>
|
||||
@@ -28,14 +29,14 @@ struct FileMetaData;
|
||||
|
||||
class MinIterComparator {
|
||||
public:
|
||||
explicit MinIterComparator(const Comparator* comparator) :
|
||||
comparator_(comparator) {}
|
||||
explicit MinIterComparator(const CompareInterface* comparator)
|
||||
: comparator_(comparator) {}
|
||||
|
||||
bool operator()(InternalIterator* a, InternalIterator* b) {
|
||||
return comparator_->Compare(a->key(), b->key()) > 0;
|
||||
}
|
||||
private:
|
||||
const Comparator* comparator_;
|
||||
const CompareInterface* comparator_;
|
||||
};
|
||||
|
||||
using MinIterHeap =
|
||||
|
||||
+71
-29
@@ -307,6 +307,9 @@ static const std::string total_blob_file_size = "total-blob-file-size";
|
||||
static const std::string live_blob_file_size = "live-blob-file-size";
|
||||
static const std::string live_blob_file_garbage_size =
|
||||
"live-blob-file-garbage-size";
|
||||
static const std::string blob_cache_capacity = "blob-cache-capacity";
|
||||
static const std::string blob_cache_usage = "blob-cache-usage";
|
||||
static const std::string blob_cache_pinned_usage = "blob-cache-pinned-usage";
|
||||
|
||||
const std::string DB::Properties::kNumFilesAtLevelPrefix =
|
||||
rocksdb_prefix + num_files_at_level_prefix;
|
||||
@@ -409,6 +412,12 @@ const std::string DB::Properties::kLiveBlobFileSize =
|
||||
rocksdb_prefix + live_blob_file_size;
|
||||
const std::string DB::Properties::kLiveBlobFileGarbageSize =
|
||||
rocksdb_prefix + live_blob_file_garbage_size;
|
||||
const std::string DB::Properties::kBlobCacheCapacity =
|
||||
rocksdb_prefix + blob_cache_capacity;
|
||||
const std::string DB::Properties::kBlobCacheUsage =
|
||||
rocksdb_prefix + blob_cache_usage;
|
||||
const std::string DB::Properties::kBlobCachePinnedUsage =
|
||||
rocksdb_prefix + blob_cache_pinned_usage;
|
||||
|
||||
const UnorderedMap<std::string, DBPropertyInfo>
|
||||
InternalStats::ppt_name_to_info = {
|
||||
@@ -570,6 +579,15 @@ const UnorderedMap<std::string, DBPropertyInfo>
|
||||
{DB::Properties::kLiveBlobFileGarbageSize,
|
||||
{false, nullptr, &InternalStats::HandleLiveBlobFileGarbageSize,
|
||||
nullptr, nullptr}},
|
||||
{DB::Properties::kBlobCacheCapacity,
|
||||
{false, nullptr, &InternalStats::HandleBlobCacheCapacity, nullptr,
|
||||
nullptr}},
|
||||
{DB::Properties::kBlobCacheUsage,
|
||||
{false, nullptr, &InternalStats::HandleBlobCacheUsage, nullptr,
|
||||
nullptr}},
|
||||
{DB::Properties::kBlobCachePinnedUsage,
|
||||
{false, nullptr, &InternalStats::HandleBlobCachePinnedUsage, nullptr,
|
||||
nullptr}},
|
||||
};
|
||||
|
||||
InternalStats::InternalStats(int num_levels, SystemClock* clock,
|
||||
@@ -585,10 +603,8 @@ InternalStats::InternalStats(int num_levels, SystemClock* clock,
|
||||
clock_(clock),
|
||||
cfd_(cfd),
|
||||
started_at_(clock->NowMicros()) {
|
||||
Cache* block_cache = nullptr;
|
||||
bool ok = GetBlockCacheForStats(&block_cache);
|
||||
if (ok) {
|
||||
assert(block_cache);
|
||||
Cache* block_cache = GetBlockCacheForStats();
|
||||
if (block_cache) {
|
||||
// Extract or create stats collector. Could fail in rare cases.
|
||||
Status s = CacheEntryStatsCollector<CacheEntryRoleStats>::GetShared(
|
||||
block_cache, clock_, &cache_entry_stats_collector_);
|
||||
@@ -597,8 +613,6 @@ InternalStats::InternalStats(int num_levels, SystemClock* clock,
|
||||
} else {
|
||||
assert(!cache_entry_stats_collector_);
|
||||
}
|
||||
} else {
|
||||
assert(!block_cache);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -851,6 +865,40 @@ bool InternalStats::HandleLiveBlobFileGarbageSize(uint64_t* value,
|
||||
return true;
|
||||
}
|
||||
|
||||
Cache* InternalStats::GetBlobCacheForStats() {
|
||||
return cfd_->ioptions()->blob_cache.get();
|
||||
}
|
||||
|
||||
bool InternalStats::HandleBlobCacheCapacity(uint64_t* value, DBImpl* /*db*/,
|
||||
Version* /*version*/) {
|
||||
Cache* blob_cache = GetBlobCacheForStats();
|
||||
if (blob_cache) {
|
||||
*value = static_cast<uint64_t>(blob_cache->GetCapacity());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool InternalStats::HandleBlobCacheUsage(uint64_t* value, DBImpl* /*db*/,
|
||||
Version* /*version*/) {
|
||||
Cache* blob_cache = GetBlobCacheForStats();
|
||||
if (blob_cache) {
|
||||
*value = static_cast<uint64_t>(blob_cache->GetUsage());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool InternalStats::HandleBlobCachePinnedUsage(uint64_t* value, DBImpl* /*db*/,
|
||||
Version* /*version*/) {
|
||||
Cache* blob_cache = GetBlobCacheForStats();
|
||||
if (blob_cache) {
|
||||
*value = static_cast<uint64_t>(blob_cache->GetPinnedUsage());
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const DBPropertyInfo* GetPropertyInfo(const Slice& property) {
|
||||
std::string ppt_name = GetPropertyNameAndArg(property).first.ToString();
|
||||
auto ppt_info_iter = InternalStats::ppt_name_to_info.find(ppt_name);
|
||||
@@ -1313,46 +1361,40 @@ bool InternalStats::HandleEstimateOldestKeyTime(uint64_t* value, DBImpl* /*db*/,
|
||||
return *value > 0 && *value < std::numeric_limits<uint64_t>::max();
|
||||
}
|
||||
|
||||
bool InternalStats::GetBlockCacheForStats(Cache** block_cache) {
|
||||
assert(block_cache != nullptr);
|
||||
Cache* InternalStats::GetBlockCacheForStats() {
|
||||
auto* table_factory = cfd_->ioptions()->table_factory.get();
|
||||
assert(table_factory != nullptr);
|
||||
*block_cache =
|
||||
table_factory->GetOptions<Cache>(TableFactory::kBlockCacheOpts());
|
||||
return *block_cache != nullptr;
|
||||
return table_factory->GetOptions<Cache>(TableFactory::kBlockCacheOpts());
|
||||
}
|
||||
|
||||
bool InternalStats::HandleBlockCacheCapacity(uint64_t* value, DBImpl* /*db*/,
|
||||
Version* /*version*/) {
|
||||
Cache* block_cache;
|
||||
bool ok = GetBlockCacheForStats(&block_cache);
|
||||
if (!ok) {
|
||||
return false;
|
||||
Cache* block_cache = GetBlockCacheForStats();
|
||||
if (block_cache) {
|
||||
*value = static_cast<uint64_t>(block_cache->GetCapacity());
|
||||
return true;
|
||||
}
|
||||
*value = static_cast<uint64_t>(block_cache->GetCapacity());
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool InternalStats::HandleBlockCacheUsage(uint64_t* value, DBImpl* /*db*/,
|
||||
Version* /*version*/) {
|
||||
Cache* block_cache;
|
||||
bool ok = GetBlockCacheForStats(&block_cache);
|
||||
if (!ok) {
|
||||
return false;
|
||||
Cache* block_cache = GetBlockCacheForStats();
|
||||
if (block_cache) {
|
||||
*value = static_cast<uint64_t>(block_cache->GetUsage());
|
||||
return true;
|
||||
}
|
||||
*value = static_cast<uint64_t>(block_cache->GetUsage());
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool InternalStats::HandleBlockCachePinnedUsage(uint64_t* value, DBImpl* /*db*/,
|
||||
Version* /*version*/) {
|
||||
Cache* block_cache;
|
||||
bool ok = GetBlockCacheForStats(&block_cache);
|
||||
if (!ok) {
|
||||
return false;
|
||||
Cache* block_cache = GetBlockCacheForStats();
|
||||
if (block_cache) {
|
||||
*value = static_cast<uint64_t>(block_cache->GetPinnedUsage());
|
||||
return true;
|
||||
}
|
||||
*value = static_cast<uint64_t>(block_cache->GetPinnedUsage());
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
void InternalStats::DumpDBMapStats(
|
||||
|
||||
Loaded 100 of 268 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user