mirror of
https://github.com/facebook/rocksdb.git
synced 2026-11-06 02:52:35 -05:00
Compare commits
204
Commits
7.3.fb
...
siying-patch-8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cb096edc3b | ||
|
|
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 | ||
|
|
fac7a23685 | ||
|
|
0e0a19832e | ||
|
|
d5d8920f2c | ||
|
|
a5d773e077 | ||
|
|
c965c9ef65 | ||
|
|
1aac814578 | ||
|
|
f87adcfb3f | ||
|
|
5d6005c780 | ||
|
|
f62c1e1e56 | ||
|
|
8cf86258b8 | ||
|
|
fff302d989 | ||
|
|
126c223714 | ||
|
|
a6691d0f65 | ||
|
|
4d31d3c2ed | ||
|
|
8353ae8b27 | ||
|
|
3d358a7e25 | ||
|
|
94329ae4ec | ||
|
|
9882652b0e | ||
|
|
2e5a323dbd | ||
|
|
69a32eecab | ||
|
|
ce419c0f10 | ||
|
|
19345de60d | ||
|
|
40dfa26049 | ||
|
|
77f4799515 | ||
|
|
b550fc0b09 | ||
|
|
751d1a3e48 | ||
|
|
cba398df8a | ||
|
|
1d2950b8dd | ||
|
|
d665afdbf3 | ||
|
|
40d19bc12c | ||
|
|
f105e1a501 | ||
|
|
bfaf8291c5 | ||
|
|
c0e0f30667 | ||
|
|
04bd347995 | ||
|
|
ad135f3ffd | ||
|
|
8273435c22 | ||
|
|
415200d792 | ||
|
|
80afa77660 | ||
|
|
1777e5f7e9 | ||
|
|
f4052d13b7 | ||
|
|
d3a3b02134 | ||
|
|
693dffd8e8 | ||
|
|
5fa6ef7f18 | ||
|
|
1a3e23a251 | ||
|
|
ecfd4aef0c | ||
|
|
f85b31a2e9 | ||
|
|
9efae14428 | ||
|
|
f890527b16 | ||
|
|
ff32346415 | ||
|
|
5cbee1f609 | ||
|
|
b6de139df5 | ||
|
|
3ee6c9baec | ||
|
|
a101c9de60 | ||
|
|
9f244b2119 | ||
|
|
2af132c341 | ||
|
|
4f78f9699b | ||
|
|
e36008d863 | ||
|
|
2f4a0ffef8 | ||
|
|
560906ab33 | ||
|
|
e9c74bc474 | ||
|
|
3e02c6e05a | ||
|
|
4bdcc80192 | ||
|
|
c1018b7516 | ||
|
|
7d36bc4273 | ||
|
|
d739de63e5 | ||
|
|
eb99e08076 | ||
|
|
21906d66f6 | ||
|
|
cf85607795 | ||
|
|
2b3c50c429 | ||
|
|
5506954b1f | ||
|
|
7b2c0140ba | ||
|
|
b8fe7df2e5 | ||
|
|
e88d8935ae | ||
|
|
ed50ccd19a | ||
|
|
e6432dfd4c | ||
|
|
a020031552 | ||
|
|
3dc6ebaf74 | ||
|
|
65893ad959 | ||
|
|
b4d0e041d0 | ||
|
|
45b1c788c4 | ||
|
|
5864900cf4 | ||
|
|
463873f1bb | ||
|
|
a00cffaf69 | ||
|
|
d04df2752a | ||
|
|
7c8c803938 | ||
|
|
151dc0038a | ||
|
|
5ab5537d79 | ||
|
|
9baeef712f | ||
|
|
a0f391cafc | ||
|
|
0adac6f88e | ||
|
|
460b44c07f | ||
|
|
4eb7b35f6d | ||
|
|
514f0b0937 | ||
|
|
e228515740 | ||
|
|
8c4ea7b851 | ||
|
|
f6e45382e9 | ||
|
|
6c50082654 | ||
|
|
91ba7837b7 | ||
|
|
c9c58a320f | ||
|
|
4cf2f6723a | ||
|
|
28ea1fb44a | ||
|
|
5490da20a5 | ||
|
|
bd170dda03 | ||
|
|
356f8c5d81 | ||
|
|
b0e190604b | ||
|
|
b71466e982 | ||
|
|
9901e7f681 | ||
|
|
af7ae912e2 | ||
|
|
a96a4a2f7b | ||
|
|
23f34c7ae5 | ||
|
|
c78a87cd71 | ||
|
|
d4081bf0be | ||
|
|
8515bd50c9 | ||
|
|
fd24e4479b | ||
|
|
700d597bd8 | ||
|
|
8e9d9156b0 | ||
|
|
253ae017fa |
No files matched your search
+87
-3
@@ -147,6 +147,13 @@ commands:
|
||||
command: |
|
||||
sudo apt-get update -y && sudo apt-get install -y libsnappy-dev zlib1g-dev libbz2-dev liblz4-dev libzstd-dev
|
||||
|
||||
install-streaming-compress-libs:
|
||||
steps:
|
||||
- run:
|
||||
name: Install streaming compression libs
|
||||
command: |
|
||||
sudo apt-get update -y && sudo apt-get install -y libzstd-dev
|
||||
|
||||
install-libprotobuf-mutator:
|
||||
steps:
|
||||
- run:
|
||||
@@ -161,6 +168,49 @@ commands:
|
||||
command: |
|
||||
echo "export PKG_CONFIG_PATH=/usr/local/OFF/:~/libprotobuf-mutator/build/external.protobuf/lib/pkgconfig/" >> $BASH_ENV
|
||||
echo "export PROTOC_BIN=~/libprotobuf-mutator/build/external.protobuf/bin/protoc" >> $BASH_ENV
|
||||
setup-folly:
|
||||
steps:
|
||||
- run:
|
||||
name: Install folly dependencies
|
||||
command: |
|
||||
sudo apt-get install libgoogle-glog-dev
|
||||
- run:
|
||||
name: Checkout folly sources
|
||||
command: |
|
||||
make checkout_folly
|
||||
|
||||
build-for-benchmarks:
|
||||
steps:
|
||||
- pre-steps
|
||||
- run:
|
||||
name: "Linux build for benchmarks"
|
||||
command: #sized for the resource-class rocksdb-benchmark-sys1
|
||||
make V=1 J=8 -j8 release
|
||||
|
||||
perform-benchmarks:
|
||||
steps:
|
||||
- run:
|
||||
name: "Run basic benchmark, 5 min"
|
||||
command: ./tools/benchmark.sh fillseq_enable_wal
|
||||
environment:
|
||||
LD_LIBRARY_PATH: /usr/local/lib
|
||||
DB_DIR: /tmp/rocksdb-benchmark-datadir
|
||||
WAL_DIR: /tmp/rocksdb-benchmark-waldir
|
||||
OUTPUT_DIR: /tmp/benchmark-results
|
||||
NUM_KEYS: 10000000
|
||||
|
||||
post-benchmarks:
|
||||
steps:
|
||||
- store_artifacts: # store the benchmark output
|
||||
path: /tmp/benchmark-results
|
||||
destination: test_logs
|
||||
- run:
|
||||
name: Send benchmark report to visualisation
|
||||
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:
|
||||
@@ -225,6 +275,7 @@ jobs:
|
||||
steps:
|
||||
- pre-steps
|
||||
- install-gflags
|
||||
- install-streaming-compress-libs
|
||||
- run: make V=1 J=32 -j32 check
|
||||
- post-steps
|
||||
|
||||
@@ -385,6 +436,18 @@ jobs:
|
||||
- run: CC=clang-10 CXX=clang++-10 ROCKSDB_DISABLE_ALIGNED_NEW=1 CLANG_ANALYZER="/usr/bin/clang++-10" CLANG_SCAN_BUILD=scan-build-10 USE_CLANG=1 make V=1 -j32 analyze # aligned new doesn't work for reason we haven't figured out. For unknown, reason passing "clang++-10" as CLANG_ANALYZER doesn't work, and we need a full path.
|
||||
- post-steps
|
||||
|
||||
build-linux-runner:
|
||||
machine: true
|
||||
resource_class: facebook/rocksdb-benchmark-sys1
|
||||
steps:
|
||||
- pre-steps
|
||||
- run:
|
||||
name: "Checked Linux build (Runner)"
|
||||
command: make V=1 J=8 -j8 check
|
||||
environment:
|
||||
LD_LIBRARY_PATH: /usr/local/lib
|
||||
- post-steps
|
||||
|
||||
build-linux-cmake-with-folly:
|
||||
machine:
|
||||
image: ubuntu-2004:202111-02
|
||||
@@ -393,7 +456,7 @@ jobs:
|
||||
- pre-steps
|
||||
- install-gflags
|
||||
- upgrade-cmake
|
||||
- run: make checkout_folly
|
||||
- setup-folly
|
||||
- run: (mkdir build && cd build && cmake -DUSE_FOLLY=1 -DWITH_GFLAGS=1 .. && make V=1 -j20 && ctest -j20)
|
||||
- post-steps
|
||||
|
||||
@@ -428,7 +491,7 @@ jobs:
|
||||
steps:
|
||||
- pre-steps
|
||||
- run: sudo add-apt-repository -y ppa:ubuntu-toolchain-r/test && sudo apt-get update -y && sudo apt-get install gcc-7 g++-7 libgflags-dev
|
||||
- run: make checkout_folly
|
||||
- setup-folly
|
||||
- run: USE_FOLLY=1 CC=gcc-7 CXX=g++-7 V=1 make -j32 check
|
||||
- post-steps
|
||||
|
||||
@@ -483,7 +546,7 @@ jobs:
|
||||
- pre-steps
|
||||
- install-clang-13
|
||||
- install-gflags
|
||||
- run: make checkout_folly
|
||||
- setup-folly
|
||||
- run: CC=clang-13 CXX=clang++-13 USE_CLANG=1 USE_FOLLY=1 COMPILE_WITH_UBSAN=1 COMPILE_WITH_ASAN=1 make -j32 check
|
||||
- post-steps
|
||||
|
||||
@@ -826,6 +889,14 @@ jobs:
|
||||
command: cd fuzz && make sst_file_writer_fuzzer db_fuzzer db_map_fuzzer
|
||||
- post-steps
|
||||
|
||||
benchmark-linux: #use a private Circle CI runner (resource_class) to run the job
|
||||
machine: true
|
||||
resource_class: facebook/rocksdb-benchmark-sys1
|
||||
steps:
|
||||
- build-for-benchmarks
|
||||
- perform-benchmarks
|
||||
- post-benchmarks
|
||||
|
||||
workflows:
|
||||
version: 2
|
||||
jobs-linux-run-tests:
|
||||
@@ -894,6 +965,19 @@ workflows:
|
||||
jobs-linux-arm:
|
||||
jobs:
|
||||
- build-linux-arm
|
||||
build-fuzzers:
|
||||
jobs:
|
||||
- build-fuzzers
|
||||
benchmark-linux:
|
||||
triggers:
|
||||
- schedule:
|
||||
cron: "0 * * * *"
|
||||
filters:
|
||||
branches:
|
||||
only:
|
||||
- main
|
||||
jobs:
|
||||
- benchmark-linux
|
||||
nightly:
|
||||
triggers:
|
||||
- schedule:
|
||||
|
||||
@@ -36,8 +36,6 @@ manifest_dump
|
||||
sst_dump
|
||||
blob_dump
|
||||
block_cache_trace_analyzer
|
||||
db_readonly_with_timestamp_test
|
||||
db_with_timestamp_basic_test
|
||||
tools/block_cache_analyzer/*.pyc
|
||||
column_aware_encoding_exp
|
||||
util/build_version.cc
|
||||
@@ -53,7 +51,6 @@ rocksdb_dump
|
||||
rocksdb_undump
|
||||
db_test2
|
||||
trace_analyzer
|
||||
trace_analyzer_test
|
||||
block_cache_trace_analyzer
|
||||
io_tracer_parser
|
||||
.DS_Store
|
||||
|
||||
+25
-7
@@ -89,7 +89,11 @@ include(CMakeDependentOption)
|
||||
if(MSVC)
|
||||
option(WITH_GFLAGS "build with GFlags" OFF)
|
||||
option(WITH_XPRESS "build with windows built in compression" OFF)
|
||||
include(${CMAKE_CURRENT_SOURCE_DIR}/thirdparty.inc)
|
||||
option(ROCKSDB_SKIP_THIRDPARTY "skip thirdparty.inc" OFF)
|
||||
|
||||
if(NOT ROCKSDB_SKIP_THIRDPARTY)
|
||||
include(${CMAKE_CURRENT_SOURCE_DIR}/thirdparty.inc)
|
||||
endif()
|
||||
else()
|
||||
if(CMAKE_SYSTEM_NAME MATCHES "FreeBSD" AND NOT CMAKE_SYSTEM_NAME MATCHES "kFreeBSD")
|
||||
# FreeBSD has jemalloc as default malloc
|
||||
@@ -190,7 +194,7 @@ else()
|
||||
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wstrict-prototypes")
|
||||
endif()
|
||||
if(MINGW)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-format -fno-asynchronous-unwind-tables")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-format")
|
||||
add_definitions(-D_POSIX_C_SOURCE=1)
|
||||
endif()
|
||||
if(NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
|
||||
@@ -583,6 +587,7 @@ include_directories(${PROJECT_SOURCE_DIR}/include)
|
||||
if(USE_FOLLY)
|
||||
include_directories(${PROJECT_SOURCE_DIR}/third-party/folly)
|
||||
add_definitions(-DUSE_FOLLY -DFOLLY_NO_CONFIG)
|
||||
list(APPEND THIRDPARTY_LIBS glog)
|
||||
endif()
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
@@ -610,6 +615,7 @@ set(SOURCES
|
||||
db/blob/blob_log_format.cc
|
||||
db/blob/blob_log_sequential_reader.cc
|
||||
db/blob/blob_log_writer.cc
|
||||
db/blob/blob_source.cc
|
||||
db/blob/prefetch_buffer_collection.cc
|
||||
db/builder.cc
|
||||
db/c.cc
|
||||
@@ -621,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
|
||||
@@ -671,6 +681,7 @@ set(SOURCES
|
||||
db/version_set.cc
|
||||
db/wal_edit.cc
|
||||
db/wal_manager.cc
|
||||
db/wide/wide_column_serialization.cc
|
||||
db/write_batch.cc
|
||||
db/write_batch_base.cc
|
||||
db/write_controller.cc
|
||||
@@ -733,7 +744,6 @@ set(SOURCES
|
||||
table/adaptive/adaptive_table_factory.cc
|
||||
table/block_based/binary_search_index_reader.cc
|
||||
table/block_based/block.cc
|
||||
table/block_based/block_based_filter_block.cc
|
||||
table/block_based/block_based_table_builder.cc
|
||||
table/block_based/block_based_table_factory.cc
|
||||
table/block_based/block_based_table_iterator.cc
|
||||
@@ -971,9 +981,13 @@ endif()
|
||||
if(USE_FOLLY)
|
||||
list(APPEND SOURCES
|
||||
third-party/folly/folly/container/detail/F14Table.cpp
|
||||
third-party/folly/folly/detail/Futex.cpp
|
||||
third-party/folly/folly/lang/SafeAssert.cpp
|
||||
third-party/folly/folly/lang/ToAscii.cpp
|
||||
third-party/folly/folly/ScopeGuard.cpp)
|
||||
third-party/folly/folly/ScopeGuard.cpp
|
||||
third-party/folly/folly/synchronization/AtomicNotification.cpp
|
||||
third-party/folly/folly/synchronization/DistributedMutex.cpp
|
||||
third-party/folly/folly/synchronization/ParkingLot.cpp)
|
||||
endif()
|
||||
|
||||
set(ROCKSDB_STATIC_LIB rocksdb${ARTIFACT_SUFFIX})
|
||||
@@ -1122,8 +1136,8 @@ if(NOT WIN32 OR ROCKSDB_INSTALL_ON_WINDOWS)
|
||||
)
|
||||
|
||||
configure_file(
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/${PROJECT_NAME}.pc.in
|
||||
${CMAKE_CURRENT_BINARY_DIR}/${PROJECT_NAME}.pc
|
||||
${PROJECT_NAME}.pc.in
|
||||
${PROJECT_NAME}.pc
|
||||
@ONLY
|
||||
)
|
||||
|
||||
@@ -1208,6 +1222,7 @@ if(WITH_TESTS)
|
||||
db/blob/blob_file_garbage_test.cc
|
||||
db/blob/blob_file_reader_test.cc
|
||||
db/blob/blob_garbage_meter_test.cc
|
||||
db/blob/blob_source_test.cc
|
||||
db/blob/db_blob_basic_test.cc
|
||||
db/blob/db_blob_compaction_test.cc
|
||||
db/blob/db_blob_corruption_test.cc
|
||||
@@ -1220,6 +1235,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
|
||||
@@ -1290,6 +1306,8 @@ 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
|
||||
db/write_controller_test.cc
|
||||
@@ -1315,7 +1333,6 @@ if(WITH_TESTS)
|
||||
options/customizable_test.cc
|
||||
options/options_settable_test.cc
|
||||
options/options_test.cc
|
||||
table/block_based/block_based_filter_block_test.cc
|
||||
table/block_based/block_based_table_reader_test.cc
|
||||
table/block_based/block_test.cc
|
||||
table/block_based/data_block_hash_index_test.cc
|
||||
@@ -1384,6 +1401,7 @@ if(WITH_TESTS)
|
||||
utilities/transactions/write_prepared_transaction_test.cc
|
||||
utilities/transactions/write_unprepared_transaction_test.cc
|
||||
utilities/transactions/lock/range/range_locking_test.cc
|
||||
utilities/transactions/timestamped_snapshot_test.cc
|
||||
utilities/ttl/ttl_test.cc
|
||||
utilities/util_merge_operators_test.cc
|
||||
utilities/write_batch_with_index/write_batch_with_index_test.cc
|
||||
|
||||
+97
@@ -1,4 +1,95 @@
|
||||
# Rocksdb Change Log
|
||||
## Unreleased
|
||||
### 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`).
|
||||
|
||||
### 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.
|
||||
|
||||
### 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.
|
||||
|
||||
## 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`.
|
||||
* Fixed a bug where a snapshot taken during SST file ingestion would be unstable.
|
||||
* Fixed a bug for non-TransactionDB with avoid_flush_during_recovery = true and TransactionDB where in case of crash, min_log_number_to_keep may not change on recovery and persisting a new MANIFEST with advanced log_numbers for some column families, results in "column family inconsistency" error on second recovery. As a solution, RocksDB will persist the new MANIFEST after successfully syncing the new WAL. If a future recovery starts from the new MANIFEST, then it means the new WAL is successfully synced. Due to the sentinel empty write batch at the beginning, kPointInTimeRecovery of WAL is guaranteed to go after this point. If future recovery starts from the old MANIFEST, it means the writing the new MANIFEST failed. We won't have the "SST ahead of WAL" error.
|
||||
* Fixed a bug where RocksDB DB::Open() may creates and writes to two new MANIFEST files even before recovery succeeds. Now writes to MANIFEST are persisted only after recovery is successful.
|
||||
* Fix a race condition in WAL size tracking which is caused by an unsafe iterator access after container is changed.
|
||||
* Fix unprotected concurrent accesses to `WritableFileWriter::filesize_` by `DB::SyncWAL()` and `DB::Put()` in two write queue mode.
|
||||
* Fix a bug in WAL tracking. Before this PR (#10087), calling `SyncWAL()` on the only WAL file of the db will not log the event in MANIFEST, thus allowing a subsequent `DB::Open` even if the WAL file is missing or corrupted.
|
||||
* Fix a bug that could return wrong results with `index_type=kHashSearch` and using `SetOptions` to change the `prefix_extractor`.
|
||||
* Fixed a bug in WAL tracking with wal_compression. WAL compression writes a kSetCompressionType record which is not associated with any sequence number. As result, WalManager::GetSortedWalsOfType() will skip these WALs and not return them to caller, e.g. Checkpoint, Backup, causing the operations to fail.
|
||||
* 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.
|
||||
* Add transaction `get_pinned` and `multi_get` to C API.
|
||||
* Add two-phase commit support to C API.
|
||||
* Add `rocksdb_transaction_get_writebatch_wi` and `rocksdb_transaction_rebuild_from_writebatch` to C API.
|
||||
* Add `rocksdb_options_get_blob_file_starting_level` and `rocksdb_options_set_blob_file_starting_level` to C API.
|
||||
* Add `blobFileStartingLevel` and `setBlobFileStartingLevel` to Java API.
|
||||
* Add SingleDelete for DB in C API
|
||||
* Add User Defined Timestamp in C API.
|
||||
* `rocksdb_comparator_with_ts_create` to create timestamp aware comparator
|
||||
* Put, Get, Delete, SingleDelete, MultiGet APIs has corresponding timestamp aware APIs with suffix `with_ts`
|
||||
* And Add C API's for Transaction, SstFileWriter, Compaction as mentioned [here](https://github.com/facebook/rocksdb/wiki/User-defined-Timestamp-(Experimental))
|
||||
* The contract for implementations of Comparator::IsSameLengthImmediateSuccessor has been updated to work around a design bug in `auto_prefix_mode`.
|
||||
* The API documentation for `auto_prefix_mode` now notes some corner cases in which it returns different results than `total_order_seek`, due to design bugs that are not easily fixed. Users using built-in comparators and keys at least the size of a fixed prefix length are not affected.
|
||||
* Obsoleted the NUM_DATA_BLOCKS_READ_PER_LEVEL stat and introduced the NUM_LEVEL_READ_PER_MULTIGET and MULTIGET_COROUTINE_COUNT stats
|
||||
* Introduced `WriteOptions::protection_bytes_per_key`, which can be used to enable key-value integrity protection for live updates.
|
||||
|
||||
### New Features
|
||||
* Add FileSystem::ReadAsync API in io_tracing
|
||||
* Add blob garbage collection parameters `blob_garbage_collection_policy` and `blob_garbage_collection_age_cutoff` to both force-enable and force-disable GC, as well as selectively override age cutoff when using CompactRange.
|
||||
* Add an extra sanity check in `GetSortedWalFiles()` (also used by `GetLiveFilesStorageInfo()`, `BackupEngine`, and `Checkpoint`) to reduce risk of successfully created backup or checkpoint failing to open because of missing WAL file.
|
||||
* Add a new column family option `blob_file_starting_level` to enable writing blob files during flushes and compactions starting from the specified LSM tree level.
|
||||
* 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)
|
||||
* Removed support for reading Bloom filters using obsolete block-based filter format. (Support for writing such filters was dropped in 7.0.) For good read performance on old DBs using these filters, a full compaction is required.
|
||||
* Per KV checksum in write batch is verified before a write batch is written to WAL to detect any corruption to the write batch (#10114).
|
||||
|
||||
### Performance Improvements
|
||||
* When compiled with folly (Meta-internal integration; experimental in open source build), improve the locking performance (CPU efficiency) of LRUCache by using folly DistributedMutex in place of standard mutex.
|
||||
|
||||
## 7.3.0 (05/20/2022)
|
||||
### Bug Fixes
|
||||
* Fixed a bug where manual flush would block forever even though flush options had wait=false.
|
||||
@@ -14,6 +105,8 @@
|
||||
* RemoteCompaction supports table_properties_collector_factories override on compaction worker.
|
||||
* Start tracking SST unique id in MANIFEST, which will be used to verify with SST properties during DB open to make sure the SST file is not overwritten or misplaced. A db option `verify_sst_unique_id_in_manifest` is introduced to enable/disable the verification, if enabled all SST files will be opened during DB-open to verify the unique id (default is false), so it's recommended to use it with `max_open_files = -1` to pre-open the files.
|
||||
* Added the ability to concurrently read data blocks from multiple files in a level in batched MultiGet. This can be enabled by setting the async_io option in ReadOptions. Using this feature requires a FileSystem that supports ReadAsync (PosixFileSystem is not supported yet for this), and for RocksDB to be compiled with folly and c++20.
|
||||
* Charge memory usage of file metadata. RocksDB holds one file metadata structure in-memory per on-disk table file. If an operation reserving memory for file metadata exceeds the avaible space left in the block
|
||||
cache at some point (i.e, causing a cache full under `LRUCacheOptions::strict_capacity_limit` = true), creation will fail with `Status::MemoryLimit()`. To opt in this feature, enable charging `CacheEntryRole::kFileMetadata` in `BlockBasedTableOptions::cache_usage_options`.
|
||||
|
||||
### Public API changes
|
||||
* Add rollback_deletion_type_callback to TransactionDBOptions so that write-prepared transactions know whether to issue a Delete or SingleDelete to cancel a previous key written during prior prepare phase. The PR aims to prevent mixing SingleDeletes and Deletes for the same key that can lead to undefined behaviors for write-prepared transactions.
|
||||
@@ -29,6 +122,7 @@
|
||||
* Add an option, `CompressionOptions::use_zstd_dict_trainer`, to indicate whether zstd dictionary trainer should be used for generating zstd compression dictionaries. The default value of this option is true for backward compatibility. When this option is set to false, zstd API `ZDICT_finalizeDictionary` is used to generate compression dictionaries.
|
||||
* Seek API which positions itself every LevelIterator on the correct data block in the correct SST file which can be parallelized if ReadOptions.async_io option is enabled.
|
||||
* Add new stat number_async_seek in PerfContext that indicates number of async calls made by seek to prefetch data.
|
||||
* Add support for user-defined timestamps to read only DB.
|
||||
|
||||
### Bug Fixes
|
||||
* RocksDB calls FileSystem::Poll API during FilePrefetchBuffer destruction which impacts performance as it waits for read requets completion which is not needed anymore. Calling FileSystem::AbortIO to abort those requests instead fixes that performance issue.
|
||||
@@ -39,6 +133,9 @@
|
||||
* ldb `--try_load_options` default to true if `--db` is specified and not creating a new DB, the user can still explicitly disable that by `--try_load_options=false` (or explicitly enable that by `--try_load_options`).
|
||||
* During Flush write or Compaction write/read, the WriteController is used to determine whether DB writes are stalled or slowed down. The priority (Env::IOPriority) can then be determined accordingly and be passed in IOOptions to the file system.
|
||||
|
||||
### Performance Improvements
|
||||
* Avoid calling malloc_usable_size() in LRU Cache's mutex.
|
||||
* Reduce DB mutex holding time when finding obsolete files to delete. When a file is trivial moved to another level, the internal files will be referenced twice internally and sometimes opened twice too. If a deletion candidate file is not the last reference, we need to destroy the reference and close the file but not deleting the file. Right now we determine it by building a set of all live files. With the improvement, we check the file against all live LSM-tree versions instead.
|
||||
|
||||
## 7.2.0 (04/15/2022)
|
||||
### Bug Fixes
|
||||
|
||||
@@ -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
|
||||
@@ -461,6 +462,8 @@ ifeq ($(USE_FOLLY),1)
|
||||
endif
|
||||
PLATFORM_CCFLAGS += -DUSE_FOLLY -DFOLLY_NO_CONFIG
|
||||
PLATFORM_CXXFLAGS += -DUSE_FOLLY -DFOLLY_NO_CONFIG
|
||||
# TODO: fix linking with fbcode compiler config
|
||||
PLATFORM_LDFLAGS += -lglog
|
||||
endif
|
||||
|
||||
ifdef TEST_CACHE_LINE_SIZE
|
||||
@@ -1381,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)
|
||||
|
||||
@@ -1594,9 +1600,6 @@ random_access_file_reader_test: $(OBJ_DIR)/file/random_access_file_reader_test.o
|
||||
file_reader_writer_test: $(OBJ_DIR)/util/file_reader_writer_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
block_based_filter_block_test: $(OBJ_DIR)/table/block_based/block_based_filter_block_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
block_based_table_reader_test: table/block_based/block_based_table_reader_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
@@ -1777,6 +1780,12 @@ write_prepared_transaction_test: $(OBJ_DIR)/utilities/transactions/write_prepare
|
||||
write_unprepared_transaction_test: $(OBJ_DIR)/utilities/transactions/write_unprepared_transaction_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
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)
|
||||
|
||||
@@ -1861,6 +1870,9 @@ blob_file_garbage_test: $(OBJ_DIR)/db/blob/blob_file_garbage_test.o $(TEST_LIBRA
|
||||
blob_file_reader_test: $(OBJ_DIR)/db/blob/blob_file_reader_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
blob_source_test: $(OBJ_DIR)/db/blob/blob_source_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
blob_garbage_meter_test: $(OBJ_DIR)/db/blob/blob_garbage_meter_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
@@ -1876,7 +1888,7 @@ testutil_test: $(OBJ_DIR)/test_util/testutil_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
io_tracer_test: $(OBJ_DIR)/trace_replay/io_tracer_test.o $(OBJ_DIR)/trace_replay/io_tracer.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
prefetch_test: $(OBJ_DIR)/file/prefetch_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
prefetch_test: $(OBJ_DIR)/file/prefetch_test.o $(OBJ_DIR)/tools/io_tracer_parser_tool.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
io_tracer_parser_test: $(OBJ_DIR)/tools/io_tracer_parser_test.o $(OBJ_DIR)/tools/io_tracer_parser_tool.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
@@ -1902,6 +1914,10 @@ db_basic_bench: $(OBJ_DIR)/microbench/db_basic_bench.o $(LIBRARY)
|
||||
|
||||
cache_reservation_manager_test: $(OBJ_DIR)/cache/cache_reservation_manager_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
wide_column_serialization_test: $(OBJ_DIR)/db/wide/wide_column_serialization_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
#-------------------------------------------------
|
||||
# make install related stuff
|
||||
PREFIX ?= /usr/local
|
||||
@@ -2344,16 +2360,20 @@ 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
|
||||
cd third-party/folly && git reset --hard 98b9b2c1124e99f50f9085ddee74ce32afffc665
|
||||
cd third-party/folly && git reset --hard beacd86d63cd71c904632262e6c36f60874d78ba
|
||||
@# A hack to remove boost dependency.
|
||||
@# NOTE: this hack is not needed if using FBCODE compiler config
|
||||
perl -pi -e 's/^(#include <boost)/\/\/$$1/' third-party/folly/folly/functional/Invoke.h
|
||||
@# NOTE: this hack is required for clang in some cases
|
||||
perl -pi -e 's/int rv = syscall/int rv = (int)syscall/' third-party/folly/folly/detail/Futex.cpp
|
||||
@# NOTE: this hack is required for gcc in some cases
|
||||
perl -pi -e 's/(__has_include.<experimental.memory_resource>.)/__cpp_rtti && $$1/' third-party/folly/folly/memory/MemoryResource.h
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Build size testing
|
||||
|
||||
@@ -30,6 +30,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"db/blob/blob_log_format.cc",
|
||||
"db/blob/blob_log_sequential_reader.cc",
|
||||
"db/blob/blob_log_writer.cc",
|
||||
"db/blob/blob_source.cc",
|
||||
"db/blob/prefetch_buffer_collection.cc",
|
||||
"db/builder.cc",
|
||||
"db/c.cc",
|
||||
@@ -37,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",
|
||||
@@ -91,6 +96,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"db/version_set.cc",
|
||||
"db/wal_edit.cc",
|
||||
"db/wal_manager.cc",
|
||||
"db/wide/wide_column_serialization.cc",
|
||||
"db/write_batch.cc",
|
||||
"db/write_batch_base.cc",
|
||||
"db/write_controller.cc",
|
||||
@@ -164,7 +170,6 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"table/adaptive/adaptive_table_factory.cc",
|
||||
"table/block_based/binary_search_index_reader.cc",
|
||||
"table/block_based/block.cc",
|
||||
"table/block_based/block_based_filter_block.cc",
|
||||
"table/block_based/block_based_table_builder.cc",
|
||||
"table/block_based/block_based_table_factory.cc",
|
||||
"table/block_based/block_based_table_iterator.cc",
|
||||
@@ -334,6 +339,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"//folly/experimental/coro:collect",
|
||||
"//folly/experimental/coro:coroutine",
|
||||
"//folly/experimental/coro:task",
|
||||
"//folly/synchronization:distributed_mutex",
|
||||
], headers=None, link_whole=False, extra_test_libs=False)
|
||||
|
||||
cpp_library_wrapper(name="rocksdb_whole_archive_lib", srcs=[
|
||||
@@ -358,6 +364,7 @@ cpp_library_wrapper(name="rocksdb_whole_archive_lib", srcs=[
|
||||
"db/blob/blob_log_format.cc",
|
||||
"db/blob/blob_log_sequential_reader.cc",
|
||||
"db/blob/blob_log_writer.cc",
|
||||
"db/blob/blob_source.cc",
|
||||
"db/blob/prefetch_buffer_collection.cc",
|
||||
"db/builder.cc",
|
||||
"db/c.cc",
|
||||
@@ -365,11 +372,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",
|
||||
@@ -419,6 +430,7 @@ cpp_library_wrapper(name="rocksdb_whole_archive_lib", srcs=[
|
||||
"db/version_set.cc",
|
||||
"db/wal_edit.cc",
|
||||
"db/wal_manager.cc",
|
||||
"db/wide/wide_column_serialization.cc",
|
||||
"db/write_batch.cc",
|
||||
"db/write_batch_base.cc",
|
||||
"db/write_controller.cc",
|
||||
@@ -492,7 +504,6 @@ cpp_library_wrapper(name="rocksdb_whole_archive_lib", srcs=[
|
||||
"table/adaptive/adaptive_table_factory.cc",
|
||||
"table/block_based/binary_search_index_reader.cc",
|
||||
"table/block_based/block.cc",
|
||||
"table/block_based/block_based_filter_block.cc",
|
||||
"table/block_based/block_based_table_builder.cc",
|
||||
"table/block_based/block_based_table_factory.cc",
|
||||
"table/block_based/block_based_table_iterator.cc",
|
||||
@@ -662,6 +673,7 @@ cpp_library_wrapper(name="rocksdb_whole_archive_lib", srcs=[
|
||||
"//folly/experimental/coro:collect",
|
||||
"//folly/experimental/coro:coroutine",
|
||||
"//folly/experimental/coro:task",
|
||||
"//folly/synchronization:distributed_mutex",
|
||||
], headers=None, link_whole=True, extra_test_libs=False)
|
||||
|
||||
cpp_library_wrapper(name="rocksdb_test_lib", srcs=[
|
||||
@@ -4798,8 +4810,8 @@ cpp_unittest_wrapper(name="blob_garbage_meter_test",
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="block_based_filter_block_test",
|
||||
srcs=["table/block_based/block_based_filter_block_test.cc"],
|
||||
cpp_unittest_wrapper(name="blob_source_test",
|
||||
srcs=["db/blob/blob_source_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
@@ -5236,6 +5248,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"],
|
||||
@@ -5754,6 +5772,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"],
|
||||
@@ -5766,6 +5790,12 @@ cpp_unittest_wrapper(name="timer_test",
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="timestamped_snapshot_test",
|
||||
srcs=["utilities/transactions/timestamped_snapshot_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="trace_analyzer_test",
|
||||
srcs=["tools/trace_analyzer_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
@@ -5814,6 +5844,12 @@ cpp_unittest_wrapper(name="wal_manager_test",
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="wide_column_serialization_test",
|
||||
srcs=["db/wide/wide_column_serialization_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="work_queue_test",
|
||||
srcs=["util/work_queue_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
|
||||
@@ -150,7 +150,8 @@ def generate_targets(repo_path, deps_map):
|
||||
"//folly/experimental/coro:blocking_wait",
|
||||
"//folly/experimental/coro:collect",
|
||||
"//folly/experimental/coro:coroutine",
|
||||
"//folly/experimental/coro:task"])
|
||||
"//folly/experimental/coro:task",
|
||||
"//folly/synchronization:distributed_mutex"])
|
||||
# rocksdb_whole_archive_lib
|
||||
TARGETS.add_library(
|
||||
"rocksdb_whole_archive_lib",
|
||||
@@ -163,7 +164,8 @@ def generate_targets(repo_path, deps_map):
|
||||
"//folly/experimental/coro:blocking_wait",
|
||||
"//folly/experimental/coro:collect",
|
||||
"//folly/experimental/coro:coroutine",
|
||||
"//folly/experimental/coro:task"],
|
||||
"//folly/experimental/coro:task",
|
||||
"//folly/synchronization:distributed_mutex"],
|
||||
headers=None,
|
||||
extra_external_deps="",
|
||||
link_whole=True)
|
||||
|
||||
Executable
+183
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env python3
|
||||
# Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
|
||||
# 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).
|
||||
|
||||
'''Access the results of benchmark runs
|
||||
Send these results on to OpenSearch graphing service
|
||||
'''
|
||||
|
||||
import argparse
|
||||
import itertools
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import requests
|
||||
from dateutil import parser
|
||||
import logging
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
class Configuration:
|
||||
opensearch_user = os.environ['ES_USER']
|
||||
opensearch_pass = os.environ['ES_PASS']
|
||||
|
||||
class BenchmarkResultException(Exception):
|
||||
def __init__(self, message, content):
|
||||
super().__init__(self, message)
|
||||
self.content = content
|
||||
|
||||
|
||||
class BenchmarkUtils:
|
||||
|
||||
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' in row:
|
||||
logging.debug(f"not 'test' in row: {row}")
|
||||
return False
|
||||
if row['test'] == '':
|
||||
logging.debug(f"row['test'] == '': {row}")
|
||||
return False
|
||||
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['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"):
|
||||
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
|
||||
'''
|
||||
l = l_in
|
||||
row = []
|
||||
while l != '':
|
||||
match_item = self.field.match(l)
|
||||
if match_item:
|
||||
item = match_item.group(0)
|
||||
row.append(item)
|
||||
l = l[len(item):]
|
||||
else:
|
||||
match_intra = self.intra.match(l)
|
||||
if match_intra:
|
||||
intra = match_intra.group(0)
|
||||
# Count the separators
|
||||
# If there are >1 then generate extra blank fields
|
||||
# White space with no true separators fakes up a single separator
|
||||
tabbed = self.sep.split(intra)
|
||||
sep_count = len(tabbed) - 1
|
||||
if sep_count == 0:
|
||||
sep_count = 1
|
||||
for i in range(sep_count-1):
|
||||
row.append('')
|
||||
l = l[len(intra):]
|
||||
else:
|
||||
raise BenchmarkResultException(
|
||||
'Invalid TSV line', f"{l_in} at {l}")
|
||||
return row
|
||||
|
||||
def parse(self, lines):
|
||||
'''Parse something that iterates 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(
|
||||
header, row[:width])} for row in rows[1:]]
|
||||
return records
|
||||
|
||||
|
||||
def load_report_from_tsv(filename: str):
|
||||
file = open(filename, 'r')
|
||||
contents = file.readlines()
|
||||
file.close()
|
||||
parser = ResultParser()
|
||||
report = parser.parse(contents)
|
||||
logging.debug(f"Loaded TSV Report: {report}")
|
||||
return report
|
||||
|
||||
|
||||
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)} sane of {len(report)} benchmarks to opensearch")
|
||||
for single_benchmark in sanitized:
|
||||
logging.debug(f"upload benchmark: {single_benchmark}")
|
||||
response = requests.post(
|
||||
esdocument,
|
||||
json=single_benchmark, auth=(os.environ['ES_USER'], os.environ['ES_PASS']))
|
||||
logging.debug(
|
||||
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
|
||||
This tool will
|
||||
|
||||
(1) Open a local tsv benchmark report file
|
||||
(2) Upload to OpenSearch document, via https/JSON
|
||||
'''
|
||||
|
||||
parser = argparse.ArgumentParser(
|
||||
description='CircleCI benchmark scraper.')
|
||||
|
||||
# --tsvfile is the name of the file to read results from
|
||||
# --esdocument is the ElasticSearch document to push these results into
|
||||
#
|
||||
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)
|
||||
if (args.upload == 'opensearch'):
|
||||
push_report_to_opensearch(reports, args.esdocument)
|
||||
else:
|
||||
push_report_to_null(reports)
|
||||
|
||||
if __name__ == '__main__':
|
||||
sys.exit(main())
|
||||
@@ -5,25 +5,31 @@
|
||||
|
||||
BAD=""
|
||||
|
||||
git grep 'namespace rocksdb' -- '*.[ch]*'
|
||||
git grep -n 'namespace rocksdb' -- '*.[ch]*'
|
||||
if [ "$?" != "1" ]; then
|
||||
echo "^^^^^ Do not hardcode namespace rocksdb. Use ROCKSDB_NAMESPACE"
|
||||
BAD=1
|
||||
fi
|
||||
|
||||
git grep -i 'nocommit' -- ':!build_tools/check-sources.sh'
|
||||
git grep -n -i 'nocommit' -- ':!build_tools/check-sources.sh'
|
||||
if [ "$?" != "1" ]; then
|
||||
echo "^^^^^ Code was not intended to be committed"
|
||||
BAD=1
|
||||
fi
|
||||
|
||||
git grep '<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 'using namespace' -- ':!build_tools' ':!docs' \
|
||||
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'
|
||||
if [ "$?" != "1" ]; then
|
||||
@@ -31,6 +37,12 @@ if [ "$?" != "1" ]; then
|
||||
BAD=1
|
||||
fi
|
||||
|
||||
git grep -n -P "[\x80-\xFF]" -- ':!docs' ':!*.md'
|
||||
if [ "$?" != "1" ]; then
|
||||
echo '^^^^ Use only ASCII characters in source files'
|
||||
BAD=1
|
||||
fi
|
||||
|
||||
if [ "$BAD" ]; then
|
||||
exit 1
|
||||
fi
|
||||
@@ -6,10 +6,10 @@ 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/d9aef9feb850b168a68736420f217b01cce11a89/master/platform009/c305944
|
||||
JEMALLOC_BASE=/mnt/gvfs/third-party2/jemalloc/b62912d333ef33f9760efa6219dbe3fe6abb3b0e/master/platform009/c305944
|
||||
NUMA_BASE=/mnt/gvfs/third-party2/numa/0af65f71e23a67bf65dc91b11f95caa39325c432/2.0.11/platform009/7f3b187
|
||||
LIBUNWIND_BASE=/mnt/gvfs/third-party2/libunwind/02486dac347645d31dce116f44e1de3177315be2/1.4/platform009/5191652
|
||||
TBB_BASE=/mnt/gvfs/third-party2/tbb/2e0ec671e550bfca347300bf3f789d9c0fff24ad/2018_U5/platform009/7f3b187
|
||||
|
||||
Vendored
+50
-2
@@ -34,13 +34,61 @@ static std::unordered_map<std::string, OptionTypeInfo>
|
||||
OptionType::kDouble, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kMutable}},
|
||||
};
|
||||
|
||||
static std::unordered_map<std::string, OptionTypeInfo>
|
||||
comp_sec_cache_options_type_info = {
|
||||
{"capacity",
|
||||
{offsetof(struct CompressedSecondaryCacheOptions, capacity),
|
||||
OptionType::kSizeT, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kMutable}},
|
||||
{"num_shard_bits",
|
||||
{offsetof(struct CompressedSecondaryCacheOptions, num_shard_bits),
|
||||
OptionType::kInt, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kMutable}},
|
||||
{"compression_type",
|
||||
{offsetof(struct CompressedSecondaryCacheOptions, compression_type),
|
||||
OptionType::kCompressionType, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kMutable}},
|
||||
{"compress_format_version",
|
||||
{offsetof(struct CompressedSecondaryCacheOptions,
|
||||
compress_format_version),
|
||||
OptionType::kUInt32T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kMutable}},
|
||||
};
|
||||
#endif // ROCKSDB_LITE
|
||||
|
||||
Status SecondaryCache::CreateFromString(
|
||||
const ConfigOptions& config_options, const std::string& value,
|
||||
std::shared_ptr<SecondaryCache>* result) {
|
||||
return LoadSharedObject<SecondaryCache>(config_options, value, nullptr,
|
||||
result);
|
||||
if (value.find("compressed_secondary_cache://") == 0) {
|
||||
std::string args = value;
|
||||
args.erase(0, std::strlen("compressed_secondary_cache://"));
|
||||
Status status;
|
||||
std::shared_ptr<SecondaryCache> sec_cache;
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
CompressedSecondaryCacheOptions sec_cache_opts;
|
||||
status = OptionTypeInfo::ParseStruct(config_options, "",
|
||||
&comp_sec_cache_options_type_info, "",
|
||||
args, &sec_cache_opts);
|
||||
if (status.ok()) {
|
||||
sec_cache = NewCompressedSecondaryCache(sec_cache_opts);
|
||||
}
|
||||
|
||||
#else
|
||||
(void)config_options;
|
||||
status = Status::NotSupported(
|
||||
"Cannot load compressed secondary cache in LITE mode ", args);
|
||||
#endif //! ROCKSDB_LITE
|
||||
|
||||
if (status.ok()) {
|
||||
result->swap(sec_cache);
|
||||
}
|
||||
return status;
|
||||
} else {
|
||||
return LoadSharedObject<SecondaryCache>(config_options, value, nullptr,
|
||||
result);
|
||||
}
|
||||
}
|
||||
|
||||
Status Cache::CreateFromString(const ConfigOptions& config_options,
|
||||
|
||||
Vendored
+36
-11
@@ -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,8 @@
|
||||
#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"
|
||||
#include "port/port.h"
|
||||
@@ -25,6 +28,7 @@
|
||||
#include "table/block_based/block_based_table_reader.h"
|
||||
#include "table/block_based/cachable_entry.h"
|
||||
#include "util/coding.h"
|
||||
#include "util/distributed_mutex.h"
|
||||
#include "util/gflags_compat.h"
|
||||
#include "util/hash.h"
|
||||
#include "util/mutexlock.h"
|
||||
@@ -76,7 +80,7 @@ DEFINE_string(secondary_cache_uri, "",
|
||||
static class std::shared_ptr<ROCKSDB_NAMESPACE::SecondaryCache> secondary_cache;
|
||||
#endif // ROCKSDB_LITE
|
||||
|
||||
DEFINE_bool(use_clock_cache, false, "");
|
||||
DEFINE_string(cache_type, "lru_cache", "Type of block cache.");
|
||||
|
||||
// ## BEGIN stress_cache_key sub-tool options ##
|
||||
// See class StressCacheKey below.
|
||||
@@ -212,7 +216,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);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -279,13 +284,19 @@ class CacheBench {
|
||||
if (max_key > (static_cast<uint64_t>(1) << max_log_)) max_log_++;
|
||||
}
|
||||
|
||||
if (FLAGS_use_clock_cache) {
|
||||
cache_ = NewClockCache(FLAGS_cache_size, FLAGS_num_shard_bits);
|
||||
if (FLAGS_cache_type == "clock_cache") {
|
||||
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);
|
||||
}
|
||||
} else {
|
||||
} else if (FLAGS_cache_type == "fast_lru_cache") {
|
||||
cache_ = NewFastLRUCache(
|
||||
FLAGS_cache_size, FLAGS_value_bytes, FLAGS_num_shard_bits,
|
||||
false /*strict_capacity_limit*/, kDefaultCacheMetadataChargePolicy);
|
||||
} else if (FLAGS_cache_type == "lru_cache") {
|
||||
LRUCacheOptions opts(FLAGS_cache_size, FLAGS_num_shard_bits, false, 0.5);
|
||||
#ifndef ROCKSDB_LITE
|
||||
if (!FLAGS_secondary_cache_uri.empty()) {
|
||||
@@ -303,6 +314,9 @@ class CacheBench {
|
||||
#endif // ROCKSDB_LITE
|
||||
|
||||
cache_ = NewLRUCache(opts);
|
||||
} else {
|
||||
fprintf(stderr, "Cache type not supported.");
|
||||
exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -312,8 +326,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());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -533,8 +548,9 @@ class CacheBench {
|
||||
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) {
|
||||
@@ -542,8 +558,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);
|
||||
@@ -579,7 +596,15 @@ class CacheBench {
|
||||
}
|
||||
|
||||
void PrintEnv() const {
|
||||
#if defined(__GNUC__) && !defined(__OPTIMIZE__)
|
||||
printf(
|
||||
"WARNING: Optimization is disabled: benchmarks unnecessarily slow\n");
|
||||
#endif
|
||||
#ifndef NDEBUG
|
||||
printf("WARNING: Assertions are enabled; benchmarks unnecessarily slow\n");
|
||||
#endif
|
||||
printf("RocksDB version : %d.%d\n", kMajorVersion, kMinorVersion);
|
||||
printf("DMutex impl name : %s\n", DMutex::kName());
|
||||
printf("Number of threads : %u\n", FLAGS_threads);
|
||||
printf("Ops per thread : %" PRIu64 "\n", FLAGS_ops_per_thread);
|
||||
printf("Cache size : %s\n",
|
||||
|
||||
Vendored
+2
@@ -22,6 +22,7 @@ std::array<std::string, kNumCacheEntryRoles> kCacheEntryRoleToCamelString{{
|
||||
"CompressionDictionaryBuildingBuffer",
|
||||
"FilterConstruction",
|
||||
"BlockBasedTableReader",
|
||||
"FileMetadata",
|
||||
"Misc",
|
||||
}};
|
||||
|
||||
@@ -36,6 +37,7 @@ std::array<std::string, kNumCacheEntryRoles> kCacheEntryRoleToHyphenString{{
|
||||
"compression-dictionary-building-buffer",
|
||||
"filter-construction",
|
||||
"block-based-table-reader",
|
||||
"file-metadata",
|
||||
"misc",
|
||||
}};
|
||||
|
||||
|
||||
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
|
||||
|
||||
Vendored
+1
@@ -180,4 +180,5 @@ template class CacheReservationManagerImpl<
|
||||
template class CacheReservationManagerImpl<CacheEntryRole::kFilterConstruction>;
|
||||
template class CacheReservationManagerImpl<CacheEntryRole::kMisc>;
|
||||
template class CacheReservationManagerImpl<CacheEntryRole::kWriteBuffer>;
|
||||
template class CacheReservationManagerImpl<CacheEntryRole::kFileMetadata>;
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
Vendored
+28
@@ -36,6 +36,12 @@ class CacheReservationManager {
|
||||
};
|
||||
virtual ~CacheReservationManager() {}
|
||||
virtual Status UpdateCacheReservation(std::size_t new_memory_used) = 0;
|
||||
// TODO(hx235): replace the usage of
|
||||
// `UpdateCacheReservation(memory_used_delta, increase)` with
|
||||
// `UpdateCacheReservation(new_memory_used)` so that we only have one
|
||||
// `UpdateCacheReservation` function
|
||||
virtual Status UpdateCacheReservation(std::size_t memory_used_delta,
|
||||
bool increase) = 0;
|
||||
virtual Status MakeCacheReservation(
|
||||
std::size_t incremental_memory_used,
|
||||
std::unique_ptr<CacheReservationManager::CacheReservationHandle>
|
||||
@@ -128,6 +134,11 @@ class CacheReservationManagerImpl
|
||||
// On keeping dummy entries the same, it always returns Status::OK().
|
||||
Status UpdateCacheReservation(std::size_t new_memory_used) override;
|
||||
|
||||
Status UpdateCacheReservation(std::size_t /* memory_used_delta */,
|
||||
bool /* increase */) override {
|
||||
return Status::NotSupported();
|
||||
}
|
||||
|
||||
// One of the two ways of reserving cache space and releasing is done through
|
||||
// destruction of CacheReservationHandle.
|
||||
// See UpdateCacheReservation() for the other way.
|
||||
@@ -254,6 +265,23 @@ class ConcurrentCacheReservationManager
|
||||
std::lock_guard<std::mutex> lock(cache_res_mgr_mu_);
|
||||
return cache_res_mgr_->UpdateCacheReservation(new_memory_used);
|
||||
}
|
||||
|
||||
inline Status UpdateCacheReservation(std::size_t memory_used_delta,
|
||||
bool increase) override {
|
||||
std::lock_guard<std::mutex> lock(cache_res_mgr_mu_);
|
||||
std::size_t total_mem_used = cache_res_mgr_->GetTotalMemoryUsed();
|
||||
Status s;
|
||||
if (!increase) {
|
||||
assert(total_mem_used >= memory_used_delta);
|
||||
s = cache_res_mgr_->UpdateCacheReservation(total_mem_used -
|
||||
memory_used_delta);
|
||||
} else {
|
||||
s = cache_res_mgr_->UpdateCacheReservation(total_mem_used +
|
||||
memory_used_delta);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
inline Status MakeCacheReservation(
|
||||
std::size_t incremental_memory_used,
|
||||
std::unique_ptr<CacheReservationManager::CacheReservationHandle> *handle)
|
||||
|
||||
+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
+157
-49
@@ -18,24 +18,43 @@
|
||||
#include "cache/clock_cache.h"
|
||||
#include "cache/fast_lru_cache.h"
|
||||
#include "cache/lru_cache.h"
|
||||
#include "port/stack_trace.h"
|
||||
#include "test_util/testharness.h"
|
||||
#include "util/coding.h"
|
||||
#include "util/string_util.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
namespace {
|
||||
|
||||
// Conversions between numeric keys/values and the types expected by Cache.
|
||||
static std::string EncodeKey(int k) {
|
||||
std::string EncodeKey16Bytes(int k) {
|
||||
std::string result;
|
||||
PutFixed32(&result, k);
|
||||
result.append(std::string(12, 'a')); // Because we need a 16B output, we
|
||||
// add a 12-byte padding.
|
||||
return result;
|
||||
}
|
||||
|
||||
int DecodeKey16Bytes(const Slice& k) {
|
||||
assert(k.size() == 16);
|
||||
return DecodeFixed32(k.data()); // Decodes only the first 4 bytes of k.
|
||||
}
|
||||
|
||||
std::string EncodeKey32Bits(int k) {
|
||||
std::string result;
|
||||
PutFixed32(&result, k);
|
||||
return result;
|
||||
}
|
||||
static int DecodeKey(const Slice& k) {
|
||||
|
||||
int DecodeKey32Bits(const Slice& k) {
|
||||
assert(k.size() == 4);
|
||||
return DecodeFixed32(k.data());
|
||||
}
|
||||
static void* EncodeValue(uintptr_t v) { return reinterpret_cast<void*>(v); }
|
||||
static int DecodeValue(void* v) {
|
||||
|
||||
void* EncodeValue(uintptr_t v) { return reinterpret_cast<void*>(v); }
|
||||
|
||||
int DecodeValue(void* v) {
|
||||
return static_cast<int>(reinterpret_cast<uintptr_t>(v));
|
||||
}
|
||||
|
||||
@@ -50,12 +69,19 @@ void eraseDeleter(const Slice& /*key*/, void* value) {
|
||||
cache->Erase("foo");
|
||||
}
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
class CacheTest : public testing::TestWithParam<std::string> {
|
||||
public:
|
||||
static CacheTest* current_;
|
||||
static std::string type_;
|
||||
|
||||
static void Deleter(const Slice& key, void* v) {
|
||||
current_->deleted_keys_.push_back(DecodeKey(key));
|
||||
if (type_ == kFast || type_ == kClock) {
|
||||
current_->deleted_keys_.push_back(DecodeKey16Bytes(key));
|
||||
} else {
|
||||
current_->deleted_keys_.push_back(DecodeKey32Bits(key));
|
||||
}
|
||||
current_->deleted_values_.push_back(DecodeValue(v));
|
||||
}
|
||||
|
||||
@@ -74,6 +100,7 @@ class CacheTest : public testing::TestWithParam<std::string> {
|
||||
: cache_(NewCache(kCacheSize, kNumShardBits, false)),
|
||||
cache2_(NewCache(kCacheSize2, kNumShardBits2, false)) {
|
||||
current_ = this;
|
||||
type_ = GetParam();
|
||||
}
|
||||
|
||||
~CacheTest() override {}
|
||||
@@ -84,10 +111,14 @@ 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(capacity);
|
||||
return NewFastLRUCache(
|
||||
capacity, 1 /*estimated_value_size*/, -1 /*num_shard_bits*/,
|
||||
false /*strict_capacity_limit*/, kDefaultCacheMetadataChargePolicy);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
@@ -106,16 +137,41 @@ 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, num_shard_bits, strict_capacity_limit,
|
||||
return NewFastLRUCache(capacity, 1 /*estimated_value_size*/,
|
||||
num_shard_bits, strict_capacity_limit,
|
||||
charge_policy);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// These functions encode/decode keys in tests cases that use
|
||||
// int keys.
|
||||
// Currently, FastLRUCache requires keys to be 16B long, whereas
|
||||
// LRUCache and ClockCache don't, so the encoding depends on
|
||||
// the cache type.
|
||||
std::string EncodeKey(int k) {
|
||||
auto type = GetParam();
|
||||
if (type == kFast || type == kClock) {
|
||||
return EncodeKey16Bytes(k);
|
||||
} else {
|
||||
return EncodeKey32Bits(k);
|
||||
}
|
||||
}
|
||||
|
||||
int DecodeKey(const Slice& k) {
|
||||
auto type = GetParam();
|
||||
if (type == kFast || type == kClock) {
|
||||
return DecodeKey16Bytes(k);
|
||||
} else {
|
||||
return DecodeKey32Bits(k);
|
||||
}
|
||||
}
|
||||
|
||||
int Lookup(std::shared_ptr<Cache> cache, int key) {
|
||||
Cache::Handle* handle = cache->Lookup(EncodeKey(key));
|
||||
const int r = (handle == nullptr) ? -1 : DecodeValue(cache->Value(handle));
|
||||
@@ -159,11 +215,19 @@ class CacheTest : public testing::TestWithParam<std::string> {
|
||||
Erase(cache2_, key);
|
||||
}
|
||||
};
|
||||
|
||||
CacheTest* CacheTest::current_;
|
||||
std::string CacheTest::type_;
|
||||
|
||||
class LRUCacheTest : public CacheTest {};
|
||||
|
||||
TEST_P(CacheTest, UsageTest) {
|
||||
auto type = GetParam();
|
||||
if (type == kFast || type == kClock) {
|
||||
ROCKSDB_GTEST_BYPASS("FastLRUCache and ClockCache require 16-byte keys.");
|
||||
return;
|
||||
}
|
||||
|
||||
// cache is std::shared_ptr and will be automatically cleaned up.
|
||||
const uint64_t kCapacity = 100000;
|
||||
auto cache = NewCache(kCapacity, 8, false, kDontChargeCacheMetadata);
|
||||
@@ -208,6 +272,12 @@ TEST_P(CacheTest, UsageTest) {
|
||||
}
|
||||
|
||||
TEST_P(CacheTest, PinnedUsageTest) {
|
||||
auto type = GetParam();
|
||||
if (type == kFast || type == kClock) {
|
||||
ROCKSDB_GTEST_BYPASS("FastLRUCache and ClockCache require 16-byte keys.");
|
||||
return;
|
||||
}
|
||||
|
||||
// cache is std::shared_ptr and will be automatically cleaned up.
|
||||
const uint64_t kCapacity = 200000;
|
||||
auto cache = NewCache(kCapacity, 8, false, kDontChargeCacheMetadata);
|
||||
@@ -429,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);
|
||||
}
|
||||
|
||||
@@ -460,12 +532,24 @@ TEST_P(CacheTest, EvictionPolicyRef) {
|
||||
}
|
||||
|
||||
TEST_P(CacheTest, EvictEmptyCache) {
|
||||
auto type = GetParam();
|
||||
if (type == kFast || type == kClock) {
|
||||
ROCKSDB_GTEST_BYPASS("FastLRUCache and ClockCache require 16-byte keys.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Insert item large than capacity to trigger eviction on empty cache.
|
||||
auto cache = NewCache(1, 0, false);
|
||||
ASSERT_OK(cache->Insert("foo", nullptr, 10, dumbDeleter));
|
||||
}
|
||||
|
||||
TEST_P(CacheTest, EraseFromDeleter) {
|
||||
auto type = GetParam();
|
||||
if (type == kFast || type == kClock) {
|
||||
ROCKSDB_GTEST_BYPASS("FastLRUCache and ClockCache require 16-byte keys.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Have deleter which will erase item from cache, which will re-enter
|
||||
// the cache at that point.
|
||||
std::shared_ptr<Cache> cache = NewCache(10, 0, false);
|
||||
@@ -534,9 +618,9 @@ TEST_P(CacheTest, NewId) {
|
||||
|
||||
class Value {
|
||||
public:
|
||||
explicit Value(size_t v) : v_(v) { }
|
||||
explicit Value(int v) : v_(v) {}
|
||||
|
||||
size_t v_;
|
||||
int v_;
|
||||
};
|
||||
|
||||
namespace {
|
||||
@@ -577,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
|
||||
@@ -584,8 +674,8 @@ TEST_P(CacheTest, SetCapacity) {
|
||||
std::shared_ptr<Cache> cache = NewCache(5, 0, false);
|
||||
std::vector<Cache::Handle*> handles(10);
|
||||
// Insert 5 entries, but not releasing.
|
||||
for (size_t i = 0; i < 5; i++) {
|
||||
std::string key = std::to_string(i + 1);
|
||||
for (int i = 0; i < 5; i++) {
|
||||
std::string key = EncodeKey(i + 1);
|
||||
Status s = cache->Insert(key, new Value(i + 1), 1, &deleter, &handles[i]);
|
||||
ASSERT_TRUE(s.ok());
|
||||
}
|
||||
@@ -599,14 +689,14 @@ TEST_P(CacheTest, SetCapacity) {
|
||||
// insert 5 more elements to cache, then release 5,
|
||||
// then decrease capacity to 7, final capacity should be 7
|
||||
// and usage should be 7
|
||||
for (size_t i = 5; i < 10; i++) {
|
||||
std::string key = std::to_string(i + 1);
|
||||
for (int i = 5; i < 10; i++) {
|
||||
std::string key = EncodeKey(i + 1);
|
||||
Status s = cache->Insert(key, new Value(i + 1), 1, &deleter, &handles[i]);
|
||||
ASSERT_TRUE(s.ok());
|
||||
}
|
||||
ASSERT_EQ(10U, cache->GetCapacity());
|
||||
ASSERT_EQ(10U, cache->GetUsage());
|
||||
for (size_t i = 0; i < 5; i++) {
|
||||
for (int i = 0; i < 5; i++) {
|
||||
cache->Release(handles[i]);
|
||||
}
|
||||
ASSERT_EQ(10U, cache->GetCapacity());
|
||||
@@ -616,7 +706,7 @@ TEST_P(CacheTest, SetCapacity) {
|
||||
ASSERT_EQ(7, cache->GetUsage());
|
||||
|
||||
// release remaining 5 to keep valgrind happy
|
||||
for (size_t i = 5; i < 10; i++) {
|
||||
for (int i = 5; i < 10; i++) {
|
||||
cache->Release(handles[i]);
|
||||
}
|
||||
|
||||
@@ -625,13 +715,21 @@ 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);
|
||||
std::vector<Cache::Handle*> handles(10);
|
||||
Status s;
|
||||
for (size_t i = 0; i < 10; i++) {
|
||||
std::string key = std::to_string(i + 1);
|
||||
for (int i = 0; i < 10; i++) {
|
||||
std::string key = EncodeKey(i + 1);
|
||||
s = cache->Insert(key, new Value(i + 1), 1, &deleter, &handles[i]);
|
||||
ASSERT_OK(s);
|
||||
ASSERT_NE(nullptr, handles[i]);
|
||||
@@ -639,29 +737,29 @@ TEST_P(LRUCacheTest, SetStrictCapacityLimit) {
|
||||
ASSERT_EQ(10, cache->GetUsage());
|
||||
|
||||
// test2: set the flag to true. Insert and check if it fails.
|
||||
std::string extra_key = "extra";
|
||||
std::string extra_key = EncodeKey(100);
|
||||
Value* extra_value = new Value(0);
|
||||
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());
|
||||
|
||||
for (size_t i = 0; i < 10; i++) {
|
||||
for (int i = 0; i < 10; i++) {
|
||||
cache->Release(handles[i]);
|
||||
}
|
||||
|
||||
// test3: init with flag being true.
|
||||
std::shared_ptr<Cache> cache2 = NewCache(5, 0, true);
|
||||
for (size_t i = 0; i < 5; i++) {
|
||||
std::string key = std::to_string(i + 1);
|
||||
for (int i = 0; i < 5; i++) {
|
||||
std::string key = EncodeKey(i + 1);
|
||||
s = cache2->Insert(key, new Value(i + 1), 1, &deleter, &handles[i]);
|
||||
ASSERT_OK(s);
|
||||
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);
|
||||
@@ -670,12 +768,18 @@ TEST_P(LRUCacheTest, SetStrictCapacityLimit) {
|
||||
ASSERT_EQ(5, cache2->GetUsage());
|
||||
ASSERT_EQ(nullptr, cache2->Lookup(extra_key));
|
||||
|
||||
for (size_t i = 0; i < 5; i++) {
|
||||
for (int i = 0; i < 5; i++) {
|
||||
cache2->Release(handles[i]);
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
@@ -684,15 +788,15 @@ TEST_P(CacheTest, OverCapacity) {
|
||||
std::vector<Cache::Handle*> handles(n+1);
|
||||
|
||||
// Insert n+1 entries, but not releasing.
|
||||
for (size_t i = 0; i < n + 1; i++) {
|
||||
std::string key = std::to_string(i + 1);
|
||||
for (int i = 0; i < static_cast<int>(n + 1); i++) {
|
||||
std::string key = EncodeKey(i + 1);
|
||||
Status s = cache->Insert(key, new Value(i + 1), 1, &deleter, &handles[i]);
|
||||
ASSERT_TRUE(s.ok());
|
||||
}
|
||||
|
||||
// Guess what's in the cache now?
|
||||
for (size_t i = 0; i < n + 1; i++) {
|
||||
std::string key = std::to_string(i + 1);
|
||||
for (int i = 0; i < static_cast<int>(n + 1); i++) {
|
||||
std::string key = EncodeKey(i + 1);
|
||||
auto h = cache->Lookup(key);
|
||||
ASSERT_TRUE(h != nullptr);
|
||||
if (h) cache->Release(h);
|
||||
@@ -700,7 +804,7 @@ TEST_P(CacheTest, OverCapacity) {
|
||||
|
||||
// the cache is over capacity since nothing could be evicted
|
||||
ASSERT_EQ(n + 1U, cache->GetUsage());
|
||||
for (size_t i = 0; i < n + 1; i++) {
|
||||
for (int i = 0; i < static_cast<int>(n + 1); i++) {
|
||||
cache->Release(handles[i]);
|
||||
}
|
||||
// Make sure eviction is triggered.
|
||||
@@ -712,14 +816,14 @@ TEST_P(CacheTest, OverCapacity) {
|
||||
// element 0 is evicted and the rest is there
|
||||
// This is consistent with the LRU policy since the element 0
|
||||
// was released first
|
||||
for (size_t i = 0; i < n + 1; i++) {
|
||||
std::string key = std::to_string(i + 1);
|
||||
for (int i = 0; i < static_cast<int>(n + 1); i++) {
|
||||
std::string key = EncodeKey(i + 1);
|
||||
auto h = cache->Lookup(key);
|
||||
if (h) {
|
||||
ASSERT_NE(i, 0U);
|
||||
ASSERT_NE(static_cast<size_t>(i), 0U);
|
||||
cache->Release(h);
|
||||
} else {
|
||||
ASSERT_EQ(i, 0U);
|
||||
ASSERT_EQ(static_cast<size_t>(i), 0U);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -745,7 +849,7 @@ TEST_P(CacheTest, ApplyToAllCacheEntriesTest) {
|
||||
std::sort(inserted.begin(), inserted.end());
|
||||
std::sort(legacy_callback_state.begin(), legacy_callback_state.end());
|
||||
ASSERT_EQ(inserted.size(), legacy_callback_state.size());
|
||||
for (size_t i = 0; i < inserted.size(); ++i) {
|
||||
for (int i = 0; i < static_cast<int>(inserted.size()); ++i) {
|
||||
EXPECT_EQ(inserted[i], legacy_callback_state[i]);
|
||||
}
|
||||
}
|
||||
@@ -773,7 +877,7 @@ TEST_P(CacheTest, ApplyToAllEntriesTest) {
|
||||
std::sort(inserted.begin(), inserted.end());
|
||||
std::sort(callback_state.begin(), callback_state.end());
|
||||
ASSERT_EQ(inserted.size(), callback_state.size());
|
||||
for (size_t i = 0; i < inserted.size(); ++i) {
|
||||
for (int i = 0; i < static_cast<int>(inserted.size()); ++i) {
|
||||
EXPECT_EQ(inserted[i], callback_state[i]);
|
||||
}
|
||||
}
|
||||
@@ -788,8 +892,10 @@ TEST_P(CacheTest, ApplyToAllEntriesDuringResize) {
|
||||
constexpr int kSpecialCharge = 2;
|
||||
constexpr int kNotSpecialCharge = 1;
|
||||
constexpr int kSpecialCount = 100;
|
||||
size_t expected_usage = 0;
|
||||
for (int i = 0; i < kSpecialCount; ++i) {
|
||||
Insert(i, i * 2, kSpecialCharge);
|
||||
expected_usage += kSpecialCharge;
|
||||
}
|
||||
|
||||
// For callback
|
||||
@@ -810,12 +916,17 @@ TEST_P(CacheTest, ApplyToAllEntriesDuringResize) {
|
||||
});
|
||||
|
||||
// In parallel, add more entries, enough to cause resize but not enough
|
||||
// to cause ejections
|
||||
for (int i = kSpecialCount * 1; i < kSpecialCount * 6; ++i) {
|
||||
// to cause ejections. (Note: if any cache shard is over capacity, there
|
||||
// will be ejections)
|
||||
for (int i = kSpecialCount * 1; i < kSpecialCount * 5; ++i) {
|
||||
Insert(i, i * 2, kNotSpecialCharge);
|
||||
expected_usage += kNotSpecialCharge;
|
||||
}
|
||||
|
||||
apply_thread.join();
|
||||
// verify no evictions
|
||||
ASSERT_EQ(cache_->GetUsage(), expected_usage);
|
||||
// verify everything seen in ApplyToAllEntries
|
||||
ASSERT_EQ(special_count, kSpecialCount);
|
||||
}
|
||||
|
||||
@@ -844,21 +955,18 @@ 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));
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
ROCKSDB_NAMESPACE::port::InstallStackTraceHandler();
|
||||
::testing::InitGoogleTest(&argc, argv);
|
||||
return RUN_ALL_TESTS();
|
||||
}
|
||||
Vendored
+517
-748
File diff suppressed because it is too large.
Load diff
Vendored
+469
-3
@@ -9,8 +9,474 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "rocksdb/cache.h"
|
||||
#include <array>
|
||||
#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 {
|
||||
|
||||
// Clock cache implementation. This is based on FastLRUCache's open-addressed
|
||||
// hash table. Importantly, it stores elements in an array, and resolves
|
||||
// collision using a probing strategy. Visibility and referenceability of
|
||||
// elements works as usual. See fast_lru_cache.h for a detailed description.
|
||||
//
|
||||
// The main difference with FastLRUCache is, not surprisingly, the eviction
|
||||
// algorithm
|
||||
// ---instead of an LRU list, we maintain a circular list with the elements
|
||||
// available for eviction, which the clock algorithm traverses to pick the next
|
||||
// victim. The clock list is represented using the array of handles, and we
|
||||
// simply mark those elements that are present in the list. This is done using
|
||||
// different clock flags, namely NONE, LOW, MEDIUM, HIGH, that represent
|
||||
// priorities: NONE means that the element is not part of the clock list, and
|
||||
// LOW to HIGH represent how close an element is from being evictable (LOW being
|
||||
// immediately evictable). When the clock pointer steps on an element that is
|
||||
// not immediately evictable, it decreases its priority.
|
||||
|
||||
constexpr double kLoadFactor = 0.35; // See fast_lru_cache.h.
|
||||
|
||||
constexpr double kStrictLoadFactor = 0.7; // See fast_lru_cache.h.
|
||||
|
||||
// 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; // TODO(opt): Only allow uint32_t?
|
||||
// The number of external refs to this entry.
|
||||
uint32_t refs;
|
||||
|
||||
static constexpr int kIsVisibleOffset = 0;
|
||||
static constexpr int kIsElementOffset = 1;
|
||||
static constexpr int kClockPriorityOffset = 2;
|
||||
static constexpr int kIsHitOffset = 4;
|
||||
static constexpr int kCachePriorityOffset = 5;
|
||||
|
||||
enum Flags : uint8_t {
|
||||
// Whether the handle is visible to Lookups.
|
||||
IS_VISIBLE = (1 << kIsVisibleOffset),
|
||||
// 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),
|
||||
CACHE_PRIORITY = (1 << kCachePriorityOffset),
|
||||
};
|
||||
uint8_t flags;
|
||||
|
||||
enum ClockPriority : uint8_t {
|
||||
NONE = (0 << kClockPriorityOffset), // Not an element in the eyes of clock.
|
||||
LOW = (1 << kClockPriorityOffset), // Immediately evictable.
|
||||
MEDIUM = (2 << kClockPriorityOffset),
|
||||
HIGH = (3 << kClockPriorityOffset)
|
||||
// Priority is NONE if and only if
|
||||
// (i) the handle is not an element, or
|
||||
// (ii) the handle is an element but it is being referenced.
|
||||
};
|
||||
|
||||
// The number of elements that hash to this slot or a lower one,
|
||||
// but wind up in a higher slot.
|
||||
uint32_t displacements;
|
||||
|
||||
std::array<char, kCacheKeySize> key_data;
|
||||
|
||||
ClockHandle() {
|
||||
value = nullptr;
|
||||
deleter = nullptr;
|
||||
hash = 0;
|
||||
total_charge = 0;
|
||||
refs = 0;
|
||||
flags = 0;
|
||||
SetIsVisible(false);
|
||||
SetIsElement(false);
|
||||
SetClockPriority(ClockPriority::NONE);
|
||||
SetCachePriority(Cache::Priority::LOW);
|
||||
displacements = 0;
|
||||
key_data.fill(0);
|
||||
}
|
||||
|
||||
Slice key() const { return Slice(key_data.data(), kCacheKeySize); }
|
||||
|
||||
// Increase the reference count by 1.
|
||||
void Ref() { refs++; }
|
||||
|
||||
// Just reduce the reference count by 1. Return true if it was last reference.
|
||||
bool Unref() {
|
||||
assert(refs > 0);
|
||||
refs--;
|
||||
return refs == 0;
|
||||
}
|
||||
|
||||
// Return true if there are external refs, false otherwise.
|
||||
bool HasRefs() const { return refs > 0; }
|
||||
|
||||
bool IsVisible() const { return flags & IS_VISIBLE; }
|
||||
|
||||
void SetIsVisible(bool is_visible) {
|
||||
if (is_visible) {
|
||||
flags |= IS_VISIBLE;
|
||||
} else {
|
||||
flags &= ~IS_VISIBLE;
|
||||
}
|
||||
}
|
||||
|
||||
bool IsElement() const { return flags & IS_ELEMENT; }
|
||||
|
||||
void SetIsElement(bool is_element) {
|
||||
if (is_element) {
|
||||
flags |= IS_ELEMENT;
|
||||
} else {
|
||||
flags &= ~IS_ELEMENT;
|
||||
}
|
||||
}
|
||||
|
||||
bool HasHit() const { return flags & HAS_HIT; }
|
||||
|
||||
void SetHit() { flags |= HAS_HIT; }
|
||||
|
||||
bool IsInClockList() 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 &= ~Flags::CACHE_PRIORITY;
|
||||
}
|
||||
}
|
||||
|
||||
ClockPriority GetClockPriority() const {
|
||||
return static_cast<ClockPriority>(flags & Flags::CLOCK_PRIORITY);
|
||||
}
|
||||
|
||||
void SetClockPriority(ClockPriority priority) {
|
||||
flags &= ~Flags::CLOCK_PRIORITY;
|
||||
flags |= priority;
|
||||
}
|
||||
|
||||
void DecreaseClockPriority() {
|
||||
uint8_t p = static_cast<uint8_t>(flags & Flags::CLOCK_PRIORITY) >>
|
||||
kClockPriorityOffset;
|
||||
assert(p > 0);
|
||||
p--;
|
||||
flags &= ~Flags::CLOCK_PRIORITY;
|
||||
ClockPriority new_priority =
|
||||
static_cast<ClockPriority>(p << kClockPriorityOffset);
|
||||
flags |= new_priority;
|
||||
}
|
||||
|
||||
void FreeData() {
|
||||
assert(refs == 0);
|
||||
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() {
|
||||
return !this->IsElement() && this->displacements == 0;
|
||||
}
|
||||
|
||||
inline bool IsTombstone() {
|
||||
return !this->IsElement() && this->displacements > 0;
|
||||
}
|
||||
|
||||
inline bool Matches(const Slice& some_key) {
|
||||
return this->IsElement() && this->key() == some_key;
|
||||
}
|
||||
}; // 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);
|
||||
|
||||
// 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.
|
||||
ClockHandle* Insert(ClockHandle* h, ClockHandle** old);
|
||||
|
||||
// Removes h from the hash table. The handle must already be off
|
||||
// the clock list.
|
||||
void Remove(ClockHandle* h);
|
||||
|
||||
// Turns a visible element h into a ghost (i.e., not visible).
|
||||
void Exclude(ClockHandle* h);
|
||||
|
||||
// Assigns a copy of h to the given slot.
|
||||
void Assign(int slot, ClockHandle* 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++) {
|
||||
ClockHandle* h = &array_[i];
|
||||
if (h->IsVisible()) {
|
||||
func(h);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 FindVisibleElement(const Slice& key, int& probe, int displacement);
|
||||
|
||||
int FindAvailableSlot(const Slice& key, int& probe, int displacement);
|
||||
|
||||
int FindVisibleElementOrAvailableSlot(const Slice& key, 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(ClockHandle*)> 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_;
|
||||
|
||||
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, insert into
|
||||
// the clock 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;
|
||||
|
||||
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;
|
||||
void ClockRemove(ClockHandle* e);
|
||||
void ClockInsert(ClockHandle* e);
|
||||
|
||||
// Free some space following strict clock policy until enough space
|
||||
// to hold (usage_ + charge) is freed or the clock list is empty
|
||||
// This function is not thread safe - it needs to be executed while
|
||||
// holding the mutex_.
|
||||
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_;
|
||||
|
||||
// Memory size for unpinned entries in the clock list.
|
||||
size_t clock_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;
|
||||
|
||||
+71
-31
@@ -10,6 +10,8 @@
|
||||
|
||||
#include "memory/jemalloc_nodump_allocator.h"
|
||||
#include "memory/memory_allocator.h"
|
||||
#include "rocksdb/convenience.h"
|
||||
#include "rocksdb/secondary_cache.h"
|
||||
#include "test_util/testharness.h"
|
||||
#include "test_util/testutil.h"
|
||||
#include "util/compression.h"
|
||||
@@ -79,37 +81,7 @@ class CompressedSecondaryCacheTest : public testing::Test {
|
||||
|
||||
void SetFailCreate(bool fail) { fail_create_ = fail; }
|
||||
|
||||
void BasicTest(bool sec_cache_is_compressed, bool use_jemalloc) {
|
||||
CompressedSecondaryCacheOptions opts;
|
||||
opts.capacity = 2048;
|
||||
opts.num_shard_bits = 0;
|
||||
opts.metadata_charge_policy = kDontChargeCacheMetadata;
|
||||
|
||||
if (sec_cache_is_compressed) {
|
||||
if (!LZ4_Supported()) {
|
||||
ROCKSDB_GTEST_SKIP("This test requires LZ4 support.");
|
||||
opts.compression_type = CompressionType::kNoCompression;
|
||||
}
|
||||
} else {
|
||||
opts.compression_type = CompressionType::kNoCompression;
|
||||
}
|
||||
|
||||
if (use_jemalloc) {
|
||||
JemallocAllocatorOptions jopts;
|
||||
std::shared_ptr<MemoryAllocator> allocator;
|
||||
std::string msg;
|
||||
if (JemallocNodumpAllocator::IsSupported(&msg)) {
|
||||
Status s = NewJemallocNodumpAllocator(jopts, &allocator);
|
||||
if (s.ok()) {
|
||||
opts.memory_allocator = allocator;
|
||||
}
|
||||
} else {
|
||||
ROCKSDB_GTEST_BYPASS("JEMALLOC not supported");
|
||||
}
|
||||
}
|
||||
std::shared_ptr<SecondaryCache> sec_cache =
|
||||
NewCompressedSecondaryCache(opts);
|
||||
|
||||
void BasicTestHelper(std::shared_ptr<SecondaryCache> sec_cache) {
|
||||
bool is_in_sec_cache{true};
|
||||
// Lookup an non-existent key.
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle0 =
|
||||
@@ -160,6 +132,38 @@ class CompressedSecondaryCacheTest : public testing::Test {
|
||||
sec_cache.reset();
|
||||
}
|
||||
|
||||
void BasicTest(bool sec_cache_is_compressed, bool use_jemalloc) {
|
||||
CompressedSecondaryCacheOptions opts;
|
||||
opts.capacity = 2048;
|
||||
opts.num_shard_bits = 0;
|
||||
opts.metadata_charge_policy = kDontChargeCacheMetadata;
|
||||
|
||||
if (sec_cache_is_compressed) {
|
||||
if (!LZ4_Supported()) {
|
||||
ROCKSDB_GTEST_SKIP("This test requires LZ4 support.");
|
||||
opts.compression_type = CompressionType::kNoCompression;
|
||||
}
|
||||
} else {
|
||||
opts.compression_type = CompressionType::kNoCompression;
|
||||
}
|
||||
|
||||
if (use_jemalloc) {
|
||||
JemallocAllocatorOptions jopts;
|
||||
std::shared_ptr<MemoryAllocator> allocator;
|
||||
std::string msg;
|
||||
if (JemallocNodumpAllocator::IsSupported(&msg)) {
|
||||
Status s = NewJemallocNodumpAllocator(jopts, &allocator);
|
||||
if (s.ok()) {
|
||||
opts.memory_allocator = allocator;
|
||||
}
|
||||
} else {
|
||||
ROCKSDB_GTEST_BYPASS("JEMALLOC not supported");
|
||||
}
|
||||
}
|
||||
std::shared_ptr<SecondaryCache> sec_cache =
|
||||
NewCompressedSecondaryCache(opts);
|
||||
}
|
||||
|
||||
void FailsTest(bool sec_cache_is_compressed) {
|
||||
CompressedSecondaryCacheOptions secondary_cache_opts;
|
||||
if (sec_cache_is_compressed) {
|
||||
@@ -547,6 +551,42 @@ TEST_F(CompressedSecondaryCacheTest,
|
||||
BasicTest(true, true);
|
||||
}
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
|
||||
TEST_F(CompressedSecondaryCacheTest, BasicTestFromStringWithNoCompression) {
|
||||
std::string sec_cache_uri =
|
||||
"compressed_secondary_cache://"
|
||||
"capacity=2048;num_shard_bits=0;compression_type=kNoCompression";
|
||||
std::shared_ptr<SecondaryCache> sec_cache;
|
||||
Status s = SecondaryCache::CreateFromString(ConfigOptions(), sec_cache_uri,
|
||||
&sec_cache);
|
||||
EXPECT_OK(s);
|
||||
BasicTestHelper(sec_cache);
|
||||
}
|
||||
|
||||
TEST_F(CompressedSecondaryCacheTest, BasicTestFromStringWithCompression) {
|
||||
std::string sec_cache_uri;
|
||||
if (LZ4_Supported()) {
|
||||
sec_cache_uri =
|
||||
"compressed_secondary_cache://"
|
||||
"capacity=2048;num_shard_bits=0;compression_type=kLZ4Compression;"
|
||||
"compress_format_version=2";
|
||||
} else {
|
||||
ROCKSDB_GTEST_SKIP("This test requires LZ4 support.");
|
||||
sec_cache_uri =
|
||||
"compressed_secondary_cache://"
|
||||
"capacity=2048;num_shard_bits=0;compression_type=kNoCompression";
|
||||
}
|
||||
|
||||
std::shared_ptr<SecondaryCache> sec_cache;
|
||||
Status s = SecondaryCache::CreateFromString(ConfigOptions(), sec_cache_uri,
|
||||
&sec_cache);
|
||||
EXPECT_OK(s);
|
||||
BasicTestHelper(sec_cache);
|
||||
}
|
||||
|
||||
#endif // ROCKSDB_LITE
|
||||
|
||||
TEST_F(CompressedSecondaryCacheTest, FailsTestWithNoCompression) {
|
||||
FailsTest(false);
|
||||
}
|
||||
|
||||
Vendored
+327
-229
@@ -12,110 +12,170 @@
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <functional>
|
||||
|
||||
#include "monitoring/perf_context_imp.h"
|
||||
#include "monitoring/statistics.h"
|
||||
#include "port/lang.h"
|
||||
#include "util/mutexlock.h"
|
||||
#include "util/distributed_mutex.h"
|
||||
#include "util/hash.h"
|
||||
#include "util/math.h"
|
||||
#include "util/random.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
namespace fast_lru_cache {
|
||||
|
||||
LRUHandleTable::LRUHandleTable(int max_upper_hash_bits)
|
||||
: length_bits_(/* historical starting size*/ 4),
|
||||
list_(new LRUHandle* [size_t{1} << length_bits_] {}),
|
||||
elems_(0),
|
||||
max_length_bits_(max_upper_hash_bits) {}
|
||||
LRUHandleTable::LRUHandleTable(int hash_bits)
|
||||
: length_bits_(hash_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];
|
||||
}
|
||||
|
||||
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;
|
||||
if (old == nullptr) {
|
||||
++elems_;
|
||||
if ((elems_ >> length_bits_) > 0) { // elems_ >= length
|
||||
// Since each cache entry is fairly large, we aim for a small
|
||||
// average linked list length (<= 1).
|
||||
Resize();
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
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;
|
||||
--elems_;
|
||||
}
|
||||
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;
|
||||
}
|
||||
return ptr;
|
||||
}
|
||||
|
||||
void LRUHandleTable::Resize() {
|
||||
if (length_bits_ >= max_length_bits_) {
|
||||
// Due to reaching limit of hash information, if we made the table bigger,
|
||||
// we would allocate more addresses but only the same number would be used.
|
||||
return;
|
||||
}
|
||||
if (length_bits_ >= 31) {
|
||||
// Avoid undefined behavior shifting uint32_t by 32.
|
||||
return;
|
||||
}
|
||||
|
||||
uint32_t old_length = uint32_t{1} << length_bits_;
|
||||
int new_length_bits = length_bits_ + 1;
|
||||
std::unique_ptr<LRUHandle* []> new_list {
|
||||
new LRUHandle* [size_t{1} << new_length_bits] {}
|
||||
};
|
||||
uint32_t count = 0;
|
||||
for (uint32_t i = 0; i < old_length; i++) {
|
||||
LRUHandle* h = list_[i];
|
||||
while (h != nullptr) {
|
||||
LRUHandle* next = h->next_hash;
|
||||
uint32_t hash = h->hash;
|
||||
LRUHandle** ptr = &new_list[hash >> (32 - new_length_bits)];
|
||||
h->next_hash = *ptr;
|
||||
*ptr = h;
|
||||
h = next;
|
||||
count++;
|
||||
// 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];
|
||||
}
|
||||
assert(elems_ == count);
|
||||
list_ = std::move(new_list);
|
||||
length_bits_ = new_length_bits;
|
||||
}
|
||||
|
||||
LRUCacheShard::LRUCacheShard(size_t capacity, bool strict_capacity_limit,
|
||||
CacheMetadataChargePolicy metadata_charge_policy,
|
||||
int max_upper_hash_bits)
|
||||
: capacity_(0),
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
LRUCacheShard::LRUCacheShard(size_t capacity, size_t estimated_value_size,
|
||||
bool strict_capacity_limit,
|
||||
CacheMetadataChargePolicy metadata_charge_policy)
|
||||
: capacity_(capacity),
|
||||
strict_capacity_limit_(strict_capacity_limit),
|
||||
table_(max_upper_hash_bits),
|
||||
table_(
|
||||
CalcHashBits(capacity, estimated_value_size, metadata_charge_policy)),
|
||||
usage_(0),
|
||||
lru_usage_(0) {
|
||||
set_metadata_charge_policy(metadata_charge_policy);
|
||||
@@ -123,30 +183,27 @@ LRUCacheShard::LRUCacheShard(size_t capacity, bool strict_capacity_limit,
|
||||
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;
|
||||
{
|
||||
MutexLock l(&mutex_);
|
||||
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);
|
||||
size_t total_charge = old->CalcTotalCharge(metadata_charge_policy_);
|
||||
assert(usage_ >= total_charge);
|
||||
usage_ -= total_charge;
|
||||
last_reference_list.push_back(old);
|
||||
table_.Remove(old);
|
||||
assert(usage_ >= old->total_charge);
|
||||
usage_ -= old->total_charge;
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,9 +214,9 @@ void LRUCacheShard::ApplyToSomeEntries(
|
||||
// The state is essentially going to be the starting hash, which works
|
||||
// nicely even if we resize between calls because we use upper-most
|
||||
// hash bits for table indexes.
|
||||
MutexLock l(&mutex_);
|
||||
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,
|
||||
@@ -177,156 +234,212 @@ void LRUCacheShard::ApplyToSomeEntries(
|
||||
}
|
||||
|
||||
table_.ApplyToEntriesRange(
|
||||
[callback](LRUHandle* h) {
|
||||
callback(h->key(), h->value, h->charge, h->deleter);
|
||||
[callback,
|
||||
metadata_charge_policy = metadata_charge_policy_](LRUHandle* h) {
|
||||
callback(h->key(), h->value, h->GetCharge(metadata_charge_policy),
|
||||
h->deleter);
|
||||
},
|
||||
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;
|
||||
size_t total_charge = e->CalcTotalCharge(metadata_charge_policy_);
|
||||
assert(lru_usage_ >= total_charge);
|
||||
lru_usage_ -= 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);
|
||||
size_t total_charge = e->CalcTotalCharge(metadata_charge_policy_);
|
||||
// Inset "e" to head of LRU list.
|
||||
e->next = &lru_;
|
||||
e->prev = lru_.prev;
|
||||
e->prev->next = e;
|
||||
e->next->prev = e;
|
||||
lru_usage_ += 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);
|
||||
size_t old_total_charge = old->CalcTotalCharge(metadata_charge_policy_);
|
||||
assert(usage_ >= old_total_charge);
|
||||
usage_ -= old_total_charge;
|
||||
deleted->push_back(old);
|
||||
table_.Remove(old);
|
||||
assert(usage_ >= old->total_charge);
|
||||
usage_ -= old->total_charge;
|
||||
deleted->push_back(*old);
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
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;
|
||||
{
|
||||
MutexLock l(&mutex_);
|
||||
DMutexLock l(mutex_);
|
||||
capacity_ = capacity;
|
||||
EvictFromLRU(0, &last_reference_list);
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
}
|
||||
|
||||
void LRUCacheShard::SetStrictCapacityLimit(bool strict_capacity_limit) {
|
||||
MutexLock l(&mutex_);
|
||||
DMutexLock l(mutex_);
|
||||
strict_capacity_limit_ = strict_capacity_limit;
|
||||
}
|
||||
|
||||
Status LRUCacheShard::InsertItem(LRUHandle* e, Cache::Handle** handle,
|
||||
bool free_handle_on_fail) {
|
||||
Status s = Status::OK();
|
||||
autovector<LRUHandle*> last_reference_list;
|
||||
size_t total_charge = e->CalcTotalCharge(metadata_charge_policy_);
|
||||
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;
|
||||
{
|
||||
MutexLock l(&mutex_);
|
||||
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(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_ + 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_ += 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);
|
||||
size_t old_total_charge =
|
||||
old->CalcTotalCharge(metadata_charge_policy_);
|
||||
assert(usage_ >= old_total_charge);
|
||||
usage_ -= old_total_charge;
|
||||
last_reference_list.push_back(old);
|
||||
table_.Remove(old);
|
||||
assert(usage_ >= old->total_charge);
|
||||
usage_ -= old->total_charge;
|
||||
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;
|
||||
{
|
||||
MutexLock 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);
|
||||
DMutexLock l(mutex_);
|
||||
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) {
|
||||
LRUHandle* e = reinterpret_cast<LRUHandle*>(h);
|
||||
MutexLock l(&mutex_);
|
||||
DMutexLock l(mutex_);
|
||||
// To create another reference - entry must be already externally referenced.
|
||||
assert(e->HasRefs());
|
||||
e->Ref();
|
||||
@@ -337,116 +450,94 @@ 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;
|
||||
{
|
||||
MutexLock l(&mutex_);
|
||||
last_reference = e->Unref();
|
||||
if (last_reference && e->InCache()) {
|
||||
DMutexLock l(mutex_);
|
||||
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) {
|
||||
size_t total_charge = e->CalcTotalCharge(metadata_charge_policy_);
|
||||
assert(usage_ >= total_charge);
|
||||
usage_ -= 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*/) {
|
||||
// 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->charge = charge;
|
||||
e->key_length = key.size();
|
||||
e->hash = hash;
|
||||
e->refs = 0;
|
||||
e->next = e->prev = nullptr;
|
||||
e->SetInCache(true);
|
||||
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;
|
||||
{
|
||||
MutexLock 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);
|
||||
size_t total_charge = e->CalcTotalCharge(metadata_charge_policy_);
|
||||
assert(usage_ >= total_charge);
|
||||
usage_ -= total_charge;
|
||||
DMutexLock l(mutex_);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
size_t LRUCacheShard::GetUsage() const {
|
||||
MutexLock l(&mutex_);
|
||||
DMutexLock l(mutex_);
|
||||
return usage_;
|
||||
}
|
||||
|
||||
size_t LRUCacheShard::GetPinnedUsage() const {
|
||||
MutexLock l(&mutex_);
|
||||
DMutexLock l(mutex_);
|
||||
assert(usage_ >= lru_usage_);
|
||||
return usage_ - lru_usage_;
|
||||
}
|
||||
|
||||
std::string LRUCacheShard::GetPrintableOptions() const { return std::string{}; }
|
||||
|
||||
LRUCache::LRUCache(size_t capacity, int num_shard_bits,
|
||||
bool strict_capacity_limit,
|
||||
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_));
|
||||
size_t per_shard = (capacity + (num_shards_ - 1)) / num_shards_;
|
||||
for (int i = 0; i < num_shards_; i++) {
|
||||
new (&shards_[i])
|
||||
LRUCacheShard(per_shard, strict_capacity_limit, metadata_charge_policy,
|
||||
/* max_upper_hash_bits */ 32 - num_shard_bits);
|
||||
LRUCacheShard(per_shard, estimated_value_size, strict_capacity_limit,
|
||||
metadata_charge_policy);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -473,7 +564,12 @@ void* LRUCache::Value(Handle* handle) {
|
||||
}
|
||||
|
||||
size_t LRUCache::GetCharge(Handle* handle) const {
|
||||
return reinterpret_cast<const LRUHandle*>(handle)->charge;
|
||||
CacheMetadataChargePolicy metadata_charge_policy = kDontChargeCacheMetadata;
|
||||
if (num_shards_ > 0) {
|
||||
metadata_charge_policy = shards_[0].metadata_charge_policy_;
|
||||
}
|
||||
return reinterpret_cast<const LRUHandle*>(handle)->GetCharge(
|
||||
metadata_charge_policy);
|
||||
}
|
||||
|
||||
Cache::DeleterFn LRUCache::GetDeleter(Handle* handle) const {
|
||||
@@ -496,7 +592,8 @@ void LRUCache::DisownData() {
|
||||
} // namespace fast_lru_cache
|
||||
|
||||
std::shared_ptr<Cache> NewFastLRUCache(
|
||||
size_t capacity, int num_shard_bits, bool strict_capacity_limit,
|
||||
size_t capacity, size_t estimated_value_size, int num_shard_bits,
|
||||
bool strict_capacity_limit,
|
||||
CacheMetadataChargePolicy metadata_charge_policy) {
|
||||
if (num_shard_bits >= 20) {
|
||||
return nullptr; // The cache cannot be sharded into too many fine pieces.
|
||||
@@ -505,7 +602,8 @@ std::shared_ptr<Cache> NewFastLRUCache(
|
||||
num_shard_bits = GetDefaultCacheShardBits(capacity);
|
||||
}
|
||||
return std::make_shared<fast_lru_cache::LRUCache>(
|
||||
capacity, num_shard_bits, strict_capacity_limit, metadata_charge_policy);
|
||||
capacity, estimated_value_size, num_shard_bits, strict_capacity_limit,
|
||||
metadata_charge_policy);
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
Vendored
+249
-74
@@ -8,44 +8,138 @@
|
||||
// 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"
|
||||
#include "port/port.h"
|
||||
#include "rocksdb/secondary_cache.h"
|
||||
#include "util/autovector.h"
|
||||
#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 charge; // TODO(opt): Only allow uint32_t?
|
||||
size_t key_length;
|
||||
size_t total_charge; // TODO(opt): Only allow uint32_t?
|
||||
// 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++; }
|
||||
@@ -60,101 +154,173 @@ 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.
|
||||
inline size_t CalcTotalCharge(
|
||||
CacheMetadataChargePolicy metadata_charge_policy) {
|
||||
size_t meta_charge = 0;
|
||||
if (metadata_charge_policy == kFullChargeCacheMetadata) {
|
||||
#ifdef ROCKSDB_MALLOC_USABLE_SIZE
|
||||
meta_charge += malloc_usable_size(static_cast<void*>(this));
|
||||
#else
|
||||
// This is the size that is used when a new handle is created.
|
||||
meta_charge += sizeof(LRUHandle) - 1 + key_length;
|
||||
#endif
|
||||
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(LRUHandle);
|
||||
// #endif
|
||||
}
|
||||
return charge + meta_charge;
|
||||
}
|
||||
|
||||
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() {
|
||||
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:
|
||||
// If the table uses more hash bits than `max_upper_hash_bits`,
|
||||
// it will eat into the bits used for sharding, which are constant
|
||||
// for a given LRUHandleTable.
|
||||
explicit LRUHandleTable(int max_upper_hash_bits);
|
||||
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_; }
|
||||
|
||||
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);
|
||||
|
||||
void Resize();
|
||||
int FindAvailableSlot(const Slice& key, int& probe, int displacement);
|
||||
|
||||
// Number of hash bits (upper because lower bits used for sharding)
|
||||
// used for table index. Length == 1 << length_bits_
|
||||
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 currently in the table.
|
||||
uint32_t elems_;
|
||||
// Number of elements in the table.
|
||||
uint32_t occupancy_;
|
||||
|
||||
// Set from max_upper_hash_bits (see constructor).
|
||||
const int max_length_bits_;
|
||||
// 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.
|
||||
class ALIGN_AS(CACHE_LINE_SIZE) LRUCacheShard final : public CacheShard {
|
||||
public:
|
||||
LRUCacheShard(size_t capacity, bool strict_capacity_limit,
|
||||
CacheMetadataChargePolicy metadata_charge_policy,
|
||||
int max_upper_hash_bits);
|
||||
LRUCacheShard(size_t capacity, size_t estimated_value_size,
|
||||
bool strict_capacity_limit,
|
||||
CacheMetadataChargePolicy metadata_charge_policy);
|
||||
~LRUCacheShard() override = default;
|
||||
|
||||
// Separate from constructor so caller can easily make an array of LRUCache
|
||||
@@ -166,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;
|
||||
@@ -210,21 +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 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_;
|
||||
@@ -262,7 +437,7 @@ class ALIGN_AS(CACHE_LINE_SIZE) LRUCacheShard final : public CacheShard {
|
||||
// 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 port::Mutex mutex_;
|
||||
mutable DMutex mutex_;
|
||||
};
|
||||
|
||||
class LRUCache
|
||||
@@ -271,7 +446,8 @@ class LRUCache
|
||||
#endif
|
||||
: public ShardedCache {
|
||||
public:
|
||||
LRUCache(size_t capacity, int num_shard_bits, bool strict_capacity_limit,
|
||||
LRUCache(size_t capacity, size_t estimated_value_size, int num_shard_bits,
|
||||
bool strict_capacity_limit,
|
||||
CacheMetadataChargePolicy metadata_charge_policy =
|
||||
kDontChargeCacheMetadata);
|
||||
~LRUCache() override;
|
||||
@@ -291,9 +467,8 @@ class LRUCache
|
||||
} // namespace fast_lru_cache
|
||||
|
||||
std::shared_ptr<Cache> NewFastLRUCache(
|
||||
size_t capacity, int num_shard_bits = -1,
|
||||
bool strict_capacity_limit = false,
|
||||
CacheMetadataChargePolicy metadata_charge_policy =
|
||||
kDefaultCacheMetadataChargePolicy);
|
||||
size_t capacity, size_t estimated_value_size, int num_shard_bits,
|
||||
bool strict_capacity_limit,
|
||||
CacheMetadataChargePolicy metadata_charge_policy);
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
Vendored
+66
-58
@@ -16,7 +16,7 @@
|
||||
#include "monitoring/perf_context_imp.h"
|
||||
#include "monitoring/statistics.h"
|
||||
#include "port/lang.h"
|
||||
#include "util/mutexlock.h"
|
||||
#include "util/distributed_mutex.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
namespace lru_cache {
|
||||
@@ -135,7 +135,7 @@ LRUCacheShard::LRUCacheShard(
|
||||
void LRUCacheShard::EraseUnRefEntries() {
|
||||
autovector<LRUHandle*> last_reference_list;
|
||||
{
|
||||
MutexLock l(&mutex_);
|
||||
DMutexLock l(mutex_);
|
||||
while (lru_.next != &lru_) {
|
||||
LRUHandle* old = lru_.next;
|
||||
// LRU list contains only elements which can be evicted.
|
||||
@@ -143,9 +143,8 @@ void LRUCacheShard::EraseUnRefEntries() {
|
||||
LRU_Remove(old);
|
||||
table_.Remove(old->key(), old->hash);
|
||||
old->SetInCache(false);
|
||||
size_t total_charge = old->CalcTotalCharge(metadata_charge_policy_);
|
||||
assert(usage_ >= total_charge);
|
||||
usage_ -= total_charge;
|
||||
assert(usage_ >= old->total_charge);
|
||||
usage_ -= old->total_charge;
|
||||
last_reference_list.push_back(old);
|
||||
}
|
||||
}
|
||||
@@ -162,7 +161,7 @@ void LRUCacheShard::ApplyToSomeEntries(
|
||||
// The state is essentially going to be the starting hash, which works
|
||||
// nicely even if we resize between calls because we use upper-most
|
||||
// hash bits for table indexes.
|
||||
MutexLock l(&mutex_);
|
||||
DMutexLock l(mutex_);
|
||||
uint32_t length_bits = table_.GetLengthBits();
|
||||
uint32_t length = uint32_t{1} << length_bits;
|
||||
|
||||
@@ -182,23 +181,25 @@ void LRUCacheShard::ApplyToSomeEntries(
|
||||
}
|
||||
|
||||
table_.ApplyToEntriesRange(
|
||||
[callback](LRUHandle* h) {
|
||||
[callback,
|
||||
metadata_charge_policy = metadata_charge_policy_](LRUHandle* h) {
|
||||
DeleterFn deleter = h->IsSecondaryCacheCompatible()
|
||||
? h->info_.helper->del_cb
|
||||
: h->info_.deleter;
|
||||
callback(h->key(), h->value, h->charge, deleter);
|
||||
callback(h->key(), h->value, h->GetCharge(metadata_charge_policy),
|
||||
deleter);
|
||||
},
|
||||
index_begin, index_end);
|
||||
}
|
||||
|
||||
void LRUCacheShard::TEST_GetLRUList(LRUHandle** lru, LRUHandle** lru_low_pri) {
|
||||
MutexLock l(&mutex_);
|
||||
DMutexLock l(mutex_);
|
||||
*lru = &lru_;
|
||||
*lru_low_pri = lru_low_pri_;
|
||||
}
|
||||
|
||||
size_t LRUCacheShard::TEST_GetLRUSize() {
|
||||
MutexLock l(&mutex_);
|
||||
DMutexLock l(mutex_);
|
||||
LRUHandle* lru_handle = lru_.next;
|
||||
size_t lru_size = 0;
|
||||
while (lru_handle != &lru_) {
|
||||
@@ -209,7 +210,7 @@ size_t LRUCacheShard::TEST_GetLRUSize() {
|
||||
}
|
||||
|
||||
double LRUCacheShard::GetHighPriPoolRatio() {
|
||||
MutexLock l(&mutex_);
|
||||
DMutexLock l(mutex_);
|
||||
return high_pri_pool_ratio_;
|
||||
}
|
||||
|
||||
@@ -222,19 +223,17 @@ void LRUCacheShard::LRU_Remove(LRUHandle* e) {
|
||||
e->next->prev = e->prev;
|
||||
e->prev->next = e->next;
|
||||
e->prev = e->next = nullptr;
|
||||
size_t total_charge = e->CalcTotalCharge(metadata_charge_policy_);
|
||||
assert(lru_usage_ >= total_charge);
|
||||
lru_usage_ -= total_charge;
|
||||
assert(lru_usage_ >= e->total_charge);
|
||||
lru_usage_ -= e->total_charge;
|
||||
if (e->InHighPriPool()) {
|
||||
assert(high_pri_pool_usage_ >= total_charge);
|
||||
high_pri_pool_usage_ -= total_charge;
|
||||
assert(high_pri_pool_usage_ >= e->total_charge);
|
||||
high_pri_pool_usage_ -= e->total_charge;
|
||||
}
|
||||
}
|
||||
|
||||
void LRUCacheShard::LRU_Insert(LRUHandle* e) {
|
||||
assert(e->next == nullptr);
|
||||
assert(e->prev == nullptr);
|
||||
size_t total_charge = e->CalcTotalCharge(metadata_charge_policy_);
|
||||
if (high_pri_pool_ratio_ > 0 && (e->IsHighPri() || e->HasHit())) {
|
||||
// Inset "e" to head of LRU list.
|
||||
e->next = &lru_;
|
||||
@@ -242,7 +241,7 @@ void LRUCacheShard::LRU_Insert(LRUHandle* e) {
|
||||
e->prev->next = e;
|
||||
e->next->prev = e;
|
||||
e->SetInHighPriPool(true);
|
||||
high_pri_pool_usage_ += total_charge;
|
||||
high_pri_pool_usage_ += e->total_charge;
|
||||
MaintainPoolSize();
|
||||
} else {
|
||||
// Insert "e" to the head of low-pri pool. Note that when
|
||||
@@ -254,7 +253,7 @@ void LRUCacheShard::LRU_Insert(LRUHandle* e) {
|
||||
e->SetInHighPriPool(false);
|
||||
lru_low_pri_ = e;
|
||||
}
|
||||
lru_usage_ += total_charge;
|
||||
lru_usage_ += e->total_charge;
|
||||
}
|
||||
|
||||
void LRUCacheShard::MaintainPoolSize() {
|
||||
@@ -263,10 +262,8 @@ void LRUCacheShard::MaintainPoolSize() {
|
||||
lru_low_pri_ = lru_low_pri_->next;
|
||||
assert(lru_low_pri_ != &lru_);
|
||||
lru_low_pri_->SetInHighPriPool(false);
|
||||
size_t total_charge =
|
||||
lru_low_pri_->CalcTotalCharge(metadata_charge_policy_);
|
||||
assert(high_pri_pool_usage_ >= total_charge);
|
||||
high_pri_pool_usage_ -= total_charge;
|
||||
assert(high_pri_pool_usage_ >= lru_low_pri_->total_charge);
|
||||
high_pri_pool_usage_ -= lru_low_pri_->total_charge;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -279,9 +276,8 @@ void LRUCacheShard::EvictFromLRU(size_t charge,
|
||||
LRU_Remove(old);
|
||||
table_.Remove(old->key(), old->hash);
|
||||
old->SetInCache(false);
|
||||
size_t old_total_charge = old->CalcTotalCharge(metadata_charge_policy_);
|
||||
assert(usage_ >= old_total_charge);
|
||||
usage_ -= old_total_charge;
|
||||
assert(usage_ >= old->total_charge);
|
||||
usage_ -= old->total_charge;
|
||||
deleted->push_back(old);
|
||||
}
|
||||
}
|
||||
@@ -289,7 +285,7 @@ void LRUCacheShard::EvictFromLRU(size_t charge,
|
||||
void LRUCacheShard::SetCapacity(size_t capacity) {
|
||||
autovector<LRUHandle*> last_reference_list;
|
||||
{
|
||||
MutexLock l(&mutex_);
|
||||
DMutexLock l(mutex_);
|
||||
capacity_ = capacity;
|
||||
high_pri_pool_capacity_ = capacity_ * high_pri_pool_ratio_;
|
||||
EvictFromLRU(0, &last_reference_list);
|
||||
@@ -308,7 +304,7 @@ void LRUCacheShard::SetCapacity(size_t capacity) {
|
||||
}
|
||||
|
||||
void LRUCacheShard::SetStrictCapacityLimit(bool strict_capacity_limit) {
|
||||
MutexLock l(&mutex_);
|
||||
DMutexLock l(mutex_);
|
||||
strict_capacity_limit_ = strict_capacity_limit;
|
||||
}
|
||||
|
||||
@@ -316,16 +312,15 @@ Status LRUCacheShard::InsertItem(LRUHandle* e, Cache::Handle** handle,
|
||||
bool free_handle_on_fail) {
|
||||
Status s = Status::OK();
|
||||
autovector<LRUHandle*> last_reference_list;
|
||||
size_t total_charge = e->CalcTotalCharge(metadata_charge_policy_);
|
||||
|
||||
{
|
||||
MutexLock l(&mutex_);
|
||||
DMutexLock l(mutex_);
|
||||
|
||||
// Free the space following strict LRU policy until enough space
|
||||
// is freed or the lru list is empty.
|
||||
EvictFromLRU(total_charge, &last_reference_list);
|
||||
EvictFromLRU(e->total_charge, &last_reference_list);
|
||||
|
||||
if ((usage_ + total_charge) > capacity_ &&
|
||||
if ((usage_ + e->total_charge) > capacity_ &&
|
||||
(strict_capacity_limit_ || handle == nullptr)) {
|
||||
e->SetInCache(false);
|
||||
if (handle == nullptr) {
|
||||
@@ -337,13 +332,13 @@ 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
|
||||
// capacity if not enough space was freed up.
|
||||
LRUHandle* old = table_.Insert(e);
|
||||
usage_ += total_charge;
|
||||
usage_ += e->total_charge;
|
||||
if (old != nullptr) {
|
||||
s = Status::OkOverwritten();
|
||||
assert(old->InCache());
|
||||
@@ -351,10 +346,8 @@ Status LRUCacheShard::InsertItem(LRUHandle* e, Cache::Handle** handle,
|
||||
if (!old->HasRefs()) {
|
||||
// old is on LRU because it's in cache and its reference count is 0.
|
||||
LRU_Remove(old);
|
||||
size_t old_total_charge =
|
||||
old->CalcTotalCharge(metadata_charge_policy_);
|
||||
assert(usage_ >= old_total_charge);
|
||||
usage_ -= old_total_charge;
|
||||
assert(usage_ >= old->total_charge);
|
||||
usage_ -= old->total_charge;
|
||||
last_reference_list.push_back(old);
|
||||
}
|
||||
}
|
||||
@@ -391,7 +384,7 @@ void LRUCacheShard::Promote(LRUHandle* e) {
|
||||
e->SetIncomplete(false);
|
||||
e->SetInCache(true);
|
||||
e->value = secondary_handle->Value();
|
||||
e->charge = secondary_handle->Size();
|
||||
e->CalcTotalCharge(secondary_handle->Size(), metadata_charge_policy_);
|
||||
delete secondary_handle;
|
||||
|
||||
// This call could fail if the cache is over capacity and
|
||||
@@ -409,8 +402,9 @@ void LRUCacheShard::Promote(LRUHandle* e) {
|
||||
} else {
|
||||
// Since the secondary cache lookup failed, mark the item as not in cache
|
||||
// Don't charge the cache as its only metadata that'll shortly be released
|
||||
MutexLock l(&mutex_);
|
||||
e->charge = 0;
|
||||
DMutexLock l(mutex_);
|
||||
// TODO
|
||||
e->CalcTotalCharge(0, metadata_charge_policy_);
|
||||
e->SetInCache(false);
|
||||
}
|
||||
}
|
||||
@@ -422,7 +416,7 @@ Cache::Handle* LRUCacheShard::Lookup(
|
||||
bool wait, Statistics* stats) {
|
||||
LRUHandle* e = nullptr;
|
||||
{
|
||||
MutexLock l(&mutex_);
|
||||
DMutexLock l(mutex_);
|
||||
e = table_.Lookup(key, hash);
|
||||
if (e != nullptr) {
|
||||
assert(e->InCache());
|
||||
@@ -495,7 +489,7 @@ Cache::Handle* LRUCacheShard::Lookup(
|
||||
|
||||
bool LRUCacheShard::Ref(Cache::Handle* h) {
|
||||
LRUHandle* e = reinterpret_cast<LRUHandle*>(h);
|
||||
MutexLock l(&mutex_);
|
||||
DMutexLock l(mutex_);
|
||||
// To create another reference - entry must be already externally referenced.
|
||||
assert(e->HasRefs());
|
||||
e->Ref();
|
||||
@@ -503,7 +497,7 @@ bool LRUCacheShard::Ref(Cache::Handle* h) {
|
||||
}
|
||||
|
||||
void LRUCacheShard::SetHighPriorityPoolRatio(double high_pri_pool_ratio) {
|
||||
MutexLock l(&mutex_);
|
||||
DMutexLock l(mutex_);
|
||||
high_pri_pool_ratio_ = high_pri_pool_ratio;
|
||||
high_pri_pool_capacity_ = capacity_ * high_pri_pool_ratio_;
|
||||
MaintainPoolSize();
|
||||
@@ -516,7 +510,7 @@ bool LRUCacheShard::Release(Cache::Handle* handle, bool erase_if_last_ref) {
|
||||
LRUHandle* e = reinterpret_cast<LRUHandle*>(handle);
|
||||
bool last_reference = false;
|
||||
{
|
||||
MutexLock l(&mutex_);
|
||||
DMutexLock l(mutex_);
|
||||
last_reference = e->Unref();
|
||||
if (last_reference && e->InCache()) {
|
||||
// The item is still in cache, and nobody else holds a reference to it.
|
||||
@@ -538,9 +532,8 @@ bool LRUCacheShard::Release(Cache::Handle* handle, bool erase_if_last_ref) {
|
||||
// usage. If value is null in the latter case, taht means the lookup
|
||||
// failed and we didn't charge the cache.
|
||||
if (last_reference && (!e->IsSecondaryCacheCompatible() || e->value)) {
|
||||
size_t total_charge = e->CalcTotalCharge(metadata_charge_policy_);
|
||||
assert(usage_ >= total_charge);
|
||||
usage_ -= total_charge;
|
||||
assert(usage_ >= e->total_charge);
|
||||
usage_ -= e->total_charge;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -573,7 +566,6 @@ Status LRUCacheShard::Insert(const Slice& key, uint32_t hash, void* value,
|
||||
#endif // __SANITIZE_THREAD__
|
||||
e->info_.deleter = deleter;
|
||||
}
|
||||
e->charge = charge;
|
||||
e->key_length = key.size();
|
||||
e->hash = hash;
|
||||
e->refs = 0;
|
||||
@@ -581,6 +573,7 @@ Status LRUCacheShard::Insert(const Slice& key, uint32_t hash, void* value,
|
||||
e->SetInCache(true);
|
||||
e->SetPriority(priority);
|
||||
memcpy(e->key_data, key.data(), key.size());
|
||||
e->CalcTotalCharge(charge, metadata_charge_policy_);
|
||||
|
||||
return InsertItem(e, handle, /* free_handle_on_fail */ true);
|
||||
}
|
||||
@@ -589,7 +582,7 @@ void LRUCacheShard::Erase(const Slice& key, uint32_t hash) {
|
||||
LRUHandle* e;
|
||||
bool last_reference = false;
|
||||
{
|
||||
MutexLock l(&mutex_);
|
||||
DMutexLock l(mutex_);
|
||||
e = table_.Remove(key, hash);
|
||||
if (e != nullptr) {
|
||||
assert(e->InCache());
|
||||
@@ -597,9 +590,8 @@ void LRUCacheShard::Erase(const Slice& key, uint32_t hash) {
|
||||
if (!e->HasRefs()) {
|
||||
// The entry is in LRU since it's in hash and has no external references
|
||||
LRU_Remove(e);
|
||||
size_t total_charge = e->CalcTotalCharge(metadata_charge_policy_);
|
||||
assert(usage_ >= total_charge);
|
||||
usage_ -= total_charge;
|
||||
assert(usage_ >= e->total_charge);
|
||||
usage_ -= e->total_charge;
|
||||
last_reference = true;
|
||||
}
|
||||
}
|
||||
@@ -614,7 +606,7 @@ void LRUCacheShard::Erase(const Slice& key, uint32_t hash) {
|
||||
|
||||
bool LRUCacheShard::IsReady(Cache::Handle* handle) {
|
||||
LRUHandle* e = reinterpret_cast<LRUHandle*>(handle);
|
||||
MutexLock l(&mutex_);
|
||||
DMutexLock l(mutex_);
|
||||
bool ready = true;
|
||||
if (e->IsPending()) {
|
||||
assert(secondary_cache_);
|
||||
@@ -625,12 +617,12 @@ bool LRUCacheShard::IsReady(Cache::Handle* handle) {
|
||||
}
|
||||
|
||||
size_t LRUCacheShard::GetUsage() const {
|
||||
MutexLock l(&mutex_);
|
||||
DMutexLock l(mutex_);
|
||||
return usage_;
|
||||
}
|
||||
|
||||
size_t LRUCacheShard::GetPinnedUsage() const {
|
||||
MutexLock l(&mutex_);
|
||||
DMutexLock l(mutex_);
|
||||
assert(usage_ >= lru_usage_);
|
||||
return usage_ - lru_usage_;
|
||||
}
|
||||
@@ -639,7 +631,7 @@ std::string LRUCacheShard::GetPrintableOptions() const {
|
||||
const int kBufferSize = 200;
|
||||
char buffer[kBufferSize];
|
||||
{
|
||||
MutexLock l(&mutex_);
|
||||
DMutexLock l(mutex_);
|
||||
snprintf(buffer, kBufferSize, " high_pri_pool_ratio: %.3lf\n",
|
||||
high_pri_pool_ratio_);
|
||||
}
|
||||
@@ -690,7 +682,12 @@ void* LRUCache::Value(Handle* handle) {
|
||||
}
|
||||
|
||||
size_t LRUCache::GetCharge(Handle* handle) const {
|
||||
return reinterpret_cast<const LRUHandle*>(handle)->charge;
|
||||
CacheMetadataChargePolicy metadata_charge_policy = kDontChargeCacheMetadata;
|
||||
if (num_shards_ > 0) {
|
||||
metadata_charge_policy = shards_[0].metadata_charge_policy_;
|
||||
}
|
||||
return reinterpret_cast<const LRUHandle*>(handle)->GetCharge(
|
||||
metadata_charge_policy);
|
||||
}
|
||||
|
||||
Cache::DeleterFn LRUCache::GetDeleter(Handle* handle) const {
|
||||
@@ -760,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
+25
-10
@@ -17,6 +17,7 @@
|
||||
#include "port/port.h"
|
||||
#include "rocksdb/secondary_cache.h"
|
||||
#include "util/autovector.h"
|
||||
#include "util/distributed_mutex.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
namespace lru_cache {
|
||||
@@ -66,7 +67,7 @@ struct LRUHandle {
|
||||
};
|
||||
LRUHandle* next;
|
||||
LRUHandle* prev;
|
||||
size_t charge; // TODO(opt): Only allow uint32_t?
|
||||
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;
|
||||
@@ -209,19 +210,32 @@ struct LRUHandle {
|
||||
delete[] reinterpret_cast<char*>(this);
|
||||
}
|
||||
|
||||
// Calculate the memory usage by metadata.
|
||||
inline size_t CalcTotalCharge(
|
||||
CacheMetadataChargePolicy metadata_charge_policy) {
|
||||
size_t meta_charge = 0;
|
||||
if (metadata_charge_policy == kFullChargeCacheMetadata) {
|
||||
inline size_t CalcuMetaCharge(
|
||||
CacheMetadataChargePolicy metadata_charge_policy) const {
|
||||
if (metadata_charge_policy != kFullChargeCacheMetadata) {
|
||||
return 0;
|
||||
} else {
|
||||
#ifdef ROCKSDB_MALLOC_USABLE_SIZE
|
||||
meta_charge += malloc_usable_size(static_cast<void*>(this));
|
||||
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.
|
||||
meta_charge += sizeof(LRUHandle) - 1 + key_length;
|
||||
return sizeof(LRUHandle) - 1 + key_length;
|
||||
#endif
|
||||
}
|
||||
return charge + meta_charge;
|
||||
}
|
||||
|
||||
// Calculate the memory usage by metadata.
|
||||
inline void CalcTotalCharge(
|
||||
size_t charge, CacheMetadataChargePolicy metadata_charge_policy) {
|
||||
total_charge = charge + CalcuMetaCharge(metadata_charge_policy);
|
||||
}
|
||||
|
||||
inline size_t GetCharge(
|
||||
CacheMetadataChargePolicy metadata_charge_policy) const {
|
||||
size_t meta_charge = CalcuMetaCharge(metadata_charge_policy);
|
||||
assert(total_charge >= meta_charge);
|
||||
return total_charge - meta_charge;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -440,7 +454,7 @@ class ALIGN_AS(CACHE_LINE_SIZE) LRUCacheShard final : public CacheShard {
|
||||
// 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 port::Mutex mutex_;
|
||||
mutable DMutex mutex_;
|
||||
|
||||
std::shared_ptr<SecondaryCache> secondary_cache_;
|
||||
};
|
||||
@@ -468,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
+271
@@ -9,6 +9,8 @@
|
||||
#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"
|
||||
#include "port/port.h"
|
||||
@@ -205,6 +207,275 @@ TEST_F(LRUCacheTest, EntriesWithPriority) {
|
||||
ValidateLRUList({"e", "f", "g", "Z", "d"}, 2);
|
||||
}
|
||||
|
||||
namespace fast_lru_cache {
|
||||
|
||||
// TODO(guido) Replicate LRU policy tests from LRUCache here.
|
||||
class FastLRUCacheTest : public testing::Test {
|
||||
public:
|
||||
FastLRUCacheTest() {}
|
||||
~FastLRUCacheTest() override { DeleteCache(); }
|
||||
|
||||
void DeleteCache() {
|
||||
if (cache_ != nullptr) {
|
||||
cache_->~LRUCacheShard();
|
||||
port::cacheline_aligned_free(cache_);
|
||||
cache_ = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
void NewCache(size_t capacity) {
|
||||
DeleteCache();
|
||||
cache_ = reinterpret_cast<fast_lru_cache::LRUCacheShard*>(
|
||||
port::cacheline_aligned_alloc(sizeof(fast_lru_cache::LRUCacheShard)));
|
||||
new (cache_) fast_lru_cache::LRUCacheShard(
|
||||
capacity, 1 /*estimated_value_size*/, false /*strict_capacity_limit*/,
|
||||
kDontChargeCacheMetadata);
|
||||
}
|
||||
|
||||
Status Insert(const std::string& key) {
|
||||
return cache_->Insert(key, 0 /*hash*/, nullptr /*value*/, 1 /*charge*/,
|
||||
nullptr /*deleter*/, nullptr /*handle*/,
|
||||
Cache::Priority::LOW);
|
||||
}
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
TEST_F(FastLRUCacheTest, ValidateKeySize) {
|
||||
NewCache(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(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
|
||||
@@ -0,0 +1,405 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#include "db/blob/blob_source.h"
|
||||
|
||||
#include <cassert>
|
||||
#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 {
|
||||
|
||||
BlobSource::BlobSource(const ImmutableOptions* immutable_options,
|
||||
const std::string& db_id,
|
||||
const std::string& db_session_id,
|
||||
BlobFileCache* blob_file_cache)
|
||||
: db_id_(db_id),
|
||||
db_session_id_(db_session_id),
|
||||
statistics_(immutable_options->statistics.get()),
|
||||
blob_file_cache_(blob_file_cache),
|
||||
blob_cache_(immutable_options->blob_cache) {}
|
||||
|
||||
BlobSource::~BlobSource() = default;
|
||||
|
||||
Status BlobSource::GetBlobFromCache(const Slice& cache_key,
|
||||
CacheHandleGuard<std::string>* blob) const {
|
||||
assert(blob);
|
||||
assert(blob->IsEmpty());
|
||||
assert(blob_cache_);
|
||||
assert(!cache_key.empty());
|
||||
|
||||
Cache::Handle* cache_handle = nullptr;
|
||||
cache_handle = GetEntryFromCache(cache_key);
|
||||
if (cache_handle != nullptr) {
|
||||
*blob = CacheHandleGuard<std::string>(blob_cache_.get(), cache_handle);
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
assert(blob->IsEmpty());
|
||||
|
||||
return Status::NotFound("Blob not found in cache");
|
||||
}
|
||||
|
||||
Status BlobSource::PutBlobIntoCache(const Slice& cache_key,
|
||||
CacheHandleGuard<std::string>* cached_blob,
|
||||
PinnableSlice* blob) const {
|
||||
assert(blob);
|
||||
assert(!cache_key.empty());
|
||||
assert(blob_cache_);
|
||||
|
||||
Status s;
|
||||
const Cache::Priority priority = Cache::Priority::LOW;
|
||||
|
||||
// Objects to be put into the cache have to be heap-allocated and
|
||||
// self-contained, i.e. own their contents. The Cache has to be able to take
|
||||
// unique ownership of them. Therefore, we copy the blob into a string
|
||||
// directly, and insert that into the cache.
|
||||
std::string* buf = new std::string();
|
||||
buf->assign(blob->data(), blob->size());
|
||||
|
||||
// TODO: support custom allocators and provide a better estimated memory
|
||||
// usage using malloc_usable_size.
|
||||
Cache::Handle* cache_handle = nullptr;
|
||||
s = InsertEntryIntoCache(cache_key, buf, buf->size(), &cache_handle,
|
||||
priority);
|
||||
if (s.ok()) {
|
||||
assert(cache_handle != nullptr);
|
||||
*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,
|
||||
uint64_t value_size,
|
||||
CompressionType compression_type,
|
||||
FilePrefetchBuffer* prefetch_buffer,
|
||||
PinnableSlice* value, uint64_t* bytes_read) {
|
||||
assert(value);
|
||||
|
||||
Status s;
|
||||
|
||||
const CacheKey cache_key = GetCacheKey(file_number, file_size, offset);
|
||||
|
||||
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_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;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
assert(blob_handle.IsEmpty());
|
||||
|
||||
const bool no_io = read_options.read_tier == kBlockCacheTier;
|
||||
if (no_io) {
|
||||
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
|
||||
// file.
|
||||
{
|
||||
CacheHandleGuard<BlobFileReader> blob_file_reader;
|
||||
s = blob_file_cache_->GetBlobFileReader(file_number, &blob_file_reader);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
assert(blob_file_reader.GetValue());
|
||||
|
||||
if (compression_type != blob_file_reader.GetValue()->GetCompressionType()) {
|
||||
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, &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_handle, value);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
assert(s.ok());
|
||||
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();
|
||||
|
||||
CacheHandleGuard<std::string> blob_handle;
|
||||
const Status s = GetBlobFromCache(key, &blob_handle);
|
||||
|
||||
if (s.ok() && blob_handle.GetValue() != nullptr) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -0,0 +1,138 @@
|
||||
// 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 "cache/cache_helpers.h"
|
||||
#include "cache/cache_key.h"
|
||||
#include "db/blob/blob_file_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 {
|
||||
|
||||
struct ImmutableOptions;
|
||||
class Status;
|
||||
class FilePrefetchBuffer;
|
||||
class Slice;
|
||||
|
||||
// BlobSource is a class that provides universal access to blobs, regardless of
|
||||
// whether they are in the blob cache, secondary cache, or (remote) storage.
|
||||
// Depending on user settings, it always fetch blobs from multi-tier cache and
|
||||
// storage with minimal cost.
|
||||
class BlobSource {
|
||||
public:
|
||||
BlobSource(const ImmutableOptions* immutable_options,
|
||||
const std::string& db_id, const std::string& db_session_id,
|
||||
BlobFileCache* blob_file_cache);
|
||||
|
||||
BlobSource(const BlobSource&) = delete;
|
||||
BlobSource& operator=(const BlobSource&) = delete;
|
||||
|
||||
~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,
|
||||
CacheHandleGuard<std::string>* blob) const;
|
||||
|
||||
Status PutBlobIntoCache(const Slice& cache_key,
|
||||
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,
|
||||
file_size);
|
||||
return base_cache_key.WithOffset(offset);
|
||||
}
|
||||
|
||||
const std::string& db_id_;
|
||||
const std::string& db_session_id_;
|
||||
|
||||
Statistics* statistics_;
|
||||
|
||||
// A cache to store blob file reader.
|
||||
BlobFileCache* blob_file_cache_;
|
||||
|
||||
// A cache to store uncompressed blobs.
|
||||
std::shared_ptr<Cache> blob_cache_;
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
File diff suppressed because it is too large.
Load diff
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -243,6 +243,84 @@ TEST_F(DBBlobCompactionTest, FilterByKeyLength) {
|
||||
Close();
|
||||
}
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
TEST_F(DBBlobCompactionTest, BlobCompactWithStartingLevel) {
|
||||
Options options = GetDefaultOptions();
|
||||
|
||||
options.enable_blob_files = true;
|
||||
options.min_blob_size = 1000;
|
||||
options.blob_file_starting_level = 5;
|
||||
options.create_if_missing = true;
|
||||
|
||||
// Open DB with fixed-prefix sst-partitioner so that compaction will cut
|
||||
// new table file when encountering a new key whose 1-byte prefix changes.
|
||||
constexpr size_t key_len = 1;
|
||||
options.sst_partitioner_factory =
|
||||
NewSstPartitionerFixedPrefixFactory(key_len);
|
||||
|
||||
ASSERT_OK(TryReopen(options));
|
||||
|
||||
constexpr size_t blob_size = 3000;
|
||||
|
||||
constexpr char first_key[] = "a";
|
||||
const std::string first_blob(blob_size, 'a');
|
||||
ASSERT_OK(Put(first_key, first_blob));
|
||||
|
||||
constexpr char second_key[] = "b";
|
||||
const std::string second_blob(2 * blob_size, 'b');
|
||||
ASSERT_OK(Put(second_key, second_blob));
|
||||
|
||||
constexpr char third_key[] = "d";
|
||||
const std::string third_blob(blob_size, 'd');
|
||||
ASSERT_OK(Put(third_key, third_blob));
|
||||
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
constexpr char fourth_key[] = "c";
|
||||
const std::string fourth_blob(blob_size, 'c');
|
||||
ASSERT_OK(Put(fourth_key, fourth_blob));
|
||||
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
ASSERT_EQ(0, GetBlobFileNumbers().size());
|
||||
ASSERT_EQ(2, NumTableFilesAtLevel(/*level=*/0));
|
||||
ASSERT_EQ(0, NumTableFilesAtLevel(/*level=*/1));
|
||||
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), /*begin=*/nullptr,
|
||||
/*end=*/nullptr));
|
||||
|
||||
// No blob file should be created since blob_file_starting_level is 5.
|
||||
ASSERT_EQ(0, GetBlobFileNumbers().size());
|
||||
ASSERT_EQ(0, NumTableFilesAtLevel(/*level=*/0));
|
||||
ASSERT_EQ(4, NumTableFilesAtLevel(/*level=*/1));
|
||||
|
||||
{
|
||||
options.blob_file_starting_level = 1;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(Put(first_key, first_blob));
|
||||
ASSERT_OK(Put(second_key, second_blob));
|
||||
ASSERT_OK(Put(third_key, third_blob));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put(fourth_key, fourth_blob));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
ASSERT_EQ(0, GetBlobFileNumbers().size());
|
||||
ASSERT_EQ(2, NumTableFilesAtLevel(/*level=*/0));
|
||||
ASSERT_EQ(0, NumTableFilesAtLevel(/*level=*/1));
|
||||
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), /*begin=*/nullptr,
|
||||
/*end=*/nullptr));
|
||||
// The compaction's output level equals to blob_file_starting_level.
|
||||
ASSERT_EQ(1, GetBlobFileNumbers().size());
|
||||
ASSERT_EQ(0, NumTableFilesAtLevel(/*level=*/0));
|
||||
ASSERT_EQ(4, NumTableFilesAtLevel(/*level=*/1));
|
||||
}
|
||||
|
||||
Close();
|
||||
}
|
||||
#endif
|
||||
|
||||
TEST_F(DBBlobCompactionTest, BlindWriteFilter) {
|
||||
Options options = GetDefaultOptions();
|
||||
options.enable_blob_files = true;
|
||||
|
||||
+7
-5
@@ -181,7 +181,10 @@ Status BuildTable(
|
||||
snapshots.empty() ? 0 : snapshots.back(), snapshot_checker);
|
||||
|
||||
std::unique_ptr<BlobFileBuilder> blob_file_builder(
|
||||
(mutable_cf_options.enable_blob_files && blob_file_additions)
|
||||
(mutable_cf_options.enable_blob_files &&
|
||||
tboptions.level_at_creation >=
|
||||
mutable_cf_options.blob_file_starting_level &&
|
||||
blob_file_additions)
|
||||
? new BlobFileBuilder(
|
||||
versions, fs, &ioptions, &mutable_cf_options, &file_options,
|
||||
job_id, tboptions.column_family_id,
|
||||
@@ -190,6 +193,7 @@ Status BuildTable(
|
||||
&blob_file_paths, blob_file_additions)
|
||||
: nullptr);
|
||||
|
||||
const std::atomic<bool> kManualCompactionCanceledFalse{false};
|
||||
CompactionIterator c_iter(
|
||||
iter, tboptions.internal_comparator.user_comparator(), &merge,
|
||||
kMaxSequenceNumber, &snapshots, earliest_write_conflict_snapshot,
|
||||
@@ -198,11 +202,9 @@ Status BuildTable(
|
||||
true /* internal key corruption is not ok */, range_del_agg.get(),
|
||||
blob_file_builder.get(), ioptions.allow_data_in_errors,
|
||||
ioptions.enforce_single_del_contracts,
|
||||
/*manual_compaction_canceled=*/kManualCompactionCanceledFalse,
|
||||
/*compaction=*/nullptr, compaction_filter.get(),
|
||||
/*shutting_down=*/nullptr,
|
||||
/*manual_compaction_paused=*/nullptr,
|
||||
/*manual_compaction_canceled=*/nullptr, db_options.info_log,
|
||||
full_history_ts_low);
|
||||
/*shutting_down=*/nullptr, db_options.info_log, full_history_ts_low);
|
||||
|
||||
c_iter.SeekToFirst();
|
||||
for (; c_iter.Valid(); c_iter.Next()) {
|
||||
|
||||
+493
-32
@@ -33,11 +33,6 @@ int geteuid() {
|
||||
return result;
|
||||
}
|
||||
|
||||
// VS < 2015
|
||||
#if defined(_MSC_VER) && (_MSC_VER < 1900)
|
||||
#define snprintf _snprintf
|
||||
#endif
|
||||
|
||||
#endif
|
||||
|
||||
const char* phase = "";
|
||||
@@ -169,6 +164,16 @@ static void CheckPinGetCF(rocksdb_t* db, const rocksdb_readoptions_t* options,
|
||||
rocksdb_pinnableslice_destroy(p);
|
||||
}
|
||||
|
||||
static void CheckMultiGetValues(size_t num_keys, char** values,
|
||||
size_t* values_sizes, char** errs,
|
||||
const char** expected) {
|
||||
for (size_t i = 0; i < num_keys; i++) {
|
||||
CheckNoError(errs[i]);
|
||||
CheckEqual(expected[i], values[i], values_sizes[i]);
|
||||
Free(&values[i]);
|
||||
}
|
||||
}
|
||||
|
||||
static void CheckIter(rocksdb_iterator_t* iter,
|
||||
const char* key, const char* val) {
|
||||
size_t len;
|
||||
@@ -267,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) {
|
||||
@@ -283,12 +374,27 @@ static rocksdb_t* CheckCompaction(rocksdb_t* db, rocksdb_options_t* options,
|
||||
CheckNoError(err);
|
||||
CheckGet(db, roptions, "baz", "bazvalue");
|
||||
|
||||
// Disable compaction
|
||||
rocksdb_disable_manual_compaction(db);
|
||||
rocksdb_compact_range(db, NULL, 0, NULL, 0);
|
||||
// should not filter anything when disabled
|
||||
CheckGet(db, roptions, "foo", "foovalue");
|
||||
CheckGet(db, roptions, "bar", "barvalue");
|
||||
CheckGet(db, roptions, "baz", "bazvalue");
|
||||
// Reenable compaction
|
||||
rocksdb_enable_manual_compaction(db);
|
||||
|
||||
// Force compaction
|
||||
rocksdb_compact_range(db, NULL, 0, NULL, 0);
|
||||
// should have filtered bar, but not foo
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -366,18 +472,49 @@ static void CheckTxnGetCF(rocksdb_transaction_t* txn,
|
||||
Free(&val);
|
||||
}
|
||||
|
||||
static void CheckTxnPinGet(rocksdb_transaction_t* txn,
|
||||
const rocksdb_readoptions_t* options,
|
||||
const char* key, const char* expected) {
|
||||
rocksdb_pinnableslice_t* p = NULL;
|
||||
const char* val = NULL;
|
||||
char* err = NULL;
|
||||
size_t val_len;
|
||||
p = rocksdb_transaction_get_pinned(txn, options, key, strlen(key), &err);
|
||||
CheckNoError(err);
|
||||
val = rocksdb_pinnableslice_value(p, &val_len);
|
||||
CheckEqual(expected, val, val_len);
|
||||
rocksdb_pinnableslice_destroy(p);
|
||||
}
|
||||
|
||||
static void CheckTxnPinGetCF(rocksdb_transaction_t* txn,
|
||||
const rocksdb_readoptions_t* options,
|
||||
rocksdb_column_family_handle_t* column_family,
|
||||
const char* key, const char* expected) {
|
||||
rocksdb_pinnableslice_t* p = NULL;
|
||||
const char* val = NULL;
|
||||
char* err = NULL;
|
||||
size_t val_len;
|
||||
p = rocksdb_transaction_get_pinned_cf(txn, options, column_family, key,
|
||||
strlen(key), &err);
|
||||
CheckNoError(err);
|
||||
val = rocksdb_pinnableslice_value(p, &val_len);
|
||||
CheckEqual(expected, val, val_len);
|
||||
rocksdb_pinnableslice_destroy(p);
|
||||
}
|
||||
|
||||
static void CheckTxnDBGet(
|
||||
rocksdb_transactiondb_t* txn_db,
|
||||
const rocksdb_readoptions_t* options,
|
||||
const char* key,
|
||||
const char* expected) {
|
||||
char* err = NULL;
|
||||
size_t val_len;
|
||||
char* val;
|
||||
val = rocksdb_transactiondb_get(txn_db, options, key, strlen(key), &val_len, &err);
|
||||
CheckNoError(err);
|
||||
CheckEqual(expected, val, val_len);
|
||||
Free(&val);
|
||||
char* err = NULL;
|
||||
size_t val_len;
|
||||
char* val;
|
||||
val = rocksdb_transactiondb_get(txn_db, options, key, strlen(key), &val_len,
|
||||
&err);
|
||||
CheckNoError(err);
|
||||
CheckEqual(expected, val, val_len);
|
||||
Free(&val);
|
||||
}
|
||||
|
||||
static void CheckTxnDBGetCF(rocksdb_transactiondb_t* txn_db,
|
||||
@@ -394,6 +531,101 @@ static void CheckTxnDBGetCF(rocksdb_transactiondb_t* txn_db,
|
||||
Free(&val);
|
||||
}
|
||||
|
||||
static void CheckTxnDBPinGet(rocksdb_transactiondb_t* txn_db,
|
||||
const rocksdb_readoptions_t* options,
|
||||
const char* key, const char* expected) {
|
||||
rocksdb_pinnableslice_t* p = NULL;
|
||||
const char* val = NULL;
|
||||
char* err = NULL;
|
||||
size_t val_len;
|
||||
p = rocksdb_transactiondb_get_pinned(txn_db, options, key, strlen(key), &err);
|
||||
CheckNoError(err);
|
||||
val = rocksdb_pinnableslice_value(p, &val_len);
|
||||
CheckEqual(expected, val, val_len);
|
||||
rocksdb_pinnableslice_destroy(p);
|
||||
}
|
||||
|
||||
static void CheckTxnDBPinGetCF(rocksdb_transactiondb_t* txn_db,
|
||||
const rocksdb_readoptions_t* options,
|
||||
rocksdb_column_family_handle_t* column_family,
|
||||
const char* key, const char* expected) {
|
||||
rocksdb_pinnableslice_t* p = NULL;
|
||||
const char* val = NULL;
|
||||
char* err = NULL;
|
||||
size_t val_len;
|
||||
p = rocksdb_transactiondb_get_pinned_cf(txn_db, options, column_family, key,
|
||||
strlen(key), &err);
|
||||
CheckNoError(err);
|
||||
val = rocksdb_pinnableslice_value(p, &val_len);
|
||||
CheckEqual(expected, val, val_len);
|
||||
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;
|
||||
@@ -914,24 +1146,9 @@ int main(int argc, char** argv) {
|
||||
char* vals[3];
|
||||
size_t vals_sizes[3];
|
||||
char* errs[3];
|
||||
const char* expected[3] = {"c", "hello", NULL};
|
||||
rocksdb_multi_get(db, roptions, 3, keys, keys_sizes, vals, vals_sizes, errs);
|
||||
|
||||
int i;
|
||||
for (i = 0; i < 3; i++) {
|
||||
CheckEqual(NULL, errs[i], 0);
|
||||
switch (i) {
|
||||
case 0:
|
||||
CheckEqual("c", vals[i], vals_sizes[i]);
|
||||
break;
|
||||
case 1:
|
||||
CheckEqual("hello", vals[i], vals_sizes[i]);
|
||||
break;
|
||||
case 2:
|
||||
CheckEqual(NULL, vals[i], vals_sizes[i]);
|
||||
break;
|
||||
}
|
||||
Free(&vals[i]);
|
||||
}
|
||||
CheckMultiGetValues(3, vals, vals_sizes, errs, expected);
|
||||
}
|
||||
|
||||
StartPhase("pin_get");
|
||||
@@ -1189,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");
|
||||
@@ -1201,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);
|
||||
@@ -1221,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);
|
||||
|
||||
@@ -1235,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);
|
||||
@@ -1382,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);
|
||||
@@ -1449,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);
|
||||
}
|
||||
|
||||
@@ -1770,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));
|
||||
@@ -1796,6 +2050,9 @@ int main(int argc, char** argv) {
|
||||
CheckCondition(262144 ==
|
||||
rocksdb_options_get_blob_compaction_readahead_size(o));
|
||||
|
||||
rocksdb_options_set_blob_file_starting_level(o, 5);
|
||||
CheckCondition(5 == rocksdb_options_get_blob_file_starting_level(o));
|
||||
|
||||
// Create a copy that should be equal to the original.
|
||||
rocksdb_options_t* copy;
|
||||
copy = rocksdb_options_create_copy(o);
|
||||
@@ -1897,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);
|
||||
@@ -2245,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);
|
||||
}
|
||||
@@ -2623,11 +2888,13 @@ int main(int argc, char** argv) {
|
||||
rocksdb_transactiondb_put(txn_db, woptions, "foo", 3, "hello", 5, &err);
|
||||
CheckNoError(err);
|
||||
CheckTxnDBGet(txn_db, roptions, "foo", "hello");
|
||||
CheckTxnDBPinGet(txn_db, roptions, "foo", "hello");
|
||||
|
||||
// delete from outside transaction
|
||||
rocksdb_transactiondb_delete(txn_db, woptions, "foo", 3, &err);
|
||||
CheckNoError(err);
|
||||
CheckTxnDBGet(txn_db, roptions, "foo", NULL);
|
||||
CheckTxnDBPinGet(txn_db, roptions, "foo", NULL);
|
||||
|
||||
// write batch into TransactionDB
|
||||
rocksdb_writebatch_t* wb = rocksdb_writebatch_create();
|
||||
@@ -2639,24 +2906,63 @@ int main(int argc, char** argv) {
|
||||
rocksdb_transactiondb_write(txn_db, woptions, wb, &err);
|
||||
rocksdb_writebatch_destroy(wb);
|
||||
CheckTxnDBGet(txn_db, roptions, "box", "c");
|
||||
CheckTxnDBPinGet(txn_db, roptions, "box", "c");
|
||||
CheckNoError(err);
|
||||
|
||||
// multi get
|
||||
{
|
||||
const char* keys[3] = {"box", "foo", "notfound"};
|
||||
const size_t keys_sizes[3] = {3, 3, 8};
|
||||
char* vals[3];
|
||||
size_t vals_sizes[3];
|
||||
char* errs[3];
|
||||
const char* expected[3] = {"c", NULL, NULL};
|
||||
rocksdb_transactiondb_multi_get(txn_db, roptions, 3, keys, keys_sizes,
|
||||
vals, vals_sizes, errs);
|
||||
CheckMultiGetValues(3, vals, vals_sizes, errs, expected);
|
||||
}
|
||||
|
||||
// begin a transaction
|
||||
txn = rocksdb_transaction_begin(txn_db, woptions, txn_options, NULL);
|
||||
// put
|
||||
rocksdb_transaction_put(txn, "foo", 3, "hello", 5, &err);
|
||||
CheckNoError(err);
|
||||
CheckTxnGet(txn, roptions, "foo", "hello");
|
||||
CheckTxnPinGet(txn, roptions, "foo", "hello");
|
||||
{
|
||||
const char* keys[3] = {"box", "foo", "notfound"};
|
||||
const size_t keys_sizes[3] = {3, 3, 8};
|
||||
char* vals[3];
|
||||
size_t vals_sizes[3];
|
||||
char* errs[3];
|
||||
const char* expected[3] = {"c", "hello", NULL};
|
||||
rocksdb_transaction_multi_get(txn, roptions, 3, keys, keys_sizes, vals,
|
||||
vals_sizes, errs);
|
||||
CheckMultiGetValues(3, vals, vals_sizes, errs, expected);
|
||||
}
|
||||
// delete
|
||||
rocksdb_transaction_delete(txn, "foo", 3, &err);
|
||||
CheckNoError(err);
|
||||
CheckTxnGet(txn, roptions, "foo", NULL);
|
||||
CheckTxnPinGet(txn, roptions, "foo", NULL);
|
||||
|
||||
rocksdb_transaction_put(txn, "foo", 3, "hello", 5, &err);
|
||||
CheckNoError(err);
|
||||
|
||||
// read from outside transaction, before commit
|
||||
CheckTxnDBGet(txn_db, roptions, "foo", NULL);
|
||||
CheckTxnDBPinGet(txn_db, roptions, "foo", NULL);
|
||||
{
|
||||
const char* keys[3] = {"box", "foo", "notfound"};
|
||||
const size_t keys_sizes[3] = {3, 3, 8};
|
||||
char* vals[3];
|
||||
size_t vals_sizes[3];
|
||||
char* errs[3];
|
||||
const char* expected[3] = {"c", NULL, NULL};
|
||||
rocksdb_transactiondb_multi_get(txn_db, roptions, 3, keys, keys_sizes,
|
||||
vals, vals_sizes, errs);
|
||||
CheckMultiGetValues(3, vals, vals_sizes, errs, expected);
|
||||
}
|
||||
|
||||
// commit
|
||||
rocksdb_transaction_commit(txn, &err);
|
||||
@@ -2664,6 +2970,18 @@ int main(int argc, char** argv) {
|
||||
|
||||
// read from outside transaction, after commit
|
||||
CheckTxnDBGet(txn_db, roptions, "foo", "hello");
|
||||
CheckTxnDBPinGet(txn_db, roptions, "foo", "hello");
|
||||
{
|
||||
const char* keys[3] = {"box", "foo", "notfound"};
|
||||
const size_t keys_sizes[3] = {3, 3, 8};
|
||||
char* vals[3];
|
||||
size_t vals_sizes[3];
|
||||
char* errs[3];
|
||||
const char* expected[3] = {"c", "hello", NULL};
|
||||
rocksdb_transactiondb_multi_get(txn_db, roptions, 3, keys, keys_sizes,
|
||||
vals, vals_sizes, errs);
|
||||
CheckMultiGetValues(3, vals, vals_sizes, errs, expected);
|
||||
}
|
||||
|
||||
// reuse old transaction
|
||||
txn = rocksdb_transaction_begin(txn_db, woptions, txn_options, txn);
|
||||
@@ -2677,9 +2995,11 @@ int main(int argc, char** argv) {
|
||||
CheckNoError(err);
|
||||
|
||||
CheckTxnDBGet(txn_db, roptions, "foo", "hello");
|
||||
CheckTxnDBPinGet(txn_db, roptions, "foo", "hello");
|
||||
rocksdb_readoptions_set_snapshot(roptions, NULL);
|
||||
rocksdb_transactiondb_release_snapshot(txn_db, snapshot);
|
||||
CheckTxnDBGet(txn_db, roptions, "foo", "hey");
|
||||
CheckTxnDBPinGet(txn_db, roptions, "foo", "hey");
|
||||
|
||||
// iterate
|
||||
rocksdb_transaction_put(txn, "bar", 3, "hi", 2, &err);
|
||||
@@ -2696,25 +3016,34 @@ int main(int argc, char** argv) {
|
||||
rocksdb_transaction_rollback(txn, &err);
|
||||
CheckNoError(err);
|
||||
CheckTxnDBGet(txn_db, roptions, "bar", NULL);
|
||||
CheckTxnDBPinGet(txn_db, roptions, "bar", NULL);
|
||||
|
||||
// save point
|
||||
rocksdb_transaction_put(txn, "foo1", 4, "hi1", 3, &err);
|
||||
rocksdb_transaction_set_savepoint(txn);
|
||||
CheckTxnGet(txn, roptions, "foo1", "hi1");
|
||||
CheckTxnPinGet(txn, roptions, "foo1", "hi1");
|
||||
rocksdb_transaction_put(txn, "foo2", 4, "hi2", 3, &err);
|
||||
CheckTxnGet(txn, roptions, "foo2", "hi2");
|
||||
CheckTxnPinGet(txn, roptions, "foo2", "hi2");
|
||||
|
||||
// rollback to savepoint
|
||||
rocksdb_transaction_rollback_to_savepoint(txn, &err);
|
||||
CheckNoError(err);
|
||||
CheckTxnGet(txn, roptions, "foo2", NULL);
|
||||
CheckTxnGet(txn, roptions, "foo1", "hi1");
|
||||
CheckTxnPinGet(txn, roptions, "foo2", NULL);
|
||||
CheckTxnPinGet(txn, roptions, "foo1", "hi1");
|
||||
CheckTxnDBGet(txn_db, roptions, "foo1", NULL);
|
||||
CheckTxnDBGet(txn_db, roptions, "foo2", NULL);
|
||||
CheckTxnDBPinGet(txn_db, roptions, "foo1", NULL);
|
||||
CheckTxnDBPinGet(txn_db, roptions, "foo2", NULL);
|
||||
rocksdb_transaction_commit(txn, &err);
|
||||
CheckNoError(err);
|
||||
CheckTxnDBGet(txn_db, roptions, "foo1", "hi1");
|
||||
CheckTxnDBGet(txn_db, roptions, "foo2", NULL);
|
||||
CheckTxnDBPinGet(txn_db, roptions, "foo1", "hi1");
|
||||
CheckTxnDBPinGet(txn_db, roptions, "foo2", NULL);
|
||||
|
||||
// Column families.
|
||||
rocksdb_column_family_handle_t* cfh;
|
||||
@@ -2726,14 +3055,38 @@ int main(int argc, char** argv) {
|
||||
8, &err);
|
||||
CheckNoError(err);
|
||||
CheckTxnDBGetCF(txn_db, roptions, cfh, "cf_foo", "cf_hello");
|
||||
CheckTxnDBPinGetCF(txn_db, roptions, cfh, "cf_foo", "cf_hello");
|
||||
{
|
||||
const rocksdb_column_family_handle_t* get_handles[2] = {cfh, cfh};
|
||||
const char* keys[2] = {"cf_foo", "notfound"};
|
||||
const size_t keys_sizes[2] = {6, 8};
|
||||
char* vals[2];
|
||||
size_t vals_sizes[2];
|
||||
char* errs[2];
|
||||
const char* expected[2] = {"cf_hello", NULL};
|
||||
rocksdb_transactiondb_multi_get_cf(txn_db, roptions, get_handles, 2, keys,
|
||||
keys_sizes, vals, vals_sizes, errs);
|
||||
CheckMultiGetValues(2, vals, vals_sizes, errs, expected);
|
||||
}
|
||||
|
||||
rocksdb_transactiondb_delete_cf(txn_db, woptions, cfh, "cf_foo", 6, &err);
|
||||
CheckNoError(err);
|
||||
CheckTxnDBGetCF(txn_db, roptions, cfh, "cf_foo", NULL);
|
||||
CheckTxnDBPinGetCF(txn_db, roptions, cfh, "cf_foo", NULL);
|
||||
|
||||
rocksdb_column_family_handle_destroy(cfh);
|
||||
// flush
|
||||
rocksdb_flushoptions_t* flush_options = rocksdb_flushoptions_create();
|
||||
rocksdb_flushoptions_set_wait(flush_options, 1);
|
||||
rocksdb_transactiondb_flush_wal(txn_db, 1, &err);
|
||||
CheckNoError(err);
|
||||
rocksdb_transactiondb_flush_cf(txn_db, flush_options, cfh, &err);
|
||||
CheckNoError(err);
|
||||
rocksdb_transactiondb_flush(txn_db, flush_options, &err);
|
||||
CheckNoError(err);
|
||||
rocksdb_flushoptions_destroy(flush_options);
|
||||
|
||||
// close and destroy
|
||||
rocksdb_column_family_handle_destroy(cfh);
|
||||
rocksdb_transaction_destroy(txn);
|
||||
rocksdb_transactiondb_close(txn_db);
|
||||
rocksdb_destroy_db(options, dbname, &err);
|
||||
@@ -2742,6 +3095,97 @@ int main(int argc, char** argv) {
|
||||
rocksdb_transactiondb_options_destroy(txn_db_options);
|
||||
}
|
||||
|
||||
StartPhase("two-phase commit");
|
||||
{
|
||||
// open a TransactionDB
|
||||
txn_db_options = rocksdb_transactiondb_options_create();
|
||||
txn_options = rocksdb_transaction_options_create();
|
||||
rocksdb_options_set_create_if_missing(options, 1);
|
||||
txn_db = rocksdb_transactiondb_open(options, txn_db_options, dbname, &err);
|
||||
CheckNoError(err);
|
||||
|
||||
rocksdb_transaction_options_set_skip_prepare(txn_options, 0);
|
||||
txn = rocksdb_transaction_begin(txn_db, woptions, txn_options, NULL);
|
||||
rocksdb_transaction_commit(txn, &err);
|
||||
CheckCondition(err != NULL);
|
||||
Free(&err);
|
||||
err = NULL;
|
||||
rocksdb_transaction_prepare(txn, &err);
|
||||
CheckCondition(err != NULL);
|
||||
Free(&err);
|
||||
err = NULL;
|
||||
rocksdb_transaction_set_name(txn, "txn1", 4, &err);
|
||||
CheckNoError(err);
|
||||
rocksdb_transaction_prepare(txn, &err);
|
||||
CheckNoError(err);
|
||||
rocksdb_transaction_commit(txn, &err);
|
||||
CheckNoError(err);
|
||||
rocksdb_transaction_destroy(txn);
|
||||
|
||||
// prepare 2 transactions and close db.
|
||||
rocksdb_transaction_t* txn1 =
|
||||
rocksdb_transaction_begin(txn_db, woptions, txn_options, NULL);
|
||||
rocksdb_transaction_put(txn1, "bar1", 4, "1", 1, &err);
|
||||
CheckNoError(err);
|
||||
rocksdb_transaction_set_name(txn1, "txn1", 4, &err);
|
||||
CheckNoError(err);
|
||||
rocksdb_transaction_prepare(txn1, &err);
|
||||
CheckNoError(err);
|
||||
rocksdb_transaction_t* txn2 =
|
||||
rocksdb_transaction_begin(txn_db, woptions, txn_options, NULL);
|
||||
rocksdb_transaction_put(txn2, "bar2", 4, "2", 1, &err);
|
||||
CheckNoError(err);
|
||||
rocksdb_transaction_set_name(txn2, "txn2", 4, &err);
|
||||
CheckNoError(err);
|
||||
rocksdb_transaction_prepare(txn2, &err);
|
||||
CheckNoError(err);
|
||||
rocksdb_transaction_destroy(txn1);
|
||||
rocksdb_transaction_destroy(txn2);
|
||||
rocksdb_transactiondb_close(txn_db);
|
||||
rocksdb_transaction_options_destroy(txn_options);
|
||||
rocksdb_transactiondb_options_destroy(txn_db_options);
|
||||
|
||||
// reopen db and get all prepared.
|
||||
txn_db_options = rocksdb_transactiondb_options_create();
|
||||
txn_options = rocksdb_transaction_options_create();
|
||||
rocksdb_options_set_error_if_exists(options, 0);
|
||||
txn_db = rocksdb_transactiondb_open(options, txn_db_options, dbname, &err);
|
||||
CheckNoError(err);
|
||||
CheckTxnDBPinGet(txn_db, roptions, "bar1", NULL);
|
||||
CheckTxnDBPinGet(txn_db, roptions, "bar2", NULL);
|
||||
size_t cnt;
|
||||
rocksdb_transaction_t** txns =
|
||||
rocksdb_transactiondb_get_prepared_transactions(txn_db, &cnt);
|
||||
CheckCondition(cnt == 2);
|
||||
size_t i;
|
||||
for (i = 0; i < cnt; i++) {
|
||||
txn = txns[i];
|
||||
size_t name_len = 0;
|
||||
char* name = rocksdb_transaction_get_name(txn, &name_len);
|
||||
CheckCondition(name_len == 4);
|
||||
if (strncmp(name, "txn1", name_len) == 0) {
|
||||
rocksdb_transaction_commit(txn, &err);
|
||||
} else if (strncmp(name, "txn2", name_len) == 0) {
|
||||
rocksdb_transaction_rollback(txn, &err);
|
||||
}
|
||||
rocksdb_free(name);
|
||||
CheckNoError(err);
|
||||
rocksdb_transaction_destroy(txn);
|
||||
}
|
||||
rocksdb_free(txns);
|
||||
CheckTxnDBGet(txn_db, roptions, "bar1", "1");
|
||||
CheckTxnDBGet(txn_db, roptions, "bar2", NULL);
|
||||
rocksdb_transactiondb_put(txn_db, woptions, "bar2", 4, "2", 1, &err);
|
||||
CheckNoError(err);
|
||||
|
||||
// close and destroy
|
||||
rocksdb_transactiondb_close(txn_db);
|
||||
rocksdb_destroy_db(options, dbname, &err);
|
||||
CheckNoError(err);
|
||||
rocksdb_transaction_options_destroy(txn_options);
|
||||
rocksdb_transactiondb_options_destroy(txn_db_options);
|
||||
}
|
||||
|
||||
StartPhase("optimistic_transactions");
|
||||
{
|
||||
rocksdb_options_t* db_options = rocksdb_options_create();
|
||||
@@ -2758,6 +3202,7 @@ int main(int argc, char** argv) {
|
||||
rocksdb_transaction_put(txn2, "key1", 4, "value1", 6, &err);
|
||||
CheckNoError(err);
|
||||
CheckTxnGet(txn1, roptions, "key", "value");
|
||||
CheckTxnPinGet(txn1, roptions, "key", "value");
|
||||
rocksdb_transaction_commit(txn1, &err);
|
||||
CheckNoError(err);
|
||||
rocksdb_transaction_commit(txn2, &err);
|
||||
@@ -2784,6 +3229,19 @@ int main(int argc, char** argv) {
|
||||
txn);
|
||||
CheckGetCF(db, roptions, cfh1, "key_cf1", "val_cf1");
|
||||
CheckTxnGetCF(txn, roptions, cfh1, "key_cf1", "val_cf1");
|
||||
CheckTxnPinGetCF(txn, roptions, cfh1, "key_cf1", "val_cf1");
|
||||
{
|
||||
const rocksdb_column_family_handle_t* get_handles[3] = {cfh1, cfh2, cfh2};
|
||||
const char* keys[3] = {"key_cf1", "key_cf2", "notfound"};
|
||||
const size_t keys_sizes[3] = {7, 7, 8};
|
||||
char* vals[3];
|
||||
size_t vals_sizes[3];
|
||||
char* errs[3];
|
||||
const char* expected[3] = {"val_cf1", "val_cf2", NULL};
|
||||
rocksdb_transaction_multi_get_cf(txn, roptions, get_handles, 3, keys,
|
||||
keys_sizes, vals, vals_sizes, errs);
|
||||
CheckMultiGetValues(3, vals, vals_sizes, errs, expected);
|
||||
}
|
||||
|
||||
// Check iterator with column family
|
||||
rocksdb_transaction_put_cf(txn, cfh1, "key1_cf", 7, "val1_cf", 7, &err);
|
||||
@@ -2829,6 +3287,9 @@ int main(int argc, char** argv) {
|
||||
CheckTxnGetCF(txn_cf, roptions, cf_handles[0], "key", "value");
|
||||
CheckTxnGetCF(txn_cf, roptions, cf_handles[1], "key_cf1", "val_cf1");
|
||||
CheckTxnGetCF(txn_cf, roptions, cf_handles[2], "key_cf2", "val_cf2");
|
||||
CheckTxnPinGetCF(txn_cf, roptions, cf_handles[0], "key", "value");
|
||||
CheckTxnPinGetCF(txn_cf, roptions, cf_handles[1], "key_cf1", "val_cf1");
|
||||
CheckTxnPinGetCF(txn_cf, roptions, cf_handles[2], "key_cf2", "val_cf2");
|
||||
rocksdb_transaction_destroy(txn_cf);
|
||||
rocksdb_options_destroy(cf_options);
|
||||
rocksdb_column_family_handle_destroy(cf_handles[0]);
|
||||
|
||||
+55
-7
@@ -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(
|
||||
@@ -619,6 +623,26 @@ ColumnFamilyData::ColumnFamilyData(
|
||||
}
|
||||
|
||||
RecalculateWriteStallConditions(mutable_cf_options_);
|
||||
|
||||
if (cf_options.table_factory->IsInstanceOf(
|
||||
TableFactory::kBlockBasedTableName()) &&
|
||||
cf_options.table_factory->GetOptions<BlockBasedTableOptions>()) {
|
||||
const BlockBasedTableOptions* bbto =
|
||||
cf_options.table_factory->GetOptions<BlockBasedTableOptions>();
|
||||
const auto& options_overrides = bbto->cache_usage_options.options_overrides;
|
||||
const auto file_metadata_charged =
|
||||
options_overrides.at(CacheEntryRole::kFileMetadata).charged;
|
||||
if (bbto->block_cache &&
|
||||
file_metadata_charged == CacheEntryRoleOptions::Decision::kEnabled) {
|
||||
// TODO(hx235): Add a `ConcurrentCacheReservationManager` at DB scope
|
||||
// responsible for reservation of `ObsoleteFileInfo` so that we can keep
|
||||
// this `file_metadata_cache_res_mgr_` nonconcurrent
|
||||
file_metadata_cache_res_mgr_.reset(new ConcurrentCacheReservationManager(
|
||||
std::make_shared<
|
||||
CacheReservationManagerImpl<CacheEntryRole::kFileMetadata>>(
|
||||
bbto->block_cache)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// DB mutex held
|
||||
@@ -675,6 +699,19 @@ 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() {
|
||||
@@ -1279,9 +1316,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
|
||||
@@ -1471,13 +1517,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),
|
||||
@@ -1486,6 +1533,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_;
|
||||
@@ -1553,7 +1601,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);
|
||||
|
||||
+22
-8
@@ -14,6 +14,7 @@
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
#include "cache/cache_reservation_manager.h"
|
||||
#include "db/memtable_list.h"
|
||||
#include "db/table_cache.h"
|
||||
#include "db/table_properties_collector.h"
|
||||
@@ -46,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
|
||||
@@ -375,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
|
||||
@@ -478,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);
|
||||
|
||||
@@ -520,9 +519,17 @@ class ColumnFamilyData {
|
||||
|
||||
ThreadLocalPtr* TEST_GetLocalSV() { return local_sv_.get(); }
|
||||
WriteBufferManager* write_buffer_mgr() { return write_buffer_manager_; }
|
||||
std::shared_ptr<CacheReservationManager>
|
||||
GetFileMetadataCacheReservationManager() {
|
||||
return file_metadata_cache_res_mgr_;
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -534,7 +541,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;
|
||||
|
||||
@@ -558,6 +565,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_;
|
||||
|
||||
@@ -618,6 +626,11 @@ class ColumnFamilyData {
|
||||
bool db_paths_registered_;
|
||||
|
||||
std::string full_history_ts_low_;
|
||||
|
||||
// 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
|
||||
@@ -664,7 +677,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;
|
||||
@@ -726,6 +739,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_;
|
||||
};
|
||||
|
||||
|
||||
+24
-36
@@ -1419,16 +1419,15 @@ TEST_P(ColumnFamilyTest, MultipleManualCompactions) {
|
||||
WaitForFlush(1);
|
||||
AssertFilesPerLevel(std::to_string(i + 1), 1);
|
||||
}
|
||||
bool cf_1_1 = true;
|
||||
std::atomic_bool cf_1_1{true};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency(
|
||||
{{"ColumnFamilyTest::MultiManual:4", "ColumnFamilyTest::MultiManual:1"},
|
||||
{"ColumnFamilyTest::MultiManual:2", "ColumnFamilyTest::MultiManual:5"},
|
||||
{"ColumnFamilyTest::MultiManual:2", "ColumnFamilyTest::MultiManual:3"}});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::BackgroundCompaction:NonTrivial:AfterRun", [&](void* /*arg*/) {
|
||||
if (cf_1_1) {
|
||||
if (cf_1_1.exchange(false)) {
|
||||
TEST_SYNC_POINT("ColumnFamilyTest::MultiManual:4");
|
||||
cf_1_1 = false;
|
||||
TEST_SYNC_POINT("ColumnFamilyTest::MultiManual:3");
|
||||
}
|
||||
});
|
||||
@@ -1515,15 +1514,14 @@ TEST_P(ColumnFamilyTest, AutomaticAndManualCompactions) {
|
||||
auto stop_token =
|
||||
dbfull()->TEST_write_controler().GetCompactionPressureToken();
|
||||
|
||||
bool cf_1_1 = true;
|
||||
std::atomic_bool cf_1_1{true};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency(
|
||||
{{"ColumnFamilyTest::AutoManual:4", "ColumnFamilyTest::AutoManual:1"},
|
||||
{"ColumnFamilyTest::AutoManual:2", "ColumnFamilyTest::AutoManual:5"},
|
||||
{"ColumnFamilyTest::AutoManual:2", "ColumnFamilyTest::AutoManual:3"}});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::BackgroundCompaction:NonTrivial:AfterRun", [&](void* /*arg*/) {
|
||||
if (cf_1_1) {
|
||||
cf_1_1 = false;
|
||||
if (cf_1_1.exchange(false)) {
|
||||
TEST_SYNC_POINT("ColumnFamilyTest::AutoManual:4");
|
||||
TEST_SYNC_POINT("ColumnFamilyTest::AutoManual:3");
|
||||
}
|
||||
@@ -1618,21 +1616,19 @@ TEST_P(ColumnFamilyTest, ManualAndAutomaticCompactions) {
|
||||
WaitForFlush(1);
|
||||
AssertFilesPerLevel(std::to_string(i + 1), 1);
|
||||
}
|
||||
bool cf_1_1 = true;
|
||||
bool cf_1_2 = true;
|
||||
std::atomic_bool cf_1_1{true};
|
||||
std::atomic_bool cf_1_2{true};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency(
|
||||
{{"ColumnFamilyTest::ManualAuto:4", "ColumnFamilyTest::ManualAuto:1"},
|
||||
{"ColumnFamilyTest::ManualAuto:5", "ColumnFamilyTest::ManualAuto:2"},
|
||||
{"ColumnFamilyTest::ManualAuto:2", "ColumnFamilyTest::ManualAuto:3"}});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::BackgroundCompaction:NonTrivial:AfterRun", [&](void* /*arg*/) {
|
||||
if (cf_1_1) {
|
||||
if (cf_1_1.exchange(false)) {
|
||||
TEST_SYNC_POINT("ColumnFamilyTest::ManualAuto:4");
|
||||
cf_1_1 = false;
|
||||
TEST_SYNC_POINT("ColumnFamilyTest::ManualAuto:3");
|
||||
} else if (cf_1_2) {
|
||||
} else if (cf_1_2.exchange(false)) {
|
||||
TEST_SYNC_POINT("ColumnFamilyTest::ManualAuto:2");
|
||||
cf_1_2 = false;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1712,8 +1708,8 @@ TEST_P(ColumnFamilyTest, SameCFManualManualCompactions) {
|
||||
WaitForFlush(1);
|
||||
AssertFilesPerLevel(std::to_string(i + 1), 1);
|
||||
}
|
||||
bool cf_1_1 = true;
|
||||
bool cf_1_2 = true;
|
||||
std::atomic_bool cf_1_1{true};
|
||||
std::atomic_bool cf_1_2{true};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency(
|
||||
{{"ColumnFamilyTest::ManualManual:4", "ColumnFamilyTest::ManualManual:2"},
|
||||
{"ColumnFamilyTest::ManualManual:4", "ColumnFamilyTest::ManualManual:5"},
|
||||
@@ -1722,13 +1718,11 @@ TEST_P(ColumnFamilyTest, SameCFManualManualCompactions) {
|
||||
"ColumnFamilyTest::ManualManual:3"}});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::BackgroundCompaction:NonTrivial:AfterRun", [&](void* /*arg*/) {
|
||||
if (cf_1_1) {
|
||||
if (cf_1_1.exchange(false)) {
|
||||
TEST_SYNC_POINT("ColumnFamilyTest::ManualManual:4");
|
||||
cf_1_1 = false;
|
||||
TEST_SYNC_POINT("ColumnFamilyTest::ManualManual:3");
|
||||
} else if (cf_1_2) {
|
||||
} else if (cf_1_2.exchange(false)) {
|
||||
TEST_SYNC_POINT("ColumnFamilyTest::ManualManual:2");
|
||||
cf_1_2 = false;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1814,8 +1808,8 @@ TEST_P(ColumnFamilyTest, SameCFManualAutomaticCompactions) {
|
||||
WaitForFlush(1);
|
||||
AssertFilesPerLevel(std::to_string(i + 1), 1);
|
||||
}
|
||||
bool cf_1_1 = true;
|
||||
bool cf_1_2 = true;
|
||||
std::atomic_bool cf_1_1{true};
|
||||
std::atomic_bool cf_1_2{true};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency(
|
||||
{{"ColumnFamilyTest::ManualAuto:4", "ColumnFamilyTest::ManualAuto:2"},
|
||||
{"ColumnFamilyTest::ManualAuto:4", "ColumnFamilyTest::ManualAuto:5"},
|
||||
@@ -1823,13 +1817,11 @@ TEST_P(ColumnFamilyTest, SameCFManualAutomaticCompactions) {
|
||||
{"ColumnFamilyTest::ManualAuto:1", "ColumnFamilyTest::ManualAuto:3"}});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::BackgroundCompaction:NonTrivial:AfterRun", [&](void* /*arg*/) {
|
||||
if (cf_1_1) {
|
||||
if (cf_1_1.exchange(false)) {
|
||||
TEST_SYNC_POINT("ColumnFamilyTest::ManualAuto:4");
|
||||
cf_1_1 = false;
|
||||
TEST_SYNC_POINT("ColumnFamilyTest::ManualAuto:3");
|
||||
} else if (cf_1_2) {
|
||||
} else if (cf_1_2.exchange(false)) {
|
||||
TEST_SYNC_POINT("ColumnFamilyTest::ManualAuto:2");
|
||||
cf_1_2 = false;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1907,8 +1899,8 @@ TEST_P(ColumnFamilyTest, SameCFManualAutomaticCompactionsLevel) {
|
||||
WaitForFlush(1);
|
||||
AssertFilesPerLevel(std::to_string(i + 1), 1);
|
||||
}
|
||||
bool cf_1_1 = true;
|
||||
bool cf_1_2 = true;
|
||||
std::atomic_bool cf_1_1{true};
|
||||
std::atomic_bool cf_1_2{true};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency(
|
||||
{{"ColumnFamilyTest::ManualAuto:4", "ColumnFamilyTest::ManualAuto:2"},
|
||||
{"ColumnFamilyTest::ManualAuto:4", "ColumnFamilyTest::ManualAuto:5"},
|
||||
@@ -1918,13 +1910,11 @@ TEST_P(ColumnFamilyTest, SameCFManualAutomaticCompactionsLevel) {
|
||||
{"ColumnFamilyTest::ManualAuto:1", "ColumnFamilyTest::ManualAuto:3"}});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::BackgroundCompaction:NonTrivial:AfterRun", [&](void* /*arg*/) {
|
||||
if (cf_1_1) {
|
||||
if (cf_1_1.exchange(false)) {
|
||||
TEST_SYNC_POINT("ColumnFamilyTest::ManualAuto:4");
|
||||
cf_1_1 = false;
|
||||
TEST_SYNC_POINT("ColumnFamilyTest::ManualAuto:3");
|
||||
} else if (cf_1_2) {
|
||||
} else if (cf_1_2.exchange(false)) {
|
||||
TEST_SYNC_POINT("ColumnFamilyTest::ManualAuto:2");
|
||||
cf_1_2 = false;
|
||||
}
|
||||
});
|
||||
|
||||
@@ -1999,8 +1989,8 @@ TEST_P(ColumnFamilyTest, SameCFAutomaticManualCompactions) {
|
||||
auto stop_token =
|
||||
dbfull()->TEST_write_controler().GetCompactionPressureToken();
|
||||
|
||||
bool cf_1_1 = true;
|
||||
bool cf_1_2 = true;
|
||||
std::atomic_bool cf_1_1{true};
|
||||
std::atomic_bool cf_1_2{true};
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->LoadDependency(
|
||||
{{"ColumnFamilyTest::AutoManual:4", "ColumnFamilyTest::AutoManual:2"},
|
||||
{"ColumnFamilyTest::AutoManual:4", "ColumnFamilyTest::AutoManual:5"},
|
||||
@@ -2008,13 +1998,11 @@ TEST_P(ColumnFamilyTest, SameCFAutomaticManualCompactions) {
|
||||
"ColumnFamilyTest::AutoManual:3"}});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::BackgroundCompaction:NonTrivial:AfterRun", [&](void* /*arg*/) {
|
||||
if (cf_1_1) {
|
||||
if (cf_1_1.exchange(false)) {
|
||||
TEST_SYNC_POINT("ColumnFamilyTest::AutoManual:4");
|
||||
cf_1_1 = false;
|
||||
TEST_SYNC_POINT("ColumnFamilyTest::AutoManual:3");
|
||||
} else if (cf_1_2) {
|
||||
} else if (cf_1_2.exchange(false)) {
|
||||
TEST_SYNC_POINT("ColumnFamilyTest::AutoManual:2");
|
||||
cf_1_2 = false;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -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_;
|
||||
};
|
||||
|
||||
|
||||
+123
-10
@@ -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,10 @@ 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),
|
||||
start_level_(_inputs[0].level),
|
||||
output_level_(_output_level),
|
||||
@@ -231,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),
|
||||
@@ -239,8 +243,23 @@ Compaction::Compaction(
|
||||
is_manual_compaction_(_manual_compaction),
|
||||
trim_ts_(_trim_ts),
|
||||
is_trivial_move_(false),
|
||||
|
||||
compaction_reason_(_compaction_reason),
|
||||
notify_on_compaction_completion_(false) {
|
||||
notify_on_compaction_completion_(false),
|
||||
enable_blob_garbage_collection_(
|
||||
_blob_garbage_collection_policy == BlobGarbageCollectionPolicy::kForce
|
||||
? true
|
||||
: (_blob_garbage_collection_policy ==
|
||||
BlobGarbageCollectionPolicy::kDisable
|
||||
? false
|
||||
: mutable_cf_options()->enable_blob_garbage_collection)),
|
||||
blob_garbage_collection_age_cutoff_(
|
||||
_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),
|
||||
penultimate_level_(EvaluatePenultimateLevel(
|
||||
immutable_options_, start_level_, output_level_)) {
|
||||
MarkFilesBeingCompacted(true);
|
||||
if (is_manual_compaction_) {
|
||||
compaction_reason_ = CompactionReason::kManualCompaction;
|
||||
@@ -265,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() {
|
||||
@@ -276,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 =
|
||||
@@ -297,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;
|
||||
}
|
||||
|
||||
@@ -325,8 +410,8 @@ bool Compaction::IsTrivialMove() const {
|
||||
}
|
||||
|
||||
if (!(start_level_ != output_level_ && num_input_levels() == 1 &&
|
||||
input(0, 0)->fd.GetPathId() == output_path_id() &&
|
||||
InputCompressionMatchesOutput())) {
|
||||
input(0, 0)->fd.GetPathId() == output_path_id() &&
|
||||
InputCompressionMatchesOutput())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -637,8 +722,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;
|
||||
}
|
||||
|
||||
// TODO: will add public like `options.preclude_last_level_data_seconds` for
|
||||
// per_key_placement feature, will check that option here. Currently, only
|
||||
// set by unittest
|
||||
bool supports_per_key_placement = false;
|
||||
TEST_SYNC_POINT_CALLBACK("Compaction::SupportsPerKeyPlacement:Enabled",
|
||||
&supports_per_key_placement);
|
||||
if (!supports_per_key_placement) {
|
||||
return kInvalidLevel;
|
||||
}
|
||||
|
||||
return penultimate_level;
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
+106
-8
@@ -81,7 +81,11 @@ class Compaction {
|
||||
std::vector<FileMetaData*> grandparents,
|
||||
bool manual_compaction = false, const std::string& trim_ts = "",
|
||||
double score = -1, bool deletion_compaction = false,
|
||||
CompactionReason compaction_reason = CompactionReason::kUnknown);
|
||||
bool l0_files_might_overlap = true,
|
||||
CompactionReason compaction_reason = CompactionReason::kUnknown,
|
||||
BlobGarbageCollectionPolicy blob_garbage_collection_policy =
|
||||
BlobGarbageCollectionPolicy::kUseDefault,
|
||||
double blob_garbage_collection_age_cutoff = -1);
|
||||
|
||||
// No copying allowed
|
||||
Compaction(const Compaction&) = delete;
|
||||
@@ -174,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_; }
|
||||
|
||||
@@ -292,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_; }
|
||||
|
||||
@@ -306,6 +334,14 @@ class Compaction {
|
||||
|
||||
uint32_t max_subcompactions() const { return max_subcompactions_; }
|
||||
|
||||
bool enable_blob_garbage_collection() const {
|
||||
return enable_blob_garbage_collection_;
|
||||
}
|
||||
|
||||
double blob_garbage_collection_age_cutoff() const {
|
||||
return blob_garbage_collection_age_cutoff_;
|
||||
}
|
||||
|
||||
// start and end are sub compact range. Null if no boundary.
|
||||
// This is used to filter out some input files' ancester's time range.
|
||||
uint64_t MinInputFileOldestAncesterTime(const InternalKey* start,
|
||||
@@ -321,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);
|
||||
@@ -328,12 +373,24 @@ 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
|
||||
// https://github.com/facebook/rocksdb/pull/4432#discussion_r221072219 and plumb
|
||||
// down appropriate key boundaries to RangeDelAggregator during compaction.
|
||||
// https://github.com/facebook/rocksdb/pull/4432#discussion_r221072219 and
|
||||
// plumb down appropriate key boundaries to RangeDelAggregator during
|
||||
// compaction.
|
||||
static std::vector<CompactionInputFiles> PopulateWithAtomicBoundaries(
|
||||
VersionStorageInfo* vstorage, std::vector<CompactionInputFiles> inputs);
|
||||
|
||||
@@ -348,7 +405,7 @@ class Compaction {
|
||||
|
||||
VersionStorageInfo* input_vstorage_;
|
||||
|
||||
const int start_level_; // the lowest level to be compacted
|
||||
const int start_level_; // the lowest level to be compacted
|
||||
const int output_level_; // levels to which output files are stored
|
||||
uint64_t max_output_file_size_;
|
||||
uint64_t max_compaction_bytes_;
|
||||
@@ -359,7 +416,7 @@ class Compaction {
|
||||
VersionEdit edit_;
|
||||
const int number_levels_;
|
||||
ColumnFamilyData* cfd_;
|
||||
Arena arena_; // Arena used to allocate space for file_levels_
|
||||
Arena arena_; // Arena used to allocate space for file_levels_
|
||||
|
||||
const uint32_t output_path_id_;
|
||||
CompressionType output_compression_;
|
||||
@@ -367,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_;
|
||||
@@ -377,7 +441,7 @@ class Compaction {
|
||||
// State used to check for number of overlapping grandparent files
|
||||
// (grandparent == "output_level_ + 1")
|
||||
std::vector<FileMetaData*> grandparents_;
|
||||
const double score_; // score that was used to pick this compaction.
|
||||
const double score_; // score that was used to pick this compaction.
|
||||
|
||||
// Is this compaction creating a file in the bottom most level?
|
||||
const bool bottommost_level_;
|
||||
@@ -412,8 +476,42 @@ class Compaction {
|
||||
// Notify on compaction completion only if listener was notified on compaction
|
||||
// begin.
|
||||
bool notify_on_compaction_completion_;
|
||||
|
||||
// Enable/disable GC collection for blobs during compaction.
|
||||
bool enable_blob_garbage_collection_;
|
||||
|
||||
// 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);
|
||||
|
||||
|
||||
@@ -28,11 +28,10 @@ CompactionIterator::CompactionIterator(
|
||||
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 Compaction* compaction,
|
||||
const CompactionFilter* compaction_filter,
|
||||
bool enforce_single_del_contracts,
|
||||
const std::atomic<bool>& manual_compaction_canceled,
|
||||
const Compaction* compaction, const CompactionFilter* compaction_filter,
|
||||
const std::atomic<bool>* shutting_down,
|
||||
const std::atomic<int>* manual_compaction_paused,
|
||||
const std::atomic<bool>* manual_compaction_canceled,
|
||||
const std::shared_ptr<Logger> info_log,
|
||||
const std::string* full_history_ts_low)
|
||||
: CompactionIterator(
|
||||
@@ -40,10 +39,10 @@ CompactionIterator::CompactionIterator(
|
||||
earliest_write_conflict_snapshot, job_snapshot, snapshot_checker, env,
|
||||
report_detailed_time, expect_valid_internal_key, range_del_agg,
|
||||
blob_file_builder, allow_data_in_errors, enforce_single_del_contracts,
|
||||
manual_compaction_canceled,
|
||||
std::unique_ptr<CompactionProxy>(
|
||||
compaction ? new RealCompaction(compaction) : nullptr),
|
||||
compaction_filter, shutting_down, manual_compaction_paused,
|
||||
manual_compaction_canceled, info_log, full_history_ts_low) {}
|
||||
compaction_filter, shutting_down, info_log, full_history_ts_low) {}
|
||||
|
||||
CompactionIterator::CompactionIterator(
|
||||
InternalIterator* input, const Comparator* cmp, MergeHelper* merge_helper,
|
||||
@@ -54,11 +53,10 @@ CompactionIterator::CompactionIterator(
|
||||
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,
|
||||
const std::atomic<bool>* shutting_down,
|
||||
const std::atomic<int>* manual_compaction_paused,
|
||||
const std::atomic<bool>* manual_compaction_canceled,
|
||||
const std::shared_ptr<Logger> info_log,
|
||||
const std::string* full_history_ts_low)
|
||||
: input_(input, cmp,
|
||||
@@ -78,7 +76,6 @@ CompactionIterator::CompactionIterator(
|
||||
compaction_(std::move(compaction)),
|
||||
compaction_filter_(compaction_filter),
|
||||
shutting_down_(shutting_down),
|
||||
manual_compaction_paused_(manual_compaction_paused),
|
||||
manual_compaction_canceled_(manual_compaction_canceled),
|
||||
info_log_(info_log),
|
||||
allow_data_in_errors_(allow_data_in_errors),
|
||||
@@ -160,10 +157,12 @@ void CompactionIterator::Next() {
|
||||
Status s = ParseInternalKey(key_, &ikey_, allow_data_in_errors_);
|
||||
// MergeUntil stops when it encounters a corrupt key and does not
|
||||
// include them in the result, so we expect the keys here to be valid.
|
||||
assert(s.ok());
|
||||
if (!s.ok()) {
|
||||
ROCKS_LOG_FATAL(info_log_, "Invalid key in compaction. %s",
|
||||
s.getState());
|
||||
ROCKS_LOG_FATAL(
|
||||
info_log_, "Invalid ikey %s in compaction. %s",
|
||||
allow_data_in_errors_ ? key_.ToString(true).c_str() : "hidden",
|
||||
s.getState());
|
||||
assert(false);
|
||||
}
|
||||
|
||||
// Keep current_key_ in sync.
|
||||
@@ -198,6 +197,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;
|
||||
@@ -520,21 +520,22 @@ 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.)
|
||||
assert(ikey_.type == kTypeValue || ikey_.type == kTypeBlobIndex);
|
||||
if (ikey_.type != kTypeValue && ikey_.type != kTypeBlobIndex) {
|
||||
ROCKS_LOG_FATAL(info_log_,
|
||||
"Unexpected key type %d for compaction output",
|
||||
ikey_.type);
|
||||
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);
|
||||
}
|
||||
assert(current_user_key_snapshot_ >= last_snapshot);
|
||||
if (current_user_key_snapshot_ < last_snapshot) {
|
||||
ROCKS_LOG_FATAL(info_log_,
|
||||
"current_user_key_snapshot_ (%" PRIu64
|
||||
"key %s, current_user_key_snapshot_ (%" PRIu64
|
||||
") < last_snapshot (%" PRIu64 ")",
|
||||
ikey_.DebugString(allow_data_in_errors_, true).c_str(),
|
||||
current_user_key_snapshot_, last_snapshot);
|
||||
assert(false);
|
||||
}
|
||||
|
||||
if (ikey_.type == kTypeBlobIndex) {
|
||||
if (ikey_.type == kTypeBlobIndex || ikey_.type == kTypeWideColumnEntity) {
|
||||
ikey_.type = kTypeValue;
|
||||
current_key_.UpdateInternalKey(ikey_.sequence, ikey_.type);
|
||||
}
|
||||
@@ -690,7 +691,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;
|
||||
}
|
||||
|
||||
@@ -770,12 +772,13 @@ void CompactionIterator::NextFromInput() {
|
||||
// Note: Dropping this key will not affect TransactionDB write-conflict
|
||||
// checking since there has already been a record returned for this key
|
||||
// in this snapshot.
|
||||
assert(last_sequence >= current_user_key_sequence_);
|
||||
if (last_sequence < current_user_key_sequence_) {
|
||||
ROCKS_LOG_FATAL(info_log_,
|
||||
"last_sequence (%" PRIu64
|
||||
"key %s, last_sequence (%" PRIu64
|
||||
") < current_user_key_sequence_ (%" PRIu64 ")",
|
||||
ikey_.DebugString(allow_data_in_errors_, true).c_str(),
|
||||
last_sequence, current_user_key_sequence_);
|
||||
assert(false);
|
||||
}
|
||||
|
||||
++iter_stats_.num_record_drop_hidden; // rule (A)
|
||||
@@ -887,10 +890,12 @@ void CompactionIterator::NextFromInput() {
|
||||
pik_status = ParseInternalKey(key_, &ikey_, allow_data_in_errors_);
|
||||
// MergeUntil stops when it encounters a corrupt key and does not
|
||||
// include them in the result, so we expect the keys here to valid.
|
||||
assert(pik_status.ok());
|
||||
if (!pik_status.ok()) {
|
||||
ROCKS_LOG_FATAL(info_log_, "Invalid key in compaction. %s",
|
||||
pik_status.getState());
|
||||
ROCKS_LOG_FATAL(
|
||||
info_log_, "Invalid key %s in compaction. %s",
|
||||
allow_data_in_errors_ ? key_.ToString(true).c_str() : "hidden",
|
||||
pik_status.getState());
|
||||
assert(false);
|
||||
}
|
||||
// Keep current_key_ in sync.
|
||||
current_key_.UpdateInternalKey(ikey_.sequence, ikey_.type);
|
||||
@@ -1070,6 +1075,52 @@ void CompactionIterator::GarbageCollectBlobIfNeeded() {
|
||||
}
|
||||
}
|
||||
|
||||
void CompactionIterator::DecideOutputLevel() {
|
||||
#ifndef NDEBUG
|
||||
// TODO: will be set by sequence number or key range, for now, it will only be
|
||||
// set 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 within the earliest snapshot, it has to output to the
|
||||
// penultimate level.
|
||||
if (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) {
|
||||
@@ -1078,6 +1129,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
|
||||
@@ -1092,16 +1147,24 @@ void CompactionIterator::PrepareOutput() {
|
||||
if (valid_ && compaction_ != nullptr &&
|
||||
!compaction_->allow_ingest_behind() && bottommost_level_ &&
|
||||
DefinitelyInSnapshot(ikey_.sequence, earliest_snapshot_) &&
|
||||
ikey_.type != kTypeMerge && current_key_committed_) {
|
||||
assert(ikey_.type != kTypeDeletion);
|
||||
assert(ikey_.type != kTypeSingleDeletion ||
|
||||
(timestamp_size_ || full_history_ts_low_));
|
||||
ikey_.type != kTypeMerge && current_key_committed_ &&
|
||||
!output_to_penultimate_level_) {
|
||||
if (ikey_.type == kTypeDeletion ||
|
||||
(ikey_.type == kTypeSingleDeletion &&
|
||||
(!timestamp_size_ || !full_history_ts_low_))) {
|
||||
ROCKS_LOG_FATAL(info_log_,
|
||||
"Unexpected key type %d for seq-zero optimization",
|
||||
ikey_.type);
|
||||
(ikey_.type == kTypeSingleDeletion && timestamp_size_ == 0)) {
|
||||
ROCKS_LOG_FATAL(
|
||||
info_log_,
|
||||
"Unexpected key %s for seq-zero optimization. "
|
||||
"earliest_snapshot %" PRIu64
|
||||
", earliest_write_conflict_snapshot %" PRIu64
|
||||
" job_snapshot %" PRIu64
|
||||
". timestamp_size: %d full_history_ts_low_ %s",
|
||||
ikey_.DebugString(allow_data_in_errors_, true).c_str(),
|
||||
earliest_snapshot_, earliest_write_conflict_snapshot_,
|
||||
job_snapshot_, static_cast<int>(timestamp_size_),
|
||||
full_history_ts_low_ != nullptr
|
||||
? Slice(*full_history_ts_low_).ToString(true).c_str()
|
||||
: "null");
|
||||
assert(false);
|
||||
}
|
||||
ikey_.sequence = 0;
|
||||
last_key_seq_zeroed_ = true;
|
||||
@@ -1132,14 +1195,17 @@ inline SequenceNumber CompactionIterator::findEarliestVisibleSnapshot(
|
||||
}
|
||||
auto snapshots_iter = std::lower_bound(
|
||||
snapshots_->begin(), snapshots_->end(), in);
|
||||
assert(prev_snapshot != nullptr);
|
||||
if (snapshots_iter == snapshots_->begin()) {
|
||||
*prev_snapshot = 0;
|
||||
} else {
|
||||
*prev_snapshot = *std::prev(snapshots_iter);
|
||||
assert(*prev_snapshot < in);
|
||||
if (*prev_snapshot >= in) {
|
||||
ROCKS_LOG_FATAL(info_log_,
|
||||
"*prev_snapshot >= in in findEarliestVisibleSnapshot");
|
||||
"*prev_snapshot (%" PRIu64 ") >= in (%" PRIu64
|
||||
") in findEarliestVisibleSnapshot",
|
||||
*prev_snapshot, in);
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
if (snapshot_checker_ == nullptr) {
|
||||
@@ -1149,9 +1215,12 @@ inline SequenceNumber CompactionIterator::findEarliestVisibleSnapshot(
|
||||
bool has_released_snapshot = !released_snapshots_.empty();
|
||||
for (; snapshots_iter != snapshots_->end(); ++snapshots_iter) {
|
||||
auto cur = *snapshots_iter;
|
||||
assert(in <= cur);
|
||||
if (in > cur) {
|
||||
ROCKS_LOG_FATAL(info_log_, "in > cur in findEarliestVisibleSnapshot");
|
||||
ROCKS_LOG_FATAL(info_log_,
|
||||
"in (%" PRIu64 ") > cur (%" PRIu64
|
||||
") in findEarliestVisibleSnapshot",
|
||||
in, cur);
|
||||
assert(false);
|
||||
}
|
||||
// Skip if cur is in released_snapshots.
|
||||
if (has_released_snapshot && released_snapshots_.count(cur) > 0) {
|
||||
|
||||
@@ -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 {
|
||||
@@ -142,12 +146,11 @@ class CompactionIterator {
|
||||
}
|
||||
|
||||
bool enable_blob_garbage_collection() const override {
|
||||
return compaction_->mutable_cf_options()->enable_blob_garbage_collection;
|
||||
return compaction_->enable_blob_garbage_collection();
|
||||
}
|
||||
|
||||
double blob_garbage_collection_age_cutoff() const override {
|
||||
return compaction_->mutable_cf_options()
|
||||
->blob_garbage_collection_age_cutoff;
|
||||
return compaction_->blob_garbage_collection_age_cutoff();
|
||||
}
|
||||
|
||||
uint64_t blob_compaction_readahead_size() const override {
|
||||
@@ -164,43 +167,56 @@ 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 Compaction* compaction = nullptr,
|
||||
const CompactionFilter* compaction_filter = nullptr,
|
||||
const std::atomic<bool>* shutting_down = nullptr,
|
||||
const std::atomic<int>* manual_compaction_paused = nullptr,
|
||||
const std::atomic<bool>* manual_compaction_canceled = 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);
|
||||
|
||||
// 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,
|
||||
std::unique_ptr<CompactionProxy> compaction,
|
||||
const CompactionFilter* compaction_filter = nullptr,
|
||||
const std::atomic<bool>* shutting_down = nullptr,
|
||||
const std::atomic<int>* manual_compaction_paused = nullptr,
|
||||
const std::atomic<bool>* manual_compaction_canceled = 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);
|
||||
|
||||
~CompactionIterator();
|
||||
|
||||
@@ -225,6 +241,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
|
||||
@@ -233,6 +255,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
|
||||
@@ -321,8 +347,7 @@ class CompactionIterator {
|
||||
std::unique_ptr<CompactionProxy> compaction_;
|
||||
const CompactionFilter* compaction_filter_;
|
||||
const std::atomic<bool>* shutting_down_;
|
||||
const std::atomic<int>* manual_compaction_paused_;
|
||||
const std::atomic<bool>* manual_compaction_canceled_;
|
||||
const std::atomic<bool>& manual_compaction_canceled_;
|
||||
bool bottommost_level_;
|
||||
bool valid_ = false;
|
||||
bool visible_at_tip_;
|
||||
@@ -416,6 +441,11 @@ 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};
|
||||
|
||||
void AdvanceInputIter() { input_.Next(); }
|
||||
|
||||
void SkipUntil(const Slice& skip_until) { input_.Seek(skip_until); }
|
||||
@@ -427,10 +457,7 @@ class CompactionIterator {
|
||||
|
||||
bool IsPausingManualCompaction() {
|
||||
// This is a best-effort facility, so memory_order_relaxed is sufficient.
|
||||
return (manual_compaction_paused_ &&
|
||||
manual_compaction_paused_->load(std::memory_order_relaxed) > 0) ||
|
||||
(manual_compaction_canceled_ &&
|
||||
manual_compaction_canceled_->load(std::memory_order_relaxed));
|
||||
return manual_compaction_canceled_.load(std::memory_order_relaxed);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -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();
|
||||
@@ -279,10 +290,9 @@ class CompactionIteratorTest : public testing::TestWithParam<bool> {
|
||||
snapshot_checker_.get(), Env::Default(),
|
||||
false /* report_detailed_time */, false, range_del_agg_.get(),
|
||||
nullptr /* blob_file_builder */, true /*allow_data_in_errors*/,
|
||||
true /*enforce_single_del_contracts*/, std::move(compaction), filter,
|
||||
&shutting_down_,
|
||||
/*manual_compaction_paused=*/nullptr,
|
||||
/*manual_compaction_canceled=*/nullptr, /*info_log=*/nullptr,
|
||||
true /*enforce_single_del_contracts*/,
|
||||
/*manual_compaction_canceled=*/kManualCompactionCanceledFalse_,
|
||||
std::move(compaction), filter, &shutting_down_, /*info_log=*/nullptr,
|
||||
full_history_ts_low));
|
||||
}
|
||||
|
||||
@@ -296,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,
|
||||
@@ -341,6 +353,7 @@ class CompactionIteratorTest : public testing::TestWithParam<bool> {
|
||||
std::unique_ptr<CompactionRangeDelAggregator> range_del_agg_;
|
||||
std::unique_ptr<SnapshotChecker> snapshot_checker_;
|
||||
std::atomic<bool> shutting_down_{false};
|
||||
const std::atomic<bool> kManualCompactionCanceledFalse_{false};
|
||||
FakeCompaction* compaction_proxy_;
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
|
||||
+223
-1564
File diff suppressed because it is too large.
Load diff
@@ -20,6 +20,7 @@
|
||||
#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"
|
||||
@@ -47,6 +48,7 @@
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
class Arena;
|
||||
class CompactionState;
|
||||
class ErrorHandler;
|
||||
class MemTable;
|
||||
class SnapshotChecker;
|
||||
@@ -56,11 +58,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(
|
||||
@@ -78,8 +160,7 @@ class CompactionJob {
|
||||
bool paranoid_file_checks, bool measure_io_stats,
|
||||
const std::string& dbname, CompactionJobStats* compaction_job_stats,
|
||||
Env::Priority thread_pri, const std::shared_ptr<IOTracer>& io_tracer,
|
||||
const std::atomic<int>* manual_compaction_paused = nullptr,
|
||||
const std::atomic<bool>* manual_compaction_canceled = nullptr,
|
||||
const std::atomic<bool>& manual_compaction_canceled,
|
||||
const std::string& db_id = "", const std::string& db_session_id = "",
|
||||
std::string full_history_ts_low = "", std::string trim_ts = "",
|
||||
BlobFileCompletionCallback* blob_callback = nullptr);
|
||||
@@ -108,11 +189,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();
|
||||
@@ -123,7 +199,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_;
|
||||
@@ -136,6 +212,8 @@ class CompactionJob {
|
||||
|
||||
IOStatus io_status_;
|
||||
|
||||
CompactionJobStats* compaction_job_stats_;
|
||||
|
||||
private:
|
||||
friend class CompactionJobTestBase;
|
||||
|
||||
@@ -151,15 +229,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,
|
||||
@@ -168,20 +245,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_;
|
||||
@@ -195,8 +264,7 @@ class CompactionJob {
|
||||
FileOptions file_options_for_read_;
|
||||
VersionSet* versions_;
|
||||
const std::atomic<bool>* shutting_down_;
|
||||
const std::atomic<int>* manual_compaction_paused_;
|
||||
const std::atomic<bool>* manual_compaction_canceled_;
|
||||
const std::atomic<bool>& manual_compaction_canceled_;
|
||||
FSDirectory* db_directory_;
|
||||
FSDirectory* blob_output_directory_;
|
||||
InstrumentedMutex* db_mutex_;
|
||||
@@ -224,14 +292,12 @@ 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;
|
||||
|
||||
// Get table file name in where it's outputting to, which should also be in
|
||||
// `output_directory_`.
|
||||
@@ -267,7 +333,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,9 +422,9 @@ class CompactionServiceCompactionJob : private CompactionJob {
|
||||
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::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);
|
||||
|
||||
|
||||
@@ -69,12 +69,138 @@ void VerifyInitializationOfCompactionJobStats(
|
||||
#endif // !defined(IOS_CROSS_COMPILE)
|
||||
}
|
||||
|
||||
// Mock FSWritableFile for testing io priority.
|
||||
// Only override the essential functions for testing compaction io priority.
|
||||
class MockTestWritableFile : public FSWritableFileOwnerWrapper {
|
||||
public:
|
||||
MockTestWritableFile(std::unique_ptr<FSWritableFile>&& file,
|
||||
Env::IOPriority io_priority)
|
||||
: FSWritableFileOwnerWrapper(std::move(file)),
|
||||
write_io_priority_(io_priority) {}
|
||||
IOStatus Append(const Slice& data, const IOOptions& options,
|
||||
IODebugContext* dbg) override {
|
||||
EXPECT_EQ(options.rate_limiter_priority, write_io_priority_);
|
||||
return target()->Append(data, options, dbg);
|
||||
}
|
||||
IOStatus Append(const Slice& data, const IOOptions& options,
|
||||
const DataVerificationInfo& verification_info,
|
||||
IODebugContext* dbg) override {
|
||||
EXPECT_EQ(options.rate_limiter_priority, write_io_priority_);
|
||||
return target()->Append(data, options, verification_info, dbg);
|
||||
}
|
||||
IOStatus Close(const IOOptions& options, IODebugContext* dbg) override {
|
||||
EXPECT_EQ(options.rate_limiter_priority, write_io_priority_);
|
||||
return target()->Close(options, dbg);
|
||||
}
|
||||
IOStatus Flush(const IOOptions& options, IODebugContext* dbg) override {
|
||||
EXPECT_EQ(options.rate_limiter_priority, write_io_priority_);
|
||||
return target()->Flush(options, dbg);
|
||||
}
|
||||
IOStatus Sync(const IOOptions& options, IODebugContext* dbg) override {
|
||||
EXPECT_EQ(options.rate_limiter_priority, write_io_priority_);
|
||||
return target()->Sync(options, dbg);
|
||||
}
|
||||
IOStatus Fsync(const IOOptions& options, IODebugContext* dbg) override {
|
||||
EXPECT_EQ(options.rate_limiter_priority, write_io_priority_);
|
||||
return target()->Fsync(options, dbg);
|
||||
}
|
||||
uint64_t GetFileSize(const IOOptions& options, IODebugContext* dbg) override {
|
||||
EXPECT_EQ(options.rate_limiter_priority, write_io_priority_);
|
||||
return target()->GetFileSize(options, dbg);
|
||||
}
|
||||
IOStatus RangeSync(uint64_t offset, uint64_t nbytes, const IOOptions& options,
|
||||
IODebugContext* dbg) override {
|
||||
EXPECT_EQ(options.rate_limiter_priority, write_io_priority_);
|
||||
return target()->RangeSync(offset, nbytes, options, dbg);
|
||||
}
|
||||
|
||||
void PrepareWrite(size_t offset, size_t len, const IOOptions& options,
|
||||
IODebugContext* dbg) override {
|
||||
EXPECT_EQ(options.rate_limiter_priority, write_io_priority_);
|
||||
target()->PrepareWrite(offset, len, options, dbg);
|
||||
}
|
||||
|
||||
IOStatus Allocate(uint64_t offset, uint64_t len, const IOOptions& options,
|
||||
IODebugContext* dbg) override {
|
||||
EXPECT_EQ(options.rate_limiter_priority, write_io_priority_);
|
||||
return target()->Allocate(offset, len, options, dbg);
|
||||
}
|
||||
|
||||
private:
|
||||
Env::IOPriority write_io_priority_;
|
||||
};
|
||||
|
||||
// Mock FSRandomAccessFile for testing io priority.
|
||||
// Only override the essential functions for testing compaction io priority.
|
||||
class MockTestRandomAccessFile : public FSRandomAccessFileOwnerWrapper {
|
||||
public:
|
||||
MockTestRandomAccessFile(std::unique_ptr<FSRandomAccessFile>&& file,
|
||||
Env::IOPriority io_priority)
|
||||
: FSRandomAccessFileOwnerWrapper(std::move(file)),
|
||||
read_io_priority_(io_priority) {}
|
||||
|
||||
IOStatus Read(uint64_t offset, size_t n, const IOOptions& options,
|
||||
Slice* result, char* scratch,
|
||||
IODebugContext* dbg) const override {
|
||||
EXPECT_EQ(options.rate_limiter_priority, read_io_priority_);
|
||||
return target()->Read(offset, n, options, result, scratch, dbg);
|
||||
}
|
||||
IOStatus Prefetch(uint64_t offset, size_t n, const IOOptions& options,
|
||||
IODebugContext* dbg) override {
|
||||
EXPECT_EQ(options.rate_limiter_priority, read_io_priority_);
|
||||
return target()->Prefetch(offset, n, options, dbg);
|
||||
}
|
||||
|
||||
private:
|
||||
Env::IOPriority read_io_priority_;
|
||||
};
|
||||
|
||||
// Mock FileSystem for testing io priority.
|
||||
class MockTestFileSystem : public FileSystemWrapper {
|
||||
public:
|
||||
explicit MockTestFileSystem(const std::shared_ptr<FileSystem>& base,
|
||||
Env::IOPriority read_io_priority,
|
||||
Env::IOPriority write_io_priority)
|
||||
: FileSystemWrapper(base),
|
||||
read_io_priority_(read_io_priority),
|
||||
write_io_priority_(write_io_priority) {}
|
||||
|
||||
static const char* kClassName() { return "MockTestFileSystem"; }
|
||||
const char* Name() const override { return kClassName(); }
|
||||
|
||||
IOStatus NewRandomAccessFile(const std::string& fname,
|
||||
const FileOptions& file_opts,
|
||||
std::unique_ptr<FSRandomAccessFile>* result,
|
||||
IODebugContext* dbg) override {
|
||||
IOStatus s = target()->NewRandomAccessFile(fname, file_opts, result, dbg);
|
||||
EXPECT_OK(s);
|
||||
result->reset(
|
||||
new MockTestRandomAccessFile(std::move(*result), read_io_priority_));
|
||||
return s;
|
||||
}
|
||||
IOStatus NewWritableFile(const std::string& fname,
|
||||
const FileOptions& file_opts,
|
||||
std::unique_ptr<FSWritableFile>* result,
|
||||
IODebugContext* dbg) override {
|
||||
IOStatus s = target()->NewWritableFile(fname, file_opts, result, dbg);
|
||||
EXPECT_OK(s);
|
||||
result->reset(
|
||||
new MockTestWritableFile(std::move(*result), write_io_priority_));
|
||||
return s;
|
||||
}
|
||||
|
||||
private:
|
||||
Env::IOPriority read_io_priority_;
|
||||
Env::IOPriority write_io_priority_;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
class CompactionJobTestBase : public testing::Test {
|
||||
protected:
|
||||
CompactionJobTestBase(std::string dbname, const Comparator* ucmp,
|
||||
std::function<std::string(uint64_t)> encode_u64_ts)
|
||||
std::function<std::string(uint64_t)> encode_u64_ts,
|
||||
bool test_io_priority)
|
||||
: dbname_(std::move(dbname)),
|
||||
ucmp_(ucmp),
|
||||
db_options_(),
|
||||
@@ -82,15 +208,16 @@ 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_),
|
||||
encode_u64_ts_(std::move(encode_u64_ts)) {
|
||||
encode_u64_ts_(std::move(encode_u64_ts)),
|
||||
test_io_priority_(test_io_priority) {
|
||||
Env* base_env = Env::Default();
|
||||
EXPECT_OK(
|
||||
test::CreateEnvFromSystem(ConfigOptions(), &base_env, &env_guard_));
|
||||
@@ -105,7 +232,12 @@ class CompactionJobTestBase : public testing::Test {
|
||||
db_options_.db_paths.emplace_back(dbname_,
|
||||
std::numeric_limits<uint64_t>::max());
|
||||
cf_options_.comparator = ucmp_;
|
||||
cf_options_.table_factory = mock_table_factory_;
|
||||
if (test_io_priority_) {
|
||||
BlockBasedTableOptions table_options;
|
||||
cf_options_.table_factory.reset(NewBlockBasedTableFactory(table_options));
|
||||
} else {
|
||||
cf_options_.table_factory = mock_table_factory_;
|
||||
}
|
||||
}
|
||||
|
||||
std::string GenerateFileName(uint64_t file_number) {
|
||||
@@ -145,6 +277,33 @@ class CompactionJobTestBase : public testing::Test {
|
||||
return blob_index;
|
||||
}
|
||||
|
||||
// Creates a table with the specificied key value pairs.
|
||||
void CreateTable(const std::string& table_name,
|
||||
const mock::KVVector& contents, uint64_t& file_size) {
|
||||
std::unique_ptr<WritableFileWriter> file_writer;
|
||||
Status s = WritableFileWriter::Create(fs_, table_name, FileOptions(),
|
||||
&file_writer, nullptr);
|
||||
ASSERT_OK(s);
|
||||
std::unique_ptr<TableBuilder> table_builder(
|
||||
cf_options_.table_factory->NewTableBuilder(
|
||||
TableBuilderOptions(*cfd_->ioptions(), mutable_cf_options_,
|
||||
cfd_->internal_comparator(),
|
||||
cfd_->int_tbl_prop_collector_factories(),
|
||||
CompressionType::kNoCompression,
|
||||
CompressionOptions(), 0 /* column_family_id */,
|
||||
kDefaultColumnFamilyName, -1 /* level */),
|
||||
file_writer.get()));
|
||||
// Build table.
|
||||
for (auto kv : contents) {
|
||||
std::string key;
|
||||
std::string value;
|
||||
std::tie(key, value) = kv;
|
||||
table_builder->Add(key, value);
|
||||
}
|
||||
ASSERT_OK(table_builder->Finish());
|
||||
file_size = table_builder->FileSize();
|
||||
}
|
||||
|
||||
void AddMockFile(const mock::KVVector& contents, int level = 0) {
|
||||
assert(contents.size() > 0);
|
||||
|
||||
@@ -198,11 +357,18 @@ class CompactionJobTestBase : public testing::Test {
|
||||
}
|
||||
|
||||
uint64_t file_number = versions_->NewFileNumber();
|
||||
EXPECT_OK(mock_table_factory_->CreateMockTable(
|
||||
env_, GenerateFileName(file_number), std::move(contents)));
|
||||
|
||||
uint64_t file_size;
|
||||
if (test_io_priority_) {
|
||||
CreateTable(GenerateFileName(file_number), contents, file_size);
|
||||
} else {
|
||||
file_size = 10;
|
||||
EXPECT_OK(mock_table_factory_->CreateMockTable(
|
||||
env_, GenerateFileName(file_number), std::move(contents)));
|
||||
}
|
||||
|
||||
VersionEdit edit;
|
||||
edit.AddFile(level, file_number, 0, 10, smallest_key, largest_key,
|
||||
edit.AddFile(level, file_number, 0, file_size, smallest_key, largest_key,
|
||||
smallest_seqno, largest_seqno, false, Temperature::kUnknown,
|
||||
oldest_blob_file_number, kUnknownOldestAncesterTime,
|
||||
kUnknownFileCreationTime, kUnknownFileChecksum,
|
||||
@@ -278,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_));
|
||||
|
||||
@@ -316,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,
|
||||
@@ -323,7 +500,15 @@ class CompactionJobTestBase : public testing::Test {
|
||||
SequenceNumber earliest_write_conflict_snapshot = kMaxSequenceNumber,
|
||||
int output_level = 1, bool verify = true,
|
||||
uint64_t expected_oldest_blob_file_number = kInvalidBlobFileNumber,
|
||||
bool check_get_priority = false) {
|
||||
bool check_get_priority = false,
|
||||
Env::IOPriority read_io_priority = Env::IO_TOTAL,
|
||||
Env::IOPriority write_io_priority = Env::IO_TOTAL) {
|
||||
// For compaction, set fs as MockTestFileSystem to check the io_priority.
|
||||
if (test_io_priority_) {
|
||||
db_options_.fs.reset(
|
||||
new MockTestFileSystem(fs_, read_io_priority, write_io_priority));
|
||||
}
|
||||
|
||||
auto cfd = versions_->GetColumnFamilySet()->GetDefault();
|
||||
|
||||
size_t num_input_files = 0;
|
||||
@@ -353,6 +538,7 @@ class CompactionJobTestBase : public testing::Test {
|
||||
SnapshotChecker* snapshot_checker = nullptr;
|
||||
ASSERT_TRUE(full_history_ts_low_.empty() ||
|
||||
ucmp_->timestamp_size() == full_history_ts_low_.size());
|
||||
const std::atomic<bool> kManualCompactionCanceledFalse{false};
|
||||
CompactionJob compaction_job(
|
||||
0, &compaction, db_options_, mutable_db_options_, env_options_,
|
||||
versions_.get(), &shutting_down_, &log_buffer, nullptr, nullptr,
|
||||
@@ -360,9 +546,9 @@ class CompactionJobTestBase : public testing::Test {
|
||||
earliest_write_conflict_snapshot, snapshot_checker, nullptr,
|
||||
table_cache_, &event_logger, false, false, dbname_,
|
||||
&compaction_job_stats_, Env::Priority::USER, nullptr /* IOTracer */,
|
||||
/*manual_compaction_paused=*/nullptr,
|
||||
/*manual_compaction_canceled=*/nullptr, env_->GenerateUniqueId(),
|
||||
DBImpl::GenerateDbSessionId(nullptr), full_history_ts_low_);
|
||||
/*manual_compaction_canceled=*/kManualCompactionCanceledFalse,
|
||||
env_->GenerateUniqueId(), DBImpl::GenerateDbSessionId(nullptr),
|
||||
full_history_ts_low_);
|
||||
VerifyInitializationOfCompactionJobStats(compaction_job_stats_);
|
||||
|
||||
compaction_job.Prepare();
|
||||
@@ -396,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) {
|
||||
@@ -444,15 +636,17 @@ class CompactionJobTestBase : public testing::Test {
|
||||
ErrorHandler error_handler_;
|
||||
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
|
||||
class CompactionJobTest : public CompactionJobTestBase {
|
||||
public:
|
||||
CompactionJobTest()
|
||||
: CompactionJobTestBase(test::PerThreadDBPath("compaction_job_test"),
|
||||
BytewiseComparator(),
|
||||
[](uint64_t /*ts*/) { return ""; }) {}
|
||||
: CompactionJobTestBase(
|
||||
test::PerThreadDBPath("compaction_job_test"), BytewiseComparator(),
|
||||
[](uint64_t /*ts*/) { return ""; }, false) {}
|
||||
};
|
||||
|
||||
TEST_F(CompactionJobTest, Simple) {
|
||||
@@ -1135,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();
|
||||
@@ -1184,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));
|
||||
@@ -1342,23 +1604,13 @@ TEST_F(CompactionJobTest, ResultSerialization) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(CompactionJobTest, GetRateLimiterPriority) {
|
||||
NewDB();
|
||||
|
||||
auto expected_results = CreateTwoFiles(false);
|
||||
auto cfd = versions_->GetColumnFamilySet()->GetDefault();
|
||||
auto files = cfd->current()->storage_info()->LevelFiles(0);
|
||||
ASSERT_EQ(2U, files.size());
|
||||
RunCompaction({files}, expected_results, {}, kMaxSequenceNumber, 1, true,
|
||||
kInvalidBlobFileNumber, true);
|
||||
}
|
||||
|
||||
class CompactionJobTimestampTest : public CompactionJobTestBase {
|
||||
public:
|
||||
CompactionJobTimestampTest()
|
||||
: CompactionJobTestBase(test::PerThreadDBPath("compaction_job_ts_test"),
|
||||
test::BytewiseComparatorWithU64TsWrapper(),
|
||||
test::EncodeInt) {}
|
||||
test::EncodeInt, false) {}
|
||||
};
|
||||
|
||||
TEST_F(CompactionJobTimestampTest, GCDisabled) {
|
||||
@@ -1474,6 +1726,69 @@ TEST_F(CompactionJobTimestampTest, SomeKeysExpired) {
|
||||
RunCompaction({files}, expected_results);
|
||||
}
|
||||
|
||||
// The io priority of the compaction reads and writes are different from
|
||||
// other DB reads and writes. To prepare the compaction input files, use the
|
||||
// default filesystem from Env. To test the io priority of the compaction
|
||||
// reads and writes, db_options_.fs is set as MockTestFileSystem.
|
||||
class CompactionJobIOPriorityTest : public CompactionJobTestBase {
|
||||
public:
|
||||
CompactionJobIOPriorityTest()
|
||||
: CompactionJobTestBase(
|
||||
test::PerThreadDBPath("compaction_job_io_priority_test"),
|
||||
BytewiseComparator(), [](uint64_t /*ts*/) { return ""; }, true) {}
|
||||
};
|
||||
|
||||
TEST_F(CompactionJobIOPriorityTest, WriteControllerStateNormal) {
|
||||
// When the state from WriteController is normal.
|
||||
NewDB();
|
||||
mock::KVVector expected_results = CreateTwoFiles(false);
|
||||
auto cfd = versions_->GetColumnFamilySet()->GetDefault();
|
||||
auto files = cfd->current()->storage_info()->LevelFiles(0);
|
||||
ASSERT_EQ(2U, files.size());
|
||||
RunCompaction({files}, expected_results, {}, kMaxSequenceNumber, 1, false,
|
||||
kInvalidBlobFileNumber, false, Env::IO_LOW, Env::IO_LOW);
|
||||
}
|
||||
|
||||
TEST_F(CompactionJobIOPriorityTest, WriteControllerStateDelayed) {
|
||||
// When the state from WriteController is Delayed.
|
||||
NewDB();
|
||||
mock::KVVector expected_results = CreateTwoFiles(false);
|
||||
auto cfd = versions_->GetColumnFamilySet()->GetDefault();
|
||||
auto files = cfd->current()->storage_info()->LevelFiles(0);
|
||||
ASSERT_EQ(2U, files.size());
|
||||
{
|
||||
std::unique_ptr<WriteControllerToken> delay_token =
|
||||
write_controller_.GetDelayToken(1000000);
|
||||
RunCompaction({files}, expected_results, {}, kMaxSequenceNumber, 1, false,
|
||||
kInvalidBlobFileNumber, false, Env::IO_USER, Env::IO_USER);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(CompactionJobIOPriorityTest, WriteControllerStateStalled) {
|
||||
// When the state from WriteController is Stalled.
|
||||
NewDB();
|
||||
mock::KVVector expected_results = CreateTwoFiles(false);
|
||||
auto cfd = versions_->GetColumnFamilySet()->GetDefault();
|
||||
auto files = cfd->current()->storage_info()->LevelFiles(0);
|
||||
ASSERT_EQ(2U, files.size());
|
||||
{
|
||||
std::unique_ptr<WriteControllerToken> stop_token =
|
||||
write_controller_.GetStopToken();
|
||||
RunCompaction({files}, expected_results, {}, kMaxSequenceNumber, 1, false,
|
||||
kInvalidBlobFileNumber, false, Env::IO_USER, Env::IO_USER);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(CompactionJobIOPriorityTest, GetRateLimiterPriority) {
|
||||
NewDB();
|
||||
mock::KVVector expected_results = CreateTwoFiles(false);
|
||||
auto cfd = versions_->GetColumnFamilySet()->GetDefault();
|
||||
auto files = cfd->current()->storage_info()->LevelFiles(0);
|
||||
ASSERT_EQ(2U, files.size());
|
||||
RunCompaction({files}, expected_results, {}, kMaxSequenceNumber, 1, false,
|
||||
kInvalidBlobFileNumber, true, Env::IO_LOW, Env::IO_LOW);
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
@@ -0,0 +1,314 @@
|
||||
// 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) {
|
||||
FileMetaData* meta = GetMetaData();
|
||||
assert(meta != nullptr);
|
||||
Status s = intput_status;
|
||||
if (s.ok()) {
|
||||
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,328 @@
|
||||
// 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);
|
||||
|
||||
// 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;
|
||||
}
|
||||
@@ -640,7 +664,12 @@ Compaction* CompactionPicker::CompactRange(
|
||||
GetCompressionType(vstorage, mutable_cf_options, output_level, 1),
|
||||
GetCompressionOptions(mutable_cf_options, vstorage, output_level),
|
||||
Temperature::kUnknown, compact_range_options.max_subcompactions,
|
||||
/* grandparents */ {}, /* is manual */ true, trim_ts);
|
||||
/* grandparents */ {}, /* is manual */ true, trim_ts, /* score */ -1,
|
||||
/* 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);
|
||||
|
||||
RegisterCompaction(c);
|
||||
vstorage->ComputeCompactionScore(ioptions_, mutable_cf_options);
|
||||
return c;
|
||||
@@ -818,8 +847,11 @@ Compaction* CompactionPicker::CompactRange(
|
||||
vstorage->base_level()),
|
||||
GetCompressionOptions(mutable_cf_options, vstorage, output_level),
|
||||
Temperature::kUnknown, compact_range_options.max_subcompactions,
|
||||
std::move(grandparents),
|
||||
/* is manual compaction */ true, trim_ts);
|
||||
std::move(grandparents), /* is manual */ true, trim_ts, /* score */ -1,
|
||||
/* 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);
|
||||
|
||||
TEST_SYNC_POINT_CALLBACK("CompactionPicker::CompactRange:Return", compaction);
|
||||
RegisterCompaction(compaction);
|
||||
|
||||
@@ -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
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include "db/db_test_util.h"
|
||||
#include "port/stack_trace.h"
|
||||
#include "table/unique_id_impl.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
@@ -294,8 +295,8 @@ TEST_F(CompactionServiceTest, BasicCompactions) {
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"Version::VerifySstUniqueIds::Passed", [&](void* arg) {
|
||||
// override job status
|
||||
auto id = static_cast<std::string*>(arg);
|
||||
assert(!id->empty());
|
||||
auto id = static_cast<UniqueId64x2*>(arg);
|
||||
assert(*id != kNullUniqueId64x2);
|
||||
verify_passed++;
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
@@ -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
-14
@@ -449,67 +449,85 @@ TEST_P(ComparatorDBTest, TwoStrComparator) {
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
void VerifyNotSuccessor(const Slice& s, const Slice& t) {
|
||||
auto bc = BytewiseComparator();
|
||||
auto rbc = ReverseBytewiseComparator();
|
||||
ASSERT_FALSE(bc->IsSameLengthImmediateSuccessor(s, t));
|
||||
ASSERT_FALSE(rbc->IsSameLengthImmediateSuccessor(s, t));
|
||||
ASSERT_FALSE(bc->IsSameLengthImmediateSuccessor(t, s));
|
||||
ASSERT_FALSE(rbc->IsSameLengthImmediateSuccessor(t, s));
|
||||
}
|
||||
|
||||
void VerifySuccessor(const Slice& s, const Slice& t) {
|
||||
auto bc = BytewiseComparator();
|
||||
auto rbc = ReverseBytewiseComparator();
|
||||
ASSERT_TRUE(bc->IsSameLengthImmediateSuccessor(s, t));
|
||||
ASSERT_FALSE(rbc->IsSameLengthImmediateSuccessor(s, t));
|
||||
ASSERT_FALSE(bc->IsSameLengthImmediateSuccessor(t, s));
|
||||
// Should be true but that increases exposure to a design bug in
|
||||
// auto_prefix_mode, so currently set to FALSE
|
||||
ASSERT_FALSE(rbc->IsSameLengthImmediateSuccessor(t, s));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
TEST_P(ComparatorDBTest, IsSameLengthImmediateSuccessor) {
|
||||
{
|
||||
// different length
|
||||
Slice s("abcxy");
|
||||
Slice t("abcxyz");
|
||||
ASSERT_FALSE(BytewiseComparator()->IsSameLengthImmediateSuccessor(s, t));
|
||||
VerifyNotSuccessor(s, t);
|
||||
}
|
||||
{
|
||||
Slice s("abcxyz");
|
||||
Slice t("abcxy");
|
||||
ASSERT_FALSE(BytewiseComparator()->IsSameLengthImmediateSuccessor(s, t));
|
||||
VerifyNotSuccessor(s, t);
|
||||
}
|
||||
{
|
||||
// not last byte different
|
||||
Slice s("abc1xyz");
|
||||
Slice t("abc2xyz");
|
||||
ASSERT_FALSE(BytewiseComparator()->IsSameLengthImmediateSuccessor(s, t));
|
||||
VerifyNotSuccessor(s, t);
|
||||
}
|
||||
{
|
||||
// same string
|
||||
Slice s("abcxyz");
|
||||
Slice t("abcxyz");
|
||||
ASSERT_FALSE(BytewiseComparator()->IsSameLengthImmediateSuccessor(s, t));
|
||||
VerifyNotSuccessor(s, t);
|
||||
}
|
||||
{
|
||||
Slice s("abcxy");
|
||||
Slice t("abcxz");
|
||||
ASSERT_TRUE(BytewiseComparator()->IsSameLengthImmediateSuccessor(s, t));
|
||||
}
|
||||
{
|
||||
Slice s("abcxz");
|
||||
Slice t("abcxy");
|
||||
ASSERT_FALSE(BytewiseComparator()->IsSameLengthImmediateSuccessor(s, t));
|
||||
VerifySuccessor(s, t);
|
||||
}
|
||||
{
|
||||
const char s_array[] = "\x50\x8a\xac";
|
||||
const char t_array[] = "\x50\x8a\xad";
|
||||
Slice s(s_array);
|
||||
Slice t(t_array);
|
||||
ASSERT_TRUE(BytewiseComparator()->IsSameLengthImmediateSuccessor(s, t));
|
||||
VerifySuccessor(s, t);
|
||||
}
|
||||
{
|
||||
const char s_array[] = "\x50\x8a\xff";
|
||||
const char t_array[] = "\x50\x8b\x00";
|
||||
Slice s(s_array, 3);
|
||||
Slice t(t_array, 3);
|
||||
ASSERT_TRUE(BytewiseComparator()->IsSameLengthImmediateSuccessor(s, t));
|
||||
VerifySuccessor(s, t);
|
||||
}
|
||||
{
|
||||
const char s_array[] = "\x50\x8a\xff\xff";
|
||||
const char t_array[] = "\x50\x8b\x00\x00";
|
||||
Slice s(s_array, 4);
|
||||
Slice t(t_array, 4);
|
||||
ASSERT_TRUE(BytewiseComparator()->IsSameLengthImmediateSuccessor(s, t));
|
||||
VerifySuccessor(s, t);
|
||||
}
|
||||
{
|
||||
const char s_array[] = "\x50\x8a\xff\xff";
|
||||
const char t_array[] = "\x50\x8b\x00\x01";
|
||||
Slice s(s_array, 4);
|
||||
Slice t(t_array, 4);
|
||||
ASSERT_FALSE(BytewiseComparator()->IsSameLengthImmediateSuccessor(s, t));
|
||||
VerifyNotSuccessor(s, t);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+99
-18
@@ -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();
|
||||
}
|
||||
@@ -1065,7 +1089,7 @@ INSTANTIATE_TEST_CASE_P(CorruptionTest, CrashDuringRecoveryWithCorruptionTest,
|
||||
// The combination of corrupting a WAL and injecting an error during subsequent
|
||||
// re-open exposes the bug of prematurely persisting a new MANIFEST with
|
||||
// advanced ColumnFamilyData::log_number.
|
||||
TEST_P(CrashDuringRecoveryWithCorruptionTest, DISABLED_CrashDuringRecovery) {
|
||||
TEST_P(CrashDuringRecoveryWithCorruptionTest, CrashDuringRecovery) {
|
||||
CloseDb();
|
||||
Options options;
|
||||
options.track_and_verify_wals_in_manifest =
|
||||
@@ -1107,7 +1131,8 @@ TEST_P(CrashDuringRecoveryWithCorruptionTest, DISABLED_CrashDuringRecovery) {
|
||||
// number. TEST_SwitchMemtable makes sure WALs are not synced and test can
|
||||
// corrupt un-sync WAL.
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "key" + std::to_string(i), "value"));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "key" + std::to_string(i),
|
||||
"value" + std::to_string(i)));
|
||||
ASSERT_OK(dbimpl->TEST_SwitchMemtable());
|
||||
}
|
||||
|
||||
@@ -1188,6 +1213,23 @@ TEST_P(CrashDuringRecoveryWithCorruptionTest, DISABLED_CrashDuringRecovery) {
|
||||
{
|
||||
options.avoid_flush_during_recovery = avoid_flush_during_recovery_;
|
||||
ASSERT_OK(DB::Open(options, dbname_, cf_descs, &handles, &db_));
|
||||
|
||||
// Verify that data is not lost.
|
||||
{
|
||||
std::string v;
|
||||
ASSERT_OK(db_->Get(ReadOptions(), handles[1], "old_key", &v));
|
||||
ASSERT_EQ("dontcare", v);
|
||||
|
||||
v.clear();
|
||||
ASSERT_OK(db_->Get(ReadOptions(), "key" + std::to_string(0), &v));
|
||||
ASSERT_EQ("value" + std::to_string(0), v);
|
||||
|
||||
// Since it's corrupting second last wal, below key is not found.
|
||||
v.clear();
|
||||
ASSERT_EQ(db_->Get(ReadOptions(), "key" + std::to_string(1), &v),
|
||||
Status::NotFound());
|
||||
}
|
||||
|
||||
for (auto* h : handles) {
|
||||
delete h;
|
||||
}
|
||||
@@ -1219,8 +1261,7 @@ TEST_P(CrashDuringRecoveryWithCorruptionTest, DISABLED_CrashDuringRecovery) {
|
||||
// The combination of corrupting a WAL and injecting an error during subsequent
|
||||
// re-open exposes the bug of prematurely persisting a new MANIFEST with
|
||||
// advanced ColumnFamilyData::log_number.
|
||||
TEST_P(CrashDuringRecoveryWithCorruptionTest,
|
||||
DISABLED_TxnDbCrashDuringRecovery) {
|
||||
TEST_P(CrashDuringRecoveryWithCorruptionTest, TxnDbCrashDuringRecovery) {
|
||||
CloseDb();
|
||||
Options options;
|
||||
options.wal_recovery_mode = WALRecoveryMode::kPointInTimeRecovery;
|
||||
@@ -1271,13 +1312,14 @@ TEST_P(CrashDuringRecoveryWithCorruptionTest,
|
||||
|
||||
// Put and flush cf0
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
ASSERT_OK(txn_db->Put(WriteOptions(), "dontcare", "value"));
|
||||
ASSERT_OK(txn_db->Put(WriteOptions(), "key" + std::to_string(i),
|
||||
"value" + std::to_string(i)));
|
||||
ASSERT_OK(dbimpl->TEST_SwitchMemtable());
|
||||
}
|
||||
|
||||
// Put cf1
|
||||
txn = txn_db->BeginTransaction(WriteOptions(), TransactionOptions());
|
||||
ASSERT_OK(txn->Put(handles[1], "foo1", "value"));
|
||||
ASSERT_OK(txn->Put(handles[1], "foo1", "value1"));
|
||||
ASSERT_OK(txn->Commit());
|
||||
|
||||
delete txn;
|
||||
@@ -1337,7 +1379,6 @@ TEST_P(CrashDuringRecoveryWithCorruptionTest,
|
||||
std::vector<uint64_t> file_nums;
|
||||
GetSortedWalFiles(file_nums);
|
||||
size_t size = file_nums.size();
|
||||
assert(size >= 2);
|
||||
uint64_t log_num = file_nums[size - 1];
|
||||
CorruptFileWithTruncation(FileType::kWalFile, log_num);
|
||||
}
|
||||
@@ -1354,6 +1395,27 @@ TEST_P(CrashDuringRecoveryWithCorruptionTest,
|
||||
{
|
||||
ASSERT_OK(TransactionDB::Open(options, txn_db_opts, dbname_, cf_descs,
|
||||
&handles, &txn_db));
|
||||
|
||||
// Verify that data is not lost.
|
||||
{
|
||||
std::string v;
|
||||
// Key not visible since it's not committed.
|
||||
ASSERT_EQ(txn_db->Get(ReadOptions(), handles[1], "foo", &v),
|
||||
Status::NotFound());
|
||||
|
||||
v.clear();
|
||||
ASSERT_OK(txn_db->Get(ReadOptions(), "key" + std::to_string(0), &v));
|
||||
ASSERT_EQ("value" + std::to_string(0), v);
|
||||
|
||||
// Last WAL is corrupted which contains two keys below.
|
||||
v.clear();
|
||||
ASSERT_EQ(txn_db->Get(ReadOptions(), "key" + std::to_string(1), &v),
|
||||
Status::NotFound());
|
||||
v.clear();
|
||||
ASSERT_EQ(txn_db->Get(ReadOptions(), handles[1], "foo1", &v),
|
||||
Status::NotFound());
|
||||
}
|
||||
|
||||
for (auto* h : handles) {
|
||||
delete h;
|
||||
}
|
||||
@@ -1396,8 +1458,7 @@ TEST_P(CrashDuringRecoveryWithCorruptionTest,
|
||||
// The combination of corrupting a WAL and injecting an error during subsequent
|
||||
// re-open exposes the bug of prematurely persisting a new MANIFEST with
|
||||
// advanced ColumnFamilyData::log_number.
|
||||
TEST_P(CrashDuringRecoveryWithCorruptionTest,
|
||||
DISABLED_CrashDuringRecoveryWithFlush) {
|
||||
TEST_P(CrashDuringRecoveryWithCorruptionTest, CrashDuringRecoveryWithFlush) {
|
||||
CloseDb();
|
||||
Options options;
|
||||
options.wal_recovery_mode = WALRecoveryMode::kPointInTimeRecovery;
|
||||
@@ -1430,7 +1491,8 @@ TEST_P(CrashDuringRecoveryWithCorruptionTest,
|
||||
// Write to default_cf and flush this cf several times to advance wal
|
||||
// number.
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "key" + std::to_string(i), "value"));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "key" + std::to_string(i),
|
||||
"value" + std::to_string(i)));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
}
|
||||
|
||||
@@ -1483,6 +1545,25 @@ TEST_P(CrashDuringRecoveryWithCorruptionTest,
|
||||
{
|
||||
options.avoid_flush_during_recovery = avoid_flush_during_recovery_;
|
||||
ASSERT_OK(DB::Open(options, dbname_, cf_descs, &handles, &db_));
|
||||
|
||||
// Verify that data is not lost.
|
||||
{
|
||||
std::string v;
|
||||
ASSERT_OK(db_->Get(ReadOptions(), handles[1], "old_key", &v));
|
||||
ASSERT_EQ("dontcare", v);
|
||||
|
||||
for (int i = 0; i < 2; ++i) {
|
||||
v.clear();
|
||||
ASSERT_OK(db_->Get(ReadOptions(), "key" + std::to_string(i), &v));
|
||||
ASSERT_EQ("value" + std::to_string(i), v);
|
||||
}
|
||||
|
||||
// Since it's corrupting last wal after Flush, below key is not found.
|
||||
v.clear();
|
||||
ASSERT_EQ(db_->Get(ReadOptions(), handles[1], "dontcare", &v),
|
||||
Status::NotFound());
|
||||
}
|
||||
|
||||
for (auto* h : handles) {
|
||||
delete h;
|
||||
}
|
||||
|
||||
+193
-72
@@ -25,6 +25,7 @@
|
||||
#endif
|
||||
#include "util/file_checksum_helper.h"
|
||||
#include "util/random.h"
|
||||
#include "utilities/counted_fs.h"
|
||||
#include "utilities/fault_injection_env.h"
|
||||
#include "utilities/merge_operators.h"
|
||||
#include "utilities/merge_operators/string_append/stringappend.h"
|
||||
@@ -578,55 +579,84 @@ TEST_F(DBBasicTest, ManifestRollOver) {
|
||||
} while (ChangeCompactOptions());
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, IdentityAcrossRestarts1) {
|
||||
TEST_F(DBBasicTest, IdentityAcrossRestarts) {
|
||||
constexpr size_t kMinIdSize = 10;
|
||||
do {
|
||||
std::string id1;
|
||||
ASSERT_OK(db_->GetDbIdentity(id1));
|
||||
for (bool with_manifest : {false, true}) {
|
||||
std::string idfilename = IdentityFileName(dbname_);
|
||||
std::string id1, tmp;
|
||||
ASSERT_OK(db_->GetDbIdentity(id1));
|
||||
ASSERT_GE(id1.size(), kMinIdSize);
|
||||
|
||||
Options options = CurrentOptions();
|
||||
Reopen(options);
|
||||
std::string id2;
|
||||
ASSERT_OK(db_->GetDbIdentity(id2));
|
||||
// id1 should match id2 because identity was not regenerated
|
||||
ASSERT_EQ(id1.compare(id2), 0);
|
||||
Options options = CurrentOptions();
|
||||
options.write_dbid_to_manifest = with_manifest;
|
||||
Reopen(options);
|
||||
std::string id2;
|
||||
ASSERT_OK(db_->GetDbIdentity(id2));
|
||||
// id2 should match id1 because identity was not regenerated
|
||||
ASSERT_EQ(id1, id2);
|
||||
ASSERT_OK(ReadFileToString(env_, idfilename, &tmp));
|
||||
ASSERT_EQ(tmp, id2);
|
||||
|
||||
std::string idfilename = IdentityFileName(dbname_);
|
||||
ASSERT_OK(env_->DeleteFile(idfilename));
|
||||
Reopen(options);
|
||||
std::string id3;
|
||||
ASSERT_OK(db_->GetDbIdentity(id3));
|
||||
if (options.write_dbid_to_manifest) {
|
||||
ASSERT_EQ(id1.compare(id3), 0);
|
||||
} else {
|
||||
// id1 should NOT match id3 because identity was regenerated
|
||||
ASSERT_NE(id1.compare(id3), 0);
|
||||
// Recover from deleted/missing IDENTITY
|
||||
ASSERT_OK(env_->DeleteFile(idfilename));
|
||||
Reopen(options);
|
||||
std::string id3;
|
||||
ASSERT_OK(db_->GetDbIdentity(id3));
|
||||
if (with_manifest) {
|
||||
// id3 should match id1 because identity was restored from manifest
|
||||
ASSERT_EQ(id1, id3);
|
||||
} else {
|
||||
// id3 should NOT match id1 because identity was regenerated
|
||||
ASSERT_NE(id1, id3);
|
||||
ASSERT_GE(id3.size(), kMinIdSize);
|
||||
}
|
||||
ASSERT_OK(ReadFileToString(env_, idfilename, &tmp));
|
||||
ASSERT_EQ(tmp, id3);
|
||||
|
||||
// Recover from truncated IDENTITY
|
||||
{
|
||||
std::unique_ptr<WritableFile> w;
|
||||
ASSERT_OK(env_->NewWritableFile(idfilename, &w, EnvOptions()));
|
||||
ASSERT_OK(w->Close());
|
||||
}
|
||||
Reopen(options);
|
||||
std::string id4;
|
||||
ASSERT_OK(db_->GetDbIdentity(id4));
|
||||
if (with_manifest) {
|
||||
// id4 should match id1 because identity was restored from manifest
|
||||
ASSERT_EQ(id1, id4);
|
||||
} else {
|
||||
// id4 should NOT match id1 because identity was regenerated
|
||||
ASSERT_NE(id1, id4);
|
||||
ASSERT_GE(id4.size(), kMinIdSize);
|
||||
}
|
||||
ASSERT_OK(ReadFileToString(env_, idfilename, &tmp));
|
||||
ASSERT_EQ(tmp, id4);
|
||||
|
||||
// Recover from overwritten IDENTITY
|
||||
std::string silly_id = "asdf123456789";
|
||||
{
|
||||
std::unique_ptr<WritableFile> w;
|
||||
ASSERT_OK(env_->NewWritableFile(idfilename, &w, EnvOptions()));
|
||||
ASSERT_OK(w->Append(silly_id));
|
||||
ASSERT_OK(w->Close());
|
||||
}
|
||||
Reopen(options);
|
||||
std::string id5;
|
||||
ASSERT_OK(db_->GetDbIdentity(id5));
|
||||
if (with_manifest) {
|
||||
// id4 should match id1 because identity was restored from manifest
|
||||
ASSERT_EQ(id1, id5);
|
||||
} else {
|
||||
ASSERT_EQ(id5, silly_id);
|
||||
}
|
||||
ASSERT_OK(ReadFileToString(env_, idfilename, &tmp));
|
||||
ASSERT_EQ(tmp, id5);
|
||||
}
|
||||
} while (ChangeCompactOptions());
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, IdentityAcrossRestarts2) {
|
||||
do {
|
||||
std::string id1;
|
||||
ASSERT_OK(db_->GetDbIdentity(id1));
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.write_dbid_to_manifest = true;
|
||||
Reopen(options);
|
||||
std::string id2;
|
||||
ASSERT_OK(db_->GetDbIdentity(id2));
|
||||
// id1 should match id2 because identity was not regenerated
|
||||
ASSERT_EQ(id1.compare(id2), 0);
|
||||
|
||||
std::string idfilename = IdentityFileName(dbname_);
|
||||
ASSERT_OK(env_->DeleteFile(idfilename));
|
||||
Reopen(options);
|
||||
std::string id3;
|
||||
ASSERT_OK(db_->GetDbIdentity(id3));
|
||||
// id1 should NOT match id3 because identity was regenerated
|
||||
ASSERT_EQ(id1, id3);
|
||||
} while (ChangeCompactOptions());
|
||||
}
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
TEST_F(DBBasicTest, Snapshot) {
|
||||
env_->SetMockSleep();
|
||||
@@ -1153,6 +1183,39 @@ TEST_F(DBBasicTest, DBClose) {
|
||||
ASSERT_EQ(env->GetCloseCount(), 3);
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, DBCloseAllDirectoryFDs) {
|
||||
Options options = GetDefaultOptions();
|
||||
std::string dbname = test::PerThreadDBPath("db_close_all_dir_fds_test");
|
||||
// Configure a specific WAL directory
|
||||
options.wal_dir = dbname + "_wal_dir";
|
||||
// Configure 3 different data directories
|
||||
options.db_paths.emplace_back(dbname + "_1", 512 * 1024);
|
||||
options.db_paths.emplace_back(dbname + "_2", 4 * 1024 * 1024);
|
||||
options.db_paths.emplace_back(dbname + "_3", 1024 * 1024 * 1024);
|
||||
|
||||
ASSERT_OK(DestroyDB(dbname, options));
|
||||
|
||||
DB* db = nullptr;
|
||||
std::unique_ptr<Env> env = NewCompositeEnv(
|
||||
std::make_shared<CountedFileSystem>(FileSystem::Default()));
|
||||
options.create_if_missing = true;
|
||||
options.env = env.get();
|
||||
Status s = DB::Open(options, dbname, &db);
|
||||
ASSERT_OK(s);
|
||||
ASSERT_TRUE(db != nullptr);
|
||||
|
||||
// Explicitly close the database to ensure the open and close counter for
|
||||
// directories are equivalent
|
||||
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_OK(s);
|
||||
delete db;
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, DBCloseFlushError) {
|
||||
std::unique_ptr<FaultInjectionTestEnv> fault_injection_env(
|
||||
new FaultInjectionTestEnv(env_));
|
||||
@@ -1467,7 +1530,7 @@ TEST_P(DBMultiGetTestWithParam, MultiGetBatchedSimpleUnsorted) {
|
||||
#endif // USE_COROUTINES
|
||||
// Skip for unbatched MultiGet
|
||||
if (!std::get<0>(GetParam())) {
|
||||
ROCKSDB_GTEST_SKIP("This test is only for batched MultiGet");
|
||||
ROCKSDB_GTEST_BYPASS("This test is only for batched MultiGet");
|
||||
return;
|
||||
}
|
||||
do {
|
||||
@@ -1521,7 +1584,7 @@ TEST_P(DBMultiGetTestWithParam, MultiGetBatchedSortedMultiFile) {
|
||||
#endif // USE_COROUTINES
|
||||
// Skip for unbatched MultiGet
|
||||
if (!std::get<0>(GetParam())) {
|
||||
ROCKSDB_GTEST_SKIP("This test is only for batched MultiGet");
|
||||
ROCKSDB_GTEST_BYPASS("This test is only for batched MultiGet");
|
||||
return;
|
||||
}
|
||||
do {
|
||||
@@ -1579,7 +1642,7 @@ TEST_P(DBMultiGetTestWithParam, MultiGetBatchedDuplicateKeys) {
|
||||
#endif // USE_COROUTINES
|
||||
// Skip for unbatched MultiGet
|
||||
if (!std::get<0>(GetParam())) {
|
||||
ROCKSDB_GTEST_SKIP("This test is only for batched MultiGet");
|
||||
ROCKSDB_GTEST_BYPASS("This test is only for batched MultiGet");
|
||||
return;
|
||||
}
|
||||
Options opts = CurrentOptions();
|
||||
@@ -1643,7 +1706,7 @@ TEST_P(DBMultiGetTestWithParam, MultiGetBatchedMultiLevel) {
|
||||
#endif // USE_COROUTINES
|
||||
// Skip for unbatched MultiGet
|
||||
if (!std::get<0>(GetParam())) {
|
||||
ROCKSDB_GTEST_SKIP("This test is only for batched MultiGet");
|
||||
ROCKSDB_GTEST_BYPASS("This test is only for batched MultiGet");
|
||||
return;
|
||||
}
|
||||
Options options = CurrentOptions();
|
||||
@@ -1729,7 +1792,7 @@ TEST_P(DBMultiGetTestWithParam, MultiGetBatchedMultiLevelMerge) {
|
||||
#endif // USE_COROUTINES
|
||||
// Skip for unbatched MultiGet
|
||||
if (!std::get<0>(GetParam())) {
|
||||
ROCKSDB_GTEST_SKIP("This test is only for batched MultiGet");
|
||||
ROCKSDB_GTEST_BYPASS("This test is only for batched MultiGet");
|
||||
return;
|
||||
}
|
||||
Options options = CurrentOptions();
|
||||
@@ -1826,7 +1889,7 @@ TEST_P(DBMultiGetTestWithParam, MultiGetBatchedValueSizeInMemory) {
|
||||
#endif // USE_COROUTINES
|
||||
// Skip for unbatched MultiGet
|
||||
if (!std::get<0>(GetParam())) {
|
||||
ROCKSDB_GTEST_SKIP("This test is only for batched MultiGet");
|
||||
ROCKSDB_GTEST_BYPASS("This test is only for batched MultiGet");
|
||||
return;
|
||||
}
|
||||
CreateAndReopenWithCF({"pikachu"}, CurrentOptions());
|
||||
@@ -1958,7 +2021,7 @@ TEST_P(DBMultiGetTestWithParam, MultiGetBatchedValueSizeMultiLevelMerge) {
|
||||
#endif // USE_COROUTINES
|
||||
// Skip for unbatched MultiGet
|
||||
if (!std::get<0>(GetParam())) {
|
||||
ROCKSDB_GTEST_SKIP("This test is only for batched MultiGet");
|
||||
ROCKSDB_GTEST_BYPASS("This test is only for batched MultiGet");
|
||||
return;
|
||||
}
|
||||
Options options = CurrentOptions();
|
||||
@@ -2158,6 +2221,7 @@ TEST_F(DBMultiGetAsyncIOTest, GetFromL0) {
|
||||
// No async IO in this case since we don't do parallel lookup in L0
|
||||
ASSERT_EQ(multiget_io_batch_size.count, 0);
|
||||
ASSERT_EQ(multiget_io_batch_size.max, 0);
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 0);
|
||||
}
|
||||
|
||||
TEST_F(DBMultiGetAsyncIOTest, GetFromL1) {
|
||||
@@ -2194,6 +2258,7 @@ TEST_F(DBMultiGetAsyncIOTest, GetFromL1) {
|
||||
// A batch of 3 async IOs is expected, one for each overlapping file in L1
|
||||
ASSERT_EQ(multiget_io_batch_size.count, 1);
|
||||
ASSERT_EQ(multiget_io_batch_size.max, 3);
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 3);
|
||||
}
|
||||
|
||||
TEST_F(DBMultiGetAsyncIOTest, LastKeyInFile) {
|
||||
@@ -2344,27 +2409,37 @@ TEST_F(DBBasicTest, MultiGetStats) {
|
||||
values.data(), s.data(), false);
|
||||
|
||||
ASSERT_EQ(values.size(), kMultiGetBatchSize);
|
||||
HistogramData hist_data_blocks;
|
||||
HistogramData hist_level;
|
||||
HistogramData hist_index_and_filter_blocks;
|
||||
HistogramData hist_sst;
|
||||
|
||||
options.statistics->histogramData(NUM_DATA_BLOCKS_READ_PER_LEVEL,
|
||||
&hist_data_blocks);
|
||||
options.statistics->histogramData(NUM_LEVEL_READ_PER_MULTIGET, &hist_level);
|
||||
options.statistics->histogramData(NUM_INDEX_AND_FILTER_BLOCKS_READ_PER_LEVEL,
|
||||
&hist_index_and_filter_blocks);
|
||||
options.statistics->histogramData(NUM_SST_READ_PER_LEVEL, &hist_sst);
|
||||
|
||||
// Maximum number of blocks read from a file system in a level.
|
||||
ASSERT_EQ(hist_data_blocks.max, 32);
|
||||
ASSERT_EQ(hist_level.max, 1);
|
||||
ASSERT_GT(hist_index_and_filter_blocks.max, 0);
|
||||
// Maximum number of sst files read from file system in a level.
|
||||
ASSERT_EQ(hist_sst.max, 2);
|
||||
|
||||
// Minimun number of blocks read in a level.
|
||||
ASSERT_EQ(hist_data_blocks.min, 4);
|
||||
ASSERT_EQ(hist_level.min, 1);
|
||||
ASSERT_GT(hist_index_and_filter_blocks.min, 0);
|
||||
// Minimun number of sst files read in a level.
|
||||
ASSERT_EQ(hist_sst.min, 1);
|
||||
|
||||
for (PinnableSlice& value : values) {
|
||||
value.Reset();
|
||||
}
|
||||
for (Status& status : s) {
|
||||
status = Status::OK();
|
||||
}
|
||||
db_->MultiGet(read_opts, handles_[1], kMultiGetBatchSize, &keys[950],
|
||||
values.data(), s.data(), false);
|
||||
options.statistics->histogramData(NUM_LEVEL_READ_PER_MULTIGET, &hist_level);
|
||||
ASSERT_EQ(hist_level.max, 2);
|
||||
}
|
||||
|
||||
// Test class for batched MultiGet with prefix extractor
|
||||
@@ -2392,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);
|
||||
}
|
||||
|
||||
@@ -4053,6 +4130,27 @@ TEST_F(DBBasicTest, DestroyDefaultCfHandle) {
|
||||
ASSERT_TRUE(db_->DestroyColumnFamilyHandle(default_cf).IsInvalidArgument());
|
||||
}
|
||||
|
||||
TEST_F(DBBasicTest, FailOpenIfLoggerCreationFail) {
|
||||
Options options = GetDefaultOptions();
|
||||
options.create_if_missing = true;
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"rocksdb::CreateLoggerFromOptions:AfterGetPath", [&](void* arg) {
|
||||
auto* s = reinterpret_cast<Status*>(arg);
|
||||
assert(s);
|
||||
*s = Status::IOError("Injected");
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
Status s = TryReopen(options);
|
||||
ASSERT_EQ(nullptr, options.info_log);
|
||||
ASSERT_TRUE(s.IsIOError());
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
TEST_F(DBBasicTest, VerifyFileChecksums) {
|
||||
Options options = GetDefaultOptions();
|
||||
@@ -4096,6 +4194,29 @@ TEST_F(DBBasicTest, VerifyFileChecksums) {
|
||||
Reopen(options);
|
||||
ASSERT_TRUE(db_->VerifyFileChecksums(ReadOptions()).IsInvalidArgument());
|
||||
}
|
||||
|
||||
// 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;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(Put("x", "y"));
|
||||
// This does not create a new WAL.
|
||||
ASSERT_OK(db_->SyncWAL());
|
||||
EXPECT_FALSE(dbfull()->GetVersionSet()->GetWalSet().GetWals().empty());
|
||||
|
||||
std::unique_ptr<LogFile> wal;
|
||||
Status s = db_->GetCurrentWalFile(&wal);
|
||||
ASSERT_OK(s);
|
||||
Close();
|
||||
|
||||
EXPECT_OK(env_->DeleteFile(LogFileName(dbname_, wal->LogNumber())));
|
||||
|
||||
ASSERT_TRUE(TryReopen(options).IsCorruption());
|
||||
}
|
||||
#endif // !ROCKSDB_LITE
|
||||
|
||||
// A test class for intercepting random reads and injecting artificial
|
||||
|
||||
+22
-15
@@ -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);
|
||||
@@ -670,12 +673,13 @@ class DBBlockCacheTest1 : public DBTestBase,
|
||||
};
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(DBBlockCacheTest1, DBBlockCacheTest1,
|
||||
::testing::Values(1, 2, 3));
|
||||
::testing::Values(1, 2));
|
||||
|
||||
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);
|
||||
@@ -686,13 +690,10 @@ TEST_P(DBBlockCacheTest1, WarmCacheWithBlocksDuringFlush) {
|
||||
table_options.partition_filters = true;
|
||||
table_options.index_type =
|
||||
BlockBasedTableOptions::IndexType::kTwoLevelIndexSearch;
|
||||
table_options.filter_policy.reset(NewBloomFilterPolicy(10, false));
|
||||
table_options.filter_policy.reset(NewBloomFilterPolicy(10));
|
||||
break;
|
||||
case 2: // block-based filter
|
||||
table_options.filter_policy.reset(NewBloomFilterPolicy(10, true));
|
||||
break;
|
||||
case 3: // full filter
|
||||
table_options.filter_policy.reset(NewBloomFilterPolicy(10, false));
|
||||
case 2: // full filter
|
||||
table_options.filter_policy.reset(NewBloomFilterPolicy(10));
|
||||
break;
|
||||
default:
|
||||
assert(false);
|
||||
@@ -935,8 +936,12 @@ 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),
|
||||
NewFastLRUCache(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)}) {
|
||||
if (!base_cache) {
|
||||
// Skip clock cache when not supported
|
||||
continue;
|
||||
@@ -1289,13 +1294,15 @@ 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),
|
||||
NewFastLRUCache(capacity)}) {
|
||||
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 and ClockCache.
|
||||
if (!cache) {
|
||||
// Skip clock cache when not supported
|
||||
continue;
|
||||
}
|
||||
|
||||
++iterations_tested;
|
||||
|
||||
Options options = CurrentOptions();
|
||||
|
||||
+450
-71
@@ -37,8 +37,6 @@ std::shared_ptr<const FilterPolicy> Create(double bits_per_key,
|
||||
return BloomLikeFilterPolicy::Create(name, bits_per_key);
|
||||
}
|
||||
const std::string kLegacyBloom = test::LegacyBloomFilterPolicy::kClassName();
|
||||
const std::string kDeprecatedBlock =
|
||||
DeprecatedBlockBasedBloomFilterPolicy::kClassName();
|
||||
const std::string kFastLocalBloom =
|
||||
test::FastLocalBloomFilterPolicy::kClassName();
|
||||
const std::string kStandard128Ribbon =
|
||||
@@ -189,7 +187,7 @@ TEST_F(DBBloomFilterTest, GetFilterByPrefixBloomCustomPrefixExtractor) {
|
||||
options.statistics = ROCKSDB_NAMESPACE::CreateDBStatistics();
|
||||
get_perf_context()->EnablePerLevelPerfContext();
|
||||
BlockBasedTableOptions bbto;
|
||||
bbto.filter_policy.reset(NewBloomFilterPolicy(10, false));
|
||||
bbto.filter_policy.reset(NewBloomFilterPolicy(10));
|
||||
if (partition_filters) {
|
||||
bbto.partition_filters = true;
|
||||
bbto.index_type = BlockBasedTableOptions::IndexType::kTwoLevelIndexSearch;
|
||||
@@ -256,6 +254,15 @@ TEST_F(DBBloomFilterTest, GetFilterByPrefixBloomCustomPrefixExtractor) {
|
||||
(*(get_perf_context()->level_to_perf_context))[0].bloom_filter_useful);
|
||||
#endif // ROCKSDB_LITE
|
||||
|
||||
// No bloom on extractor changed, after re-open
|
||||
options.prefix_extractor.reset(NewCappedPrefixTransform(10));
|
||||
Reopen(options);
|
||||
ASSERT_EQ("NOT_FOUND", Get("foobarbar"));
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_USEFUL), 3);
|
||||
ASSERT_EQ(
|
||||
3,
|
||||
(*(get_perf_context()->level_to_perf_context))[0].bloom_filter_useful);
|
||||
|
||||
get_perf_context()->Reset();
|
||||
}
|
||||
}
|
||||
@@ -267,7 +274,7 @@ TEST_F(DBBloomFilterTest, GetFilterByPrefixBloom) {
|
||||
options.statistics = ROCKSDB_NAMESPACE::CreateDBStatistics();
|
||||
get_perf_context()->EnablePerLevelPerfContext();
|
||||
BlockBasedTableOptions bbto;
|
||||
bbto.filter_policy.reset(NewBloomFilterPolicy(10, false));
|
||||
bbto.filter_policy.reset(NewBloomFilterPolicy(10));
|
||||
if (partition_filters) {
|
||||
bbto.partition_filters = true;
|
||||
bbto.index_type = BlockBasedTableOptions::IndexType::kTwoLevelIndexSearch;
|
||||
@@ -331,7 +338,7 @@ TEST_F(DBBloomFilterTest, WholeKeyFilterProp) {
|
||||
get_perf_context()->EnablePerLevelPerfContext();
|
||||
|
||||
BlockBasedTableOptions bbto;
|
||||
bbto.filter_policy.reset(NewBloomFilterPolicy(10, false));
|
||||
bbto.filter_policy.reset(NewBloomFilterPolicy(10));
|
||||
bbto.whole_key_filtering = false;
|
||||
if (partition_filters) {
|
||||
bbto.partition_filters = true;
|
||||
@@ -747,7 +754,6 @@ TEST_P(DBBloomFilterTestWithParam, SkipFilterOnEssentiallyZeroBpk) {
|
||||
INSTANTIATE_TEST_CASE_P(
|
||||
FormatDef, DBBloomFilterTestDefFormatVersion,
|
||||
::testing::Values(
|
||||
std::make_tuple(kDeprecatedBlock, false, test::kDefaultFormatVersion),
|
||||
std::make_tuple(kAutoBloom, true, test::kDefaultFormatVersion),
|
||||
std::make_tuple(kAutoBloom, false, test::kDefaultFormatVersion),
|
||||
std::make_tuple(kAutoRibbon, false, test::kDefaultFormatVersion)));
|
||||
@@ -755,7 +761,6 @@ INSTANTIATE_TEST_CASE_P(
|
||||
INSTANTIATE_TEST_CASE_P(
|
||||
FormatDef, DBBloomFilterTestWithParam,
|
||||
::testing::Values(
|
||||
std::make_tuple(kDeprecatedBlock, false, test::kDefaultFormatVersion),
|
||||
std::make_tuple(kAutoBloom, true, test::kDefaultFormatVersion),
|
||||
std::make_tuple(kAutoBloom, false, test::kDefaultFormatVersion),
|
||||
std::make_tuple(kAutoRibbon, false, test::kDefaultFormatVersion)));
|
||||
@@ -763,7 +768,6 @@ INSTANTIATE_TEST_CASE_P(
|
||||
INSTANTIATE_TEST_CASE_P(
|
||||
FormatLatest, DBBloomFilterTestWithParam,
|
||||
::testing::Values(
|
||||
std::make_tuple(kDeprecatedBlock, false, kLatestFormatVersion),
|
||||
std::make_tuple(kAutoBloom, true, kLatestFormatVersion),
|
||||
std::make_tuple(kAutoBloom, false, kLatestFormatVersion),
|
||||
std::make_tuple(kAutoRibbon, false, kLatestFormatVersion)));
|
||||
@@ -829,8 +833,6 @@ std::shared_ptr<const FilterPolicy> kCompatibilityRibbonPolicy{
|
||||
NewRibbonFilterPolicy(20, -1)};
|
||||
|
||||
std::vector<CompatibilityConfig> kCompatibilityConfigs = {
|
||||
{Create(20, kDeprecatedBlock), false,
|
||||
BlockBasedTableOptions().format_version},
|
||||
{kCompatibilityBloomPolicy, false, BlockBasedTableOptions().format_version},
|
||||
{kCompatibilityBloomPolicy, true, BlockBasedTableOptions().format_version},
|
||||
{kCompatibilityBloomPolicy, false, /* legacy Bloom */ 4U},
|
||||
@@ -878,9 +880,8 @@ TEST_F(DBBloomFilterTest, BloomFilterCompatibility) {
|
||||
ASSERT_EQ("val", Get(prefix + "Z")); // Filter positive
|
||||
// Filter negative, with high probability
|
||||
ASSERT_EQ("NOT_FOUND", Get(prefix + "Q"));
|
||||
// FULL_POSITIVE does not include block-based filter case (j == 0)
|
||||
EXPECT_EQ(TestGetAndResetTickerCount(options, BLOOM_FILTER_FULL_POSITIVE),
|
||||
j == 0 ? 0 : 2);
|
||||
2);
|
||||
EXPECT_EQ(TestGetAndResetTickerCount(options, BLOOM_FILTER_USEFUL), 1);
|
||||
}
|
||||
}
|
||||
@@ -904,7 +905,7 @@ class ChargeFilterConstructionTestWithParam
|
||||
detect_filter_construct_corruption_(std::get<3>(GetParam())) {
|
||||
if (charge_filter_construction_ ==
|
||||
CacheEntryRoleOptions::Decision::kDisabled ||
|
||||
policy_ == kDeprecatedBlock || policy_ == kLegacyBloom) {
|
||||
policy_ == kLegacyBloom) {
|
||||
// For these cases, we only interested in whether filter construction
|
||||
// cache charging happens instead of its accuracy. Therefore we don't
|
||||
// need many keys.
|
||||
@@ -1031,8 +1032,6 @@ INSTANTIATE_TEST_CASE_P(
|
||||
std::make_tuple(CacheEntryRoleOptions::Decision::kEnabled,
|
||||
kStandard128Ribbon, true, true),
|
||||
|
||||
std::make_tuple(CacheEntryRoleOptions::Decision::kEnabled,
|
||||
kDeprecatedBlock, false, false),
|
||||
std::make_tuple(CacheEntryRoleOptions::Decision::kEnabled, kLegacyBloom,
|
||||
false, false)));
|
||||
|
||||
@@ -1109,11 +1108,10 @@ TEST_P(ChargeFilterConstructionTestWithParam, Basic) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (policy == kDeprecatedBlock || policy == kLegacyBloom) {
|
||||
if (policy == kLegacyBloom) {
|
||||
EXPECT_EQ(filter_construction_cache_res_peaks.size(), 0)
|
||||
<< "There shouldn't be filter construction cache charging as this "
|
||||
"feature does not support kDeprecatedBlock "
|
||||
"nor kLegacyBloom";
|
||||
"feature does not support kLegacyBloom";
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1798,10 +1796,9 @@ class SliceTransformLimitedDomain : public SliceTransform {
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(DBBloomFilterTest, PrefixExtractorFullFilter) {
|
||||
TEST_F(DBBloomFilterTest, PrefixExtractorWithFilter1) {
|
||||
BlockBasedTableOptions bbto;
|
||||
// Full Filter Block
|
||||
bbto.filter_policy.reset(ROCKSDB_NAMESPACE::NewBloomFilterPolicy(10, false));
|
||||
bbto.filter_policy.reset(ROCKSDB_NAMESPACE::NewBloomFilterPolicy(10));
|
||||
bbto.whole_key_filtering = false;
|
||||
|
||||
Options options = CurrentOptions();
|
||||
@@ -1827,10 +1824,9 @@ TEST_F(DBBloomFilterTest, PrefixExtractorFullFilter) {
|
||||
ASSERT_EQ(Get("zzzzz_AAAA"), "val5");
|
||||
}
|
||||
|
||||
TEST_F(DBBloomFilterTest, PrefixExtractorBlockFilter) {
|
||||
TEST_F(DBBloomFilterTest, PrefixExtractorWithFilter2) {
|
||||
BlockBasedTableOptions bbto;
|
||||
// Block Filter Block
|
||||
bbto.filter_policy.reset(ROCKSDB_NAMESPACE::NewBloomFilterPolicy(10, true));
|
||||
bbto.filter_policy.reset(ROCKSDB_NAMESPACE::NewBloomFilterPolicy(10));
|
||||
|
||||
Options options = CurrentOptions();
|
||||
options.prefix_extractor = std::make_shared<SliceTransformLimitedDomain>();
|
||||
@@ -2221,7 +2217,6 @@ class BloomStatsTestWithParam
|
||||
} else {
|
||||
BlockBasedTableOptions table_options;
|
||||
if (partition_filters_) {
|
||||
assert(bfp_impl_ != kDeprecatedBlock);
|
||||
table_options.partition_filters = partition_filters_;
|
||||
table_options.index_type =
|
||||
BlockBasedTableOptions::IndexType::kTwoLevelIndexSearch;
|
||||
@@ -2346,9 +2341,7 @@ TEST_P(BloomStatsTestWithParam, BloomStatsTestWithIter) {
|
||||
ASSERT_OK(iter->status());
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(value3, iter->value().ToString());
|
||||
// The seek doesn't check block-based bloom filter because last index key
|
||||
// starts with the same prefix we're seeking to.
|
||||
uint64_t expected_hits = bfp_impl_ == kDeprecatedBlock ? 1 : 2;
|
||||
uint64_t expected_hits = 2;
|
||||
ASSERT_EQ(expected_hits, get_perf_context()->bloom_sst_hit_count);
|
||||
|
||||
iter->Seek(key2);
|
||||
@@ -2360,8 +2353,7 @@ TEST_P(BloomStatsTestWithParam, BloomStatsTestWithIter) {
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(
|
||||
BloomStatsTestWithParam, BloomStatsTestWithParam,
|
||||
::testing::Values(std::make_tuple(kDeprecatedBlock, false),
|
||||
std::make_tuple(kLegacyBloom, false),
|
||||
::testing::Values(std::make_tuple(kLegacyBloom, false),
|
||||
std::make_tuple(kLegacyBloom, true),
|
||||
std::make_tuple(kFastLocalBloom, false),
|
||||
std::make_tuple(kFastLocalBloom, true),
|
||||
@@ -2486,7 +2478,7 @@ TEST_F(DBBloomFilterTest, OptimizeFiltersForHits) {
|
||||
options.level_compaction_dynamic_level_bytes = true;
|
||||
BlockBasedTableOptions bbto;
|
||||
bbto.cache_index_and_filter_blocks = true;
|
||||
bbto.filter_policy.reset(NewBloomFilterPolicy(10, true));
|
||||
bbto.filter_policy.reset(NewBloomFilterPolicy(10));
|
||||
bbto.whole_key_filtering = true;
|
||||
options.table_factory.reset(NewBlockBasedTableFactory(bbto));
|
||||
options.optimize_filters_for_hits = true;
|
||||
@@ -2674,7 +2666,6 @@ int CountIter(std::unique_ptr<Iterator>& iter, const Slice& key) {
|
||||
// as the upper bound and two keys are adjacent according to the comparator.
|
||||
TEST_F(DBBloomFilterTest, DynamicBloomFilterUpperBound) {
|
||||
for (const auto& bfp_impl : BloomLikeFilterPolicy::GetAllFixedImpls()) {
|
||||
int using_full_builder = bfp_impl != kDeprecatedBlock;
|
||||
Options options;
|
||||
options.create_if_missing = true;
|
||||
options.env = CurrentOptions().env;
|
||||
@@ -2726,8 +2717,7 @@ TEST_F(DBBloomFilterTest, DynamicBloomFilterUpperBound) {
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
ASSERT_EQ(CountIter(iter, "abcdxx00"), 4);
|
||||
// should check bloom filter since upper bound meets requirement
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_CHECKED),
|
||||
2 + using_full_builder);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_CHECKED), 3);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_USEFUL), 0);
|
||||
}
|
||||
{
|
||||
@@ -2740,8 +2730,7 @@ TEST_F(DBBloomFilterTest, DynamicBloomFilterUpperBound) {
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
ASSERT_EQ(CountIter(iter, "abcdxx01"), 4);
|
||||
// should skip bloom filter since upper bound is too long
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_CHECKED),
|
||||
2 + using_full_builder);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_CHECKED), 3);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_USEFUL), 0);
|
||||
}
|
||||
{
|
||||
@@ -2754,8 +2743,7 @@ TEST_F(DBBloomFilterTest, DynamicBloomFilterUpperBound) {
|
||||
ASSERT_EQ(CountIter(iter, "abcdxx02"), 4);
|
||||
// should check bloom filter since upper bound matches transformed seek
|
||||
// key
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_CHECKED),
|
||||
2 + using_full_builder * 2);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_CHECKED), 4);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_USEFUL), 0);
|
||||
}
|
||||
{
|
||||
@@ -2768,8 +2756,7 @@ TEST_F(DBBloomFilterTest, DynamicBloomFilterUpperBound) {
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
ASSERT_EQ(CountIter(iter, "aaaaaaaa"), 0);
|
||||
// should skip bloom filter since mismatch is found
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_CHECKED),
|
||||
2 + using_full_builder * 2);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_CHECKED), 4);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_USEFUL), 0);
|
||||
}
|
||||
ASSERT_OK(dbfull()->SetOptions({{"prefix_extractor", "fixed:3"}}));
|
||||
@@ -2782,23 +2769,52 @@ TEST_F(DBBloomFilterTest, DynamicBloomFilterUpperBound) {
|
||||
read_options.iterate_upper_bound = &upper_bound;
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
ASSERT_EQ(CountIter(iter, "abc"), 4);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_CHECKED),
|
||||
2 + using_full_builder * 2);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_CHECKED), 4);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_USEFUL), 0);
|
||||
}
|
||||
ASSERT_OK(dbfull()->SetOptions({{"prefix_extractor", "capped:4"}}));
|
||||
// Same with re-open
|
||||
options.prefix_extractor.reset(NewFixedPrefixTransform(3));
|
||||
Reopen(options);
|
||||
{
|
||||
Slice upper_bound("abd");
|
||||
ReadOptions read_options;
|
||||
read_options.prefix_same_as_start = true;
|
||||
read_options.iterate_upper_bound = &upper_bound;
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
ASSERT_EQ(CountIter(iter, "abc"), 4);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_CHECKED), 4);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_USEFUL), 0);
|
||||
}
|
||||
// Set back to capped:4 and verify BF is always read
|
||||
options.prefix_extractor.reset(NewCappedPrefixTransform(4));
|
||||
Reopen(options);
|
||||
{
|
||||
// set back to capped:4 and verify BF is always read
|
||||
Slice upper_bound("abd");
|
||||
ReadOptions read_options;
|
||||
read_options.prefix_same_as_start = true;
|
||||
read_options.iterate_upper_bound = &upper_bound;
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
ASSERT_EQ(CountIter(iter, "abc"), 0);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_CHECKED),
|
||||
3 + using_full_builder * 2);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_CHECKED), 5);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_USEFUL), 1);
|
||||
}
|
||||
// Same if there's a problem initally loading prefix transform
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"BlockBasedTable::Open::ForceNullTablePrefixExtractor",
|
||||
[&](void* arg) { *static_cast<bool*>(arg) = true; });
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
Reopen(options);
|
||||
{
|
||||
Slice upper_bound("abd");
|
||||
ReadOptions read_options;
|
||||
read_options.prefix_same_as_start = true;
|
||||
read_options.iterate_upper_bound = &upper_bound;
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
ASSERT_EQ(CountIter(iter, "abc"), 0);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_CHECKED), 6);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_USEFUL), 2);
|
||||
}
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2806,7 +2822,6 @@ TEST_F(DBBloomFilterTest, DynamicBloomFilterUpperBound) {
|
||||
// verify iterators can read all SST files using the latest config.
|
||||
TEST_F(DBBloomFilterTest, DynamicBloomFilterMultipleSST) {
|
||||
for (const auto& bfp_impl : BloomLikeFilterPolicy::GetAllFixedImpls()) {
|
||||
int using_full_builder = bfp_impl != kDeprecatedBlock;
|
||||
Options options;
|
||||
options.env = CurrentOptions().env;
|
||||
options.create_if_missing = true;
|
||||
@@ -2840,11 +2855,9 @@ TEST_F(DBBloomFilterTest, DynamicBloomFilterMultipleSST) {
|
||||
read_options.iterate_upper_bound = &upper_bound;
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_options));
|
||||
ASSERT_EQ(CountIter(iter, "foo"), 2);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_CHECKED),
|
||||
1 + using_full_builder);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_CHECKED), 2);
|
||||
ASSERT_EQ(CountIter(iter, "gpk"), 0);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_CHECKED),
|
||||
1 + using_full_builder);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_CHECKED), 2);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_USEFUL), 0);
|
||||
|
||||
// second SST with capped:3 BF
|
||||
@@ -2857,14 +2870,12 @@ TEST_F(DBBloomFilterTest, DynamicBloomFilterMultipleSST) {
|
||||
// BF is cappped:3 now
|
||||
std::unique_ptr<Iterator> iter_tmp(db_->NewIterator(read_options));
|
||||
ASSERT_EQ(CountIter(iter_tmp, "foo"), 4);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_CHECKED),
|
||||
2 + using_full_builder * 2);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_CHECKED), 4);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_USEFUL), 0);
|
||||
ASSERT_EQ(CountIter(iter_tmp, "gpk"), 0);
|
||||
// both counters are incremented because BF is "not changed" for 1 of the
|
||||
// 2 SST files, so filter is checked once and found no match.
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_CHECKED),
|
||||
3 + using_full_builder * 2);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_CHECKED), 5);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_USEFUL), 1);
|
||||
}
|
||||
|
||||
@@ -2882,25 +2893,21 @@ TEST_F(DBBloomFilterTest, DynamicBloomFilterMultipleSST) {
|
||||
std::unique_ptr<Iterator> iter_tmp(db_->NewIterator(read_options));
|
||||
ASSERT_EQ(CountIter(iter_tmp, "foo"), 9);
|
||||
// the first and last BF are checked
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_CHECKED),
|
||||
4 + using_full_builder * 3);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_CHECKED), 7);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_USEFUL), 1);
|
||||
ASSERT_EQ(CountIter(iter_tmp, "gpk"), 0);
|
||||
// only last BF is checked and not found
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_CHECKED),
|
||||
5 + using_full_builder * 3);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_CHECKED), 8);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_USEFUL), 2);
|
||||
}
|
||||
|
||||
// iter_old can only see the first SST, so checked plus 1
|
||||
ASSERT_EQ(CountIter(iter_old, "foo"), 4);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_CHECKED),
|
||||
6 + using_full_builder * 3);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_CHECKED), 9);
|
||||
// iter was created after the first setoptions call so only full filter
|
||||
// will check the filter
|
||||
ASSERT_EQ(CountIter(iter, "foo"), 2);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_CHECKED),
|
||||
6 + using_full_builder * 4);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_CHECKED), 10);
|
||||
|
||||
{
|
||||
// keys in all three SSTs are visible to iterator
|
||||
@@ -2908,12 +2915,10 @@ TEST_F(DBBloomFilterTest, DynamicBloomFilterMultipleSST) {
|
||||
// so +2 for checked counter
|
||||
std::unique_ptr<Iterator> iter_all(db_->NewIterator(read_options));
|
||||
ASSERT_EQ(CountIter(iter_all, "foo"), 9);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_CHECKED),
|
||||
7 + using_full_builder * 5);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_CHECKED), 12);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_USEFUL), 2);
|
||||
ASSERT_EQ(CountIter(iter_all, "gpk"), 0);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_CHECKED),
|
||||
8 + using_full_builder * 5);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_CHECKED), 13);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_USEFUL), 3);
|
||||
}
|
||||
ASSERT_OK(dbfull()->SetOptions({{"prefix_extractor", "capped:3"}}));
|
||||
@@ -2924,12 +2929,10 @@ TEST_F(DBBloomFilterTest, DynamicBloomFilterMultipleSST) {
|
||||
ASSERT_EQ(CountIter(iter_all, "foo"), 6);
|
||||
// all three SST are checked because the current options has the same as
|
||||
// the remaining SST (capped:3)
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_CHECKED),
|
||||
9 + using_full_builder * 7);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_CHECKED), 16);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_USEFUL), 3);
|
||||
ASSERT_EQ(CountIter(iter_all, "gpk"), 0);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_CHECKED),
|
||||
10 + using_full_builder * 7);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_CHECKED), 17);
|
||||
ASSERT_EQ(TestGetTickerCount(options, BLOOM_FILTER_PREFIX_USEFUL), 4);
|
||||
}
|
||||
// TODO(Zhongyi): Maybe also need to add Get calls to test point look up?
|
||||
@@ -3110,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
|
||||
|
||||
+300
-79
@@ -30,10 +30,100 @@ 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:
|
||||
#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
|
||||
|
||||
/*
|
||||
* 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 +200,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 +296,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 +1108,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 +4385,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 +5244,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:
|
||||
@@ -6387,6 +6540,74 @@ INSTANTIATE_TEST_CASE_P(DBCompactionTestBlobGC, DBCompactionTestBlobGC,
|
||||
::testing::Combine(::testing::Values(0.0, 0.5, 1.0),
|
||||
::testing::Bool()));
|
||||
|
||||
TEST_P(DBCompactionTestBlobGC, CompactionWithBlobGCOverrides) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
options.enable_blob_files = true;
|
||||
options.blob_file_size = 32; // one blob per file
|
||||
options.enable_blob_garbage_collection = true;
|
||||
options.blob_garbage_collection_age_cutoff = 0;
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
for (int i = 0; i < 128; i += 2) {
|
||||
ASSERT_OK(Put("key" + std::to_string(i), "value" + std::to_string(i)));
|
||||
ASSERT_OK(
|
||||
Put("key" + std::to_string(i + 1), "value" + std::to_string(i + 1)));
|
||||
ASSERT_OK(Flush());
|
||||
}
|
||||
|
||||
std::vector<uint64_t> original_blob_files = GetBlobFileNumbers();
|
||||
ASSERT_EQ(original_blob_files.size(), 128);
|
||||
|
||||
// Note: turning off enable_blob_files before the compaction results in
|
||||
// garbage collected values getting inlined.
|
||||
ASSERT_OK(db_->SetOptions({{"enable_blob_files", "false"}}));
|
||||
|
||||
CompactRangeOptions cro;
|
||||
cro.blob_garbage_collection_policy = BlobGarbageCollectionPolicy::kForce;
|
||||
cro.blob_garbage_collection_age_cutoff = blob_gc_age_cutoff_;
|
||||
|
||||
ASSERT_OK(db_->CompactRange(cro, nullptr, nullptr));
|
||||
|
||||
// Check that the GC stats are correct
|
||||
{
|
||||
VersionSet* const versions = dbfull()->GetVersionSet();
|
||||
assert(versions);
|
||||
assert(versions->GetColumnFamilySet());
|
||||
|
||||
ColumnFamilyData* const cfd = versions->GetColumnFamilySet()->GetDefault();
|
||||
assert(cfd);
|
||||
|
||||
const InternalStats* const internal_stats = cfd->internal_stats();
|
||||
assert(internal_stats);
|
||||
|
||||
const auto& compaction_stats = internal_stats->TEST_GetCompactionStats();
|
||||
ASSERT_GE(compaction_stats.size(), 2);
|
||||
|
||||
ASSERT_GE(compaction_stats[1].bytes_read_blob, 0);
|
||||
ASSERT_EQ(compaction_stats[1].bytes_written_blob, 0);
|
||||
}
|
||||
|
||||
const size_t cutoff_index = static_cast<size_t>(
|
||||
cro.blob_garbage_collection_age_cutoff * original_blob_files.size());
|
||||
const size_t expected_num_files = original_blob_files.size() - cutoff_index;
|
||||
|
||||
const std::vector<uint64_t> new_blob_files = GetBlobFileNumbers();
|
||||
|
||||
ASSERT_EQ(new_blob_files.size(), expected_num_files);
|
||||
|
||||
// Original blob files below the cutoff should be gone, original blob files
|
||||
// at or above the cutoff should be still there
|
||||
for (size_t i = cutoff_index; i < original_blob_files.size(); ++i) {
|
||||
ASSERT_EQ(new_blob_files[i - cutoff_index], original_blob_files[i]);
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < 128; ++i) {
|
||||
ASSERT_EQ(Get("key" + std::to_string(i)), "value" + std::to_string(i));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(DBCompactionTestBlobGC, CompactionWithBlobGC) {
|
||||
Options options;
|
||||
options.env = env_;
|
||||
|
||||
+40
-1
@@ -124,6 +124,9 @@ Status DBImpl::GetLiveFiles(std::vector<std::string>& ret,
|
||||
}
|
||||
|
||||
Status DBImpl::GetSortedWalFiles(VectorLogPtr& files) {
|
||||
// Record tracked WALs as a (minimum) cross-check for directory scan
|
||||
std::vector<uint64_t> required_by_manifest;
|
||||
|
||||
// If caller disabled deletions, this function should return files that are
|
||||
// guaranteed not to be deleted until deletions are re-enabled. We need to
|
||||
// wait for pending purges to finish since WalManager doesn't know which
|
||||
@@ -137,6 +140,13 @@ Status DBImpl::GetSortedWalFiles(VectorLogPtr& files) {
|
||||
while (pending_purge_obsolete_files_ > 0 || bg_purge_scheduled_ > 0) {
|
||||
bg_cv_.Wait();
|
||||
}
|
||||
|
||||
// Record tracked WALs as a (minimum) cross-check for directory scan
|
||||
const auto& manifest_wals = versions_->GetWalSet().GetWals();
|
||||
required_by_manifest.reserve(manifest_wals.size());
|
||||
for (const auto& wal : manifest_wals) {
|
||||
required_by_manifest.push_back(wal.first);
|
||||
}
|
||||
}
|
||||
|
||||
Status s = wal_manager_.GetSortedWalFiles(files);
|
||||
@@ -150,6 +160,29 @@ Status DBImpl::GetSortedWalFiles(VectorLogPtr& files) {
|
||||
assert(deletions_disabled.IsNotSupported());
|
||||
}
|
||||
|
||||
if (s.ok()) {
|
||||
// Verify includes those required by manifest (one sorted list is superset
|
||||
// of the other)
|
||||
auto required = required_by_manifest.begin();
|
||||
auto included = files.begin();
|
||||
|
||||
while (required != required_by_manifest.end()) {
|
||||
if (included == files.end() || *required < (*included)->LogNumber()) {
|
||||
// FAIL - did not find
|
||||
return Status::Corruption(
|
||||
"WAL file " + std::to_string(*required) +
|
||||
" required by manifest but not in directory list");
|
||||
}
|
||||
if (*required == (*included)->LogNumber()) {
|
||||
++required;
|
||||
++included;
|
||||
} else {
|
||||
assert(*required > (*included)->LogNumber());
|
||||
++included;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -349,7 +382,13 @@ Status DBImpl::GetLiveFilesStorageInfo(
|
||||
TEST_SYNC_POINT("CheckpointImpl::CreateCheckpoint:SavedLiveFiles2");
|
||||
|
||||
if (s.ok()) {
|
||||
s = FlushWAL(false /* sync */);
|
||||
// To maximize the effectiveness of track_and_verify_wals_in_manifest,
|
||||
// sync WAL when it is enabled.
|
||||
s = FlushWAL(
|
||||
immutable_db_options_.track_and_verify_wals_in_manifest /* sync */);
|
||||
if (s.IsNotSupported()) { // read-only DB or similar
|
||||
s = Status::OK();
|
||||
}
|
||||
}
|
||||
|
||||
TEST_SYNC_POINT("CheckpointImpl::CreateCustomCheckpoint:AfterGetLive1");
|
||||
|
||||
+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));
|
||||
|
||||
|
||||
@@ -40,17 +40,52 @@ size_t CompactedDBImpl::FindFile(const Slice& key) {
|
||||
|
||||
Status CompactedDBImpl::Get(const ReadOptions& options, ColumnFamilyHandle*,
|
||||
const Slice& key, PinnableSlice* value) {
|
||||
return Get(options, /*column_family*/ nullptr, key, value,
|
||||
/*timestamp*/ nullptr);
|
||||
}
|
||||
|
||||
Status CompactedDBImpl::Get(const ReadOptions& options, ColumnFamilyHandle*,
|
||||
const Slice& key, PinnableSlice* value,
|
||||
std::string* timestamp) {
|
||||
assert(user_comparator_);
|
||||
if (options.timestamp || user_comparator_->timestamp_size()) {
|
||||
// TODO: support timestamp
|
||||
return Status::NotSupported();
|
||||
if (options.timestamp) {
|
||||
const Status s = FailIfTsMismatchCf(
|
||||
DefaultColumnFamily(), *(options.timestamp), /*ts_for_read=*/true);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
} else {
|
||||
const Status s = FailIfCfHasTs(DefaultColumnFamily());
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the timestamps for returning results so that we can distinguish
|
||||
// between tombstone or key that has never been written
|
||||
if (timestamp) {
|
||||
timestamp->clear();
|
||||
}
|
||||
|
||||
GetWithTimestampReadCallback read_cb(kMaxSequenceNumber);
|
||||
std::string* ts =
|
||||
user_comparator_->timestamp_size() > 0 ? timestamp : nullptr;
|
||||
LookupKey lkey(key, kMaxSequenceNumber, options.timestamp);
|
||||
GetContext get_context(user_comparator_, nullptr, nullptr, nullptr,
|
||||
GetContext::kNotFound, key, value, nullptr, nullptr,
|
||||
nullptr, true, nullptr, nullptr);
|
||||
LookupKey lkey(key, kMaxSequenceNumber);
|
||||
Status s = files_.files[FindFile(key)].fd.table_reader->Get(
|
||||
options, lkey.internal_key(), &get_context, nullptr);
|
||||
GetContext::kNotFound, lkey.user_key(), value, ts,
|
||||
nullptr, nullptr, true, nullptr, nullptr, nullptr,
|
||||
nullptr, &read_cb);
|
||||
|
||||
const FdWithKeyRange& f = files_.files[FindFile(lkey.user_key())];
|
||||
if (user_comparator_->CompareWithoutTimestamp(
|
||||
key, /*a_has_ts=*/false,
|
||||
ExtractUserKeyAndStripTimestamp(f.smallest_key,
|
||||
user_comparator_->timestamp_size()),
|
||||
/*b_has_ts=*/false) < 0) {
|
||||
return Status::NotFound();
|
||||
}
|
||||
Status s = f.fd.table_reader->Get(options, lkey.internal_key(), &get_context,
|
||||
nullptr);
|
||||
if (!s.ok() && !s.IsNotFound()) {
|
||||
return s;
|
||||
}
|
||||
@@ -60,37 +95,73 @@ Status CompactedDBImpl::Get(const ReadOptions& options, ColumnFamilyHandle*,
|
||||
return Status::NotFound();
|
||||
}
|
||||
|
||||
std::vector<Status> CompactedDBImpl::MultiGet(const ReadOptions& options,
|
||||
const std::vector<ColumnFamilyHandle*>&,
|
||||
std::vector<Status> CompactedDBImpl::MultiGet(
|
||||
const ReadOptions& options, const std::vector<ColumnFamilyHandle*>&,
|
||||
const std::vector<Slice>& keys, std::vector<std::string>* values) {
|
||||
return MultiGet(options, keys, values, /*timestamps*/ nullptr);
|
||||
}
|
||||
|
||||
std::vector<Status> CompactedDBImpl::MultiGet(
|
||||
const ReadOptions& options, const std::vector<ColumnFamilyHandle*>&,
|
||||
const std::vector<Slice>& keys, std::vector<std::string>* values,
|
||||
std::vector<std::string>* timestamps) {
|
||||
assert(user_comparator_);
|
||||
if (user_comparator_->timestamp_size() || options.timestamp) {
|
||||
// TODO: support timestamp
|
||||
return std::vector<Status>(keys.size(), Status::NotSupported());
|
||||
size_t num_keys = keys.size();
|
||||
|
||||
if (options.timestamp) {
|
||||
Status s = FailIfTsMismatchCf(DefaultColumnFamily(), *(options.timestamp),
|
||||
/*ts_for_read=*/true);
|
||||
if (!s.ok()) {
|
||||
return std::vector<Status>(num_keys, s);
|
||||
}
|
||||
} else {
|
||||
Status s = FailIfCfHasTs(DefaultColumnFamily());
|
||||
if (!s.ok()) {
|
||||
return std::vector<Status>(num_keys, s);
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the timestamps for returning results so that we can distinguish
|
||||
// between tombstone or key that has never been written
|
||||
if (timestamps) {
|
||||
for (auto& ts : *timestamps) {
|
||||
ts.clear();
|
||||
}
|
||||
}
|
||||
|
||||
GetWithTimestampReadCallback read_cb(kMaxSequenceNumber);
|
||||
autovector<TableReader*, 16> reader_list;
|
||||
for (const auto& key : keys) {
|
||||
const FdWithKeyRange& f = files_.files[FindFile(key)];
|
||||
if (user_comparator_->Compare(key, ExtractUserKey(f.smallest_key)) < 0) {
|
||||
LookupKey lkey(key, kMaxSequenceNumber, options.timestamp);
|
||||
const FdWithKeyRange& f = files_.files[FindFile(lkey.user_key())];
|
||||
if (user_comparator_->CompareWithoutTimestamp(
|
||||
key, /*a_has_ts=*/false,
|
||||
ExtractUserKeyAndStripTimestamp(f.smallest_key,
|
||||
user_comparator_->timestamp_size()),
|
||||
/*b_has_ts=*/false) < 0) {
|
||||
reader_list.push_back(nullptr);
|
||||
} else {
|
||||
LookupKey lkey(key, kMaxSequenceNumber);
|
||||
f.fd.table_reader->Prepare(lkey.internal_key());
|
||||
reader_list.push_back(f.fd.table_reader);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Status> statuses(keys.size(), Status::NotFound());
|
||||
values->resize(keys.size());
|
||||
std::vector<Status> statuses(num_keys, Status::NotFound());
|
||||
values->resize(num_keys);
|
||||
if (timestamps) {
|
||||
timestamps->resize(num_keys);
|
||||
}
|
||||
int idx = 0;
|
||||
for (auto* r : reader_list) {
|
||||
if (r != nullptr) {
|
||||
PinnableSlice pinnable_val;
|
||||
std::string& value = (*values)[idx];
|
||||
GetContext get_context(user_comparator_, nullptr, nullptr, nullptr,
|
||||
GetContext::kNotFound, keys[idx], &pinnable_val,
|
||||
nullptr, nullptr, nullptr, true, nullptr, nullptr);
|
||||
LookupKey lkey(keys[idx], kMaxSequenceNumber);
|
||||
LookupKey lkey(keys[idx], kMaxSequenceNumber, options.timestamp);
|
||||
std::string* timestamp = timestamps ? &(*timestamps)[idx] : nullptr;
|
||||
GetContext get_context(
|
||||
user_comparator_, nullptr, nullptr, nullptr, GetContext::kNotFound,
|
||||
lkey.user_key(), &pinnable_val,
|
||||
user_comparator_->timestamp_size() > 0 ? timestamp : nullptr, nullptr,
|
||||
nullptr, true, nullptr, nullptr, nullptr, nullptr, &read_cb);
|
||||
Status s = r->Get(options, lkey.internal_key(), &get_context, nullptr);
|
||||
assert(static_cast<size_t>(idx) < statuses.size());
|
||||
if (!s.ok() && !s.IsNotFound()) {
|
||||
|
||||
@@ -28,12 +28,25 @@ class CompactedDBImpl : public DBImpl {
|
||||
virtual Status Get(const ReadOptions& options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
PinnableSlice* value) override;
|
||||
|
||||
Status Get(const ReadOptions& options, ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableSlice* value,
|
||||
std::string* timestamp) override;
|
||||
|
||||
using DB::MultiGet;
|
||||
// Note that CompactedDBImpl::MultiGet is not the optimized version of
|
||||
// MultiGet to use.
|
||||
// TODO: optimize CompactedDBImpl::MultiGet, see DBImpl::MultiGet for details.
|
||||
virtual std::vector<Status> MultiGet(
|
||||
const ReadOptions& options,
|
||||
const std::vector<ColumnFamilyHandle*>&,
|
||||
const std::vector<Slice>& keys, std::vector<std::string>* values)
|
||||
override;
|
||||
const ReadOptions& options, const std::vector<ColumnFamilyHandle*>&,
|
||||
const std::vector<Slice>& keys,
|
||||
std::vector<std::string>* values) override;
|
||||
|
||||
std::vector<Status> MultiGet(const ReadOptions& options,
|
||||
const std::vector<ColumnFamilyHandle*>&,
|
||||
const std::vector<Slice>& keys,
|
||||
std::vector<std::string>* values,
|
||||
std::vector<std::string>* timestamps) override;
|
||||
|
||||
using DBImpl::Put;
|
||||
virtual Status Put(const WriteOptions& /*options*/,
|
||||
@@ -41,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*/,
|
||||
|
||||
+283
-101
@@ -101,6 +101,7 @@
|
||||
#include "util/compression.h"
|
||||
#include "util/crc32c.h"
|
||||
#include "util/defer.h"
|
||||
#include "util/distributed_mutex.h"
|
||||
#include "util/hash_containers.h"
|
||||
#include "util/mutexlock.h"
|
||||
#include "util/stop_watch.h"
|
||||
@@ -145,6 +146,8 @@ void DumpSupportInfo(Logger* logger) {
|
||||
}
|
||||
ROCKS_LOG_HEADER(logger, "Fast CRC32 supported: %s",
|
||||
crc32c::IsFastCrc32Supported().c_str());
|
||||
|
||||
ROCKS_LOG_HEADER(logger, "DMutex implementation: %s", DMutex::kName());
|
||||
}
|
||||
} // namespace
|
||||
|
||||
@@ -153,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_),
|
||||
@@ -241,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.
|
||||
@@ -260,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()));
|
||||
|
||||
@@ -511,6 +514,19 @@ void DBImpl::CancelAllBackgroundWork(bool wait) {
|
||||
WaitForBackgroundWork();
|
||||
}
|
||||
|
||||
Status DBImpl::MaybeReleaseTimestampedSnapshotsAndCheck() {
|
||||
size_t num_snapshots = 0;
|
||||
ReleaseTimestampedSnapshotsOlderThan(std::numeric_limits<uint64_t>::max(),
|
||||
&num_snapshots);
|
||||
|
||||
// If there is unreleased snapshot, fail the close call
|
||||
if (num_snapshots > 0) {
|
||||
return Status::Aborted("Cannot close DB with unreleased snapshot.");
|
||||
}
|
||||
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status DBImpl::CloseHelper() {
|
||||
// Guarantee that there is no background error recovery in progress before
|
||||
// continuing with the shutdown
|
||||
@@ -560,38 +576,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) {
|
||||
@@ -714,24 +698,40 @@ Status DBImpl::CloseHelper() {
|
||||
write_buffer_manager_->RemoveDBFromQueue(wbm_stall_.get());
|
||||
}
|
||||
|
||||
IOStatus io_s = directories_.Close(IOOptions(), nullptr /* dbg */);
|
||||
if (!io_s.ok()) {
|
||||
ret = io_s;
|
||||
}
|
||||
if (ret.IsAborted()) {
|
||||
// Reserve IsAborted() error for those where users didn't release
|
||||
// certain resource and they can release them and come back and
|
||||
// retry. In this case, we wrap this exception to something else.
|
||||
return Status::Incomplete(ret.ToString());
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
Status DBImpl::CloseImpl() { return CloseHelper(); }
|
||||
|
||||
DBImpl::~DBImpl() {
|
||||
// TODO: remove this.
|
||||
init_logger_creation_s_.PermitUncheckedError();
|
||||
|
||||
InstrumentedMutexLock closing_lock_guard(&closing_mutex_);
|
||||
if (!closed_) {
|
||||
closed_ = true;
|
||||
closing_status_ = CloseHelper();
|
||||
closing_status_.PermitUncheckedError();
|
||||
if (closed_) {
|
||||
return;
|
||||
}
|
||||
|
||||
closed_ = true;
|
||||
|
||||
{
|
||||
const Status s = MaybeReleaseTimestampedSnapshotsAndCheck();
|
||||
s.PermitUncheckedError();
|
||||
}
|
||||
|
||||
closing_status_ = CloseImpl();
|
||||
closing_status_.PermitUncheckedError();
|
||||
}
|
||||
|
||||
void DBImpl::MaybeIgnoreError(Status* s) const {
|
||||
@@ -786,8 +786,9 @@ Status DBImpl::StartPeriodicWorkScheduler() {
|
||||
return periodic_work_scheduler_->Register(
|
||||
this, mutable_db_options_.stats_dump_period_sec,
|
||||
mutable_db_options_.stats_persist_period_sec);
|
||||
#endif // !ROCKSDB_LITE
|
||||
#else
|
||||
return Status::OK();
|
||||
#endif // !ROCKSDB_LITE
|
||||
}
|
||||
|
||||
// esitmate the total size of stats_history_
|
||||
@@ -1337,6 +1338,7 @@ Status DBImpl::FlushWAL(bool sync) {
|
||||
}
|
||||
|
||||
Status DBImpl::SyncWAL() {
|
||||
TEST_SYNC_POINT("DBImpl::SyncWAL:Begin");
|
||||
autovector<log::Writer*, 1> logs_to_sync;
|
||||
bool need_log_dir_sync;
|
||||
uint64_t current_log_number;
|
||||
@@ -1349,7 +1351,7 @@ Status DBImpl::SyncWAL() {
|
||||
current_log_number = logfile_number_;
|
||||
|
||||
while (logs_.front().number <= current_log_number &&
|
||||
logs_.front().getting_synced) {
|
||||
logs_.front().IsSyncing()) {
|
||||
log_sync_cv_.Wait();
|
||||
}
|
||||
// First check that logs are safe to sync in background.
|
||||
@@ -1366,8 +1368,7 @@ Status DBImpl::SyncWAL() {
|
||||
for (auto it = logs_.begin();
|
||||
it != logs_.end() && it->number <= current_log_number; ++it) {
|
||||
auto& log = *it;
|
||||
assert(!log.getting_synced);
|
||||
log.getting_synced = true;
|
||||
log.PrepareForSync();
|
||||
logs_to_sync.push_back(log.writer);
|
||||
}
|
||||
|
||||
@@ -1440,24 +1441,24 @@ Status DBImpl::MarkLogsSynced(uint64_t up_to, bool synced_dir) {
|
||||
VersionEdit synced_wals;
|
||||
for (auto it = logs_.begin(); it != logs_.end() && it->number <= up_to;) {
|
||||
auto& wal = *it;
|
||||
assert(wal.getting_synced);
|
||||
assert(wal.IsSyncing());
|
||||
|
||||
if (logs_.size() > 1) {
|
||||
if (immutable_db_options_.track_and_verify_wals_in_manifest &&
|
||||
wal.writer->file()->GetFileSize() > 0) {
|
||||
synced_wals.AddWal(wal.number,
|
||||
WalMetadata(wal.writer->file()->GetFileSize()));
|
||||
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_);
|
||||
it = logs_.erase(it);
|
||||
} else {
|
||||
wal.getting_synced = false;
|
||||
wal.FinishSync();
|
||||
++it;
|
||||
}
|
||||
}
|
||||
assert(logs_.empty() || logs_[0].number > up_to ||
|
||||
(logs_.size() == 1 && !logs_[0].getting_synced));
|
||||
(logs_.size() == 1 && !logs_[0].IsSyncing()));
|
||||
|
||||
Status s;
|
||||
if (synced_wals.IsWalAddition()) {
|
||||
@@ -1477,8 +1478,7 @@ void DBImpl::MarkLogsNotSynced(uint64_t up_to) {
|
||||
for (auto it = logs_.begin(); it != logs_.end() && it->number <= up_to;
|
||||
++it) {
|
||||
auto& wal = *it;
|
||||
assert(wal.getting_synced);
|
||||
wal.getting_synced = false;
|
||||
wal.FinishSync();
|
||||
}
|
||||
log_sync_cv_.SignalAll();
|
||||
}
|
||||
@@ -1715,6 +1715,8 @@ Status DBImpl::Get(const ReadOptions& read_options,
|
||||
Status DBImpl::Get(const ReadOptions& read_options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
PinnableSlice* value, std::string* timestamp) {
|
||||
assert(value != nullptr);
|
||||
value->Reset();
|
||||
GetImplOptions get_impl_options;
|
||||
get_impl_options.column_family = column_family;
|
||||
get_impl_options.value = value;
|
||||
@@ -1731,8 +1733,9 @@ Status DBImpl::GetImpl(const ReadOptions& read_options, const Slice& key,
|
||||
assert(get_impl_options.column_family);
|
||||
|
||||
if (read_options.timestamp) {
|
||||
const Status s = FailIfTsSizesMismatch(get_impl_options.column_family,
|
||||
*(read_options.timestamp));
|
||||
const Status s = FailIfTsMismatchCf(get_impl_options.column_family,
|
||||
*(read_options.timestamp),
|
||||
/*ts_for_read=*/true);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -1743,6 +1746,12 @@ Status DBImpl::GetImpl(const ReadOptions& read_options, const Slice& key,
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the timestamps for returning results so that we can distinguish
|
||||
// between tombstone or key that has never been written
|
||||
if (get_impl_options.timestamp) {
|
||||
get_impl_options.timestamp->clear();
|
||||
}
|
||||
|
||||
GetWithTimestampReadCallback read_cb(0); // Will call Refresh
|
||||
|
||||
PERF_CPU_TIMER_GUARD(get_cpu_nanos, immutable_db_options_.clock);
|
||||
@@ -1784,11 +1793,7 @@ Status DBImpl::GetImpl(const ReadOptions& read_options, const Slice& key,
|
||||
// data for the snapshot, so the reader would see neither data that was be
|
||||
// visible to the snapshot before compaction nor the newer data inserted
|
||||
// afterwards.
|
||||
if (last_seq_same_as_publish_seq_) {
|
||||
snapshot = versions_->LastSequence();
|
||||
} else {
|
||||
snapshot = versions_->LastPublishedSequence();
|
||||
}
|
||||
snapshot = GetLastPublishedSequence();
|
||||
if (get_impl_options.callback) {
|
||||
// The unprep_seqs are not published for write unprepared, so it could be
|
||||
// that max_visible_seq is larger. Seek to the std::max of the two.
|
||||
@@ -1956,8 +1961,8 @@ std::vector<Status> DBImpl::MultiGet(
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
assert(column_family[i]);
|
||||
if (read_options.timestamp) {
|
||||
stat_list[i] =
|
||||
FailIfTsSizesMismatch(column_family[i], *(read_options.timestamp));
|
||||
stat_list[i] = FailIfTsMismatchCf(
|
||||
column_family[i], *(read_options.timestamp), /*ts_for_read=*/true);
|
||||
if (!stat_list[i].ok()) {
|
||||
should_fail = true;
|
||||
}
|
||||
@@ -2181,11 +2186,7 @@ bool DBImpl::MultiCFSnapshot(
|
||||
// version because a flush happening in between may compact away data for
|
||||
// the snapshot, but the snapshot is earlier than the data overwriting it,
|
||||
// so users may see wrong results.
|
||||
if (last_seq_same_as_publish_seq_) {
|
||||
*snapshot = versions_->LastSequence();
|
||||
} else {
|
||||
*snapshot = versions_->LastPublishedSequence();
|
||||
}
|
||||
*snapshot = GetLastPublishedSequence();
|
||||
}
|
||||
} else {
|
||||
// If we end up with the same issue of memtable geting sealed during 2
|
||||
@@ -2216,11 +2217,7 @@ bool DBImpl::MultiCFSnapshot(
|
||||
// acquire the lock so we're sure to succeed
|
||||
mutex_.Lock();
|
||||
}
|
||||
if (last_seq_same_as_publish_seq_) {
|
||||
*snapshot = versions_->LastSequence();
|
||||
} else {
|
||||
*snapshot = versions_->LastPublishedSequence();
|
||||
}
|
||||
*snapshot = GetLastPublishedSequence();
|
||||
} else {
|
||||
*snapshot =
|
||||
static_cast_with_check<const SnapshotImpl>(read_options.snapshot)
|
||||
@@ -2291,7 +2288,8 @@ void DBImpl::MultiGet(const ReadOptions& read_options, const size_t num_keys,
|
||||
ColumnFamilyHandle* cfh = column_families[i];
|
||||
assert(cfh);
|
||||
if (read_options.timestamp) {
|
||||
statuses[i] = FailIfTsSizesMismatch(cfh, *(read_options.timestamp));
|
||||
statuses[i] = FailIfTsMismatchCf(cfh, *(read_options.timestamp),
|
||||
/*ts_for_read=*/true);
|
||||
if (!statuses[i].ok()) {
|
||||
should_fail = true;
|
||||
}
|
||||
@@ -2326,6 +2324,7 @@ void DBImpl::MultiGet(const ReadOptions& read_options, const size_t num_keys,
|
||||
autovector<KeyContext*, MultiGetContext::MAX_BATCH_SIZE> sorted_keys;
|
||||
sorted_keys.resize(num_keys);
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
values[i].Reset();
|
||||
key_context.emplace_back(column_families[i], keys[i], &values[i],
|
||||
timestamps ? ×tamps[i] : nullptr,
|
||||
&statuses[i]);
|
||||
@@ -2472,6 +2471,7 @@ void DBImpl::MultiGet(const ReadOptions& read_options,
|
||||
autovector<KeyContext*, MultiGetContext::MAX_BATCH_SIZE> sorted_keys;
|
||||
sorted_keys.resize(num_keys);
|
||||
for (size_t i = 0; i < num_keys; ++i) {
|
||||
values[i].Reset();
|
||||
key_context.emplace_back(column_family, keys[i], &values[i],
|
||||
timestamps ? ×tamps[i] : nullptr,
|
||||
&statuses[i]);
|
||||
@@ -2560,6 +2560,16 @@ Status DBImpl::MultiGetImpl(
|
||||
PERF_CPU_TIMER_GUARD(get_cpu_nanos, immutable_db_options_.clock);
|
||||
StopWatch sw(immutable_db_options_.clock, stats_, DB_MULTIGET);
|
||||
|
||||
assert(sorted_keys);
|
||||
// Clear the timestamps for returning results so that we can distinguish
|
||||
// between tombstone or key that has never been written
|
||||
for (auto* kctx : *sorted_keys) {
|
||||
assert(kctx);
|
||||
if (kctx->timestamp) {
|
||||
kctx->timestamp->clear();
|
||||
}
|
||||
}
|
||||
|
||||
// For each of the given keys, apply the entire "get" process as follows:
|
||||
// First look in the memtable, then in the immutable memtable (if any).
|
||||
// s is both in/out. When in, s could either be OK or MergeInProgress.
|
||||
@@ -2772,7 +2782,6 @@ Status DBImpl::CreateColumnFamilyImpl(const ColumnFamilyOptions& cf_options,
|
||||
s = cfd->AddDirectories(&dummy_created_dirs);
|
||||
}
|
||||
if (s.ok()) {
|
||||
single_column_family_mode_ = false;
|
||||
auto* cfd =
|
||||
versions_->GetColumnFamilySet()->GetColumnFamily(column_family_name);
|
||||
assert(cfd != nullptr);
|
||||
@@ -2942,8 +2951,8 @@ Iterator* DBImpl::NewIterator(const ReadOptions& read_options,
|
||||
assert(column_family);
|
||||
|
||||
if (read_options.timestamp) {
|
||||
const Status s =
|
||||
FailIfTsSizesMismatch(column_family, *(read_options.timestamp));
|
||||
const Status s = FailIfTsMismatchCf(
|
||||
column_family, *(read_options.timestamp), /*ts_for_read=*/true);
|
||||
if (!s.ok()) {
|
||||
return NewErrorIterator(s);
|
||||
}
|
||||
@@ -3084,7 +3093,8 @@ Status DBImpl::NewIterators(
|
||||
if (read_options.timestamp) {
|
||||
for (auto* cf : column_families) {
|
||||
assert(cf);
|
||||
const Status s = FailIfTsSizesMismatch(cf, *(read_options.timestamp));
|
||||
const Status s = FailIfTsMismatchCf(cf, *(read_options.timestamp),
|
||||
/*ts_for_read=*/true);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -3146,6 +3156,48 @@ const Snapshot* DBImpl::GetSnapshotForWriteConflictBoundary() {
|
||||
}
|
||||
#endif // ROCKSDB_LITE
|
||||
|
||||
std::pair<Status, std::shared_ptr<const Snapshot>>
|
||||
DBImpl::CreateTimestampedSnapshot(SequenceNumber snapshot_seq, uint64_t ts) {
|
||||
assert(ts != std::numeric_limits<uint64_t>::max());
|
||||
|
||||
auto ret = CreateTimestampedSnapshotImpl(snapshot_seq, ts, /*lock=*/true);
|
||||
return ret;
|
||||
}
|
||||
|
||||
std::shared_ptr<const SnapshotImpl> DBImpl::GetTimestampedSnapshot(
|
||||
uint64_t ts) const {
|
||||
InstrumentedMutexLock lock_guard(&mutex_);
|
||||
return timestamped_snapshots_.GetSnapshot(ts);
|
||||
}
|
||||
|
||||
void DBImpl::ReleaseTimestampedSnapshotsOlderThan(uint64_t ts,
|
||||
size_t* remaining_total_ss) {
|
||||
autovector<std::shared_ptr<const SnapshotImpl>> snapshots_to_release;
|
||||
{
|
||||
InstrumentedMutexLock lock_guard(&mutex_);
|
||||
timestamped_snapshots_.ReleaseSnapshotsOlderThan(ts, snapshots_to_release);
|
||||
}
|
||||
snapshots_to_release.clear();
|
||||
|
||||
if (remaining_total_ss) {
|
||||
InstrumentedMutexLock lock_guard(&mutex_);
|
||||
*remaining_total_ss = static_cast<size_t>(snapshots_.count());
|
||||
}
|
||||
}
|
||||
|
||||
Status DBImpl::GetTimestampedSnapshots(
|
||||
uint64_t ts_lb, uint64_t ts_ub,
|
||||
std::vector<std::shared_ptr<const Snapshot>>& timestamped_snapshots) const {
|
||||
if (ts_lb >= ts_ub) {
|
||||
return Status::InvalidArgument(
|
||||
"timestamp lower bound must be smaller than upper bound");
|
||||
}
|
||||
timestamped_snapshots.clear();
|
||||
InstrumentedMutexLock lock_guard(&mutex_);
|
||||
timestamped_snapshots_.GetSnapshots(ts_lb, ts_ub, timestamped_snapshots);
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
SnapshotImpl* DBImpl::GetSnapshotImpl(bool is_write_conflict_boundary,
|
||||
bool lock) {
|
||||
int64_t unix_time = 0;
|
||||
@@ -3155,6 +3207,8 @@ SnapshotImpl* DBImpl::GetSnapshotImpl(bool is_write_conflict_boundary,
|
||||
|
||||
if (lock) {
|
||||
mutex_.Lock();
|
||||
} else {
|
||||
mutex_.AssertHeld();
|
||||
}
|
||||
// returns null if the underlying memtable does not support snapshot.
|
||||
if (!is_snapshot_supported_) {
|
||||
@@ -3164,9 +3218,7 @@ SnapshotImpl* DBImpl::GetSnapshotImpl(bool is_write_conflict_boundary,
|
||||
delete s;
|
||||
return nullptr;
|
||||
}
|
||||
auto snapshot_seq = last_seq_same_as_publish_seq_
|
||||
? versions_->LastSequence()
|
||||
: versions_->LastPublishedSequence();
|
||||
auto snapshot_seq = GetLastPublishedSequence();
|
||||
SnapshotImpl* snapshot =
|
||||
snapshots_.New(s, snapshot_seq, unix_time, is_write_conflict_boundary);
|
||||
if (lock) {
|
||||
@@ -3175,6 +3227,115 @@ SnapshotImpl* DBImpl::GetSnapshotImpl(bool is_write_conflict_boundary,
|
||||
return snapshot;
|
||||
}
|
||||
|
||||
std::pair<Status, std::shared_ptr<const SnapshotImpl>>
|
||||
DBImpl::CreateTimestampedSnapshotImpl(SequenceNumber snapshot_seq, uint64_t ts,
|
||||
bool lock) {
|
||||
int64_t unix_time = 0;
|
||||
immutable_db_options_.clock->GetCurrentTime(&unix_time)
|
||||
.PermitUncheckedError(); // Ignore error
|
||||
SnapshotImpl* s = new SnapshotImpl;
|
||||
|
||||
const bool need_update_seq = (snapshot_seq != kMaxSequenceNumber);
|
||||
|
||||
if (lock) {
|
||||
mutex_.Lock();
|
||||
} else {
|
||||
mutex_.AssertHeld();
|
||||
}
|
||||
// returns null if the underlying memtable does not support snapshot.
|
||||
if (!is_snapshot_supported_) {
|
||||
if (lock) {
|
||||
mutex_.Unlock();
|
||||
}
|
||||
delete s;
|
||||
return std::make_pair(
|
||||
Status::NotSupported("Memtable does not support snapshot"), nullptr);
|
||||
}
|
||||
|
||||
// Caller is not write thread, thus didn't provide a valid snapshot_seq.
|
||||
// Obtain seq from db.
|
||||
if (!need_update_seq) {
|
||||
snapshot_seq = GetLastPublishedSequence();
|
||||
}
|
||||
|
||||
std::shared_ptr<const SnapshotImpl> latest =
|
||||
timestamped_snapshots_.GetSnapshot(std::numeric_limits<uint64_t>::max());
|
||||
|
||||
// If there is already a latest timestamped snapshot, then we need to do some
|
||||
// checks.
|
||||
if (latest) {
|
||||
uint64_t latest_snap_ts = latest->GetTimestamp();
|
||||
SequenceNumber latest_snap_seq = latest->GetSequenceNumber();
|
||||
assert(latest_snap_seq <= snapshot_seq);
|
||||
bool needs_create_snap = true;
|
||||
Status status;
|
||||
std::shared_ptr<const SnapshotImpl> ret;
|
||||
if (latest_snap_ts > ts) {
|
||||
// A snapshot created later cannot have smaller timestamp than a previous
|
||||
// timestamped snapshot.
|
||||
needs_create_snap = false;
|
||||
std::ostringstream oss;
|
||||
oss << "snapshot exists with larger timestamp " << latest_snap_ts << " > "
|
||||
<< ts;
|
||||
status = Status::InvalidArgument(oss.str());
|
||||
} else if (latest_snap_ts == ts) {
|
||||
if (latest_snap_seq == snapshot_seq) {
|
||||
// We are requesting the same sequence number and timestamp, thus can
|
||||
// safely reuse (share) the current latest timestamped snapshot.
|
||||
needs_create_snap = false;
|
||||
ret = latest;
|
||||
} else if (latest_snap_seq < snapshot_seq) {
|
||||
// There may have been writes to the database since the latest
|
||||
// timestamped snapshot, yet we are still requesting the same
|
||||
// timestamp. In this case, we cannot create the new timestamped
|
||||
// snapshot.
|
||||
needs_create_snap = false;
|
||||
std::ostringstream oss;
|
||||
oss << "Allocated seq is " << snapshot_seq
|
||||
<< ", while snapshot exists with smaller seq " << latest_snap_seq
|
||||
<< " but same timestamp " << ts;
|
||||
status = Status::InvalidArgument(oss.str());
|
||||
}
|
||||
}
|
||||
if (!needs_create_snap) {
|
||||
if (lock) {
|
||||
mutex_.Unlock();
|
||||
}
|
||||
delete s;
|
||||
return std::make_pair(status, ret);
|
||||
} else {
|
||||
status.PermitUncheckedError();
|
||||
}
|
||||
}
|
||||
|
||||
SnapshotImpl* snapshot =
|
||||
snapshots_.New(s, snapshot_seq, unix_time,
|
||||
/*is_write_conflict_boundary=*/true, ts);
|
||||
|
||||
std::shared_ptr<const SnapshotImpl> ret(
|
||||
snapshot,
|
||||
std::bind(&DBImpl::ReleaseSnapshot, this, std::placeholders::_1));
|
||||
timestamped_snapshots_.AddSnapshot(ret);
|
||||
|
||||
// Caller is from write thread, and we need to update database's sequence
|
||||
// number.
|
||||
if (need_update_seq) {
|
||||
assert(versions_);
|
||||
if (last_seq_same_as_publish_seq_) {
|
||||
versions_->SetLastSequence(snapshot_seq);
|
||||
} else {
|
||||
// TODO: support write-prepared/write-unprepared transactions with two
|
||||
// write queues.
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (lock) {
|
||||
mutex_.Unlock();
|
||||
}
|
||||
return std::make_pair(Status::OK(), ret);
|
||||
}
|
||||
|
||||
namespace {
|
||||
using CfdList = autovector<ColumnFamilyData*, 2>;
|
||||
bool CfdListContains(const CfdList& list, ColumnFamilyData* cfd) {
|
||||
@@ -3200,11 +3361,7 @@ void DBImpl::ReleaseSnapshot(const Snapshot* s) {
|
||||
snapshots_.Delete(casted_s);
|
||||
uint64_t oldest_snapshot;
|
||||
if (snapshots_.empty()) {
|
||||
if (last_seq_same_as_publish_seq_) {
|
||||
oldest_snapshot = versions_->LastSequence();
|
||||
} else {
|
||||
oldest_snapshot = versions_->LastPublishedSequence();
|
||||
}
|
||||
oldest_snapshot = GetLastPublishedSequence();
|
||||
} else {
|
||||
oldest_snapshot = snapshots_.oldest()->number_;
|
||||
}
|
||||
@@ -4095,13 +4252,14 @@ Status DBImpl::Close() {
|
||||
if (closed_) {
|
||||
return closing_status_;
|
||||
}
|
||||
|
||||
{
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
// If there is unreleased snapshot, fail the close call
|
||||
if (!snapshots_.empty()) {
|
||||
return Status::Aborted("Cannot close DB with unreleased snapshot.");
|
||||
const Status s = MaybeReleaseTimestampedSnapshotsAndCheck();
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
closing_status_ = CloseImpl();
|
||||
closed_ = true;
|
||||
return closing_status_;
|
||||
@@ -4383,6 +4541,19 @@ Status DBImpl::RenameTempFileToOptionsFile(const std::string& file_name) {
|
||||
s = dir_obj->FsyncWithDirOptions(IOOptions(), nullptr,
|
||||
DirFsyncOptions(options_file_name));
|
||||
}
|
||||
if (s.ok()) {
|
||||
Status temp_s = dir_obj->Close(IOOptions(), nullptr);
|
||||
// The default Close() could return "NotSupproted" and we bypass it
|
||||
// if it is not impelmented. Detailed explanations can be found in
|
||||
// db/db_impl/db_impl.h
|
||||
if (!temp_s.ok()) {
|
||||
if (temp_s.IsNotSupported()) {
|
||||
temp_s.PermitUncheckedError();
|
||||
} else {
|
||||
s = temp_s;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (s.ok()) {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
@@ -4509,6 +4680,8 @@ Status DBImpl::GetLatestSequenceForKey(
|
||||
*timestamp != std::string(ts_sz, '\xff')) ||
|
||||
(*seq == kMaxSequenceNumber && timestamp->empty()));
|
||||
|
||||
TEST_SYNC_POINT_CALLBACK("DBImpl::GetLatestSequenceForKey:mem", timestamp);
|
||||
|
||||
if (*seq != kMaxSequenceNumber) {
|
||||
// Found a sequence number, no need to check immutable memtables
|
||||
*found_record_for_key = true;
|
||||
@@ -4573,6 +4746,7 @@ Status DBImpl::GetLatestSequenceForKey(
|
||||
(*seq != kMaxSequenceNumber &&
|
||||
*timestamp != std::string(ts_sz, '\xff')) ||
|
||||
(*seq == kMaxSequenceNumber && timestamp->empty()));
|
||||
|
||||
if (*seq != kMaxSequenceNumber) {
|
||||
// Found a sequence number, no need to check SST files
|
||||
assert(0 == ts_sz || *timestamp != std::string(ts_sz, '\xff'));
|
||||
@@ -4809,19 +4983,6 @@ Status DBImpl::IngestExternalFiles(
|
||||
}
|
||||
}
|
||||
if (status.ok()) {
|
||||
int consumed_seqno_count =
|
||||
ingestion_jobs[0].ConsumedSequenceNumbersCount();
|
||||
for (size_t i = 1; i != num_cfs; ++i) {
|
||||
consumed_seqno_count =
|
||||
std::max(consumed_seqno_count,
|
||||
ingestion_jobs[i].ConsumedSequenceNumbersCount());
|
||||
}
|
||||
if (consumed_seqno_count > 0) {
|
||||
const SequenceNumber last_seqno = versions_->LastSequence();
|
||||
versions_->SetLastAllocatedSequence(last_seqno + consumed_seqno_count);
|
||||
versions_->SetLastPublishedSequence(last_seqno + consumed_seqno_count);
|
||||
versions_->SetLastSequence(last_seqno + consumed_seqno_count);
|
||||
}
|
||||
autovector<ColumnFamilyData*> cfds_to_commit;
|
||||
autovector<const MutableCFOptions*> mutable_cf_options_list;
|
||||
autovector<autovector<VersionEdit*>> edit_lists;
|
||||
@@ -4851,6 +5012,27 @@ Status DBImpl::IngestExternalFiles(
|
||||
status =
|
||||
versions_->LogAndApply(cfds_to_commit, mutable_cf_options_list,
|
||||
edit_lists, &mutex_, directories_.GetDbDir());
|
||||
// It is safe to update VersionSet last seqno here after LogAndApply since
|
||||
// LogAndApply persists last sequence number from VersionEdits,
|
||||
// which are from file's largest seqno and not from VersionSet.
|
||||
//
|
||||
// It is necessary to update last seqno here since LogAndApply releases
|
||||
// mutex when persisting MANIFEST file, and the snapshots taken during
|
||||
// that period will not be stable if VersionSet last seqno is updated
|
||||
// before LogAndApply.
|
||||
int consumed_seqno_count =
|
||||
ingestion_jobs[0].ConsumedSequenceNumbersCount();
|
||||
for (size_t i = 1; i != num_cfs; ++i) {
|
||||
consumed_seqno_count =
|
||||
std::max(consumed_seqno_count,
|
||||
ingestion_jobs[i].ConsumedSequenceNumbersCount());
|
||||
}
|
||||
if (consumed_seqno_count > 0) {
|
||||
const SequenceNumber last_seqno = versions_->LastSequence();
|
||||
versions_->SetLastAllocatedSequence(last_seqno + consumed_seqno_count);
|
||||
versions_->SetLastPublishedSequence(last_seqno + consumed_seqno_count);
|
||||
versions_->SetLastSequence(last_seqno + consumed_seqno_count);
|
||||
}
|
||||
}
|
||||
|
||||
if (status.ok()) {
|
||||
|
||||
+241
-37
@@ -32,6 +32,7 @@
|
||||
#include "db/log_writer.h"
|
||||
#include "db/logs_with_prep_tracker.h"
|
||||
#include "db/memtable_list.h"
|
||||
#include "db/post_memtable_callback.h"
|
||||
#include "db/pre_release_callback.h"
|
||||
#include "db/range_del_aggregator.h"
|
||||
#include "db/read_callback.h"
|
||||
@@ -113,6 +114,68 @@ class Directories {
|
||||
|
||||
FSDirectory* GetDbDir() { return db_dir_.get(); }
|
||||
|
||||
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
|
||||
// handle this error so that Close() does not fail after upgrading when
|
||||
// run on FileSystems that have not implemented `Directory::Close()` or
|
||||
// `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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
if (wal_dir_) {
|
||||
s = wal_dir_->Close(options, dbg);
|
||||
if (!temp_s.ok()) {
|
||||
if (temp_s.IsNotSupported()) {
|
||||
temp_s.PermitUncheckedError();
|
||||
} else {
|
||||
s = 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();
|
||||
return s;
|
||||
}
|
||||
|
||||
private:
|
||||
std::unique_ptr<FSDirectory> db_dir_;
|
||||
std::vector<std::unique_ptr<FSDirectory>> data_dirs_;
|
||||
@@ -154,6 +217,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;
|
||||
@@ -283,6 +351,19 @@ class DBImpl : public DB {
|
||||
|
||||
virtual const Snapshot* GetSnapshot() override;
|
||||
virtual void ReleaseSnapshot(const Snapshot* snapshot) override;
|
||||
// Create a timestamped snapshot. This snapshot can be shared by multiple
|
||||
// readers. If any of them uses it for write conflict checking, then
|
||||
// is_write_conflict_boundary is true. For simplicity, set it to true by
|
||||
// default.
|
||||
std::pair<Status, std::shared_ptr<const Snapshot>> CreateTimestampedSnapshot(
|
||||
SequenceNumber snapshot_seq, uint64_t ts);
|
||||
std::shared_ptr<const SnapshotImpl> GetTimestampedSnapshot(uint64_t ts) const;
|
||||
void ReleaseTimestampedSnapshotsOlderThan(
|
||||
uint64_t ts, size_t* remaining_total_ss = nullptr);
|
||||
Status GetTimestampedSnapshots(uint64_t ts_lb, uint64_t ts_ub,
|
||||
std::vector<std::shared_ptr<const Snapshot>>&
|
||||
timestamped_snapshots) const;
|
||||
|
||||
using DB::GetProperty;
|
||||
virtual bool GetProperty(ColumnFamilyHandle* column_family,
|
||||
const Slice& property, std::string* value) override;
|
||||
@@ -1160,6 +1241,8 @@ class DBImpl : public DB {
|
||||
static void TEST_ResetDbSessionIdGen();
|
||||
static std::string GenerateDbSessionId(Env* env);
|
||||
|
||||
bool seq_per_batch() const { return seq_per_batch_; }
|
||||
|
||||
protected:
|
||||
const std::string dbname_;
|
||||
// TODO(peterd): unify with VersionSet::db_id_
|
||||
@@ -1170,6 +1253,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_;
|
||||
@@ -1183,6 +1267,9 @@ class DBImpl : public DB {
|
||||
InstrumentedMutex trace_mutex_;
|
||||
BlockCacheTracer block_cache_tracer_;
|
||||
|
||||
// constant false canceled flag, used when the compaction is not manual
|
||||
const std::atomic<bool> kManualCompactionCanceledFalse_{false};
|
||||
|
||||
// State below is protected by mutex_
|
||||
// With two_write_queues enabled, some of the variables that accessed during
|
||||
// WriteToWAL need different synchronization: log_empty_, alive_log_files_,
|
||||
@@ -1207,9 +1294,6 @@ class DBImpl : public DB {
|
||||
// only used for dynamically adjusting max_total_wal_size. it is a sum of
|
||||
// [write_buffer_size * max_write_buffer_number] over all column families
|
||||
uint64_t max_total_in_memory_state_;
|
||||
// If true, we have only one (default) column family. We use this to optimize
|
||||
// some code-paths
|
||||
bool single_column_family_mode_;
|
||||
|
||||
// The options to access storage files
|
||||
const FileOptions file_options_;
|
||||
@@ -1240,6 +1324,39 @@ class DBImpl : public DB {
|
||||
|
||||
std::atomic<bool> shutting_down_;
|
||||
|
||||
// RecoveryContext struct stores the context about version edits along
|
||||
// with corresponding column_family_data and column_family_options.
|
||||
class RecoveryContext {
|
||||
public:
|
||||
~RecoveryContext() {
|
||||
for (auto& edit_list : edit_lists_) {
|
||||
for (auto* edit : edit_list) {
|
||||
delete edit;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void UpdateVersionEdits(ColumnFamilyData* cfd, const VersionEdit& edit) {
|
||||
assert(cfd != nullptr);
|
||||
if (map_.find(cfd->GetID()) == map_.end()) {
|
||||
uint32_t size = static_cast<uint32_t>(map_.size());
|
||||
map_.emplace(cfd->GetID(), size);
|
||||
cfds_.emplace_back(cfd);
|
||||
mutable_cf_opts_.emplace_back(cfd->GetLatestMutableCFOptions());
|
||||
edit_lists_.emplace_back(autovector<VersionEdit*>());
|
||||
}
|
||||
uint32_t i = map_[cfd->GetID()];
|
||||
edit_lists_[i].emplace_back(new VersionEdit(edit));
|
||||
}
|
||||
|
||||
std::unordered_map<uint32_t, uint32_t> map_; // cf_id to index;
|
||||
autovector<ColumnFamilyData*> cfds_;
|
||||
autovector<const MutableCFOptions*> mutable_cf_opts_;
|
||||
autovector<autovector<VersionEdit*>> edit_lists_;
|
||||
// files_to_delete_ contains sst files
|
||||
std::unordered_set<std::string> files_to_delete_;
|
||||
};
|
||||
|
||||
// Except in DB::Open(), WriteOptionsFile can only be called when:
|
||||
// Persist options to options file.
|
||||
// If need_mutex_lock = false, the method will lock DB mutex.
|
||||
@@ -1309,7 +1426,8 @@ class DBImpl : public DB {
|
||||
uint64_t* log_used = nullptr, uint64_t log_ref = 0,
|
||||
bool disable_memtable = false, uint64_t* seq_used = nullptr,
|
||||
size_t batch_cnt = 0,
|
||||
PreReleaseCallback* pre_release_callback = nullptr);
|
||||
PreReleaseCallback* pre_release_callback = nullptr,
|
||||
PostMemTableCallback* post_memtable_callback = nullptr);
|
||||
|
||||
Status PipelinedWriteImpl(const WriteOptions& options, WriteBatch* updates,
|
||||
WriteCallback* callback = nullptr,
|
||||
@@ -1356,16 +1474,21 @@ class DBImpl : public DB {
|
||||
// be made to the descriptor are added to *edit.
|
||||
// recovered_seq is set to less than kMaxSequenceNumber if the log's tail is
|
||||
// skipped.
|
||||
// recovery_ctx stores the context about version edits and all those
|
||||
// edits are persisted to new Manifest after successfully syncing the new WAL.
|
||||
virtual Status Recover(
|
||||
const std::vector<ColumnFamilyDescriptor>& column_families,
|
||||
bool read_only = false, bool error_if_wal_file_exists = false,
|
||||
bool error_if_data_exists_in_wals = false,
|
||||
uint64_t* recovered_seq = nullptr);
|
||||
uint64_t* recovered_seq = nullptr,
|
||||
RecoveryContext* recovery_ctx = nullptr);
|
||||
|
||||
virtual bool OwnTablesAndLogs() const { return true; }
|
||||
|
||||
// Set DB identity file, and write DB ID to manifest if necessary.
|
||||
Status SetDBId(bool read_only);
|
||||
// Setup DB identity file, and write DB ID to manifest if necessary.
|
||||
Status SetupDBId(bool read_only, RecoveryContext* recovery_ctx);
|
||||
// Assign db_id_ and write DB ID to manifest if necessary.
|
||||
void SetDBId(std::string&& id, bool read_only, RecoveryContext* recovery_ctx);
|
||||
|
||||
// REQUIRES: db mutex held when calling this function, but the db mutex can
|
||||
// be released and re-acquired. Db mutex will be held when the function
|
||||
@@ -1374,20 +1497,41 @@ class DBImpl : public DB {
|
||||
// not referenced in the MANIFEST (e.g.
|
||||
// 1. It's best effort recovery;
|
||||
// 2. The VersionEdits referencing the SST files are appended to
|
||||
// MANIFEST, DB crashes when syncing the MANIFEST, the VersionEdits are
|
||||
// RecoveryContext, DB crashes when syncing the MANIFEST, the VersionEdits are
|
||||
// still not synced to MANIFEST during recovery.)
|
||||
// We delete these SST files. In the
|
||||
// It stores the SST files to be deleted in RecoveryContext. In the
|
||||
// meantime, we find out the largest file number present in the paths, and
|
||||
// bump up the version set's next_file_number_ to be 1 + largest_file_number.
|
||||
Status DeleteUnreferencedSstFiles();
|
||||
// recovery_ctx stores the context about version edits and files to be
|
||||
// deleted. All those edits are persisted to new Manifest after successfully
|
||||
// syncing the new WAL.
|
||||
Status DeleteUnreferencedSstFiles(RecoveryContext* recovery_ctx);
|
||||
|
||||
// SetDbSessionId() should be called in the constuctor DBImpl()
|
||||
// to ensure that db_session_id_ gets updated every time the DB is opened
|
||||
void SetDbSessionId();
|
||||
|
||||
Status FailIfCfHasTs(const ColumnFamilyHandle* column_family) const;
|
||||
Status FailIfTsSizesMismatch(const ColumnFamilyHandle* column_family,
|
||||
const Slice& ts) const;
|
||||
Status FailIfTsMismatchCf(ColumnFamilyHandle* column_family, const Slice& ts,
|
||||
bool ts_for_read) const;
|
||||
|
||||
// recovery_ctx stores the context about version edits and
|
||||
// LogAndApplyForRecovery persist all those edits to new Manifest after
|
||||
// successfully syncing new WAL.
|
||||
// LogAndApplyForRecovery should be called only once during recovery and it
|
||||
// should be called when RocksDB writes to a first new MANIFEST since this
|
||||
// recovery.
|
||||
Status LogAndApplyForRecovery(const RecoveryContext& recovery_ctx);
|
||||
|
||||
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;
|
||||
@@ -1465,12 +1609,38 @@ class DBImpl : public DB {
|
||||
return s;
|
||||
}
|
||||
|
||||
bool IsSyncing() { return getting_synced; }
|
||||
|
||||
uint64_t GetPreSyncSize() {
|
||||
assert(getting_synced);
|
||||
return pre_sync_size;
|
||||
}
|
||||
|
||||
void PrepareForSync() {
|
||||
assert(!getting_synced);
|
||||
// Size is expected to be monotonically increasing.
|
||||
assert(writer->file()->GetFlushedSize() >= pre_sync_size);
|
||||
getting_synced = true;
|
||||
pre_sync_size = writer->file()->GetFlushedSize();
|
||||
}
|
||||
|
||||
void FinishSync() {
|
||||
assert(getting_synced);
|
||||
getting_synced = false;
|
||||
}
|
||||
|
||||
uint64_t number;
|
||||
// Visual Studio doesn't support deque's member to be noncopyable because
|
||||
// of a std::unique_ptr as a member.
|
||||
log::Writer* writer; // own
|
||||
|
||||
private:
|
||||
// true for some prefix of logs_
|
||||
bool getting_synced = false;
|
||||
// The size of the file before the sync happens. This amount is guaranteed
|
||||
// to be persisted even if appends happen during sync so it can be used for
|
||||
// tracking the synced size in MANIFEST.
|
||||
uint64_t pre_sync_size = 0;
|
||||
};
|
||||
|
||||
// PurgeFileInfo is a structure to hold information of files to be deleted in
|
||||
@@ -1526,7 +1696,11 @@ class DBImpl : public DB {
|
||||
output_path_id(_output_path_id),
|
||||
exclusive(_exclusive),
|
||||
disallow_trivial_move(_disallow_trivial_move),
|
||||
canceled(_canceled) {}
|
||||
canceled(_canceled ? *_canceled : canceled_internal_storage) {}
|
||||
// When _canceled is not provided by ther user, we assign the reference of
|
||||
// canceled_internal_storage to it to consolidate canceled and
|
||||
// manual_compaction_paused since DisableManualCompaction() might be
|
||||
// called
|
||||
|
||||
ColumnFamilyData* cfd;
|
||||
int input_level;
|
||||
@@ -1543,7 +1717,12 @@ class DBImpl : public DB {
|
||||
InternalKey* manual_end = nullptr; // how far we are compacting
|
||||
InternalKey tmp_storage; // Used to keep track of compaction progress
|
||||
InternalKey tmp_storage1; // Used to keep track of compaction progress
|
||||
std::atomic<bool>* canceled; // Compaction canceled by the user?
|
||||
|
||||
// When the user provides a canceled pointer in CompactRangeOptions, the
|
||||
// above varaibe is the reference of the user-provided
|
||||
// `canceled`, otherwise, it is the reference of canceled_internal_storage
|
||||
std::atomic<bool> canceled_internal_storage = false;
|
||||
std::atomic<bool>& canceled; // Compaction canceled pointer reference
|
||||
};
|
||||
struct PrepickedCompaction {
|
||||
// background compaction takes ownership of `compaction`.
|
||||
@@ -1645,7 +1824,8 @@ class DBImpl : public DB {
|
||||
// corrupted_log_found is set to true if we recover from a corrupted log file.
|
||||
Status RecoverLogFiles(const std::vector<uint64_t>& log_numbers,
|
||||
SequenceNumber* next_sequence, bool read_only,
|
||||
bool* corrupted_log_found);
|
||||
bool* corrupted_log_found,
|
||||
RecoveryContext* recovery_ctx);
|
||||
|
||||
// The following two methods are used to flush a memtable to
|
||||
// storage. The first one is used at database RecoveryTime (when the
|
||||
@@ -1779,9 +1959,12 @@ class DBImpl : public DB {
|
||||
Status PreprocessWrite(const WriteOptions& write_options, bool* need_log_sync,
|
||||
WriteContext* write_context);
|
||||
|
||||
WriteBatch* MergeBatch(const WriteThread::WriteGroup& write_group,
|
||||
WriteBatch* tmp_batch, size_t* write_with_wal,
|
||||
WriteBatch** to_be_cached_state);
|
||||
// Merge write batches in the write group into merged_batch.
|
||||
// Returns OK if merge is successful.
|
||||
// Returns Corruption if corruption in write batch is detected.
|
||||
Status MergeBatch(const WriteThread::WriteGroup& write_group,
|
||||
WriteBatch* tmp_batch, WriteBatch** merged_batch,
|
||||
size_t* write_with_wal, WriteBatch** to_be_cached_state);
|
||||
|
||||
// rate_limiter_priority is used to charge `DBOptions::rate_limiter`
|
||||
// for automatic WAL flush (`Options::manual_wal_flush` == false)
|
||||
@@ -1789,12 +1972,13 @@ class DBImpl : public DB {
|
||||
IOStatus WriteToWAL(const WriteBatch& merged_batch, log::Writer* log_writer,
|
||||
uint64_t* log_used, uint64_t* log_size,
|
||||
Env::IOPriority rate_limiter_priority,
|
||||
bool with_db_mutex = false, bool with_log_mutex = false);
|
||||
LogFileNumberSize& log_file_number_size);
|
||||
|
||||
IOStatus WriteToWAL(const WriteThread::WriteGroup& write_group,
|
||||
log::Writer* log_writer, uint64_t* log_used,
|
||||
bool need_log_sync, bool need_log_dir_sync,
|
||||
SequenceNumber sequence);
|
||||
SequenceNumber sequence,
|
||||
LogFileNumberSize& log_file_number_size);
|
||||
|
||||
IOStatus ConcurrentWriteToWAL(const WriteThread::WriteGroup& write_group,
|
||||
uint64_t* log_used,
|
||||
@@ -1815,7 +1999,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,
|
||||
@@ -1827,7 +2010,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
|
||||
@@ -1919,10 +2101,24 @@ class DBImpl : public DB {
|
||||
SnapshotImpl* GetSnapshotImpl(bool is_write_conflict_boundary,
|
||||
bool lock = true);
|
||||
|
||||
// If snapshot_seq != kMaxSequenceNumber, then this function can only be
|
||||
// called from the write thread that publishes sequence numbers to readers.
|
||||
// For 1) write-committed, or 2) write-prepared + one-write-queue, this will
|
||||
// be the write thread performing memtable writes. For write-prepared with
|
||||
// two write queues, this will be the write thread writing commit marker to
|
||||
// the WAL.
|
||||
// If snapshot_seq == kMaxSequenceNumber, this function is called by a caller
|
||||
// ensuring no writes to the database.
|
||||
std::pair<Status, std::shared_ptr<const SnapshotImpl>>
|
||||
CreateTimestampedSnapshotImpl(SequenceNumber snapshot_seq, uint64_t ts,
|
||||
bool lock = true);
|
||||
|
||||
uint64_t GetMaxTotalWalSize() const;
|
||||
|
||||
FSDirectory* GetDataDir(ColumnFamilyData* cfd, size_t path_id) const;
|
||||
|
||||
Status MaybeReleaseTimestampedSnapshotsAndCheck();
|
||||
|
||||
Status CloseHelper();
|
||||
|
||||
void WaitForBackgroundWork();
|
||||
@@ -2124,11 +2320,7 @@ class DBImpl : public DB {
|
||||
// are protected by locking both mutex_ and log_write_mutex_, and reads must
|
||||
// be under either mutex_ or log_write_mutex_.
|
||||
std::deque<LogFileNumberSize> alive_log_files_;
|
||||
// Caching the result of `alive_log_files_.back()` so that we do not have to
|
||||
// call `alive_log_files_.back()` in the write thread (WriteToWAL()) which
|
||||
// requires locking db mutex if log_mutex_ is not already held in
|
||||
// two-write-queues mode.
|
||||
std::deque<LogFileNumberSize>::reverse_iterator alive_log_files_tail_;
|
||||
|
||||
// Log files that aren't fully synced, and the current log file.
|
||||
// Synchronization:
|
||||
// - push_back() is done from write_thread_ with locked mutex_ and
|
||||
@@ -2192,6 +2384,8 @@ class DBImpl : public DB {
|
||||
|
||||
SnapshotList snapshots_;
|
||||
|
||||
TimestampedSnapshotList timestamped_snapshots_;
|
||||
|
||||
// For each background job, pending_outputs_ keeps the current file number at
|
||||
// the time that background job started.
|
||||
// FindObsoleteFiles()/PurgeObsoleteFiles() never deletes any file that has
|
||||
@@ -2302,12 +2496,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
|
||||
@@ -2410,10 +2598,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,
|
||||
@@ -2479,8 +2669,9 @@ inline Status DBImpl::FailIfCfHasTs(
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
inline Status DBImpl::FailIfTsSizesMismatch(
|
||||
const ColumnFamilyHandle* column_family, const Slice& ts) const {
|
||||
inline Status DBImpl::FailIfTsMismatchCf(ColumnFamilyHandle* column_family,
|
||||
const Slice& ts,
|
||||
bool ts_for_read) const {
|
||||
if (!column_family) {
|
||||
return Status::InvalidArgument("column family handle cannot be null");
|
||||
}
|
||||
@@ -2500,6 +2691,19 @@ inline Status DBImpl::FailIfTsSizesMismatch(
|
||||
<< ts_sz << " given";
|
||||
return Status::InvalidArgument(oss.str());
|
||||
}
|
||||
if (ts_for_read) {
|
||||
auto cfh = static_cast_with_check<ColumnFamilyHandleImpl>(column_family);
|
||||
auto cfd = cfh->cfd();
|
||||
std::string current_ts_low = cfd->GetFullHistoryTsLow();
|
||||
if (!current_ts_low.empty() &&
|
||||
ucmp->CompareTimestamp(ts, current_ts_low) < 0) {
|
||||
std::stringstream oss;
|
||||
oss << "Read timestamp: " << ts.ToString(true)
|
||||
<< " is smaller than full_history_ts_low: "
|
||||
<< Slice(current_ts_low).ToString(true) << std::endl;
|
||||
return Status::InvalidArgument(oss.str());
|
||||
}
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
|
||||
@@ -88,14 +88,13 @@ IOStatus DBImpl::SyncClosedLogs(JobContext* job_context) {
|
||||
autovector<log::Writer*, 1> logs_to_sync;
|
||||
uint64_t current_log_number = logfile_number_;
|
||||
while (logs_.front().number < current_log_number &&
|
||||
logs_.front().getting_synced) {
|
||||
logs_.front().IsSyncing()) {
|
||||
log_sync_cv_.Wait();
|
||||
}
|
||||
for (auto it = logs_.begin();
|
||||
it != logs_.end() && it->number < current_log_number; ++it) {
|
||||
auto& log = *it;
|
||||
assert(!log.getting_synced);
|
||||
log.getting_synced = true;
|
||||
log.PrepareForSync();
|
||||
logs_to_sync.push_back(log.writer);
|
||||
}
|
||||
|
||||
@@ -952,6 +951,8 @@ Status DBImpl::IncreaseFullHistoryTsLowImpl(ColumnFamilyData* cfd,
|
||||
VersionEdit edit;
|
||||
edit.SetColumnFamily(cfd->GetID());
|
||||
edit.SetFullHistoryTsLow(ts_low);
|
||||
TEST_SYNC_POINT_CALLBACK("DBImpl::IncreaseFullHistoryTsLowImpl:BeforeEdit",
|
||||
&edit);
|
||||
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
std::string current_ts_low = cfd->GetFullHistoryTsLow();
|
||||
@@ -959,12 +960,25 @@ Status DBImpl::IncreaseFullHistoryTsLowImpl(ColumnFamilyData* cfd,
|
||||
assert(ucmp->timestamp_size() == ts_low.size() && !ts_low.empty());
|
||||
if (!current_ts_low.empty() &&
|
||||
ucmp->CompareTimestamp(ts_low, current_ts_low) < 0) {
|
||||
return Status::InvalidArgument(
|
||||
"Cannot decrease full_history_timestamp_low");
|
||||
return Status::InvalidArgument("Cannot decrease full_history_ts_low");
|
||||
}
|
||||
|
||||
return versions_->LogAndApply(cfd, *cfd->GetLatestMutableCFOptions(), &edit,
|
||||
&mutex_);
|
||||
Status s = versions_->LogAndApply(cfd, *cfd->GetLatestMutableCFOptions(),
|
||||
&edit, &mutex_);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
current_ts_low = cfd->GetFullHistoryTsLow();
|
||||
if (!current_ts_low.empty() &&
|
||||
ucmp->CompareTimestamp(current_ts_low, ts_low) > 0) {
|
||||
std::stringstream oss;
|
||||
oss << "full_history_ts_low: " << Slice(current_ts_low).ToString(true)
|
||||
<< " is set to be higher than the requested "
|
||||
"timestamp: "
|
||||
<< Slice(ts_low).ToString(true) << std::endl;
|
||||
return Status::TryAgain(oss.str());
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status DBImpl::CompactRangeInternal(const CompactRangeOptions& options,
|
||||
@@ -1217,6 +1231,10 @@ Status DBImpl::CompactFiles(const CompactionOptions& compact_options,
|
||||
|
||||
// Perform CompactFiles
|
||||
TEST_SYNC_POINT("TestCompactFiles::IngestExternalFile2");
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"TestCompactFiles:PausingManualCompaction:3",
|
||||
reinterpret_cast<void*>(
|
||||
const_cast<std::atomic<int>*>(&manual_compaction_paused_)));
|
||||
{
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
|
||||
@@ -1372,7 +1390,7 @@ Status DBImpl::CompactFilesImpl(
|
||||
c->mutable_cf_options()->paranoid_file_checks,
|
||||
c->mutable_cf_options()->report_bg_io_stats, dbname_,
|
||||
&compaction_job_stats, Env::Priority::USER, io_tracer_,
|
||||
&manual_compaction_paused_, nullptr, db_id_, db_session_id_,
|
||||
kManualCompactionCanceledFalse_, db_id_, db_session_id_,
|
||||
c->column_family_data()->GetFullHistoryTsLow(), c->trim_ts(),
|
||||
&blob_callback_);
|
||||
|
||||
@@ -1838,8 +1856,7 @@ Status DBImpl::RunManualCompaction(
|
||||
// and `CompactRangeOptions::canceled` might not work well together.
|
||||
while (bg_bottom_compaction_scheduled_ > 0 ||
|
||||
bg_compaction_scheduled_ > 0) {
|
||||
if (manual_compaction_paused_ > 0 ||
|
||||
(manual.canceled != nullptr && *manual.canceled == true)) {
|
||||
if (manual_compaction_paused_ > 0 || manual.canceled == true) {
|
||||
// Pretend the error came from compaction so the below cleanup/error
|
||||
// handling code can process it.
|
||||
manual.done = true;
|
||||
@@ -2220,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) {
|
||||
{
|
||||
@@ -2376,10 +2393,18 @@ Status DBImpl::EnableAutoCompaction(
|
||||
return s;
|
||||
}
|
||||
|
||||
// NOTE: Calling DisableManualCompaction() may overwrite the
|
||||
// user-provided canceled variable in CompactRangeOptions
|
||||
void DBImpl::DisableManualCompaction() {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
manual_compaction_paused_.fetch_add(1, std::memory_order_release);
|
||||
|
||||
// Mark the canceled as true when the cancellation is triggered by
|
||||
// manual_compaction_paused (may overwrite user-provided `canceled`)
|
||||
for (const auto& manual_compaction : manual_compaction_dequeue_) {
|
||||
manual_compaction->canceled = true;
|
||||
}
|
||||
|
||||
// Wake up manual compactions waiting to start.
|
||||
bg_cv_.SignalAll();
|
||||
|
||||
@@ -2392,6 +2417,11 @@ void DBImpl::DisableManualCompaction() {
|
||||
}
|
||||
}
|
||||
|
||||
// NOTE: In contrast to DisableManualCompaction(), calling
|
||||
// EnableManualCompaction() does NOT overwrite the user-provided *canceled
|
||||
// variable to be false since there is NO CHANCE a canceled compaction
|
||||
// is uncanceled. In other words, a canceled compaction must have been
|
||||
// dropped out of the manual compaction queue, when we disable it.
|
||||
void DBImpl::EnableManualCompaction() {
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
assert(manual_compaction_paused_ > 0);
|
||||
@@ -2586,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);
|
||||
@@ -2745,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);
|
||||
@@ -3037,10 +3059,7 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
|
||||
if (shutting_down_.load(std::memory_order_acquire)) {
|
||||
status = Status::ShutdownInProgress();
|
||||
} else if (is_manual &&
|
||||
manual_compaction_paused_.load(std::memory_order_acquire) > 0) {
|
||||
status = Status::Incomplete(Status::SubCode::kManualCompactionPaused);
|
||||
} else if (is_manual && manual_compaction->canceled &&
|
||||
manual_compaction->canceled->load(std::memory_order_acquire)) {
|
||||
manual_compaction->canceled.load(std::memory_order_acquire)) {
|
||||
status = Status::Incomplete(Status::SubCode::kManualCompactionPaused);
|
||||
}
|
||||
} else {
|
||||
@@ -3289,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());
|
||||
@@ -3357,6 +3384,7 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
|
||||
GetSnapshotContext(job_context, &snapshot_seqs,
|
||||
&earliest_write_conflict_snapshot, &snapshot_checker);
|
||||
assert(is_snapshot_supported_ || snapshots_.empty());
|
||||
|
||||
CompactionJob compaction_job(
|
||||
job_context->job_id, c.get(), immutable_db_options_,
|
||||
mutable_db_options_, file_options_for_compaction_, versions_.get(),
|
||||
@@ -3368,9 +3396,9 @@ Status DBImpl::BackgroundCompaction(bool* made_progress,
|
||||
c->mutable_cf_options()->paranoid_file_checks,
|
||||
c->mutable_cf_options()->report_bg_io_stats, dbname_,
|
||||
&compaction_job_stats, thread_pri, io_tracer_,
|
||||
is_manual ? &manual_compaction_paused_ : nullptr,
|
||||
is_manual ? manual_compaction->canceled : nullptr, db_id_,
|
||||
db_session_id_, c->column_family_data()->GetFullHistoryTsLow(),
|
||||
is_manual ? manual_compaction->canceled
|
||||
: kManualCompactionCanceledFalse_,
|
||||
db_id_, db_session_id_, c->column_family_data()->GetFullHistoryTsLow(),
|
||||
c->trim_ts(), &blob_callback_);
|
||||
compaction_job.Prepare();
|
||||
|
||||
|
||||
+82
-60
@@ -19,6 +19,7 @@
|
||||
#include "logging/logging.h"
|
||||
#include "port/port.h"
|
||||
#include "util/autovector.h"
|
||||
#include "util/defer.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
@@ -166,8 +167,8 @@ void DBImpl::FindObsoleteFiles(JobContext* job_context, bool force,
|
||||
job_context->log_number = MinLogNumberToKeep();
|
||||
job_context->prev_log_number = versions_->prev_log_number();
|
||||
|
||||
versions_->AddLiveFiles(&job_context->sst_live, &job_context->blob_live);
|
||||
if (doing_the_full_scan) {
|
||||
versions_->AddLiveFiles(&job_context->sst_live, &job_context->blob_live);
|
||||
InfoLogPrefix info_log_prefix(!immutable_db_options_.db_log_dir.empty(),
|
||||
dbname_);
|
||||
std::set<std::string> paths;
|
||||
@@ -242,8 +243,32 @@ void DBImpl::FindObsoleteFiles(JobContext* job_context, bool force,
|
||||
log_file, immutable_db_options_.db_log_dir);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Instead of filling ob_context->sst_live and job_context->blob_live,
|
||||
// directly remove files that show up in any Version. This is because
|
||||
// candidate files tend to be a small percentage of all files, so it is
|
||||
// usually cheaper to check them against every version, compared to
|
||||
// building a map for all files.
|
||||
versions_->RemoveLiveFiles(job_context->sst_delete_files,
|
||||
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()) {
|
||||
@@ -280,7 +305,7 @@ void DBImpl::FindObsoleteFiles(JobContext* job_context, bool force,
|
||||
}
|
||||
while (!logs_.empty() && logs_.front().number < min_log_number) {
|
||||
auto& log = logs_.front();
|
||||
if (log.getting_synced) {
|
||||
if (log.IsSyncing()) {
|
||||
log_sync_cv_.Wait();
|
||||
// logs_ could have changed while we were waiting.
|
||||
continue;
|
||||
@@ -300,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();
|
||||
}
|
||||
|
||||
@@ -395,8 +417,10 @@ void DBImpl::PurgeObsoleteFiles(JobContext& state, bool schedule_only) {
|
||||
state.manifest_delete_files.size());
|
||||
// We may ignore the dbname when generating the file names.
|
||||
for (auto& file : state.sst_delete_files) {
|
||||
candidate_files.emplace_back(
|
||||
MakeTableFileName(file.metadata->fd.GetNumber()), file.path);
|
||||
if (!file.only_delete_metadata) {
|
||||
candidate_files.emplace_back(
|
||||
MakeTableFileName(file.metadata->fd.GetNumber()), file.path);
|
||||
}
|
||||
if (file.metadata->table_reader_handle) {
|
||||
table_cache_->Release(file.metadata->table_reader_handle);
|
||||
}
|
||||
@@ -863,49 +887,61 @@ uint64_t PrecomputeMinLogNumberToKeep2PC(
|
||||
return min_log_number_to_keep;
|
||||
}
|
||||
|
||||
Status DBImpl::SetDBId(bool read_only) {
|
||||
void DBImpl::SetDBId(std::string&& id, bool read_only,
|
||||
RecoveryContext* recovery_ctx) {
|
||||
assert(db_id_.empty());
|
||||
assert(!id.empty());
|
||||
db_id_ = std::move(id);
|
||||
if (!read_only && immutable_db_options_.write_dbid_to_manifest) {
|
||||
assert(recovery_ctx != nullptr);
|
||||
assert(versions_->GetColumnFamilySet() != nullptr);
|
||||
VersionEdit edit;
|
||||
edit.SetDBId(db_id_);
|
||||
versions_->db_id_ = db_id_;
|
||||
recovery_ctx->UpdateVersionEdits(
|
||||
versions_->GetColumnFamilySet()->GetDefault(), edit);
|
||||
}
|
||||
}
|
||||
|
||||
Status DBImpl::SetupDBId(bool read_only, RecoveryContext* recovery_ctx) {
|
||||
Status s;
|
||||
// Happens when immutable_db_options_.write_dbid_to_manifest is set to true
|
||||
// the very first time.
|
||||
if (db_id_.empty()) {
|
||||
// Check for the IDENTITY file and create it if not there.
|
||||
s = fs_->FileExists(IdentityFileName(dbname_), IOOptions(), nullptr);
|
||||
// Typically Identity file is created in NewDB() and for some reason if
|
||||
// it is no longer available then at this point DB ID is not in Identity
|
||||
// file or Manifest.
|
||||
if (s.IsNotFound()) {
|
||||
// Create a new DB ID, saving to file only if allowed
|
||||
if (read_only) {
|
||||
db_id_ = env_->GenerateUniqueId();
|
||||
return Status::OK();
|
||||
} else {
|
||||
s = SetIdentityFile(env_, dbname_);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
// Check for the IDENTITY file and create it if not there or
|
||||
// broken or not matching manifest
|
||||
std::string db_id_in_file;
|
||||
s = fs_->FileExists(IdentityFileName(dbname_), IOOptions(), nullptr);
|
||||
if (s.ok()) {
|
||||
s = GetDbIdentityFromIdentityFile(&db_id_in_file);
|
||||
if (s.ok() && !db_id_in_file.empty()) {
|
||||
if (db_id_.empty()) {
|
||||
// Loaded from file and wasn't already known from manifest
|
||||
SetDBId(std::move(db_id_in_file), read_only, recovery_ctx);
|
||||
return s;
|
||||
} else if (db_id_ == db_id_in_file) {
|
||||
// Loaded from file and matches manifest
|
||||
return s;
|
||||
}
|
||||
} else if (!s.ok()) {
|
||||
assert(s.IsIOError());
|
||||
return s;
|
||||
}
|
||||
s = GetDbIdentityFromIdentityFile(&db_id_);
|
||||
if (immutable_db_options_.write_dbid_to_manifest && s.ok()) {
|
||||
VersionEdit edit;
|
||||
edit.SetDBId(db_id_);
|
||||
Options options;
|
||||
MutableCFOptions mutable_cf_options(options);
|
||||
versions_->db_id_ = db_id_;
|
||||
s = versions_->LogAndApply(versions_->GetColumnFamilySet()->GetDefault(),
|
||||
mutable_cf_options, &edit, &mutex_, nullptr,
|
||||
/* new_descriptor_log */ false);
|
||||
}
|
||||
} else if (!read_only) {
|
||||
}
|
||||
if (s.IsNotFound()) {
|
||||
s = Status::OK();
|
||||
}
|
||||
if (!s.ok()) {
|
||||
assert(s.IsIOError());
|
||||
return s;
|
||||
}
|
||||
// Otherwise IDENTITY file is missing or no good.
|
||||
// Generate new id if needed
|
||||
if (db_id_.empty()) {
|
||||
SetDBId(env_->GenerateUniqueId(), read_only, recovery_ctx);
|
||||
}
|
||||
// Persist it to IDENTITY file if allowed
|
||||
if (!read_only) {
|
||||
s = SetIdentityFile(env_, dbname_, db_id_);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
Status DBImpl::DeleteUnreferencedSstFiles() {
|
||||
Status DBImpl::DeleteUnreferencedSstFiles(RecoveryContext* recovery_ctx) {
|
||||
mutex_.AssertHeld();
|
||||
std::vector<std::string> paths;
|
||||
paths.push_back(NormalizePath(dbname_ + std::string(1, kFilePathSeparator)));
|
||||
@@ -925,7 +961,6 @@ Status DBImpl::DeleteUnreferencedSstFiles() {
|
||||
|
||||
uint64_t next_file_number = versions_->current_next_file_number();
|
||||
uint64_t largest_file_number = next_file_number;
|
||||
std::set<std::string> files_to_delete;
|
||||
Status s;
|
||||
for (const auto& path : paths) {
|
||||
std::vector<std::string> files;
|
||||
@@ -943,8 +978,9 @@ Status DBImpl::DeleteUnreferencedSstFiles() {
|
||||
const std::string normalized_fpath = path + fname;
|
||||
largest_file_number = std::max(largest_file_number, number);
|
||||
if (type == kTableFile && number >= next_file_number &&
|
||||
files_to_delete.find(normalized_fpath) == files_to_delete.end()) {
|
||||
files_to_delete.insert(normalized_fpath);
|
||||
recovery_ctx->files_to_delete_.find(normalized_fpath) ==
|
||||
recovery_ctx->files_to_delete_.end()) {
|
||||
recovery_ctx->files_to_delete_.emplace(normalized_fpath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -961,21 +997,7 @@ Status DBImpl::DeleteUnreferencedSstFiles() {
|
||||
assert(versions_->GetColumnFamilySet());
|
||||
ColumnFamilyData* default_cfd = versions_->GetColumnFamilySet()->GetDefault();
|
||||
assert(default_cfd);
|
||||
s = versions_->LogAndApply(
|
||||
default_cfd, *default_cfd->GetLatestMutableCFOptions(), &edit, &mutex_,
|
||||
directories_.GetDbDir(), /*new_descriptor_log*/ false);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
mutex_.Unlock();
|
||||
for (const auto& fname : files_to_delete) {
|
||||
s = env_->DeleteFile(fname);
|
||||
if (!s.ok()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
mutex_.Lock();
|
||||
recovery_ctx->UpdateVersionEdits(default_cfd, edit);
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
+273
-178
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,7 +403,7 @@ IOStatus Directories::SetDirectories(FileSystem* fs, const std::string& dbname,
|
||||
Status DBImpl::Recover(
|
||||
const std::vector<ColumnFamilyDescriptor>& column_families, bool read_only,
|
||||
bool error_if_wal_file_exists, bool error_if_data_exists_in_wals,
|
||||
uint64_t* recovered_seq) {
|
||||
uint64_t* recovered_seq, RecoveryContext* recovery_ctx) {
|
||||
mutex_.AssertHeld();
|
||||
|
||||
bool is_new_db = false;
|
||||
@@ -524,9 +528,10 @@ Status DBImpl::Recover(
|
||||
return s;
|
||||
}
|
||||
}
|
||||
s = SetDBId(read_only);
|
||||
s = SetupDBId(read_only, recovery_ctx);
|
||||
ROCKS_LOG_INFO(immutable_db_options_.info_log, "DB ID: %s\n", db_id_.c_str());
|
||||
if (s.ok() && !read_only) {
|
||||
s = DeleteUnreferencedSstFiles();
|
||||
s = DeleteUnreferencedSstFiles(recovery_ctx);
|
||||
}
|
||||
|
||||
if (immutable_db_options_.paranoid_checks && s.ok()) {
|
||||
@@ -541,10 +546,6 @@ Status DBImpl::Recover(
|
||||
}
|
||||
}
|
||||
}
|
||||
// DB mutex is already held
|
||||
if (s.ok() && immutable_db_options_.persist_stats_to_disk) {
|
||||
s = InitPersistStatsColumnFamily();
|
||||
}
|
||||
|
||||
std::vector<std::string> files_in_wal_dir;
|
||||
if (s.ok()) {
|
||||
@@ -561,10 +562,6 @@ Status DBImpl::Recover(
|
||||
default_cf_handle_ = new ColumnFamilyHandleImpl(
|
||||
versions_->GetColumnFamilySet()->GetDefault(), this, &mutex_);
|
||||
default_cf_internal_stats_ = default_cf_handle_->cfd()->internal_stats();
|
||||
// TODO(Zhongyi): handle single_column_family_mode_ when
|
||||
// persistent_stats is enabled
|
||||
single_column_family_mode_ =
|
||||
versions_->GetColumnFamilySet()->NumberOfColumnFamilies() == 1;
|
||||
|
||||
// Recover from all newer log files than the ones named in the
|
||||
// descriptor (new log files may have been added by the previous
|
||||
@@ -614,7 +611,10 @@ Status DBImpl::Recover(
|
||||
WalNumber max_wal_number =
|
||||
versions_->GetWalSet().GetWals().rbegin()->first;
|
||||
edit.DeleteWalsBefore(max_wal_number + 1);
|
||||
s = versions_->LogAndApplyToDefaultColumnFamily(&edit, &mutex_);
|
||||
assert(recovery_ctx != nullptr);
|
||||
assert(versions_->GetColumnFamilySet() != nullptr);
|
||||
recovery_ctx->UpdateVersionEdits(
|
||||
versions_->GetColumnFamilySet()->GetDefault(), edit);
|
||||
}
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
@@ -650,8 +650,8 @@ Status DBImpl::Recover(
|
||||
std::sort(wals.begin(), wals.end());
|
||||
|
||||
bool corrupted_wal_found = false;
|
||||
s = RecoverLogFiles(wals, &next_sequence, read_only,
|
||||
&corrupted_wal_found);
|
||||
s = RecoverLogFiles(wals, &next_sequence, read_only, &corrupted_wal_found,
|
||||
recovery_ctx);
|
||||
if (corrupted_wal_found && recovered_seq != nullptr) {
|
||||
*recovered_seq = next_sequence;
|
||||
}
|
||||
@@ -830,10 +830,155 @@ Status DBImpl::InitPersistStatsColumnFamily() {
|
||||
return s;
|
||||
}
|
||||
|
||||
Status DBImpl::LogAndApplyForRecovery(const RecoveryContext& recovery_ctx) {
|
||||
mutex_.AssertHeld();
|
||||
assert(versions_->descriptor_log_ == nullptr);
|
||||
Status s = versions_->LogAndApply(
|
||||
recovery_ctx.cfds_, recovery_ctx.mutable_cf_opts_,
|
||||
recovery_ctx.edit_lists_, &mutex_, directories_.GetDbDir());
|
||||
if (s.ok() && !(recovery_ctx.files_to_delete_.empty())) {
|
||||
mutex_.Unlock();
|
||||
for (const auto& fname : recovery_ctx.files_to_delete_) {
|
||||
s = env_->DeleteFile(fname);
|
||||
if (!s.ok()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
mutex_.Lock();
|
||||
}
|
||||
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,
|
||||
bool* corrupted_wal_found) {
|
||||
bool* corrupted_wal_found,
|
||||
RecoveryContext* recovery_ctx) {
|
||||
struct LogReporter : public log::Reader::Reporter {
|
||||
Env* env;
|
||||
Logger* info_log;
|
||||
@@ -871,20 +1016,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;
|
||||
@@ -975,9 +1108,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(),
|
||||
@@ -987,12 +1122,22 @@ Status DBImpl::RecoverLogFiles(const std::vector<uint64_t>& wal_numbers,
|
||||
|
||||
// We create a new batch and initialize with a valid prot_info_ to store
|
||||
// the data checksums
|
||||
WriteBatch batch(0, 0, 8, 0);
|
||||
WriteBatch batch;
|
||||
|
||||
status = WriteBatchInternal::SetContents(&batch, record);
|
||||
if (!status.ok()) {
|
||||
return status;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
SequenceNumber sequence = WriteBatchInternal::Sequence(&batch);
|
||||
|
||||
@@ -1011,83 +1156,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
|
||||
@@ -1291,44 +1366,36 @@ Status DBImpl::RecoverLogFiles(const std::vector<uint64_t>& wal_numbers,
|
||||
// VersionSet::next_file_number_ always to be strictly greater than any
|
||||
// log number
|
||||
versions_->MarkFileNumberUsed(max_wal_number + 1);
|
||||
assert(recovery_ctx != nullptr);
|
||||
|
||||
autovector<ColumnFamilyData*> cfds;
|
||||
autovector<const MutableCFOptions*> cf_opts;
|
||||
autovector<autovector<VersionEdit*>> edit_lists;
|
||||
for (auto* cfd : *versions_->GetColumnFamilySet()) {
|
||||
cfds.push_back(cfd);
|
||||
cf_opts.push_back(cfd->GetLatestMutableCFOptions());
|
||||
auto iter = version_edits.find(cfd->GetID());
|
||||
assert(iter != version_edits.end());
|
||||
edit_lists.push_back({&iter->second});
|
||||
recovery_ctx->UpdateVersionEdits(cfd, iter->second);
|
||||
}
|
||||
|
||||
std::unique_ptr<VersionEdit> wal_deletion;
|
||||
if (flushed) {
|
||||
wal_deletion = std::make_unique<VersionEdit>();
|
||||
VersionEdit wal_deletion;
|
||||
if (immutable_db_options_.track_and_verify_wals_in_manifest) {
|
||||
wal_deletion->DeleteWalsBefore(max_wal_number + 1);
|
||||
wal_deletion.DeleteWalsBefore(max_wal_number + 1);
|
||||
}
|
||||
if (!allow_2pc()) {
|
||||
// In non-2pc mode, flushing the memtables of the column families
|
||||
// means we can advance min_log_number_to_keep.
|
||||
wal_deletion->SetMinLogNumberToKeep(max_wal_number + 1);
|
||||
wal_deletion.SetMinLogNumberToKeep(max_wal_number + 1);
|
||||
}
|
||||
edit_lists.back().push_back(wal_deletion.get());
|
||||
assert(versions_->GetColumnFamilySet() != nullptr);
|
||||
recovery_ctx->UpdateVersionEdits(
|
||||
versions_->GetColumnFamilySet()->GetDefault(), wal_deletion);
|
||||
}
|
||||
|
||||
// write MANIFEST with update
|
||||
status = versions_->LogAndApply(cfds, cf_opts, edit_lists, &mutex_,
|
||||
directories_.GetDbDir(),
|
||||
/*new_descriptor_log=*/true);
|
||||
}
|
||||
}
|
||||
|
||||
if (status.ok()) {
|
||||
if (data_seen && !flushed) {
|
||||
status = RestoreAliveLogFiles(wal_numbers);
|
||||
} else {
|
||||
// If there's no data in the WAL, or we flushed all the data, still
|
||||
} else if (!wal_numbers.empty()) { // If there's no data in the WAL, or we
|
||||
// flushed all the data, still
|
||||
// truncate the log file. If the process goes into a crash loop before
|
||||
// the file is deleted, the preallocated space will never get freed.
|
||||
const bool truncate = !read_only;
|
||||
@@ -1413,7 +1480,6 @@ Status DBImpl::RestoreAliveLogFiles(const std::vector<uint64_t>& wal_numbers) {
|
||||
total_log_size_ += log.size;
|
||||
alive_log_files_.push_back(log);
|
||||
}
|
||||
alive_log_files_tail_ = alive_log_files_.rbegin();
|
||||
if (two_write_queues_) {
|
||||
log_write_mutex_.Unlock();
|
||||
}
|
||||
@@ -1724,6 +1790,7 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname,
|
||||
}
|
||||
|
||||
*dbptr = nullptr;
|
||||
assert(handles);
|
||||
handles->clear();
|
||||
|
||||
size_t max_write_buffer_size = 0;
|
||||
@@ -1733,6 +1800,13 @@ 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 = 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()) {
|
||||
std::vector<std::string> paths;
|
||||
@@ -1766,11 +1840,13 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname,
|
||||
}
|
||||
|
||||
impl->wal_in_db_path_ = impl->immutable_db_options_.IsWalDirSameAsDBPath();
|
||||
|
||||
RecoveryContext recovery_ctx;
|
||||
impl->mutex_.Lock();
|
||||
|
||||
// Handles create_if_missing, error_if_exists
|
||||
uint64_t recovered_seq(kMaxSequenceNumber);
|
||||
s = impl->Recover(column_families, false, false, false, &recovered_seq);
|
||||
s = impl->Recover(column_families, false, false, false, &recovered_seq,
|
||||
&recovery_ctx);
|
||||
if (s.ok()) {
|
||||
uint64_t new_log_number = impl->versions_->NewFileNumber();
|
||||
log::Writer* new_log = nullptr;
|
||||
@@ -1787,46 +1863,11 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname,
|
||||
}
|
||||
|
||||
if (s.ok()) {
|
||||
// set column family handles
|
||||
for (auto cf : column_families) {
|
||||
auto cfd =
|
||||
impl->versions_->GetColumnFamilySet()->GetColumnFamily(cf.name);
|
||||
if (cfd != nullptr) {
|
||||
handles->push_back(
|
||||
new ColumnFamilyHandleImpl(cfd, impl, &impl->mutex_));
|
||||
impl->NewThreadStatusCfInfo(cfd);
|
||||
} else {
|
||||
if (db_options.create_missing_column_families) {
|
||||
// missing column family, create it
|
||||
ColumnFamilyHandle* handle;
|
||||
impl->mutex_.Unlock();
|
||||
s = impl->CreateColumnFamily(cf.options, cf.name, &handle);
|
||||
impl->mutex_.Lock();
|
||||
if (s.ok()) {
|
||||
handles->push_back(handle);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
s = Status::InvalidArgument("Column family not found", cf.name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (s.ok()) {
|
||||
SuperVersionContext sv_context(/* create_superversion */ true);
|
||||
for (auto cfd : *impl->versions_->GetColumnFamilySet()) {
|
||||
impl->InstallSuperVersionAndScheduleWork(
|
||||
cfd, &sv_context, *cfd->GetLatestMutableCFOptions());
|
||||
}
|
||||
sv_context.Clean();
|
||||
if (impl->two_write_queues_) {
|
||||
impl->log_write_mutex_.Lock();
|
||||
}
|
||||
impl->alive_log_files_.push_back(
|
||||
DBImpl::LogFileNumberSize(impl->logfile_number_));
|
||||
impl->alive_log_files_tail_ = impl->alive_log_files_.rbegin();
|
||||
if (impl->two_write_queues_) {
|
||||
impl->log_write_mutex_.Unlock();
|
||||
}
|
||||
@@ -1847,8 +1888,12 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname,
|
||||
WriteOptions write_options;
|
||||
uint64_t log_used, log_size;
|
||||
log::Writer* log_writer = impl->logs_.back().writer;
|
||||
LogFileNumberSize& log_file_number_size = impl->alive_log_files_.back();
|
||||
|
||||
assert(log_writer->get_log_number() == log_file_number_size.number);
|
||||
impl->mutex_.AssertHeld();
|
||||
s = impl->WriteToWAL(empty_batch, log_writer, &log_used, &log_size,
|
||||
Env::IO_TOTAL, /*with_db_mutex==*/true);
|
||||
Env::IO_TOTAL, log_file_number_size);
|
||||
if (s.ok()) {
|
||||
// Need to fsync, otherwise it might get lost after a power reset.
|
||||
s = impl->FlushWAL(false);
|
||||
@@ -1860,6 +1905,53 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname,
|
||||
}
|
||||
}
|
||||
}
|
||||
if (s.ok()) {
|
||||
s = impl->LogAndApplyForRecovery(recovery_ctx);
|
||||
}
|
||||
|
||||
if (s.ok() && impl->immutable_db_options_.persist_stats_to_disk) {
|
||||
impl->mutex_.AssertHeld();
|
||||
s = impl->InitPersistStatsColumnFamily();
|
||||
}
|
||||
|
||||
if (s.ok()) {
|
||||
// set column family handles
|
||||
for (auto cf : column_families) {
|
||||
auto cfd =
|
||||
impl->versions_->GetColumnFamilySet()->GetColumnFamily(cf.name);
|
||||
if (cfd != nullptr) {
|
||||
handles->push_back(
|
||||
new ColumnFamilyHandleImpl(cfd, impl, &impl->mutex_));
|
||||
impl->NewThreadStatusCfInfo(cfd);
|
||||
} else {
|
||||
if (db_options.create_missing_column_families) {
|
||||
// missing column family, create it
|
||||
ColumnFamilyHandle* handle = nullptr;
|
||||
impl->mutex_.Unlock();
|
||||
s = impl->CreateColumnFamily(cf.options, cf.name, &handle);
|
||||
impl->mutex_.Lock();
|
||||
if (s.ok()) {
|
||||
handles->push_back(handle);
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
s = Status::InvalidArgument("Column family not found", cf.name);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (s.ok()) {
|
||||
SuperVersionContext sv_context(/* create_superversion */ true);
|
||||
for (auto cfd : *impl->versions_->GetColumnFamilySet()) {
|
||||
impl->InstallSuperVersionAndScheduleWork(
|
||||
cfd, &sv_context, *cfd->GetLatestMutableCFOptions());
|
||||
}
|
||||
sv_context.Clean();
|
||||
}
|
||||
|
||||
if (s.ok() && impl->immutable_db_options_.persist_stats_to_disk) {
|
||||
// try to read format version
|
||||
s = impl->PersistentStatsProcessFormatVersion();
|
||||
@@ -1930,21 +2022,24 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname,
|
||||
// vast majority of all files), we'll pass the size to SstFileManager.
|
||||
// For all other files SstFileManager will query the size from filesystem.
|
||||
|
||||
std::vector<LiveFileMetaData> metadata;
|
||||
|
||||
// TODO: Once GetLiveFilesMetaData supports blob files, update the logic
|
||||
// below to get known_file_sizes for blob files.
|
||||
impl->mutex_.Lock();
|
||||
impl->versions_->GetLiveFilesMetaData(&metadata);
|
||||
impl->mutex_.Unlock();
|
||||
std::vector<ColumnFamilyMetaData> metadata;
|
||||
impl->GetAllColumnFamilyMetaData(&metadata);
|
||||
|
||||
std::unordered_map<std::string, uint64_t> known_file_sizes;
|
||||
for (const auto& md : metadata) {
|
||||
std::string name = md.name;
|
||||
if (!name.empty() && name[0] == '/') {
|
||||
name = name.substr(1);
|
||||
for (const auto& lmd : md.levels) {
|
||||
for (const auto& fmd : lmd.files) {
|
||||
known_file_sizes[fmd.relative_filename] = fmd.size;
|
||||
}
|
||||
}
|
||||
for (const auto& bmd : md.blob_files) {
|
||||
std::string name = bmd.blob_file_name;
|
||||
// The BlobMetaData.blob_file_name may start with "/".
|
||||
if (!name.empty() && name[0] == '/') {
|
||||
name = name.substr(1);
|
||||
}
|
||||
known_file_sizes[name] = bmd.blob_file_size;
|
||||
}
|
||||
known_file_sizes[name] = md.size;
|
||||
}
|
||||
|
||||
std::vector<std::string> paths;
|
||||
|
||||
@@ -47,8 +47,8 @@ Status DBImplReadOnly::Get(const ReadOptions& read_options,
|
||||
|
||||
assert(column_family);
|
||||
if (read_options.timestamp) {
|
||||
const Status s =
|
||||
FailIfTsSizesMismatch(column_family, *(read_options.timestamp));
|
||||
const Status s = FailIfTsMismatchCf(
|
||||
column_family, *(read_options.timestamp), /*ts_for_read=*/true);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -58,6 +58,13 @@ Status DBImplReadOnly::Get(const ReadOptions& read_options,
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the timestamps for returning results so that we can distinguish
|
||||
// between tombstone or key that has never been written
|
||||
if (timestamp) {
|
||||
timestamp->clear();
|
||||
}
|
||||
|
||||
const Comparator* ucmp = column_family->GetComparator();
|
||||
assert(ucmp);
|
||||
std::string* ts = ucmp->timestamp_size() > 0 ? timestamp : nullptr;
|
||||
@@ -107,8 +114,8 @@ Iterator* DBImplReadOnly::NewIterator(const ReadOptions& read_options,
|
||||
ColumnFamilyHandle* column_family) {
|
||||
assert(column_family);
|
||||
if (read_options.timestamp) {
|
||||
const Status s =
|
||||
FailIfTsSizesMismatch(column_family, *(read_options.timestamp));
|
||||
const Status s = FailIfTsMismatchCf(
|
||||
column_family, *(read_options.timestamp), /*ts_for_read=*/true);
|
||||
if (!s.ok()) {
|
||||
return NewErrorIterator(s);
|
||||
}
|
||||
@@ -148,7 +155,8 @@ Status DBImplReadOnly::NewIterators(
|
||||
if (read_options.timestamp) {
|
||||
for (auto* cf : column_families) {
|
||||
assert(cf);
|
||||
const Status s = FailIfTsSizesMismatch(cf, *(read_options.timestamp));
|
||||
const Status s = FailIfTsMismatchCf(cf, *(read_options.timestamp),
|
||||
/*ts_for_read=*/true);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
@@ -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*/,
|
||||
|
||||
@@ -33,7 +33,8 @@ DBImplSecondary::~DBImplSecondary() {}
|
||||
Status DBImplSecondary::Recover(
|
||||
const std::vector<ColumnFamilyDescriptor>& column_families,
|
||||
bool /*readonly*/, bool /*error_if_wal_file_exists*/,
|
||||
bool /*error_if_data_exists_in_wals*/, uint64_t*) {
|
||||
bool /*error_if_data_exists_in_wals*/, uint64_t*,
|
||||
RecoveryContext* /*recovery_ctx*/) {
|
||||
mutex_.AssertHeld();
|
||||
|
||||
JobContext job_context(0);
|
||||
@@ -61,8 +62,6 @@ Status DBImplSecondary::Recover(
|
||||
default_cf_handle_ = new ColumnFamilyHandleImpl(
|
||||
versions_->GetColumnFamilySet()->GetDefault(), this, &mutex_);
|
||||
default_cf_internal_stats_ = default_cf_handle_->cfd()->internal_stats();
|
||||
single_column_family_mode_ =
|
||||
versions_->GetColumnFamilySet()->NumberOfColumnFamilies() == 1;
|
||||
|
||||
std::unordered_set<ColumnFamilyData*> cfds_changed;
|
||||
s = FindAndRecoverLogFiles(&cfds_changed, &job_context);
|
||||
@@ -329,23 +328,43 @@ Status DBImplSecondary::RecoverLogFiles(
|
||||
Status DBImplSecondary::Get(const ReadOptions& read_options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
PinnableSlice* value) {
|
||||
return GetImpl(read_options, column_family, key, value);
|
||||
return GetImpl(read_options, column_family, key, value,
|
||||
/*timestamp*/ nullptr);
|
||||
}
|
||||
|
||||
Status DBImplSecondary::Get(const ReadOptions& read_options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
PinnableSlice* value, std::string* timestamp) {
|
||||
return GetImpl(read_options, column_family, key, value, timestamp);
|
||||
}
|
||||
|
||||
Status DBImplSecondary::GetImpl(const ReadOptions& read_options,
|
||||
ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableSlice* pinnable_val) {
|
||||
const Slice& key, PinnableSlice* pinnable_val,
|
||||
std::string* timestamp) {
|
||||
assert(pinnable_val != nullptr);
|
||||
PERF_CPU_TIMER_GUARD(get_cpu_nanos, immutable_db_options_.clock);
|
||||
StopWatch sw(immutable_db_options_.clock, stats_, DB_GET);
|
||||
PERF_TIMER_GUARD(get_snapshot_time);
|
||||
|
||||
assert(column_family);
|
||||
const Comparator* ucmp = column_family->GetComparator();
|
||||
assert(ucmp);
|
||||
if (ucmp->timestamp_size() || read_options.timestamp) {
|
||||
// TODO: support timestamp
|
||||
return Status::NotSupported();
|
||||
if (read_options.timestamp) {
|
||||
const Status s = FailIfTsMismatchCf(
|
||||
column_family, *(read_options.timestamp), /*ts_for_read=*/true);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
} else {
|
||||
const Status s = FailIfCfHasTs(column_family);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
// Clear the timestamp for returning results so that we can distinguish
|
||||
// between tombstone or key that has never been written later.
|
||||
if (timestamp) {
|
||||
timestamp->clear();
|
||||
}
|
||||
|
||||
auto cfh = static_cast<ColumnFamilyHandleImpl*>(column_family);
|
||||
@@ -359,23 +378,27 @@ Status DBImplSecondary::GetImpl(const ReadOptions& read_options,
|
||||
// Acquire SuperVersion
|
||||
SuperVersion* super_version = GetAndRefSuperVersion(cfd);
|
||||
SequenceNumber snapshot = versions_->LastSequence();
|
||||
GetWithTimestampReadCallback read_cb(snapshot);
|
||||
MergeContext merge_context;
|
||||
SequenceNumber max_covering_tombstone_seq = 0;
|
||||
Status s;
|
||||
LookupKey lkey(key, snapshot);
|
||||
LookupKey lkey(key, snapshot, read_options.timestamp);
|
||||
PERF_TIMER_STOP(get_snapshot_time);
|
||||
|
||||
bool done = false;
|
||||
if (super_version->mem->Get(lkey, pinnable_val->GetSelf(),
|
||||
/*timestamp=*/nullptr, &s, &merge_context,
|
||||
&max_covering_tombstone_seq, read_options)) {
|
||||
const Comparator* ucmp = column_family->GetComparator();
|
||||
assert(ucmp);
|
||||
std::string* ts = ucmp->timestamp_size() > 0 ? timestamp : nullptr;
|
||||
if (super_version->mem->Get(lkey, pinnable_val->GetSelf(), ts, &s,
|
||||
&merge_context, &max_covering_tombstone_seq,
|
||||
read_options, &read_cb)) {
|
||||
done = true;
|
||||
pinnable_val->PinSelf();
|
||||
RecordTick(stats_, MEMTABLE_HIT);
|
||||
} else if ((s.ok() || s.IsMergeInProgress()) &&
|
||||
super_version->imm->Get(
|
||||
lkey, pinnable_val->GetSelf(), /*timestamp=*/nullptr, &s,
|
||||
&merge_context, &max_covering_tombstone_seq, read_options)) {
|
||||
lkey, pinnable_val->GetSelf(), ts, &s, &merge_context,
|
||||
&max_covering_tombstone_seq, read_options, &read_cb)) {
|
||||
done = true;
|
||||
pinnable_val->PinSelf();
|
||||
RecordTick(stats_, MEMTABLE_HIT);
|
||||
@@ -387,9 +410,11 @@ Status DBImplSecondary::GetImpl(const ReadOptions& read_options,
|
||||
if (!done) {
|
||||
PERF_TIMER_GUARD(get_from_output_files_time);
|
||||
PinnedIteratorsManager pinned_iters_mgr;
|
||||
super_version->current->Get(read_options, lkey, pinnable_val,
|
||||
/*timestamp=*/nullptr, &s, &merge_context,
|
||||
&max_covering_tombstone_seq, &pinned_iters_mgr);
|
||||
super_version->current->Get(
|
||||
read_options, lkey, pinnable_val, ts, &s, &merge_context,
|
||||
&max_covering_tombstone_seq, &pinned_iters_mgr, /*value_found*/ nullptr,
|
||||
/*key_exists*/ nullptr, /*seq*/ nullptr, &read_cb, /*is_blob*/ nullptr,
|
||||
/*do_merge*/ true);
|
||||
RecordTick(stats_, MEMTABLE_MISS);
|
||||
}
|
||||
{
|
||||
@@ -416,11 +441,17 @@ Iterator* DBImplSecondary::NewIterator(const ReadOptions& read_options,
|
||||
}
|
||||
|
||||
assert(column_family);
|
||||
const Comparator* ucmp = column_family->GetComparator();
|
||||
assert(ucmp);
|
||||
if (ucmp->timestamp_size() || read_options.timestamp) {
|
||||
// TODO: support timestamp
|
||||
return NewErrorIterator(Status::NotSupported());
|
||||
if (read_options.timestamp) {
|
||||
const Status s = FailIfTsMismatchCf(
|
||||
column_family, *(read_options.timestamp), /*ts_for_read=*/true);
|
||||
if (!s.ok()) {
|
||||
return NewErrorIterator(s);
|
||||
}
|
||||
} else {
|
||||
const Status s = FailIfCfHasTs(column_family);
|
||||
if (!s.ok()) {
|
||||
return NewErrorIterator(s);
|
||||
}
|
||||
}
|
||||
|
||||
Iterator* result = nullptr;
|
||||
@@ -479,17 +510,22 @@ Status DBImplSecondary::NewIterators(
|
||||
if (iterators == nullptr) {
|
||||
return Status::InvalidArgument("iterators not allowed to be nullptr");
|
||||
}
|
||||
|
||||
if (read_options.timestamp) {
|
||||
// TODO: support timestamp
|
||||
return Status::NotSupported();
|
||||
for (auto* cf : column_families) {
|
||||
assert(cf);
|
||||
const Status s = FailIfTsMismatchCf(cf, *(read_options.timestamp),
|
||||
/*ts_for_read=*/true);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (auto* cf : column_families) {
|
||||
assert(cf);
|
||||
const Comparator* ucmp = cf->GetComparator();
|
||||
assert(ucmp);
|
||||
if (ucmp->timestamp_size()) {
|
||||
// TODO: support timestamp
|
||||
return Status::NotSupported();
|
||||
const Status s = FailIfCfHasTs(cf);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -502,7 +538,7 @@ Status DBImplSecondary::NewIterators(
|
||||
// TODO (yanqin) support snapshot.
|
||||
return Status::NotSupported("snapshot not supported in secondary mode");
|
||||
} else {
|
||||
SequenceNumber read_seq = versions_->LastSequence();
|
||||
SequenceNumber read_seq(kMaxSequenceNumber);
|
||||
for (auto cfh : column_families) {
|
||||
ColumnFamilyData* cfd = static_cast<ColumnFamilyHandleImpl*>(cfh)->cfd();
|
||||
iterators->push_back(
|
||||
@@ -646,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;
|
||||
@@ -659,9 +689,31 @@ Status DB::OpenAsSecondary(
|
||||
s = CreateLoggerFromOptions(secondary_path, tmp_opts, &tmp_opts.info_log);
|
||||
if (!s.ok()) {
|
||||
tmp_opts.info_log = nullptr;
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
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(
|
||||
@@ -783,8 +835,8 @@ Status DBImplSecondary::CompactWithoutInstallation(
|
||||
file_options_for_compaction_, versions_.get(), &shutting_down_,
|
||||
&log_buffer, output_dir.get(), stats_, &mutex_, &error_handler_,
|
||||
input.snapshots, table_cache_, &event_logger_, dbname_, io_tracer_,
|
||||
options.canceled, input.db_id, db_session_id_, secondary_path_, input,
|
||||
result);
|
||||
options.canceled ? *options.canceled : kManualCompactionCanceledFalse_,
|
||||
input.db_id, db_session_id_, secondary_path_, input, result);
|
||||
|
||||
mutex_.Unlock();
|
||||
s = compaction_job.Run();
|
||||
|
||||
@@ -81,18 +81,41 @@ class DBImplSecondary : public DBImpl {
|
||||
// and log_readers_ to facilitate future operations.
|
||||
Status Recover(const std::vector<ColumnFamilyDescriptor>& column_families,
|
||||
bool read_only, bool error_if_wal_file_exists,
|
||||
bool error_if_data_exists_in_wals,
|
||||
uint64_t* = nullptr) override;
|
||||
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;
|
||||
|
||||
Status Get(const ReadOptions& options, ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableSlice* value,
|
||||
std::string* timestamp) override;
|
||||
|
||||
Status GetImpl(const ReadOptions& options, ColumnFamilyHandle* column_family,
|
||||
const Slice& key, PinnableSlice* value);
|
||||
const Slice& key, PinnableSlice* value,
|
||||
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;
|
||||
|
||||
@@ -114,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*/,
|
||||
|
||||
+206
-79
@@ -30,13 +30,24 @@ Status DBImpl::Put(const WriteOptions& o, ColumnFamilyHandle* column_family,
|
||||
|
||||
Status DBImpl::Put(const WriteOptions& o, ColumnFamilyHandle* column_family,
|
||||
const Slice& key, const Slice& ts, const Slice& val) {
|
||||
const Status s = FailIfTsSizesMismatch(column_family, ts);
|
||||
const Status s = FailIfTsMismatchCf(column_family, ts, /*ts_for_read=*/false);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
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);
|
||||
@@ -63,7 +74,7 @@ Status DBImpl::Delete(const WriteOptions& write_options,
|
||||
Status DBImpl::Delete(const WriteOptions& write_options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
const Slice& ts) {
|
||||
const Status s = FailIfTsSizesMismatch(column_family, ts);
|
||||
const Status s = FailIfTsMismatchCf(column_family, ts, /*ts_for_read=*/false);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -83,7 +94,7 @@ Status DBImpl::SingleDelete(const WriteOptions& write_options,
|
||||
Status DBImpl::SingleDelete(const WriteOptions& write_options,
|
||||
ColumnFamilyHandle* column_family, const Slice& key,
|
||||
const Slice& ts) {
|
||||
const Status s = FailIfTsSizesMismatch(column_family, ts);
|
||||
const Status s = FailIfTsMismatchCf(column_family, ts, /*ts_for_read=*/false);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -106,15 +117,31 @@ void DBImpl::SetRecoverableStatePreReleaseCallback(
|
||||
}
|
||||
|
||||
Status DBImpl::Write(const WriteOptions& write_options, WriteBatch* my_batch) {
|
||||
return WriteImpl(write_options, my_batch, /*callback=*/nullptr,
|
||||
/*log_used=*/nullptr);
|
||||
Status s;
|
||||
if (write_options.protection_bytes_per_key > 0) {
|
||||
s = WriteBatchInternal::UpdateProtectionInfo(
|
||||
my_batch, write_options.protection_bytes_per_key);
|
||||
}
|
||||
if (s.ok()) {
|
||||
s = WriteImpl(write_options, my_batch, /*callback=*/nullptr,
|
||||
/*log_used=*/nullptr);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
Status DBImpl::WriteWithCallback(const WriteOptions& write_options,
|
||||
WriteBatch* my_batch,
|
||||
WriteCallback* callback) {
|
||||
return WriteImpl(write_options, my_batch, callback, nullptr);
|
||||
Status s;
|
||||
if (write_options.protection_bytes_per_key > 0) {
|
||||
s = WriteBatchInternal::UpdateProtectionInfo(
|
||||
my_batch, write_options.protection_bytes_per_key);
|
||||
}
|
||||
if (s.ok()) {
|
||||
s = WriteImpl(write_options, my_batch, callback, nullptr);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
#endif // ROCKSDB_LITE
|
||||
|
||||
@@ -126,8 +153,13 @@ Status DBImpl::WriteImpl(const WriteOptions& write_options,
|
||||
uint64_t* log_used, uint64_t log_ref,
|
||||
bool disable_memtable, uint64_t* seq_used,
|
||||
size_t batch_cnt,
|
||||
PreReleaseCallback* pre_release_callback) {
|
||||
PreReleaseCallback* pre_release_callback,
|
||||
PostMemTableCallback* post_memtable_callback) {
|
||||
assert(!seq_per_batch_ || batch_cnt != 0);
|
||||
assert(my_batch == nullptr || my_batch->Count() == 0 ||
|
||||
write_options.protection_bytes_per_key == 0 ||
|
||||
write_options.protection_bytes_per_key ==
|
||||
my_batch->GetProtectionBytesPerKey());
|
||||
if (my_batch == nullptr) {
|
||||
return Status::InvalidArgument("Batch is nullptr!");
|
||||
} else if (!disable_memtable &&
|
||||
@@ -155,6 +187,10 @@ Status DBImpl::WriteImpl(const WriteOptions& write_options,
|
||||
"rate-limiting automatic WAL flush, which requires "
|
||||
"`WriteOptions::disableWAL` and "
|
||||
"`DBOptions::manual_wal_flush` both set to false");
|
||||
} else if (write_options.protection_bytes_per_key != 0 &&
|
||||
write_options.protection_bytes_per_key != 8) {
|
||||
return Status::InvalidArgument(
|
||||
"`WriteOptions::protection_bytes_per_key` must be zero or eight");
|
||||
}
|
||||
// TODO: this use of operator bool on `tracer_` can avoid unnecessary lock
|
||||
// grabs but does not seem thread-safe.
|
||||
@@ -185,6 +221,15 @@ Status DBImpl::WriteImpl(const WriteOptions& write_options,
|
||||
return Status::NotSupported(
|
||||
"pipelined_writes is not compatible with unordered_write");
|
||||
}
|
||||
if (immutable_db_options_.enable_pipelined_write &&
|
||||
post_memtable_callback != nullptr) {
|
||||
return Status::NotSupported(
|
||||
"pipelined write currently does not honor post_memtable_callback");
|
||||
}
|
||||
if (seq_per_batch_ && post_memtable_callback != nullptr) {
|
||||
return Status::NotSupported(
|
||||
"seq_per_batch currently does not honor post_memtable_callback");
|
||||
}
|
||||
// Otherwise IsLatestPersistentState optimization does not make sense
|
||||
assert(!WriteBatchInternal::IsLatestPersistentState(my_batch) ||
|
||||
disable_memtable);
|
||||
@@ -241,7 +286,8 @@ Status DBImpl::WriteImpl(const WriteOptions& write_options,
|
||||
|
||||
PERF_TIMER_GUARD(write_pre_and_post_process_time);
|
||||
WriteThread::Writer w(write_options, my_batch, callback, log_ref,
|
||||
disable_memtable, batch_cnt, pre_release_callback);
|
||||
disable_memtable, batch_cnt, pre_release_callback,
|
||||
post_memtable_callback);
|
||||
StopWatch write_sw(immutable_db_options_.clock, stats_, DB_WRITE);
|
||||
|
||||
write_thread_.JoinBatchGroup(&w);
|
||||
@@ -268,6 +314,16 @@ Status DBImpl::WriteImpl(const WriteOptions& write_options,
|
||||
// we're responsible for exit batch group
|
||||
// TODO(myabandeh): propagate status to write_group
|
||||
auto last_sequence = w.write_group->last_sequence;
|
||||
for (auto* tmp_w : *(w.write_group)) {
|
||||
assert(tmp_w);
|
||||
if (tmp_w->post_memtable_callback) {
|
||||
Status tmp_s =
|
||||
(*tmp_w->post_memtable_callback)(last_sequence, disable_memtable);
|
||||
// TODO: propagate the execution status of post_memtable_callback to
|
||||
// caller.
|
||||
assert(tmp_s.ok());
|
||||
}
|
||||
}
|
||||
versions_->SetLastSequence(last_sequence);
|
||||
MemTableInsertStatusCheck(w.status);
|
||||
write_thread_.ExitAsBatchGroupFollower(&w);
|
||||
@@ -319,7 +375,11 @@ Status DBImpl::WriteImpl(const WriteOptions& write_options,
|
||||
|
||||
PERF_TIMER_START(write_pre_and_post_process_time);
|
||||
}
|
||||
|
||||
log::Writer* log_writer = logs_.back().writer;
|
||||
LogFileNumberSize& log_file_number_size = alive_log_files_.back();
|
||||
|
||||
assert(log_writer->get_log_number() == log_file_number_size.number);
|
||||
|
||||
mutex_.Unlock();
|
||||
|
||||
@@ -419,7 +479,8 @@ Status DBImpl::WriteImpl(const WriteOptions& write_options,
|
||||
if (status.ok() && !write_options.disableWAL) {
|
||||
PERF_TIMER_GUARD(write_wal_time);
|
||||
io_s = WriteToWAL(write_group, log_writer, log_used, need_log_sync,
|
||||
need_log_dir_sync, last_sequence + 1);
|
||||
need_log_dir_sync, last_sequence + 1,
|
||||
log_file_number_size);
|
||||
}
|
||||
} else {
|
||||
if (status.ok() && !write_options.disableWAL) {
|
||||
@@ -507,15 +568,18 @@ Status DBImpl::WriteImpl(const WriteOptions& write_options,
|
||||
}
|
||||
PERF_TIMER_START(write_pre_and_post_process_time);
|
||||
|
||||
if (!io_s.ok()) {
|
||||
// Check WriteToWAL status
|
||||
IOStatusCheck(io_s);
|
||||
}
|
||||
if (!w.CallbackFailed()) {
|
||||
if (!io_s.ok()) {
|
||||
assert(pre_release_cb_status.ok());
|
||||
IOStatusCheck(io_s);
|
||||
} else {
|
||||
WriteStatusCheck(pre_release_cb_status);
|
||||
}
|
||||
} else {
|
||||
assert(io_s.ok() && pre_release_cb_status.ok());
|
||||
assert(pre_release_cb_status.ok());
|
||||
}
|
||||
|
||||
if (need_log_sync) {
|
||||
@@ -545,6 +609,16 @@ Status DBImpl::WriteImpl(const WriteOptions& write_options,
|
||||
}
|
||||
if (should_exit_batch_group) {
|
||||
if (status.ok()) {
|
||||
for (auto* tmp_w : write_group) {
|
||||
assert(tmp_w);
|
||||
if (tmp_w->post_memtable_callback) {
|
||||
Status tmp_s =
|
||||
(*tmp_w->post_memtable_callback)(last_sequence, disable_memtable);
|
||||
// TODO: propagate the execution status of post_memtable_callback to
|
||||
// caller.
|
||||
assert(tmp_s.ok());
|
||||
}
|
||||
}
|
||||
// Note: if we are to resume after non-OK statuses we need to revisit how
|
||||
// we reacts to non-OK statuses here.
|
||||
versions_->SetLastSequence(last_sequence);
|
||||
@@ -586,6 +660,10 @@ Status DBImpl::PipelinedWriteImpl(const WriteOptions& write_options,
|
||||
w.status = PreprocessWrite(write_options, &need_log_sync, &write_context);
|
||||
PERF_TIMER_START(write_pre_and_post_process_time);
|
||||
log::Writer* log_writer = logs_.back().writer;
|
||||
LogFileNumberSize& log_file_number_size = alive_log_files_.back();
|
||||
|
||||
assert(log_writer->get_log_number() == log_file_number_size.number);
|
||||
|
||||
mutex_.Unlock();
|
||||
|
||||
// This can set non-OK status if callback fail.
|
||||
@@ -649,17 +727,17 @@ Status DBImpl::PipelinedWriteImpl(const WriteOptions& write_options,
|
||||
wal_write_group.size - 1);
|
||||
RecordTick(stats_, WRITE_DONE_BY_OTHER, wal_write_group.size - 1);
|
||||
}
|
||||
io_s = WriteToWAL(wal_write_group, log_writer, log_used, need_log_sync,
|
||||
need_log_dir_sync, current_sequence);
|
||||
io_s =
|
||||
WriteToWAL(wal_write_group, log_writer, log_used, need_log_sync,
|
||||
need_log_dir_sync, current_sequence, log_file_number_size);
|
||||
w.status = io_s;
|
||||
}
|
||||
|
||||
if (!w.CallbackFailed()) {
|
||||
if (!io_s.ok()) {
|
||||
IOStatusCheck(io_s);
|
||||
} else {
|
||||
WriteStatusCheck(w.status);
|
||||
}
|
||||
if (!io_s.ok()) {
|
||||
// Check WriteToWAL status
|
||||
IOStatusCheck(io_s);
|
||||
} else if (!w.CallbackFailed()) {
|
||||
WriteStatusCheck(w.status);
|
||||
}
|
||||
|
||||
if (need_log_sync) {
|
||||
@@ -895,11 +973,18 @@ Status DBImpl::WriteImplWALOnly(
|
||||
seq_inc = total_batch_cnt;
|
||||
}
|
||||
Status status;
|
||||
IOStatus io_s;
|
||||
io_s.PermitUncheckedError(); // Allow io_s to be uninitialized
|
||||
if (!write_options.disableWAL) {
|
||||
io_s = ConcurrentWriteToWAL(write_group, log_used, &last_sequence, seq_inc);
|
||||
IOStatus io_s =
|
||||
ConcurrentWriteToWAL(write_group, log_used, &last_sequence, seq_inc);
|
||||
status = io_s;
|
||||
// last_sequence may not be set if there is an error
|
||||
// This error checking and return is moved up to avoid using uninitialized
|
||||
// last_sequence.
|
||||
if (!io_s.ok()) {
|
||||
IOStatusCheck(io_s);
|
||||
write_thread->ExitAsBatchGroupLeader(write_group, status);
|
||||
return status;
|
||||
}
|
||||
} else {
|
||||
// Otherwise we inc seq number to do solely the seq allocation
|
||||
last_sequence = versions_->FetchAddLastAllocatedSequence(seq_inc);
|
||||
@@ -934,11 +1019,7 @@ Status DBImpl::WriteImplWALOnly(
|
||||
PERF_TIMER_START(write_pre_and_post_process_time);
|
||||
|
||||
if (!w.CallbackFailed()) {
|
||||
if (!io_s.ok()) {
|
||||
IOStatusCheck(io_s);
|
||||
} else {
|
||||
WriteStatusCheck(status);
|
||||
}
|
||||
WriteStatusCheck(status);
|
||||
}
|
||||
if (status.ok()) {
|
||||
size_t index = 0;
|
||||
@@ -1041,12 +1122,18 @@ Status DBImpl::PreprocessWrite(const WriteOptions& write_options,
|
||||
|
||||
PERF_TIMER_GUARD(write_scheduling_flushes_compactions_time);
|
||||
|
||||
assert(!single_column_family_mode_ ||
|
||||
versions_->GetColumnFamilySet()->NumberOfColumnFamilies() == 1);
|
||||
if (UNLIKELY(status.ok() && !single_column_family_mode_ &&
|
||||
total_log_size_ > GetMaxTotalWalSize())) {
|
||||
WaitForPendingWrites();
|
||||
status = SwitchWAL(write_context);
|
||||
if (UNLIKELY(status.ok() && total_log_size_ > GetMaxTotalWalSize())) {
|
||||
assert(versions_);
|
||||
const ColumnFamilySet* const column_families =
|
||||
versions_->GetColumnFamilySet();
|
||||
assert(column_families);
|
||||
size_t num_cfs = column_families->NumberOfColumnFamilies();
|
||||
|
||||
assert(num_cfs >= 1);
|
||||
if (num_cfs > 1) {
|
||||
WaitForPendingWrites();
|
||||
status = SwitchWAL(write_context);
|
||||
}
|
||||
}
|
||||
|
||||
if (UNLIKELY(status.ok() && write_buffer_manager_->ShouldFlush())) {
|
||||
@@ -1105,17 +1192,16 @@ Status DBImpl::PreprocessWrite(const WriteOptions& write_options,
|
||||
// Note: there does not seem to be a reason to wait for parallel sync at
|
||||
// this early step but it is not important since parallel sync (SyncWAL) and
|
||||
// need_log_sync are usually not used together.
|
||||
while (logs_.front().getting_synced) {
|
||||
while (logs_.front().IsSyncing()) {
|
||||
log_sync_cv_.Wait();
|
||||
}
|
||||
for (auto& log : logs_) {
|
||||
assert(!log.getting_synced);
|
||||
// This is just to prevent the logs to be synced by a parallel SyncWAL
|
||||
// call. We will do the actual syncing later after we will write to the
|
||||
// WAL.
|
||||
// Note: there does not seem to be a reason to set this early before we
|
||||
// actually write to the WAL
|
||||
log.getting_synced = true;
|
||||
log.PrepareForSync();
|
||||
}
|
||||
} else {
|
||||
*need_log_sync = false;
|
||||
@@ -1124,13 +1210,13 @@ Status DBImpl::PreprocessWrite(const WriteOptions& write_options,
|
||||
return status;
|
||||
}
|
||||
|
||||
WriteBatch* DBImpl::MergeBatch(const WriteThread::WriteGroup& write_group,
|
||||
WriteBatch* tmp_batch, size_t* write_with_wal,
|
||||
WriteBatch** to_be_cached_state) {
|
||||
Status DBImpl::MergeBatch(const WriteThread::WriteGroup& write_group,
|
||||
WriteBatch* tmp_batch, WriteBatch** merged_batch,
|
||||
size_t* write_with_wal,
|
||||
WriteBatch** to_be_cached_state) {
|
||||
assert(write_with_wal != nullptr);
|
||||
assert(tmp_batch != nullptr);
|
||||
assert(*to_be_cached_state == nullptr);
|
||||
WriteBatch* merged_batch = nullptr;
|
||||
*write_with_wal = 0;
|
||||
auto* leader = write_group.leader;
|
||||
assert(!leader->disable_wal); // Same holds for all in the batch group
|
||||
@@ -1139,22 +1225,24 @@ WriteBatch* DBImpl::MergeBatch(const WriteThread::WriteGroup& write_group,
|
||||
// we simply write the first WriteBatch to WAL if the group only
|
||||
// contains one batch, that batch should be written to the WAL,
|
||||
// and the batch is not wanting to be truncated
|
||||
merged_batch = leader->batch;
|
||||
if (WriteBatchInternal::IsLatestPersistentState(merged_batch)) {
|
||||
*to_be_cached_state = merged_batch;
|
||||
*merged_batch = leader->batch;
|
||||
if (WriteBatchInternal::IsLatestPersistentState(*merged_batch)) {
|
||||
*to_be_cached_state = *merged_batch;
|
||||
}
|
||||
*write_with_wal = 1;
|
||||
} else {
|
||||
// WAL needs all of the batches flattened into a single batch.
|
||||
// We could avoid copying here with an iov-like AddRecord
|
||||
// interface
|
||||
merged_batch = tmp_batch;
|
||||
*merged_batch = tmp_batch;
|
||||
for (auto writer : write_group) {
|
||||
if (!writer->CallbackFailed()) {
|
||||
Status s = WriteBatchInternal::Append(merged_batch, writer->batch,
|
||||
Status s = WriteBatchInternal::Append(*merged_batch, writer->batch,
|
||||
/*WAL_only*/ true);
|
||||
// Always returns Status::OK.
|
||||
assert(s.ok());
|
||||
if (!s.ok()) {
|
||||
tmp_batch->Clear();
|
||||
return s;
|
||||
}
|
||||
if (WriteBatchInternal::IsLatestPersistentState(writer->batch)) {
|
||||
// We only need to cache the last of such write batch
|
||||
*to_be_cached_state = writer->batch;
|
||||
@@ -1163,7 +1251,8 @@ WriteBatch* DBImpl::MergeBatch(const WriteThread::WriteGroup& write_group,
|
||||
}
|
||||
}
|
||||
}
|
||||
return merged_batch;
|
||||
// return merged_batch;
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
// When two_write_queues_ is disabled, this function is called from the only
|
||||
@@ -1172,18 +1261,15 @@ IOStatus DBImpl::WriteToWAL(const WriteBatch& merged_batch,
|
||||
log::Writer* log_writer, uint64_t* log_used,
|
||||
uint64_t* log_size,
|
||||
Env::IOPriority rate_limiter_priority,
|
||||
bool with_db_mutex, bool with_log_mutex) {
|
||||
LogFileNumberSize& log_file_number_size) {
|
||||
assert(log_size != nullptr);
|
||||
|
||||
// Assert mutex explicitly.
|
||||
if (with_db_mutex) {
|
||||
mutex_.AssertHeld();
|
||||
} else if (two_write_queues_) {
|
||||
log_write_mutex_.AssertHeld();
|
||||
assert(with_log_mutex);
|
||||
}
|
||||
|
||||
Slice log_entry = WriteBatchInternal::Contents(&merged_batch);
|
||||
TEST_SYNC_POINT_CALLBACK("DBImpl::WriteToWAL:log_entry", &log_entry);
|
||||
auto s = merged_batch.VerifyChecksum();
|
||||
if (!s.ok()) {
|
||||
return status_to_io_status(std::move(s));
|
||||
}
|
||||
*log_size = log_entry.size();
|
||||
// When two_write_queues_ WriteToWAL has to be protected from concurretn calls
|
||||
// from the two queues anyway and log_write_mutex_ is already held. Otherwise
|
||||
@@ -1205,12 +1291,7 @@ IOStatus DBImpl::WriteToWAL(const WriteBatch& merged_batch,
|
||||
*log_used = logfile_number_;
|
||||
}
|
||||
total_log_size_ += log_entry.size();
|
||||
if (with_db_mutex || with_log_mutex) {
|
||||
assert(alive_log_files_tail_ == alive_log_files_.rbegin());
|
||||
assert(alive_log_files_tail_ != alive_log_files_.rend());
|
||||
}
|
||||
LogFileNumberSize& last_alive_log = *alive_log_files_tail_;
|
||||
last_alive_log.AddSize(*log_size);
|
||||
log_file_number_size.AddSize(*log_size);
|
||||
log_empty_ = false;
|
||||
return io_s;
|
||||
}
|
||||
@@ -1218,15 +1299,21 @@ IOStatus DBImpl::WriteToWAL(const WriteBatch& merged_batch,
|
||||
IOStatus DBImpl::WriteToWAL(const WriteThread::WriteGroup& write_group,
|
||||
log::Writer* log_writer, uint64_t* log_used,
|
||||
bool need_log_sync, bool need_log_dir_sync,
|
||||
SequenceNumber sequence) {
|
||||
SequenceNumber sequence,
|
||||
LogFileNumberSize& log_file_number_size) {
|
||||
IOStatus io_s;
|
||||
assert(!two_write_queues_);
|
||||
assert(!write_group.leader->disable_wal);
|
||||
// Same holds for all in the batch group
|
||||
size_t write_with_wal = 0;
|
||||
WriteBatch* to_be_cached_state = nullptr;
|
||||
WriteBatch* merged_batch = MergeBatch(write_group, &tmp_batch_,
|
||||
&write_with_wal, &to_be_cached_state);
|
||||
WriteBatch* merged_batch;
|
||||
io_s = status_to_io_status(MergeBatch(write_group, &tmp_batch_, &merged_batch,
|
||||
&write_with_wal, &to_be_cached_state));
|
||||
if (UNLIKELY(!io_s.ok())) {
|
||||
return io_s;
|
||||
}
|
||||
|
||||
if (merged_batch == write_group.leader->batch) {
|
||||
write_group.leader->log_used = logfile_number_;
|
||||
} else if (write_with_wal > 1) {
|
||||
@@ -1239,7 +1326,8 @@ IOStatus DBImpl::WriteToWAL(const WriteThread::WriteGroup& write_group,
|
||||
|
||||
uint64_t log_size;
|
||||
io_s = WriteToWAL(*merged_batch, log_writer, log_used, &log_size,
|
||||
write_group.leader->rate_limiter_priority);
|
||||
write_group.leader->rate_limiter_priority,
|
||||
log_file_number_size);
|
||||
if (to_be_cached_state) {
|
||||
cached_recoverable_state_ = *to_be_cached_state;
|
||||
cached_recoverable_state_empty_ = false;
|
||||
@@ -1315,8 +1403,12 @@ IOStatus DBImpl::ConcurrentWriteToWAL(
|
||||
WriteBatch tmp_batch;
|
||||
size_t write_with_wal = 0;
|
||||
WriteBatch* to_be_cached_state = nullptr;
|
||||
WriteBatch* merged_batch =
|
||||
MergeBatch(write_group, &tmp_batch, &write_with_wal, &to_be_cached_state);
|
||||
WriteBatch* merged_batch;
|
||||
io_s = status_to_io_status(MergeBatch(write_group, &tmp_batch, &merged_batch,
|
||||
&write_with_wal, &to_be_cached_state));
|
||||
if (UNLIKELY(!io_s.ok())) {
|
||||
return io_s;
|
||||
}
|
||||
|
||||
// We need to lock log_write_mutex_ since logs_ and alive_log_files might be
|
||||
// pushed back concurrently
|
||||
@@ -1333,10 +1425,14 @@ IOStatus DBImpl::ConcurrentWriteToWAL(
|
||||
WriteBatchInternal::SetSequence(merged_batch, sequence);
|
||||
|
||||
log::Writer* log_writer = logs_.back().writer;
|
||||
LogFileNumberSize& log_file_number_size = alive_log_files_.back();
|
||||
|
||||
assert(log_writer->get_log_number() == log_file_number_size.number);
|
||||
|
||||
uint64_t log_size;
|
||||
io_s = WriteToWAL(*merged_batch, log_writer, log_used, &log_size,
|
||||
write_group.leader->rate_limiter_priority,
|
||||
/*with_db_mutex=*/false, /*with_log_mutex=*/true);
|
||||
log_file_number_size);
|
||||
if (to_be_cached_state) {
|
||||
cached_recoverable_state_ = *to_be_cached_state;
|
||||
cached_recoverable_state_empty_ = false;
|
||||
@@ -1992,7 +2088,6 @@ Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context) {
|
||||
log_dir_synced_ = false;
|
||||
logs_.emplace_back(logfile_number_, new_log);
|
||||
alive_log_files_.push_back(LogFileNumberSize(logfile_number_));
|
||||
alive_log_files_tail_ = alive_log_files_.rbegin();
|
||||
}
|
||||
log_write_mutex_.Unlock();
|
||||
}
|
||||
@@ -2127,7 +2222,8 @@ Status DB::Put(const WriteOptions& opt, ColumnFamilyHandle* column_family,
|
||||
// Pre-allocate size of write batch conservatively.
|
||||
// 8 bytes are taken by header, 4 bytes for count, 1 byte for type,
|
||||
// and we allocate 11 extra bytes for key length, as well as value length.
|
||||
WriteBatch batch(key.size() + value.size() + 24);
|
||||
WriteBatch batch(key.size() + value.size() + 24, 0 /* max_bytes */,
|
||||
opt.protection_bytes_per_key, 0 /* default_cf_ts_sz */);
|
||||
Status s = batch.Put(column_family, key, value);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
@@ -2141,7 +2237,9 @@ Status DB::Put(const WriteOptions& opt, ColumnFamilyHandle* column_family,
|
||||
assert(default_cf);
|
||||
const Comparator* const default_cf_ucmp = default_cf->GetComparator();
|
||||
assert(default_cf_ucmp);
|
||||
WriteBatch batch(0, 0, 0, default_cf_ucmp->timestamp_size());
|
||||
WriteBatch batch(0 /* reserved_bytes */, 0 /* max_bytes */,
|
||||
opt.protection_bytes_per_key,
|
||||
default_cf_ucmp->timestamp_size());
|
||||
Status s = batch.Put(column_family, key, ts, value);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
@@ -2149,9 +2247,31 @@ 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;
|
||||
WriteBatch batch(0 /* reserved_bytes */, 0 /* max_bytes */,
|
||||
opt.protection_bytes_per_key, 0 /* default_cf_ts_sz */);
|
||||
Status s = batch.Delete(column_family, key);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
@@ -2165,7 +2285,9 @@ Status DB::Delete(const WriteOptions& opt, ColumnFamilyHandle* column_family,
|
||||
assert(default_cf);
|
||||
const Comparator* const default_cf_ucmp = default_cf->GetComparator();
|
||||
assert(default_cf_ucmp);
|
||||
WriteBatch batch(0, 0, 0, default_cf_ucmp->timestamp_size());
|
||||
WriteBatch batch(0 /* reserved_bytes */, 0 /* max_bytes */,
|
||||
opt.protection_bytes_per_key,
|
||||
default_cf_ucmp->timestamp_size());
|
||||
Status s = batch.Delete(column_family, key, ts);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
@@ -2175,7 +2297,8 @@ Status DB::Delete(const WriteOptions& opt, ColumnFamilyHandle* column_family,
|
||||
|
||||
Status DB::SingleDelete(const WriteOptions& opt,
|
||||
ColumnFamilyHandle* column_family, const Slice& key) {
|
||||
WriteBatch batch;
|
||||
WriteBatch batch(0 /* reserved_bytes */, 0 /* max_bytes */,
|
||||
opt.protection_bytes_per_key, 0 /* default_cf_ts_sz */);
|
||||
Status s = batch.SingleDelete(column_family, key);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
@@ -2190,7 +2313,9 @@ Status DB::SingleDelete(const WriteOptions& opt,
|
||||
assert(default_cf);
|
||||
const Comparator* const default_cf_ucmp = default_cf->GetComparator();
|
||||
assert(default_cf_ucmp);
|
||||
WriteBatch batch(0, 0, 0, default_cf_ucmp->timestamp_size());
|
||||
WriteBatch batch(0 /* reserved_bytes */, 0 /* max_bytes */,
|
||||
opt.protection_bytes_per_key,
|
||||
default_cf_ucmp->timestamp_size());
|
||||
Status s = batch.SingleDelete(column_family, key, ts);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
@@ -2201,7 +2326,8 @@ Status DB::SingleDelete(const WriteOptions& opt,
|
||||
Status DB::DeleteRange(const WriteOptions& opt,
|
||||
ColumnFamilyHandle* column_family,
|
||||
const Slice& begin_key, const Slice& end_key) {
|
||||
WriteBatch batch;
|
||||
WriteBatch batch(0 /* reserved_bytes */, 0 /* max_bytes */,
|
||||
opt.protection_bytes_per_key, 0 /* default_cf_ts_sz */);
|
||||
Status s = batch.DeleteRange(column_family, begin_key, end_key);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
@@ -2211,7 +2337,8 @@ Status DB::DeleteRange(const WriteOptions& opt,
|
||||
|
||||
Status DB::Merge(const WriteOptions& opt, ColumnFamilyHandle* column_family,
|
||||
const Slice& key, const Slice& value) {
|
||||
WriteBatch batch;
|
||||
WriteBatch batch(0 /* reserved_bytes */, 0 /* max_bytes */,
|
||||
opt.protection_bytes_per_key, 0 /* default_cf_ts_sz */);
|
||||
Status s = batch.Merge(column_family, key, value);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
|
||||
@@ -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
|
||||
|
||||
+536
-72
@@ -15,7 +15,7 @@ enum class WriteBatchOpType {
|
||||
kSingleDelete,
|
||||
kDeleteRange,
|
||||
kMerge,
|
||||
kBlobIndex,
|
||||
kPutEntity,
|
||||
kNum,
|
||||
};
|
||||
|
||||
@@ -25,56 +25,104 @@ WriteBatchOpType operator+(WriteBatchOpType lhs, const int rhs) {
|
||||
return static_cast<WriteBatchOpType>(static_cast<T>(lhs) + rhs);
|
||||
}
|
||||
|
||||
class DbKvChecksumTest
|
||||
: public DBTestBase,
|
||||
public ::testing::WithParamInterface<std::tuple<WriteBatchOpType, char>> {
|
||||
enum class WriteMode {
|
||||
// `Write()` a `WriteBatch` constructed with `protection_bytes_per_key > 0`.
|
||||
kWriteProtectedBatch = 0,
|
||||
// `Write()` a `WriteBatch` constructed with `protection_bytes_per_key == 0`.
|
||||
// Protection is enabled via `WriteOptions::protection_bytes_per_key > 0`.
|
||||
kWriteUnprotectedBatch,
|
||||
// TODO(ajkr): add a mode that uses `Write()` wrappers, e.g., `Put()`.
|
||||
kNum,
|
||||
};
|
||||
|
||||
// Integer addition is needed for `::testing::Range()` to take the enum type.
|
||||
WriteMode operator+(WriteMode lhs, const int rhs) {
|
||||
using T = std::underlying_type<WriteMode>::type;
|
||||
return static_cast<WriteMode>(static_cast<T>(lhs) + rhs);
|
||||
}
|
||||
|
||||
std::pair<WriteBatch, Status> GetWriteBatch(ColumnFamilyHandle* cf_handle,
|
||||
size_t protection_bytes_per_key,
|
||||
WriteBatchOpType op_type) {
|
||||
Status s;
|
||||
WriteBatch wb(0 /* reserved_bytes */, 0 /* max_bytes */,
|
||||
protection_bytes_per_key, 0 /* default_cf_ts_sz */);
|
||||
switch (op_type) {
|
||||
case WriteBatchOpType::kPut:
|
||||
s = wb.Put(cf_handle, "key", "val");
|
||||
break;
|
||||
case WriteBatchOpType::kDelete:
|
||||
s = wb.Delete(cf_handle, "key");
|
||||
break;
|
||||
case WriteBatchOpType::kSingleDelete:
|
||||
s = wb.SingleDelete(cf_handle, "key");
|
||||
break;
|
||||
case WriteBatchOpType::kDeleteRange:
|
||||
s = wb.DeleteRange(cf_handle, "begin", "end");
|
||||
break;
|
||||
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 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());
|
||||
}
|
||||
|
||||
std::pair<WriteBatch, Status> GetWriteBatch(ColumnFamilyHandle* cf_handle) {
|
||||
Status s;
|
||||
WriteBatch wb(0 /* reserved_bytes */, 0 /* max_bytes */,
|
||||
8 /* protection_bytes_per_entry */, 0 /* default_cf_ts_sz */);
|
||||
switch (op_type_) {
|
||||
case WriteBatchOpType::kPut:
|
||||
s = wb.Put(cf_handle, "key", "val");
|
||||
break;
|
||||
case WriteBatchOpType::kDelete:
|
||||
s = wb.Delete(cf_handle, "key");
|
||||
break;
|
||||
case WriteBatchOpType::kSingleDelete:
|
||||
s = wb.SingleDelete(cf_handle, "key");
|
||||
break;
|
||||
case WriteBatchOpType::kDeleteRange:
|
||||
s = wb.DeleteRange(cf_handle, "begin", "end");
|
||||
break;
|
||||
case WriteBatchOpType::kMerge:
|
||||
s = wb.Merge(cf_handle, "key", "val");
|
||||
break;
|
||||
case WriteBatchOpType::kBlobIndex: {
|
||||
// TODO(ajkr): use public API once available.
|
||||
uint32_t cf_id;
|
||||
if (cf_handle == nullptr) {
|
||||
cf_id = 0;
|
||||
} else {
|
||||
cf_id = cf_handle->GetID();
|
||||
}
|
||||
|
||||
std::string blob_index;
|
||||
BlobIndex::EncodeInlinedTTL(&blob_index, /* expiration */ 9876543210,
|
||||
"val");
|
||||
|
||||
s = WriteBatchInternal::PutBlobIndex(&wb, cf_id, "key", blob_index);
|
||||
break;
|
||||
Status ExecuteWrite(ColumnFamilyHandle* cf_handle) {
|
||||
switch (write_mode_) {
|
||||
case WriteMode::kWriteProtectedBatch: {
|
||||
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 WriteBatchOpType::kNum:
|
||||
case WriteMode::kWriteUnprotectedBatch: {
|
||||
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;
|
||||
return db_->Write(write_opts, &batch_and_status.first);
|
||||
}
|
||||
case WriteMode::kNum:
|
||||
assert(false);
|
||||
}
|
||||
return {std::move(wb), std::move(s)};
|
||||
return Status::NotSupported("WriteMode " +
|
||||
std::to_string(static_cast<int>(write_mode_)));
|
||||
}
|
||||
|
||||
void CorruptNextByteCallBack(void* arg) {
|
||||
@@ -95,46 +143,60 @@ class DbKvChecksumTest
|
||||
protected:
|
||||
WriteBatchOpType op_type_;
|
||||
char corrupt_byte_addend_;
|
||||
WriteMode write_mode_;
|
||||
size_t corrupt_byte_offset_ = 0;
|
||||
size_t entry_len_ = std::numeric_limits<size_t>::max();
|
||||
};
|
||||
|
||||
std::string GetTestNameSuffix(
|
||||
::testing::TestParamInfo<std::tuple<WriteBatchOpType, char>> info) {
|
||||
std::ostringstream oss;
|
||||
switch (std::get<0>(info.param)) {
|
||||
std::string GetOpTypeString(const WriteBatchOpType& op_type) {
|
||||
switch (op_type) {
|
||||
case WriteBatchOpType::kPut:
|
||||
oss << "Put";
|
||||
break;
|
||||
return "Put";
|
||||
case WriteBatchOpType::kDelete:
|
||||
oss << "Delete";
|
||||
break;
|
||||
return "Delete";
|
||||
case WriteBatchOpType::kSingleDelete:
|
||||
oss << "SingleDelete";
|
||||
break;
|
||||
return "SingleDelete";
|
||||
case WriteBatchOpType::kDeleteRange:
|
||||
oss << "DeleteRange";
|
||||
break;
|
||||
return "DeleteRange";
|
||||
case WriteBatchOpType::kMerge:
|
||||
oss << "Merge";
|
||||
break;
|
||||
case WriteBatchOpType::kBlobIndex:
|
||||
oss << "BlobIndex";
|
||||
break;
|
||||
return "Merge";
|
||||
case WriteBatchOpType::kPutEntity:
|
||||
return "PutEntity";
|
||||
case WriteBatchOpType::kNum:
|
||||
assert(false);
|
||||
}
|
||||
oss << "Add"
|
||||
<< static_cast<int>(static_cast<unsigned char>(std::get<1>(info.param)));
|
||||
return oss.str();
|
||||
assert(false);
|
||||
return "";
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(
|
||||
DbKvChecksumTest, DbKvChecksumTest,
|
||||
::testing::Combine(::testing::Range(static_cast<WriteBatchOpType>(0),
|
||||
WriteBatchOpType::kNum),
|
||||
::testing::Values(2, 103, 251)),
|
||||
GetTestNameSuffix);
|
||||
::testing::Values(2, 103, 251),
|
||||
::testing::Range(static_cast<WriteMode>(0),
|
||||
WriteMode::kNum)),
|
||||
[](const testing::TestParamInfo<
|
||||
std::tuple<WriteBatchOpType, char, WriteMode>>& args) {
|
||||
std::ostringstream oss;
|
||||
oss << GetOpTypeString(std::get<0>(args.param)) << "Add"
|
||||
<< static_cast<int>(
|
||||
static_cast<unsigned char>(std::get<1>(args.param)));
|
||||
switch (std::get<2>(args.param)) {
|
||||
case WriteMode::kWriteProtectedBatch:
|
||||
oss << "WriteProtectedBatch";
|
||||
break;
|
||||
case WriteMode::kWriteUnprotectedBatch:
|
||||
oss << "WriteUnprotectedBatch";
|
||||
break;
|
||||
case WriteMode::kNum:
|
||||
assert(false);
|
||||
}
|
||||
return oss.str();
|
||||
});
|
||||
|
||||
// TODO(ajkr): add a test that corrupts the `WriteBatch` contents. Such
|
||||
// corruptions should only be detectable in `WriteMode::kWriteProtectedBatch`.
|
||||
|
||||
TEST_P(DbKvChecksumTest, MemTableAddCorrupted) {
|
||||
// This test repeatedly attempts to write `WriteBatch`es containing a single
|
||||
@@ -157,11 +219,13 @@ TEST_P(DbKvChecksumTest, MemTableAddCorrupted) {
|
||||
Reopen(options);
|
||||
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
auto batch_and_status = GetWriteBatch(nullptr /* cf_handle */);
|
||||
ASSERT_OK(batch_and_status.second);
|
||||
ASSERT_TRUE(
|
||||
db_->Write(WriteOptions(), &batch_and_status.first).IsCorruption());
|
||||
ASSERT_TRUE(ExecuteWrite(nullptr /* cf_handle */).IsCorruption());
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
|
||||
// In case the above callback is not invoked, this test will run
|
||||
// numeric_limits<size_t>::max() times until it reports an error (or will
|
||||
// exhaust disk space). Added this assert to report error early.
|
||||
ASSERT_TRUE(entry_len_ < std::numeric_limits<size_t>::max());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,14 +252,414 @@ TEST_P(DbKvChecksumTest, MemTableAddWithColumnFamilyCorrupted) {
|
||||
ReopenWithColumnFamilies({kDefaultColumnFamilyName, "pikachu"}, options);
|
||||
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
auto batch_and_status = GetWriteBatch(handles_[1]);
|
||||
ASSERT_OK(batch_and_status.second);
|
||||
ASSERT_TRUE(
|
||||
db_->Write(WriteOptions(), &batch_and_status.first).IsCorruption());
|
||||
ASSERT_TRUE(ExecuteWrite(handles_[1]).IsCorruption());
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
|
||||
// In case the above callback is not invoked, this test will run
|
||||
// numeric_limits<size_t>::max() times until it reports an error (or will
|
||||
// exhaust disk space). Added this assert to report error early.
|
||||
ASSERT_TRUE(entry_len_ < std::numeric_limits<size_t>::max());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(DbKvChecksumTest, NoCorruptionCase) {
|
||||
// If this test fails, we may have found a piece of malfunctioned hardware
|
||||
auto batch_and_status =
|
||||
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());
|
||||
}
|
||||
|
||||
TEST_P(DbKvChecksumTest, WriteToWALCorrupted) {
|
||||
// This test repeatedly attempts to write `WriteBatch`es containing a single
|
||||
// entry of type `op_type_`. Each attempt has one byte corrupted by adding
|
||||
// `corrupt_byte_addend_` to its original value. The test repeats until an
|
||||
// attempt has been made on each byte in the encoded write batch. All attempts
|
||||
// are expected to fail with `Status::Corruption`
|
||||
Options options = CurrentOptions();
|
||||
if (op_type_ == WriteBatchOpType::kMerge) {
|
||||
options.merge_operator = MergeOperators::CreateStringAppendOperator();
|
||||
}
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::WriteToWAL:log_entry",
|
||||
std::bind(&DbKvChecksumTest::CorruptNextByteCallBack, this,
|
||||
std::placeholders::_1));
|
||||
// First 8 bytes are for sequence number which is not protected in write batch
|
||||
corrupt_byte_offset_ = 8;
|
||||
|
||||
while (MoreBytesToCorrupt()) {
|
||||
// Corrupted write batch leads to read-only mode, so we have to
|
||||
// reopen for every attempt.
|
||||
Reopen(options);
|
||||
auto log_size_pre_write = dbfull()->TEST_total_log_size();
|
||||
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
ASSERT_TRUE(ExecuteWrite(nullptr /* cf_handle */).IsCorruption());
|
||||
// Confirm that nothing was written to WAL
|
||||
ASSERT_EQ(log_size_pre_write, dbfull()->TEST_total_log_size());
|
||||
ASSERT_TRUE(dbfull()->TEST_GetBGError().IsCorruption());
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
|
||||
// In case the above callback is not invoked, this test will run
|
||||
// numeric_limits<size_t>::max() times until it reports an error (or will
|
||||
// exhaust disk space). Added this assert to report error early.
|
||||
ASSERT_TRUE(entry_len_ < std::numeric_limits<size_t>::max());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(DbKvChecksumTest, WriteToWALWithColumnFamilyCorrupted) {
|
||||
// This test repeatedly attempts to write `WriteBatch`es containing a single
|
||||
// entry of type `op_type_`. Each attempt has one byte corrupted by adding
|
||||
// `corrupt_byte_addend_` to its original value. The test repeats until an
|
||||
// attempt has been made on each byte in the encoded write batch. All attempts
|
||||
// are expected to fail with `Status::Corruption`
|
||||
Options options = CurrentOptions();
|
||||
if (op_type_ == WriteBatchOpType::kMerge) {
|
||||
options.merge_operator = MergeOperators::CreateStringAppendOperator();
|
||||
}
|
||||
CreateAndReopenWithCF({"pikachu"}, options);
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::WriteToWAL:log_entry",
|
||||
std::bind(&DbKvChecksumTest::CorruptNextByteCallBack, this,
|
||||
std::placeholders::_1));
|
||||
// First 8 bytes are for sequence number which is not protected in write batch
|
||||
corrupt_byte_offset_ = 8;
|
||||
|
||||
while (MoreBytesToCorrupt()) {
|
||||
// Corrupted write batch leads to read-only mode, so we have to
|
||||
// reopen for every attempt.
|
||||
ReopenWithColumnFamilies({kDefaultColumnFamilyName, "pikachu"}, options);
|
||||
auto log_size_pre_write = dbfull()->TEST_total_log_size();
|
||||
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
ASSERT_TRUE(ExecuteWrite(nullptr /* cf_handle */).IsCorruption());
|
||||
// Confirm that nothing was written to WAL
|
||||
ASSERT_EQ(log_size_pre_write, dbfull()->TEST_total_log_size());
|
||||
ASSERT_TRUE(dbfull()->TEST_GetBGError().IsCorruption());
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
|
||||
// In case the above callback is not invoked, this test will run
|
||||
// numeric_limits<size_t>::max() times until it reports an error (or will
|
||||
// exhaust disk space). Added this assert to report error early.
|
||||
ASSERT_TRUE(entry_len_ < std::numeric_limits<size_t>::max());
|
||||
}
|
||||
}
|
||||
|
||||
class DbKvChecksumTestMergedBatch
|
||||
: public DbKvChecksumTestBase,
|
||||
public ::testing::WithParamInterface<
|
||||
std::tuple<WriteBatchOpType, WriteBatchOpType, char>> {
|
||||
public:
|
||||
DbKvChecksumTestMergedBatch()
|
||||
: 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());
|
||||
}
|
||||
|
||||
protected:
|
||||
WriteBatchOpType op_type1_;
|
||||
WriteBatchOpType op_type2_;
|
||||
char corrupt_byte_addend_;
|
||||
};
|
||||
|
||||
void CorruptWriteBatch(Slice* content, size_t offset,
|
||||
char corrupt_byte_addend) {
|
||||
ASSERT_TRUE(offset < content->size());
|
||||
char* buf = const_cast<char*>(content->data());
|
||||
buf[offset] += corrupt_byte_addend;
|
||||
}
|
||||
|
||||
TEST_P(DbKvChecksumTestMergedBatch, NoCorruptionCase) {
|
||||
// Veirfy write batch checksum after write batch append
|
||||
auto batch1 = GetWriteBatch(GetCFHandleToUse(nullptr, op_type1_),
|
||||
8 /* protection_bytes_per_key */, op_type1_);
|
||||
ASSERT_OK(batch1.second);
|
||||
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));
|
||||
ASSERT_OK(batch1.first.VerifyChecksum());
|
||||
}
|
||||
|
||||
TEST_P(DbKvChecksumTestMergedBatch, WriteToWALCorrupted) {
|
||||
// This test has two writers repeatedly attempt to write `WriteBatch`es
|
||||
// containing a single entry of type op_type1_ and op_type2_ respectively. The
|
||||
// leader of the write group writes the batch containinng the entry of type
|
||||
// op_type1_. One byte of the pre-merged write batches is corrupted by adding
|
||||
// `corrupt_byte_addend_` to the batch's original value during each attempt.
|
||||
// The test repeats until an attempt has been made on each byte in both
|
||||
// pre-merged write batches. All attempts are expected to fail with
|
||||
// `Status::Corruption`.
|
||||
Options options = CurrentOptions();
|
||||
if (op_type1_ == WriteBatchOpType::kMerge ||
|
||||
op_type2_ == WriteBatchOpType::kMerge) {
|
||||
options.merge_operator = MergeOperators::CreateStringAppendOperator();
|
||||
}
|
||||
|
||||
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(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();
|
||||
// First 8 bytes are for sequence number which is not protected in write batch
|
||||
size_t corrupt_byte_offset = 8;
|
||||
|
||||
std::atomic<bool> follower_joined{false};
|
||||
std::atomic<int> leader_count{0};
|
||||
port::Thread follower_thread;
|
||||
// This callback should only be called by the leader thread
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"WriteThread::JoinBatchGroup:Wait2", [&](void* arg_leader) {
|
||||
auto* leader = reinterpret_cast<WriteThread::Writer*>(arg_leader);
|
||||
ASSERT_EQ(leader->state, WriteThread::STATE_GROUP_LEADER);
|
||||
|
||||
// This callback should only be called by the follower thread
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"WriteThread::JoinBatchGroup:Wait", [&](void* arg_follower) {
|
||||
auto* follower =
|
||||
reinterpret_cast<WriteThread::Writer*>(arg_follower);
|
||||
// The leader thread will wait on this bool and hence wait until
|
||||
// this writer joins the write group
|
||||
ASSERT_NE(follower->state, WriteThread::STATE_GROUP_LEADER);
|
||||
if (corrupt_byte_offset >= leader_batch_size) {
|
||||
Slice batch_content = follower->batch->Data();
|
||||
CorruptWriteBatch(&batch_content,
|
||||
corrupt_byte_offset - leader_batch_size,
|
||||
corrupt_byte_addend_);
|
||||
}
|
||||
// Leader busy waits on this flag
|
||||
follower_joined = true;
|
||||
// So the follower does not enter the outer callback at
|
||||
// WriteThread::JoinBatchGroup:Wait2
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
});
|
||||
|
||||
// Start the other writer thread which will join the write group as
|
||||
// follower
|
||||
follower_thread = port::Thread([&]() {
|
||||
follower_batch_and_status =
|
||||
GetWriteBatch(GetCFHandleToUse(nullptr, 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)
|
||||
.IsCorruption());
|
||||
});
|
||||
|
||||
ASSERT_EQ(leader->batch->GetDataSize(), leader_batch_size);
|
||||
if (corrupt_byte_offset < leader_batch_size) {
|
||||
Slice batch_content = leader->batch->Data();
|
||||
CorruptWriteBatch(&batch_content, corrupt_byte_offset,
|
||||
corrupt_byte_addend_);
|
||||
}
|
||||
leader_count++;
|
||||
while (!follower_joined) {
|
||||
// busy waiting
|
||||
}
|
||||
});
|
||||
while (corrupt_byte_offset < total_bytes) {
|
||||
// Reopen DB since it failed WAL write which lead to read-only mode
|
||||
Reopen(options);
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
auto log_size_pre_write = dbfull()->TEST_total_log_size();
|
||||
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());
|
||||
follower_thread.join();
|
||||
// Prevent leader thread from entering this callback
|
||||
SyncPoint::GetInstance()->ClearCallBack("WriteThread::JoinBatchGroup:Wait");
|
||||
ASSERT_EQ(1, leader_count);
|
||||
// Nothing should have been written to WAL
|
||||
ASSERT_EQ(log_size_pre_write, dbfull()->TEST_total_log_size());
|
||||
ASSERT_TRUE(dbfull()->TEST_GetBGError().IsCorruption());
|
||||
|
||||
corrupt_byte_offset++;
|
||||
if (corrupt_byte_offset == leader_batch_size) {
|
||||
// skip over the sequence number part of follower's write batch
|
||||
corrupt_byte_offset += 8;
|
||||
}
|
||||
follower_joined = false;
|
||||
leader_count = 0;
|
||||
}
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
}
|
||||
|
||||
TEST_P(DbKvChecksumTestMergedBatch, WriteToWALWithColumnFamilyCorrupted) {
|
||||
// This test has two writers repeatedly attempt to write `WriteBatch`es
|
||||
// containing a single entry of type op_type1_ and op_type2_ respectively. The
|
||||
// leader of the write group writes the batch containinng the entry of type
|
||||
// op_type1_. One byte of the pre-merged write batches is corrupted by adding
|
||||
// `corrupt_byte_addend_` to the batch's original value during each attempt.
|
||||
// The test repeats until an attempt has been made on each byte in both
|
||||
// pre-merged write batches. All attempts are expected to fail with
|
||||
// `Status::Corruption`.
|
||||
Options options = CurrentOptions();
|
||||
if (op_type1_ == WriteBatchOpType::kMerge ||
|
||||
op_type2_ == WriteBatchOpType::kMerge) {
|
||||
options.merge_operator = MergeOperators::CreateStringAppendOperator();
|
||||
}
|
||||
CreateAndReopenWithCF({"ramen"}, options);
|
||||
|
||||
auto leader_batch_and_status =
|
||||
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(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();
|
||||
// First 8 bytes are for sequence number which is not protected in write batch
|
||||
size_t corrupt_byte_offset = 8;
|
||||
|
||||
std::atomic<bool> follower_joined{false};
|
||||
std::atomic<int> leader_count{0};
|
||||
port::Thread follower_thread;
|
||||
// This callback should only be called by the leader thread
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"WriteThread::JoinBatchGroup:Wait2", [&](void* arg_leader) {
|
||||
auto* leader = reinterpret_cast<WriteThread::Writer*>(arg_leader);
|
||||
ASSERT_EQ(leader->state, WriteThread::STATE_GROUP_LEADER);
|
||||
|
||||
// This callback should only be called by the follower thread
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"WriteThread::JoinBatchGroup:Wait", [&](void* arg_follower) {
|
||||
auto* follower =
|
||||
reinterpret_cast<WriteThread::Writer*>(arg_follower);
|
||||
// The leader thread will wait on this bool and hence wait until
|
||||
// this writer joins the write group
|
||||
ASSERT_NE(follower->state, WriteThread::STATE_GROUP_LEADER);
|
||||
if (corrupt_byte_offset >= leader_batch_size) {
|
||||
Slice batch_content =
|
||||
WriteBatchInternal::Contents(follower->batch);
|
||||
CorruptWriteBatch(&batch_content,
|
||||
corrupt_byte_offset - leader_batch_size,
|
||||
corrupt_byte_addend_);
|
||||
}
|
||||
follower_joined = true;
|
||||
// So the follower does not enter the outer callback at
|
||||
// WriteThread::JoinBatchGroup:Wait2
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
});
|
||||
|
||||
// Start the other writer thread which will join the write group as
|
||||
// follower
|
||||
follower_thread = port::Thread([&]() {
|
||||
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)
|
||||
.IsCorruption());
|
||||
});
|
||||
|
||||
ASSERT_EQ(leader->batch->GetDataSize(), leader_batch_size);
|
||||
if (corrupt_byte_offset < leader_batch_size) {
|
||||
Slice batch_content = WriteBatchInternal::Contents(leader->batch);
|
||||
CorruptWriteBatch(&batch_content, corrupt_byte_offset,
|
||||
corrupt_byte_addend_);
|
||||
}
|
||||
leader_count++;
|
||||
while (!follower_joined) {
|
||||
// busy waiting
|
||||
}
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
while (corrupt_byte_offset < total_bytes) {
|
||||
// Reopen DB since it failed WAL write which lead to read-only mode
|
||||
ReopenWithColumnFamilies({kDefaultColumnFamilyName, "ramen"}, options);
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
auto log_size_pre_write = dbfull()->TEST_total_log_size();
|
||||
leader_batch_and_status =
|
||||
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());
|
||||
follower_thread.join();
|
||||
// Prevent leader thread from entering this callback
|
||||
SyncPoint::GetInstance()->ClearCallBack("WriteThread::JoinBatchGroup:Wait");
|
||||
|
||||
ASSERT_EQ(1, leader_count);
|
||||
// Nothing should have been written to WAL
|
||||
ASSERT_EQ(log_size_pre_write, dbfull()->TEST_total_log_size());
|
||||
ASSERT_TRUE(dbfull()->TEST_GetBGError().IsCorruption());
|
||||
|
||||
corrupt_byte_offset++;
|
||||
if (corrupt_byte_offset == leader_batch_size) {
|
||||
// skip over the sequence number part of follower's write batch
|
||||
corrupt_byte_offset += 8;
|
||||
}
|
||||
follower_joined = false;
|
||||
leader_count = 0;
|
||||
}
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
}
|
||||
|
||||
INSTANTIATE_TEST_CASE_P(
|
||||
DbKvChecksumTestMergedBatch, DbKvChecksumTestMergedBatch,
|
||||
::testing::Combine(::testing::Range(static_cast<WriteBatchOpType>(0),
|
||||
WriteBatchOpType::kNum),
|
||||
::testing::Range(static_cast<WriteBatchOpType>(0),
|
||||
WriteBatchOpType::kNum),
|
||||
::testing::Values(2, 103, 251)),
|
||||
[](const testing::TestParamInfo<
|
||||
std::tuple<WriteBatchOpType, WriteBatchOpType, char>>& args) {
|
||||
std::ostringstream oss;
|
||||
oss << GetOpTypeString(std::get<0>(args.param))
|
||||
<< GetOpTypeString(std::get<1>(args.param)) << "Add"
|
||||
<< static_cast<int>(
|
||||
static_cast<unsigned char>(std::get<2>(args.param)));
|
||||
return oss.str();
|
||||
});
|
||||
|
||||
// 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) {
|
||||
|
||||
@@ -220,6 +220,7 @@ TEST_F(DBOptionsTest, SetMutableTableOptions) {
|
||||
|
||||
ColumnFamilyHandle* cfh = dbfull()->DefaultColumnFamily();
|
||||
Options c_opts = dbfull()->GetOptions(cfh);
|
||||
|
||||
const auto* c_bbto =
|
||||
c_opts.table_factory->GetOptions<BlockBasedTableOptions>();
|
||||
ASSERT_NE(c_bbto, nullptr);
|
||||
@@ -452,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
|
||||
|
||||
@@ -139,8 +139,6 @@ TEST_P(DBRateLimiterOnReadTest, Get) {
|
||||
}
|
||||
|
||||
TEST_P(DBRateLimiterOnReadTest, NewMultiGet) {
|
||||
// The new void-returning `MultiGet()` APIs use `MultiRead()`, which does not
|
||||
// yet support rate limiting.
|
||||
if (use_direct_io_ && !IsDirectIOSupported()) {
|
||||
return;
|
||||
}
|
||||
@@ -149,6 +147,7 @@ TEST_P(DBRateLimiterOnReadTest, NewMultiGet) {
|
||||
ASSERT_EQ(0, options_.rate_limiter->GetTotalRequests(Env::IO_USER));
|
||||
|
||||
const int kNumKeys = kNumFiles * kNumKeysPerFile;
|
||||
int64_t expected = 0;
|
||||
{
|
||||
std::vector<std::string> key_bufs;
|
||||
key_bufs.reserve(kNumKeys);
|
||||
@@ -160,13 +159,19 @@ TEST_P(DBRateLimiterOnReadTest, NewMultiGet) {
|
||||
}
|
||||
std::vector<Status> statuses(kNumKeys);
|
||||
std::vector<PinnableSlice> values(kNumKeys);
|
||||
const int64_t prev_total_rl_req = options_.rate_limiter->GetTotalRequests();
|
||||
db_->MultiGet(GetReadOptions(), dbfull()->DefaultColumnFamily(), kNumKeys,
|
||||
keys.data(), values.data(), statuses.data());
|
||||
const int64_t cur_total_rl_req = options_.rate_limiter->GetTotalRequests();
|
||||
for (int i = 0; i < kNumKeys; ++i) {
|
||||
ASSERT_TRUE(statuses[i].IsNotSupported());
|
||||
ASSERT_TRUE(statuses[i].ok());
|
||||
}
|
||||
ASSERT_GT(cur_total_rl_req, prev_total_rl_req);
|
||||
ASSERT_EQ(cur_total_rl_req - prev_total_rl_req,
|
||||
options_.rate_limiter->GetTotalRequests(Env::IO_USER));
|
||||
}
|
||||
ASSERT_EQ(0, options_.rate_limiter->GetTotalRequests(Env::IO_USER));
|
||||
expected += kNumKeys;
|
||||
ASSERT_EQ(expected, options_.rate_limiter->GetTotalRequests(Env::IO_USER));
|
||||
}
|
||||
|
||||
TEST_P(DBRateLimiterOnReadTest, OldMultiGet) {
|
||||
|
||||
@@ -15,6 +15,55 @@ class DBReadOnlyTestWithTimestamp : public DBBasicTestWithTimestampBase {
|
||||
public:
|
||||
DBReadOnlyTestWithTimestamp()
|
||||
: DBBasicTestWithTimestampBase("db_readonly_test_with_timestamp") {}
|
||||
|
||||
protected:
|
||||
#ifndef ROCKSDB_LITE
|
||||
void CheckDBOpenedAsCompactedDBWithOneLevel0File() {
|
||||
VersionSet* const versions = dbfull()->GetVersionSet();
|
||||
ASSERT_NE(versions, nullptr);
|
||||
|
||||
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);
|
||||
|
||||
// Only 1 L0 file.
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(0));
|
||||
// L0 is the max level.
|
||||
ASSERT_EQ(storage_info->num_non_empty_levels(), 1);
|
||||
}
|
||||
|
||||
void CheckDBOpenedAsCompactedDBWithOnlyHighestNonEmptyLevelFiles() {
|
||||
VersionSet* const versions = dbfull()->GetVersionSet();
|
||||
ASSERT_NE(versions, nullptr);
|
||||
|
||||
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);
|
||||
|
||||
// L0 has no files.
|
||||
ASSERT_EQ(0, NumTableFilesAtLevel(0));
|
||||
|
||||
// All other levels have no files except the highest level with files.
|
||||
for (int i = 1; i < storage_info->num_non_empty_levels() - 1; ++i) {
|
||||
ASSERT_FALSE(storage_info->LevelFilesBrief(i).num_files > 0);
|
||||
}
|
||||
|
||||
// The highest level with files have some files.
|
||||
int highest_non_empty_level = storage_info->num_non_empty_levels() - 1;
|
||||
ASSERT_TRUE(
|
||||
storage_info->LevelFilesBrief(highest_non_empty_level).num_files > 0);
|
||||
}
|
||||
#endif // !ROCKSDB_LITE
|
||||
};
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
@@ -101,6 +150,46 @@ TEST_F(DBReadOnlyTestWithTimestamp,
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(DBReadOnlyTestWithTimestamp,
|
||||
IteratorAndGetWriteWithTimestampReadWithoutTimestamp) {
|
||||
const int kNumKeysPerFile = 128;
|
||||
const 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::string write_timestamp = Timestamp(1, 0);
|
||||
WriteOptions write_opts;
|
||||
for (uint64_t key = 0; key <= kMaxKey; ++key) {
|
||||
Status s = db_->Put(write_opts, Key1(key), write_timestamp,
|
||||
"value" + std::to_string(key));
|
||||
ASSERT_OK(s);
|
||||
}
|
||||
|
||||
// Reopen the database in read only mode to test its timestamp support.
|
||||
Close();
|
||||
ASSERT_OK(ReadOnlyReopen(options));
|
||||
ReadOptions read_opts;
|
||||
{
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_opts));
|
||||
ASSERT_FALSE(iter->Valid());
|
||||
ASSERT_TRUE(iter->status().IsInvalidArgument());
|
||||
}
|
||||
|
||||
for (uint64_t key = 0; key <= kMaxKey; ++key) {
|
||||
std::string value_from_get;
|
||||
ASSERT_TRUE(
|
||||
db_->Get(read_opts, Key1(key), &value_from_get).IsInvalidArgument());
|
||||
}
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(DBReadOnlyTestWithTimestamp, IteratorAndGet) {
|
||||
const int kNumKeysPerFile = 128;
|
||||
const uint64_t kMaxKey = 1024;
|
||||
@@ -320,6 +409,546 @@ TEST_F(DBReadOnlyTestWithTimestamp,
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(DBReadOnlyTestWithTimestamp,
|
||||
IteratorsWriteWithTimestampReadWithoutTimestamp) {
|
||||
const int kNumKeysPerFile = 128;
|
||||
const 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::string write_timestamp = Timestamp(1, 0);
|
||||
WriteOptions write_opts;
|
||||
for (uint64_t key = 0; key <= kMaxKey; ++key) {
|
||||
Status s = db_->Put(write_opts, Key1(key), write_timestamp,
|
||||
"value" + std::to_string(key));
|
||||
ASSERT_OK(s);
|
||||
}
|
||||
|
||||
// Reopen the database in read only mode to test its timestamp support.
|
||||
Close();
|
||||
ASSERT_OK(ReadOnlyReopen(options));
|
||||
ReadOptions read_opts;
|
||||
{
|
||||
std::vector<Iterator*> iters;
|
||||
ASSERT_TRUE(
|
||||
db_->NewIterators(read_opts, {db_->DefaultColumnFamily()}, &iters)
|
||||
.IsInvalidArgument());
|
||||
}
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(DBReadOnlyTestWithTimestamp, CompactedDBGetReadTimestampSizeMismatch) {
|
||||
const int kNumKeysPerFile = 1026;
|
||||
const uint64_t kMaxKey = 1024;
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
options.create_if_missing = true;
|
||||
options.disable_auto_compactions = 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);
|
||||
std::string write_timestamp = Timestamp(1, 0);
|
||||
WriteOptions write_opts;
|
||||
for (uint64_t key = 0; key <= kMaxKey; ++key) {
|
||||
Status s = db_->Put(write_opts, Key1(key), write_timestamp,
|
||||
"value" + std::to_string(0));
|
||||
ASSERT_OK(s);
|
||||
}
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
Close();
|
||||
|
||||
// Reopen the database in read only mode as a Compacted DB to test its
|
||||
// timestamp support.
|
||||
options.max_open_files = -1;
|
||||
ASSERT_OK(ReadOnlyReopen(options));
|
||||
CheckDBOpenedAsCompactedDBWithOneLevel0File();
|
||||
|
||||
ReadOptions read_opts;
|
||||
std::string different_size_read_timestamp;
|
||||
PutFixed32(&different_size_read_timestamp, 2);
|
||||
Slice different_size_read_ts = different_size_read_timestamp;
|
||||
read_opts.timestamp = &different_size_read_ts;
|
||||
for (uint64_t key = 0; key <= kMaxKey; ++key) {
|
||||
std::string value_from_get;
|
||||
std::string timestamp;
|
||||
ASSERT_TRUE(db_->Get(read_opts, Key1(key), &value_from_get, ×tamp)
|
||||
.IsInvalidArgument());
|
||||
}
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(DBReadOnlyTestWithTimestamp,
|
||||
CompactedDBGetReadTimestampSpecifiedWithoutWriteTimestamp) {
|
||||
const int kNumKeysPerFile = 1026;
|
||||
const uint64_t kMaxKey = 1024;
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
options.create_if_missing = true;
|
||||
options.disable_auto_compactions = true;
|
||||
options.memtable_factory.reset(
|
||||
test::NewSpecialSkipListFactory(kNumKeysPerFile));
|
||||
DestroyAndReopen(options);
|
||||
WriteOptions write_opts;
|
||||
for (uint64_t key = 0; key <= kMaxKey; ++key) {
|
||||
Status s = db_->Put(write_opts, Key1(key), "value" + std::to_string(0));
|
||||
ASSERT_OK(s);
|
||||
}
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
Close();
|
||||
|
||||
// Reopen the database in read only mode as a Compacted DB to test its
|
||||
// timestamp support.
|
||||
options.max_open_files = -1;
|
||||
ASSERT_OK(ReadOnlyReopen(options));
|
||||
CheckDBOpenedAsCompactedDBWithOneLevel0File();
|
||||
|
||||
ReadOptions read_opts;
|
||||
const std::string read_timestamp = Timestamp(2, 0);
|
||||
Slice read_ts = read_timestamp;
|
||||
read_opts.timestamp = &read_ts;
|
||||
for (uint64_t key = 0; key <= kMaxKey; ++key) {
|
||||
std::string value_from_get;
|
||||
std::string timestamp;
|
||||
ASSERT_TRUE(db_->Get(read_opts, Key1(key), &value_from_get, ×tamp)
|
||||
.IsInvalidArgument());
|
||||
}
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(DBReadOnlyTestWithTimestamp,
|
||||
CompactedDBGetWriteWithTimestampReadWithoutTimestamp) {
|
||||
const int kNumKeysPerFile = 1026;
|
||||
const uint64_t kMaxKey = 1024;
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
options.create_if_missing = true;
|
||||
options.disable_auto_compactions = 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);
|
||||
std::string write_timestamp = Timestamp(1, 0);
|
||||
WriteOptions write_opts;
|
||||
for (uint64_t key = 0; key <= kMaxKey; ++key) {
|
||||
Status s = db_->Put(write_opts, Key1(key), write_timestamp,
|
||||
"value" + std::to_string(0));
|
||||
ASSERT_OK(s);
|
||||
}
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
Close();
|
||||
|
||||
// Reopen the database in read only mode as a Compacted DB to test its
|
||||
// timestamp support.
|
||||
options.max_open_files = -1;
|
||||
ASSERT_OK(ReadOnlyReopen(options));
|
||||
CheckDBOpenedAsCompactedDBWithOneLevel0File();
|
||||
|
||||
ReadOptions read_opts;
|
||||
for (uint64_t key = 0; key <= kMaxKey; ++key) {
|
||||
std::string value_from_get;
|
||||
ASSERT_TRUE(
|
||||
db_->Get(read_opts, Key1(key), &value_from_get).IsInvalidArgument());
|
||||
}
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(DBReadOnlyTestWithTimestamp, CompactedDBGetWithOnlyOneL0File) {
|
||||
const int kNumKeysPerFile = 1026 * 2;
|
||||
const uint64_t kMaxKey = 1024;
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
options.create_if_missing = true;
|
||||
options.disable_auto_compactions = 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<uint64_t> start_keys = {1, 0};
|
||||
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)};
|
||||
for (size_t i = 0; i < write_timestamps.size(); ++i) {
|
||||
WriteOptions write_opts;
|
||||
for (uint64_t key = start_keys[i]; key <= kMaxKey; ++key) {
|
||||
Status s = db_->Put(write_opts, Key1(key), write_timestamps[i],
|
||||
"value" + std::to_string(i));
|
||||
ASSERT_OK(s);
|
||||
}
|
||||
}
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
Close();
|
||||
|
||||
// Reopen the database in read only mode as a Compacted DB to test its
|
||||
// timestamp support.
|
||||
options.max_open_files = -1;
|
||||
ASSERT_OK(ReadOnlyReopen(options));
|
||||
CheckDBOpenedAsCompactedDBWithOneLevel0File();
|
||||
|
||||
for (size_t i = 0; i < read_timestamps.size(); ++i) {
|
||||
ReadOptions read_opts;
|
||||
Slice read_ts = read_timestamps[i];
|
||||
read_opts.timestamp = &read_ts;
|
||||
int count = 0;
|
||||
for (uint64_t key = start_keys[i]; key <= kMaxKey; ++key, ++count) {
|
||||
std::string value_from_get;
|
||||
std::string timestamp;
|
||||
ASSERT_OK(db_->Get(read_opts, Key1(key), &value_from_get, ×tamp));
|
||||
ASSERT_EQ("value" + std::to_string(i), value_from_get);
|
||||
ASSERT_EQ(write_timestamps[i], timestamp);
|
||||
}
|
||||
size_t expected_count = kMaxKey - start_keys[i] + 1;
|
||||
ASSERT_EQ(expected_count, count);
|
||||
}
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(DBReadOnlyTestWithTimestamp,
|
||||
CompactedDBGetWithOnlyHighestNonEmptyLevelFiles) {
|
||||
const int kNumKeysPerFile = 128;
|
||||
const uint64_t kMaxKey = 1024;
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
options.create_if_missing = true;
|
||||
options.disable_auto_compactions = 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<uint64_t> start_keys = {1, 0};
|
||||
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)};
|
||||
for (size_t i = 0; i < write_timestamps.size(); ++i) {
|
||||
WriteOptions write_opts;
|
||||
for (uint64_t key = start_keys[i]; key <= kMaxKey; ++key) {
|
||||
Status s = db_->Put(write_opts, Key1(key), write_timestamps[i],
|
||||
"value" + std::to_string(i));
|
||||
ASSERT_OK(s);
|
||||
}
|
||||
}
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
Close();
|
||||
|
||||
// Reopen the database in read only mode as a Compacted DB to test its
|
||||
// timestamp support.
|
||||
options.max_open_files = -1;
|
||||
ASSERT_OK(ReadOnlyReopen(options));
|
||||
CheckDBOpenedAsCompactedDBWithOnlyHighestNonEmptyLevelFiles();
|
||||
|
||||
for (size_t i = 0; i < read_timestamps.size(); ++i) {
|
||||
ReadOptions read_opts;
|
||||
Slice read_ts = read_timestamps[i];
|
||||
read_opts.timestamp = &read_ts;
|
||||
int count = 0;
|
||||
for (uint64_t key = start_keys[i]; key <= kMaxKey; ++key, ++count) {
|
||||
std::string value_from_get;
|
||||
std::string timestamp;
|
||||
ASSERT_OK(db_->Get(read_opts, Key1(key), &value_from_get, ×tamp));
|
||||
ASSERT_EQ("value" + std::to_string(i), value_from_get);
|
||||
ASSERT_EQ(write_timestamps[i], timestamp);
|
||||
}
|
||||
size_t expected_count = kMaxKey - start_keys[i] + 1;
|
||||
ASSERT_EQ(expected_count, count);
|
||||
}
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(DBReadOnlyTestWithTimestamp,
|
||||
CompactedDBMultiGetReadTimestampSizeMismatch) {
|
||||
const int kNumKeysPerFile = 1026;
|
||||
const uint64_t kMaxKey = 1024;
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
options.create_if_missing = true;
|
||||
options.disable_auto_compactions = 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);
|
||||
std::string write_timestamp = Timestamp(1, 0);
|
||||
WriteOptions write_opts;
|
||||
for (uint64_t key = 0; key <= kMaxKey; ++key) {
|
||||
Status s = db_->Put(write_opts, Key1(key), write_timestamp,
|
||||
"value" + std::to_string(0));
|
||||
ASSERT_OK(s);
|
||||
}
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
Close();
|
||||
|
||||
// Reopen the database in read only mode as a Compacted DB to test its
|
||||
// timestamp support.
|
||||
options.max_open_files = -1;
|
||||
ASSERT_OK(ReadOnlyReopen(options));
|
||||
CheckDBOpenedAsCompactedDBWithOneLevel0File();
|
||||
|
||||
ReadOptions read_opts;
|
||||
std::string different_size_read_timestamp;
|
||||
PutFixed32(&different_size_read_timestamp, 2);
|
||||
Slice different_size_read_ts = different_size_read_timestamp;
|
||||
read_opts.timestamp = &different_size_read_ts;
|
||||
std::vector<std::string> key_strs;
|
||||
std::vector<Slice> keys;
|
||||
for (uint64_t key = 0; key <= kMaxKey; ++key) {
|
||||
key_strs.push_back(Key1(key));
|
||||
}
|
||||
for (const auto& key_str : key_strs) {
|
||||
keys.emplace_back(key_str);
|
||||
}
|
||||
std::vector<std::string> values;
|
||||
std::vector<std::string> timestamps;
|
||||
std::vector<Status> status_list =
|
||||
db_->MultiGet(read_opts, keys, &values, ×tamps);
|
||||
for (const auto& status : status_list) {
|
||||
ASSERT_TRUE(status.IsInvalidArgument());
|
||||
}
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(DBReadOnlyTestWithTimestamp,
|
||||
CompactedDBMultiGetReadTimestampSpecifiedWithoutWriteTimestamp) {
|
||||
const int kNumKeysPerFile = 1026;
|
||||
const uint64_t kMaxKey = 1024;
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
options.create_if_missing = true;
|
||||
options.disable_auto_compactions = true;
|
||||
options.memtable_factory.reset(
|
||||
test::NewSpecialSkipListFactory(kNumKeysPerFile));
|
||||
DestroyAndReopen(options);
|
||||
WriteOptions write_opts;
|
||||
for (uint64_t key = 0; key <= kMaxKey; ++key) {
|
||||
Status s = db_->Put(write_opts, Key1(key), "value" + std::to_string(0));
|
||||
ASSERT_OK(s);
|
||||
}
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
Close();
|
||||
|
||||
// Reopen the database in read only mode as a Compacted DB to test its
|
||||
// timestamp support.
|
||||
options.max_open_files = -1;
|
||||
ASSERT_OK(ReadOnlyReopen(options));
|
||||
CheckDBOpenedAsCompactedDBWithOneLevel0File();
|
||||
|
||||
ReadOptions read_opts;
|
||||
std::string read_timestamp = Timestamp(2, 0);
|
||||
Slice read_ts = read_timestamp;
|
||||
read_opts.timestamp = &read_ts;
|
||||
std::vector<std::string> key_strs;
|
||||
std::vector<Slice> keys;
|
||||
for (uint64_t key = 0; key <= kMaxKey; ++key) {
|
||||
key_strs.push_back(Key1(key));
|
||||
}
|
||||
for (const auto& key_str : key_strs) {
|
||||
keys.emplace_back(key_str);
|
||||
}
|
||||
std::vector<std::string> values;
|
||||
std::vector<std::string> timestamps;
|
||||
std::vector<Status> status_list =
|
||||
db_->MultiGet(read_opts, keys, &values, ×tamps);
|
||||
for (const auto& status : status_list) {
|
||||
ASSERT_TRUE(status.IsInvalidArgument());
|
||||
}
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(DBReadOnlyTestWithTimestamp,
|
||||
CompactedDBMultiGetWriteWithTimestampReadWithoutTimestamp) {
|
||||
const int kNumKeysPerFile = 1026;
|
||||
const uint64_t kMaxKey = 1024;
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
options.create_if_missing = true;
|
||||
options.disable_auto_compactions = 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);
|
||||
std::string write_timestamp = Timestamp(1, 0);
|
||||
WriteOptions write_opts;
|
||||
for (uint64_t key = 0; key <= kMaxKey; ++key) {
|
||||
Status s = db_->Put(write_opts, Key1(key), write_timestamp,
|
||||
"value" + std::to_string(0));
|
||||
ASSERT_OK(s);
|
||||
}
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
Close();
|
||||
|
||||
// Reopen the database in read only mode as a Compacted DB to test its
|
||||
// timestamp support.
|
||||
options.max_open_files = -1;
|
||||
ASSERT_OK(ReadOnlyReopen(options));
|
||||
CheckDBOpenedAsCompactedDBWithOneLevel0File();
|
||||
|
||||
ReadOptions read_opts;
|
||||
std::vector<std::string> key_strs;
|
||||
std::vector<Slice> keys;
|
||||
for (uint64_t key = 0; key <= kMaxKey; ++key) {
|
||||
key_strs.push_back(Key1(key));
|
||||
}
|
||||
for (const auto& key_str : key_strs) {
|
||||
keys.emplace_back(key_str);
|
||||
}
|
||||
std::vector<std::string> values;
|
||||
std::vector<Status> status_list = db_->MultiGet(read_opts, keys, &values);
|
||||
for (const auto& status : status_list) {
|
||||
ASSERT_TRUE(status.IsInvalidArgument());
|
||||
}
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(DBReadOnlyTestWithTimestamp, CompactedDBMultiGetWithOnlyOneL0File) {
|
||||
const int kNumKeysPerFile = 1026 * 2;
|
||||
const uint64_t kMaxKey = 1024;
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
options.create_if_missing = true;
|
||||
options.disable_auto_compactions = 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<uint64_t> start_keys = {1, 0};
|
||||
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)};
|
||||
for (size_t i = 0; i < write_timestamps.size(); ++i) {
|
||||
WriteOptions write_opts;
|
||||
for (uint64_t key = start_keys[i]; key <= kMaxKey; ++key) {
|
||||
Status s = db_->Put(write_opts, Key1(key), write_timestamps[i],
|
||||
"value" + std::to_string(i));
|
||||
ASSERT_OK(s);
|
||||
}
|
||||
}
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
Close();
|
||||
|
||||
// Reopen the database in read only mode as a Compacted DB to test its
|
||||
// timestamp support.
|
||||
options.max_open_files = -1;
|
||||
ASSERT_OK(ReadOnlyReopen(options));
|
||||
CheckDBOpenedAsCompactedDBWithOneLevel0File();
|
||||
|
||||
for (size_t i = 0; i < write_timestamps.size(); ++i) {
|
||||
ReadOptions read_opts;
|
||||
Slice read_ts = read_timestamps[i];
|
||||
read_opts.timestamp = &read_ts;
|
||||
std::vector<std::string> key_strs;
|
||||
std::vector<Slice> keys;
|
||||
for (uint64_t key = start_keys[i]; key <= kMaxKey; ++key) {
|
||||
key_strs.push_back(Key1(key));
|
||||
}
|
||||
for (const auto& key_str : key_strs) {
|
||||
keys.emplace_back(key_str);
|
||||
}
|
||||
size_t batch_size = kMaxKey - start_keys[i] + 1;
|
||||
std::vector<std::string> values;
|
||||
std::vector<std::string> timestamps;
|
||||
std::vector<Status> status_list =
|
||||
db_->MultiGet(read_opts, keys, &values, ×tamps);
|
||||
ASSERT_EQ(batch_size, values.size());
|
||||
ASSERT_EQ(batch_size, timestamps.size());
|
||||
for (uint64_t idx = 0; idx < values.size(); ++idx) {
|
||||
ASSERT_EQ("value" + std::to_string(i), values[idx]);
|
||||
ASSERT_EQ(write_timestamps[i], timestamps[idx]);
|
||||
ASSERT_OK(status_list[idx]);
|
||||
}
|
||||
}
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(DBReadOnlyTestWithTimestamp,
|
||||
CompactedDBMultiGetWithOnlyHighestNonEmptyLevelFiles) {
|
||||
const int kNumKeysPerFile = 128;
|
||||
const uint64_t kMaxKey = 1024;
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
options.create_if_missing = true;
|
||||
options.disable_auto_compactions = 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<uint64_t> start_keys = {1, 0};
|
||||
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)};
|
||||
for (size_t i = 0; i < write_timestamps.size(); ++i) {
|
||||
WriteOptions write_opts;
|
||||
for (uint64_t key = start_keys[i]; key <= kMaxKey; ++key) {
|
||||
Status s = db_->Put(write_opts, Key1(key), write_timestamps[i],
|
||||
"value" + std::to_string(i));
|
||||
ASSERT_OK(s);
|
||||
}
|
||||
}
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), nullptr, nullptr));
|
||||
Close();
|
||||
|
||||
// Reopen the database in read only mode as a Compacted DB to test its
|
||||
// timestamp support.
|
||||
options.max_open_files = -1;
|
||||
ASSERT_OK(ReadOnlyReopen(options));
|
||||
CheckDBOpenedAsCompactedDBWithOnlyHighestNonEmptyLevelFiles();
|
||||
|
||||
for (size_t i = 0; i < write_timestamps.size(); ++i) {
|
||||
ReadOptions read_opts;
|
||||
Slice read_ts = read_timestamps[i];
|
||||
read_opts.timestamp = &read_ts;
|
||||
std::vector<std::string> key_strs;
|
||||
std::vector<Slice> keys;
|
||||
for (uint64_t key = start_keys[i]; key <= kMaxKey; ++key) {
|
||||
key_strs.push_back(Key1(key));
|
||||
}
|
||||
for (const auto& key_str : key_strs) {
|
||||
keys.emplace_back(key_str);
|
||||
}
|
||||
size_t batch_size = kMaxKey - start_keys[i] + 1;
|
||||
std::vector<std::string> values;
|
||||
std::vector<std::string> timestamps;
|
||||
std::vector<Status> status_list =
|
||||
db_->MultiGet(read_opts, keys, &values, ×tamps);
|
||||
ASSERT_EQ(batch_size, values.size());
|
||||
ASSERT_EQ(batch_size, timestamps.size());
|
||||
for (uint64_t idx = 0; idx < values.size(); ++idx) {
|
||||
ASSERT_EQ("value" + std::to_string(i), values[idx]);
|
||||
ASSERT_EQ(write_timestamps[i], timestamps[idx]);
|
||||
ASSERT_OK(status_list[idx]);
|
||||
}
|
||||
}
|
||||
|
||||
Close();
|
||||
}
|
||||
#endif // !ROCKSDB_LITE
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
|
||||
+438
-10
@@ -9,6 +9,7 @@
|
||||
|
||||
#include "db/db_impl/db_impl_secondary.h"
|
||||
#include "db/db_test_util.h"
|
||||
#include "db/db_with_timestamp_test_util.h"
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/utilities/transaction_db.h"
|
||||
#include "test_util/sync_point.h"
|
||||
@@ -18,10 +19,10 @@
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
class DBSecondaryTest : public DBTestBase {
|
||||
class DBSecondaryTestBase : public DBBasicTestWithTimestampBase {
|
||||
public:
|
||||
DBSecondaryTest()
|
||||
: DBTestBase("db_secondary_test", /*env_do_fsync=*/true),
|
||||
explicit DBSecondaryTestBase(const std::string& dbname)
|
||||
: DBBasicTestWithTimestampBase(dbname),
|
||||
secondary_path_(),
|
||||
handles_secondary_(),
|
||||
db_secondary_(nullptr) {
|
||||
@@ -29,7 +30,7 @@ class DBSecondaryTest : public DBTestBase {
|
||||
test::PerThreadDBPath(env_, "/db_secondary_test_secondary");
|
||||
}
|
||||
|
||||
~DBSecondaryTest() override {
|
||||
~DBSecondaryTestBase() override {
|
||||
CloseSecondary();
|
||||
if (getenv("KEEP_DB") != nullptr) {
|
||||
fprintf(stdout, "Secondary DB is still at %s\n", secondary_path_.c_str());
|
||||
@@ -73,17 +74,17 @@ class DBSecondaryTest : public DBTestBase {
|
||||
DB* db_secondary_;
|
||||
};
|
||||
|
||||
void DBSecondaryTest::OpenSecondary(const Options& options) {
|
||||
void DBSecondaryTestBase::OpenSecondary(const Options& options) {
|
||||
ASSERT_OK(TryOpenSecondary(options));
|
||||
}
|
||||
|
||||
Status DBSecondaryTest::TryOpenSecondary(const Options& options) {
|
||||
Status DBSecondaryTestBase::TryOpenSecondary(const Options& options) {
|
||||
Status s =
|
||||
DB::OpenAsSecondary(options, dbname_, secondary_path_, &db_secondary_);
|
||||
return s;
|
||||
}
|
||||
|
||||
void DBSecondaryTest::OpenSecondaryWithColumnFamilies(
|
||||
void DBSecondaryTestBase::OpenSecondaryWithColumnFamilies(
|
||||
const std::vector<std::string>& column_families, const Options& options) {
|
||||
std::vector<ColumnFamilyDescriptor> cf_descs;
|
||||
cf_descs.emplace_back(kDefaultColumnFamilyName, options);
|
||||
@@ -95,9 +96,10 @@ void DBSecondaryTest::OpenSecondaryWithColumnFamilies(
|
||||
ASSERT_OK(s);
|
||||
}
|
||||
|
||||
void DBSecondaryTest::CheckFileTypeCounts(const std::string& dir,
|
||||
int expected_log, int expected_sst,
|
||||
int expected_manifest) const {
|
||||
void DBSecondaryTestBase::CheckFileTypeCounts(const std::string& dir,
|
||||
int expected_log,
|
||||
int expected_sst,
|
||||
int expected_manifest) const {
|
||||
std::vector<std::string> filenames;
|
||||
ASSERT_OK(env_->GetChildren(dir, &filenames));
|
||||
|
||||
@@ -116,6 +118,35 @@ void DBSecondaryTest::CheckFileTypeCounts(const std::string& dir,
|
||||
ASSERT_EQ(expected_manifest, manifest_cnt);
|
||||
}
|
||||
|
||||
class DBSecondaryTest : public DBSecondaryTestBase {
|
||||
public:
|
||||
explicit DBSecondaryTest() : DBSecondaryTestBase("db_secondary_test") {}
|
||||
};
|
||||
|
||||
TEST_F(DBSecondaryTest, FailOpenIfLoggerCreationFail) {
|
||||
Options options = GetDefaultOptions();
|
||||
options.create_if_missing = true;
|
||||
Reopen(options);
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"rocksdb::CreateLoggerFromOptions:AfterGetPath", [&](void* arg) {
|
||||
auto* s = reinterpret_cast<Status*>(arg);
|
||||
assert(s);
|
||||
*s = Status::IOError("Injected");
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
options.max_open_files = -1;
|
||||
Status s = TryOpenSecondary(options);
|
||||
ASSERT_EQ(nullptr, options.info_log);
|
||||
ASSERT_TRUE(s.IsIOError());
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
}
|
||||
|
||||
TEST_F(DBSecondaryTest, NonExistingDb) {
|
||||
Destroy(last_options_);
|
||||
|
||||
@@ -1254,6 +1285,403 @@ TEST_F(DBSecondaryTest, OpenWithTransactionDB) {
|
||||
ASSERT_OK(TryOpenSecondary(options));
|
||||
}
|
||||
|
||||
class DBSecondaryTestWithTimestamp : public DBSecondaryTestBase {
|
||||
public:
|
||||
explicit DBSecondaryTestWithTimestamp()
|
||||
: DBSecondaryTestBase("db_secondary_test_with_timestamp") {}
|
||||
};
|
||||
TEST_F(DBSecondaryTestWithTimestamp, IteratorAndGetReadTimestampSizeMismatch) {
|
||||
const int kNumKeysPerFile = 128;
|
||||
const 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::string write_timestamp = Timestamp(1, 0);
|
||||
WriteOptions write_opts;
|
||||
for (uint64_t key = 0; key <= kMaxKey; ++key) {
|
||||
Status s = db_->Put(write_opts, Key1(key), write_timestamp,
|
||||
"value" + std::to_string(key));
|
||||
ASSERT_OK(s);
|
||||
}
|
||||
|
||||
// Reopen the database as secondary instance to test its timestamp support.
|
||||
Close();
|
||||
options.max_open_files = -1;
|
||||
ASSERT_OK(ReopenAsSecondary(options));
|
||||
|
||||
ReadOptions read_opts;
|
||||
std::string different_size_read_timestamp;
|
||||
PutFixed32(&different_size_read_timestamp, 2);
|
||||
Slice different_size_read_ts = different_size_read_timestamp;
|
||||
read_opts.timestamp = &different_size_read_ts;
|
||||
{
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_opts));
|
||||
ASSERT_FALSE(iter->Valid());
|
||||
ASSERT_TRUE(iter->status().IsInvalidArgument());
|
||||
}
|
||||
|
||||
for (uint64_t key = 0; key <= kMaxKey; ++key) {
|
||||
std::string value_from_get;
|
||||
std::string timestamp;
|
||||
ASSERT_TRUE(db_->Get(read_opts, Key1(key), &value_from_get, ×tamp)
|
||||
.IsInvalidArgument());
|
||||
}
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(DBSecondaryTestWithTimestamp,
|
||||
IteratorAndGetReadTimestampSpecifiedWithoutWriteTimestamp) {
|
||||
const int kNumKeysPerFile = 128;
|
||||
const uint64_t kMaxKey = 1024;
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
options.create_if_missing = true;
|
||||
options.memtable_factory.reset(
|
||||
test::NewSpecialSkipListFactory(kNumKeysPerFile));
|
||||
DestroyAndReopen(options);
|
||||
WriteOptions write_opts;
|
||||
for (uint64_t key = 0; key <= kMaxKey; ++key) {
|
||||
Status s = db_->Put(write_opts, Key1(key), "value" + std::to_string(key));
|
||||
ASSERT_OK(s);
|
||||
}
|
||||
|
||||
// Reopen the database as secondary instance to test its timestamp support.
|
||||
Close();
|
||||
options.max_open_files = -1;
|
||||
ASSERT_OK(ReopenAsSecondary(options));
|
||||
|
||||
ReadOptions read_opts;
|
||||
const std::string read_timestamp = Timestamp(2, 0);
|
||||
Slice read_ts = read_timestamp;
|
||||
read_opts.timestamp = &read_ts;
|
||||
{
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_opts));
|
||||
ASSERT_FALSE(iter->Valid());
|
||||
ASSERT_TRUE(iter->status().IsInvalidArgument());
|
||||
}
|
||||
|
||||
for (uint64_t key = 0; key <= kMaxKey; ++key) {
|
||||
std::string value_from_get;
|
||||
std::string timestamp;
|
||||
ASSERT_TRUE(db_->Get(read_opts, Key1(key), &value_from_get, ×tamp)
|
||||
.IsInvalidArgument());
|
||||
}
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(DBSecondaryTestWithTimestamp,
|
||||
IteratorAndGetWriteWithTimestampReadWithoutTimestamp) {
|
||||
const int kNumKeysPerFile = 128;
|
||||
const 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::string write_timestamp = Timestamp(1, 0);
|
||||
WriteOptions write_opts;
|
||||
for (uint64_t key = 0; key <= kMaxKey; ++key) {
|
||||
Status s = db_->Put(write_opts, Key1(key), write_timestamp,
|
||||
"value" + std::to_string(key));
|
||||
ASSERT_OK(s);
|
||||
}
|
||||
|
||||
// Reopen the database as secondary instance to test its timestamp support.
|
||||
Close();
|
||||
options.max_open_files = -1;
|
||||
ASSERT_OK(ReopenAsSecondary(options));
|
||||
|
||||
ReadOptions read_opts;
|
||||
{
|
||||
std::unique_ptr<Iterator> iter(db_->NewIterator(read_opts));
|
||||
ASSERT_FALSE(iter->Valid());
|
||||
ASSERT_TRUE(iter->status().IsInvalidArgument());
|
||||
}
|
||||
|
||||
for (uint64_t key = 0; key <= kMaxKey; ++key) {
|
||||
std::string value_from_get;
|
||||
ASSERT_TRUE(
|
||||
db_->Get(read_opts, Key1(key), &value_from_get).IsInvalidArgument());
|
||||
}
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(DBSecondaryTestWithTimestamp, IteratorAndGet) {
|
||||
const int kNumKeysPerFile = 128;
|
||||
const 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<uint64_t> start_keys = {1, 0};
|
||||
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)};
|
||||
for (size_t i = 0; i < write_timestamps.size(); ++i) {
|
||||
WriteOptions write_opts;
|
||||
for (uint64_t key = start_keys[i]; key <= kMaxKey; ++key) {
|
||||
Status s = db_->Put(write_opts, Key1(key), write_timestamps[i],
|
||||
"value" + std::to_string(i));
|
||||
ASSERT_OK(s);
|
||||
}
|
||||
}
|
||||
|
||||
// Reopen the database as secondary instance to test its timestamp support.
|
||||
Close();
|
||||
options.max_open_files = -1;
|
||||
ASSERT_OK(ReopenAsSecondary(options));
|
||||
|
||||
auto get_value_and_check = [](DB* db, ReadOptions read_opts, Slice key,
|
||||
Slice expected_value, std::string expected_ts) {
|
||||
std::string value_from_get;
|
||||
std::string timestamp;
|
||||
ASSERT_OK(db->Get(read_opts, key.ToString(), &value_from_get, ×tamp));
|
||||
ASSERT_EQ(expected_value, value_from_get);
|
||||
ASSERT_EQ(expected_ts, timestamp);
|
||||
};
|
||||
for (size_t i = 0; i < read_timestamps.size(); ++i) {
|
||||
ReadOptions read_opts;
|
||||
Slice read_ts = read_timestamps[i];
|
||||
read_opts.timestamp = &read_ts;
|
||||
std::unique_ptr<Iterator> it(db_->NewIterator(read_opts));
|
||||
int count = 0;
|
||||
uint64_t key = 0;
|
||||
// Forward iterate.
|
||||
for (it->Seek(Key1(0)), key = start_keys[i]; it->Valid();
|
||||
it->Next(), ++count, ++key) {
|
||||
CheckIterUserEntry(it.get(), Key1(key), kTypeValue,
|
||||
"value" + std::to_string(i), write_timestamps[i]);
|
||||
get_value_and_check(db_, read_opts, it->key(), it->value(),
|
||||
write_timestamps[i]);
|
||||
}
|
||||
size_t expected_count = kMaxKey - start_keys[i] + 1;
|
||||
ASSERT_EQ(expected_count, count);
|
||||
|
||||
// Backward iterate.
|
||||
count = 0;
|
||||
for (it->SeekForPrev(Key1(kMaxKey)), key = kMaxKey; it->Valid();
|
||||
it->Prev(), ++count, --key) {
|
||||
CheckIterUserEntry(it.get(), Key1(key), kTypeValue,
|
||||
"value" + std::to_string(i), write_timestamps[i]);
|
||||
get_value_and_check(db_, read_opts, it->key(), it->value(),
|
||||
write_timestamps[i]);
|
||||
}
|
||||
ASSERT_EQ(static_cast<size_t>(kMaxKey) - start_keys[i] + 1, count);
|
||||
|
||||
// SeekToFirst()/SeekToLast() with lower/upper bounds.
|
||||
// Then iter with lower and upper bounds.
|
||||
uint64_t l = 0;
|
||||
uint64_t r = kMaxKey + 1;
|
||||
while (l < r) {
|
||||
std::string lb_str = Key1(l);
|
||||
Slice lb = lb_str;
|
||||
std::string ub_str = Key1(r);
|
||||
Slice ub = ub_str;
|
||||
read_opts.iterate_lower_bound = &lb;
|
||||
read_opts.iterate_upper_bound = &ub;
|
||||
it.reset(db_->NewIterator(read_opts));
|
||||
for (it->SeekToFirst(), key = std::max(l, start_keys[i]), count = 0;
|
||||
it->Valid(); it->Next(), ++key, ++count) {
|
||||
CheckIterUserEntry(it.get(), Key1(key), kTypeValue,
|
||||
"value" + std::to_string(i), write_timestamps[i]);
|
||||
get_value_and_check(db_, read_opts, it->key(), it->value(),
|
||||
write_timestamps[i]);
|
||||
}
|
||||
ASSERT_EQ(r - std::max(l, start_keys[i]), count);
|
||||
|
||||
for (it->SeekToLast(), key = std::min(r, kMaxKey + 1), count = 0;
|
||||
it->Valid(); it->Prev(), --key, ++count) {
|
||||
CheckIterUserEntry(it.get(), Key1(key - 1), kTypeValue,
|
||||
"value" + std::to_string(i), write_timestamps[i]);
|
||||
get_value_and_check(db_, read_opts, it->key(), it->value(),
|
||||
write_timestamps[i]);
|
||||
}
|
||||
l += (kMaxKey / 100);
|
||||
r -= (kMaxKey / 100);
|
||||
}
|
||||
}
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(DBSecondaryTestWithTimestamp, IteratorsReadTimestampSizeMismatch) {
|
||||
const int kNumKeysPerFile = 128;
|
||||
const 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::string write_timestamp = Timestamp(1, 0);
|
||||
WriteOptions write_opts;
|
||||
for (uint64_t key = 0; key <= kMaxKey; ++key) {
|
||||
Status s = db_->Put(write_opts, Key1(key), write_timestamp,
|
||||
"value" + std::to_string(key));
|
||||
ASSERT_OK(s);
|
||||
}
|
||||
|
||||
// Reopen the database as secondary instance to test its timestamp support.
|
||||
Close();
|
||||
options.max_open_files = -1;
|
||||
ASSERT_OK(ReopenAsSecondary(options));
|
||||
|
||||
ReadOptions read_opts;
|
||||
std::string different_size_read_timestamp;
|
||||
PutFixed32(&different_size_read_timestamp, 2);
|
||||
Slice different_size_read_ts = different_size_read_timestamp;
|
||||
read_opts.timestamp = &different_size_read_ts;
|
||||
{
|
||||
std::vector<Iterator*> iters;
|
||||
ASSERT_TRUE(
|
||||
db_->NewIterators(read_opts, {db_->DefaultColumnFamily()}, &iters)
|
||||
.IsInvalidArgument());
|
||||
}
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(DBSecondaryTestWithTimestamp,
|
||||
IteratorsReadTimestampSpecifiedWithoutWriteTimestamp) {
|
||||
const int kNumKeysPerFile = 128;
|
||||
const uint64_t kMaxKey = 1024;
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
options.create_if_missing = true;
|
||||
options.memtable_factory.reset(
|
||||
test::NewSpecialSkipListFactory(kNumKeysPerFile));
|
||||
DestroyAndReopen(options);
|
||||
WriteOptions write_opts;
|
||||
for (uint64_t key = 0; key <= kMaxKey; ++key) {
|
||||
Status s = db_->Put(write_opts, Key1(key), "value" + std::to_string(key));
|
||||
ASSERT_OK(s);
|
||||
}
|
||||
|
||||
// Reopen the database as secondary instance to test its timestamp support.
|
||||
Close();
|
||||
options.max_open_files = -1;
|
||||
ASSERT_OK(ReopenAsSecondary(options));
|
||||
|
||||
ReadOptions read_opts;
|
||||
const std::string read_timestamp = Timestamp(2, 0);
|
||||
Slice read_ts = read_timestamp;
|
||||
read_opts.timestamp = &read_ts;
|
||||
{
|
||||
std::vector<Iterator*> iters;
|
||||
ASSERT_TRUE(
|
||||
db_->NewIterators(read_opts, {db_->DefaultColumnFamily()}, &iters)
|
||||
.IsInvalidArgument());
|
||||
}
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(DBSecondaryTestWithTimestamp,
|
||||
IteratorsWriteWithTimestampReadWithoutTimestamp) {
|
||||
const int kNumKeysPerFile = 128;
|
||||
const 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::string write_timestamp = Timestamp(1, 0);
|
||||
WriteOptions write_opts;
|
||||
for (uint64_t key = 0; key <= kMaxKey; ++key) {
|
||||
Status s = db_->Put(write_opts, Key1(key), write_timestamp,
|
||||
"value" + std::to_string(key));
|
||||
ASSERT_OK(s);
|
||||
}
|
||||
|
||||
// Reopen the database as secondary instance to test its timestamp support.
|
||||
Close();
|
||||
options.max_open_files = -1;
|
||||
ASSERT_OK(ReopenAsSecondary(options));
|
||||
|
||||
ReadOptions read_opts;
|
||||
{
|
||||
std::vector<Iterator*> iters;
|
||||
ASSERT_TRUE(
|
||||
db_->NewIterators(read_opts, {db_->DefaultColumnFamily()}, &iters)
|
||||
.IsInvalidArgument());
|
||||
}
|
||||
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(DBSecondaryTestWithTimestamp, Iterators) {
|
||||
const int kNumKeysPerFile = 128;
|
||||
const 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::string write_timestamp = Timestamp(1, 0);
|
||||
const std::string read_timestamp = Timestamp(2, 0);
|
||||
WriteOptions write_opts;
|
||||
for (uint64_t key = 0; key <= kMaxKey; ++key) {
|
||||
Status s = db_->Put(write_opts, Key1(key), write_timestamp,
|
||||
"value" + std::to_string(key));
|
||||
ASSERT_OK(s);
|
||||
}
|
||||
|
||||
// Reopen the database as secondary instance to test its timestamp support.
|
||||
Close();
|
||||
options.max_open_files = -1;
|
||||
ASSERT_OK(ReopenAsSecondary(options));
|
||||
|
||||
ReadOptions read_opts;
|
||||
Slice read_ts = read_timestamp;
|
||||
read_opts.timestamp = &read_ts;
|
||||
std::vector<Iterator*> iters;
|
||||
ASSERT_OK(db_->NewIterators(read_opts, {db_->DefaultColumnFamily()}, &iters));
|
||||
ASSERT_EQ(static_cast<uint64_t>(1), iters.size());
|
||||
|
||||
int count = 0;
|
||||
uint64_t key = 0;
|
||||
// Forward iterate.
|
||||
for (iters[0]->Seek(Key1(0)), key = 0; iters[0]->Valid();
|
||||
iters[0]->Next(), ++count, ++key) {
|
||||
CheckIterUserEntry(iters[0], Key1(key), kTypeValue,
|
||||
"value" + std::to_string(key), write_timestamp);
|
||||
}
|
||||
|
||||
size_t expected_count = kMaxKey - 0 + 1;
|
||||
ASSERT_EQ(expected_count, count);
|
||||
delete iters[0];
|
||||
|
||||
Close();
|
||||
}
|
||||
#endif //! ROCKSDB_LITE
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
+43
-2
@@ -8,6 +8,7 @@
|
||||
// found in the LICENSE file. See the AUTHORS file for names of contributors.
|
||||
|
||||
#include "db/db_test_util.h"
|
||||
#include "env/mock_env.h"
|
||||
#include "file/sst_file_manager_impl.h"
|
||||
#include "port/port.h"
|
||||
#include "port/stack_trace.h"
|
||||
@@ -961,7 +962,6 @@ TEST_F(DBSSTTest, OpenDBWithExistingTrash) {
|
||||
ASSERT_NOK(env_->FileExists(dbname_ + "/" + "003.sst.trash"));
|
||||
}
|
||||
|
||||
|
||||
// Create a DB with 2 db_paths, and generate multiple files in the 2
|
||||
// db_paths using CompactRangeOptions, make sure that files that were
|
||||
// deleted from first db_path were deleted using DeleteScheduler and
|
||||
@@ -1233,7 +1233,9 @@ TEST_F(DBSSTTest, CancellingCompactionsWorks) {
|
||||
ASSERT_GT(completed_compactions, 0);
|
||||
ASSERT_EQ(sfm->GetCompactionsReservedSize(), 0);
|
||||
// Make sure the stat is bumped
|
||||
ASSERT_GT(dbfull()->immutable_db_options().statistics.get()->getTickerCount(COMPACTION_CANCELLED), 0);
|
||||
ASSERT_GT(dbfull()->immutable_db_options().statistics.get()->getTickerCount(
|
||||
COMPACTION_CANCELLED),
|
||||
0);
|
||||
ASSERT_EQ(0,
|
||||
dbfull()->immutable_db_options().statistics.get()->getTickerCount(
|
||||
FILES_MARKED_TRASH));
|
||||
@@ -1629,6 +1631,45 @@ TEST_F(DBSSTTest, GetTotalSstFilesSize) {
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
}
|
||||
|
||||
TEST_F(DBSSTTest, OpenDBWithoutGetFileSizeInvocations) {
|
||||
Options options = CurrentOptions();
|
||||
std::unique_ptr<MockEnv> env{MockEnv::Create(Env::Default())};
|
||||
options.env = env.get();
|
||||
options.disable_auto_compactions = true;
|
||||
options.compression = kNoCompression;
|
||||
options.enable_blob_files = true;
|
||||
options.blob_file_size = 32; // create one blob per file
|
||||
options.skip_checking_sst_file_sizes_on_db_open = true;
|
||||
|
||||
DestroyAndReopen(options);
|
||||
// Generate 5 files in L0
|
||||
for (int i = 0; i < 5; i++) {
|
||||
for (int j = 0; j < 10; j++) {
|
||||
std::string val = "val_file_" + std::to_string(i);
|
||||
ASSERT_OK(Put(Key(j), val));
|
||||
}
|
||||
ASSERT_OK(Flush());
|
||||
}
|
||||
Close();
|
||||
|
||||
bool is_get_file_size_called = false;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"MockFileSystem::GetFileSize:CheckFileType", [&](void* arg) {
|
||||
std::string* filename = reinterpret_cast<std::string*>(arg);
|
||||
if (filename->find(".blob") != std::string::npos) {
|
||||
is_get_file_size_called = true;
|
||||
}
|
||||
});
|
||||
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
Reopen(options);
|
||||
ASSERT_FALSE(is_get_file_size_called);
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
Destroy(options);
|
||||
}
|
||||
|
||||
TEST_F(DBSSTTest, GetTotalSstFilesSizeVersionsFilesShared) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
|
||||
+171
-26
@@ -321,7 +321,7 @@ TEST_F(DBTest, MixedSlowdownOptions) {
|
||||
// We need the 2nd write to trigger delay. This is because delay is
|
||||
// estimated based on the last write size which is 0 for the first write.
|
||||
ASSERT_OK(dbfull()->Put(wo, "foo2", "bar2"));
|
||||
token.reset();
|
||||
token.reset();
|
||||
|
||||
for (auto& t : threads) {
|
||||
t.join();
|
||||
@@ -379,7 +379,7 @@ TEST_F(DBTest, MixedSlowdownOptionsInQueue) {
|
||||
// We need the 2nd write to trigger delay. This is because delay is
|
||||
// estimated based on the last write size which is 0 for the first write.
|
||||
ASSERT_OK(dbfull()->Put(wo, "foo2", "bar2"));
|
||||
token.reset();
|
||||
token.reset();
|
||||
|
||||
for (auto& t : threads) {
|
||||
t.join();
|
||||
@@ -448,7 +448,7 @@ TEST_F(DBTest, MixedSlowdownOptionsStop) {
|
||||
// We need the 2nd write to trigger delay. This is because delay is
|
||||
// estimated based on the last write size which is 0 for the first write.
|
||||
ASSERT_OK(dbfull()->Put(wo, "foo2", "bar2"));
|
||||
token.reset();
|
||||
token.reset();
|
||||
|
||||
for (auto& t : threads) {
|
||||
t.join();
|
||||
@@ -483,7 +483,6 @@ TEST_F(DBTest, LevelLimitReopen) {
|
||||
}
|
||||
#endif // ROCKSDB_LITE
|
||||
|
||||
|
||||
TEST_F(DBTest, PutSingleDeleteGet) {
|
||||
do {
|
||||
CreateAndReopenWithCF({"pikachu"}, CurrentOptions());
|
||||
@@ -782,7 +781,6 @@ TEST_F(DBTest, GetFromImmutableLayer) {
|
||||
} while (ChangeOptions());
|
||||
}
|
||||
|
||||
|
||||
TEST_F(DBTest, GetLevel0Ordering) {
|
||||
do {
|
||||
CreateAndReopenWithCF({"pikachu"}, CurrentOptions());
|
||||
@@ -1844,6 +1842,8 @@ TEST_F(DBTest, Snapshot) {
|
||||
uint64_t time_snap1 = GetTimeOldestSnapshots();
|
||||
ASSERT_GT(time_snap1, 0U);
|
||||
ASSERT_EQ(GetSequenceOldestSnapshots(), s1->GetSequenceNumber());
|
||||
ASSERT_EQ(GetTimeOldestSnapshots(),
|
||||
static_cast<uint64_t>(s1->GetUnixTime()));
|
||||
ASSERT_OK(Put(0, "foo", "0v2"));
|
||||
ASSERT_OK(Put(1, "foo", "1v2"));
|
||||
|
||||
@@ -1853,6 +1853,8 @@ TEST_F(DBTest, Snapshot) {
|
||||
ASSERT_EQ(2U, GetNumSnapshots());
|
||||
ASSERT_EQ(time_snap1, GetTimeOldestSnapshots());
|
||||
ASSERT_EQ(GetSequenceOldestSnapshots(), s1->GetSequenceNumber());
|
||||
ASSERT_EQ(GetTimeOldestSnapshots(),
|
||||
static_cast<uint64_t>(s1->GetUnixTime()));
|
||||
ASSERT_OK(Put(0, "foo", "0v3"));
|
||||
ASSERT_OK(Put(1, "foo", "1v3"));
|
||||
|
||||
@@ -1861,6 +1863,8 @@ TEST_F(DBTest, Snapshot) {
|
||||
ASSERT_EQ(3U, GetNumSnapshots());
|
||||
ASSERT_EQ(time_snap1, GetTimeOldestSnapshots());
|
||||
ASSERT_EQ(GetSequenceOldestSnapshots(), s1->GetSequenceNumber());
|
||||
ASSERT_EQ(GetTimeOldestSnapshots(),
|
||||
static_cast<uint64_t>(s1->GetUnixTime()));
|
||||
|
||||
ASSERT_OK(Put(0, "foo", "0v4"));
|
||||
ASSERT_OK(Put(1, "foo", "1v4"));
|
||||
@@ -1877,6 +1881,8 @@ TEST_F(DBTest, Snapshot) {
|
||||
ASSERT_EQ(2U, GetNumSnapshots());
|
||||
ASSERT_EQ(time_snap1, GetTimeOldestSnapshots());
|
||||
ASSERT_EQ(GetSequenceOldestSnapshots(), s1->GetSequenceNumber());
|
||||
ASSERT_EQ(GetTimeOldestSnapshots(),
|
||||
static_cast<uint64_t>(s1->GetUnixTime()));
|
||||
ASSERT_EQ("0v1", Get(0, "foo", s1));
|
||||
ASSERT_EQ("1v1", Get(1, "foo", s1));
|
||||
ASSERT_EQ("0v2", Get(0, "foo", s2));
|
||||
@@ -1892,6 +1898,8 @@ TEST_F(DBTest, Snapshot) {
|
||||
ASSERT_EQ(1U, GetNumSnapshots());
|
||||
ASSERT_LT(time_snap1, GetTimeOldestSnapshots());
|
||||
ASSERT_EQ(GetSequenceOldestSnapshots(), s2->GetSequenceNumber());
|
||||
ASSERT_EQ(GetTimeOldestSnapshots(),
|
||||
static_cast<uint64_t>(s2->GetUnixTime()));
|
||||
|
||||
db_->ReleaseSnapshot(s2);
|
||||
ASSERT_EQ(0U, GetNumSnapshots());
|
||||
@@ -2859,6 +2867,18 @@ class ModelDB : public DB {
|
||||
assert(false);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int64_t GetUnixTime() const override {
|
||||
// no need to call this
|
||||
assert(false);
|
||||
return 0;
|
||||
}
|
||||
|
||||
uint64_t GetTimestamp() const override {
|
||||
// no need to call this
|
||||
assert(false);
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
explicit ModelDB(const Options& options) : options_(options) {}
|
||||
@@ -2877,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;
|
||||
@@ -3451,7 +3480,40 @@ TEST_F(DBTest, BlockBasedTablePrefixIndexTest) {
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put("k2", "v2"));
|
||||
|
||||
// Reopen it without prefix extractor, make sure everything still works.
|
||||
// Reopen with different prefix extractor, make sure everything still works.
|
||||
// RocksDB should just fall back to the binary index.
|
||||
options.prefix_extractor.reset(NewFixedPrefixTransform(2));
|
||||
|
||||
Reopen(options);
|
||||
ASSERT_EQ("v1", Get("k1"));
|
||||
ASSERT_EQ("v2", Get("k2"));
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
// Back to original
|
||||
ASSERT_OK(dbfull()->SetOptions({{"prefix_extractor", "fixed:1"}}));
|
||||
ASSERT_EQ("v1", Get("k1"));
|
||||
ASSERT_EQ("v2", Get("k2"));
|
||||
#endif // !ROCKSDB_LITE
|
||||
|
||||
// Same if there's a problem initally loading prefix transform
|
||||
options.prefix_extractor.reset(NewFixedPrefixTransform(1));
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"BlockBasedTable::Open::ForceNullTablePrefixExtractor",
|
||||
[&](void* arg) { *static_cast<bool*>(arg) = true; });
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
Reopen(options);
|
||||
ASSERT_EQ("v1", Get("k1"));
|
||||
ASSERT_EQ("v2", Get("k2"));
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
// Change again
|
||||
ASSERT_OK(dbfull()->SetOptions({{"prefix_extractor", "fixed:2"}}));
|
||||
ASSERT_EQ("v1", Get("k1"));
|
||||
ASSERT_EQ("v2", Get("k2"));
|
||||
#endif // !ROCKSDB_LITE
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
|
||||
// Reopen with no prefix extractor, make sure everything still works.
|
||||
// RocksDB should just fall back to the binary index.
|
||||
table_options.index_type = BlockBasedTableOptions::kBinarySearch;
|
||||
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
|
||||
@@ -3461,6 +3523,7 @@ TEST_F(DBTest, BlockBasedTablePrefixIndexTest) {
|
||||
ASSERT_EQ("v1", Get("k1"));
|
||||
ASSERT_EQ("v2", Get("k2"));
|
||||
}
|
||||
|
||||
TEST_F(DBTest, BlockBasedTablePrefixHashIndexTest) {
|
||||
// create a DB with block prefix index
|
||||
BlockBasedTableOptions table_options;
|
||||
@@ -3751,7 +3814,7 @@ TEST_F(DBTest, FIFOCompactionWithTTLTest) {
|
||||
|
||||
options.compaction_options_fifo.max_table_files_size = 150 << 10; // 150KB
|
||||
options.compaction_options_fifo.allow_compaction = false;
|
||||
options.ttl = 1 * 60 * 60 ; // 1 hour
|
||||
options.ttl = 1 * 60 * 60; // 1 hour
|
||||
options = CurrentOptions(options);
|
||||
DestroyAndReopen(options);
|
||||
|
||||
@@ -3825,7 +3888,7 @@ TEST_F(DBTest, FIFOCompactionWithTTLTest) {
|
||||
options.write_buffer_size = 10 << 10; // 10KB
|
||||
options.compaction_options_fifo.max_table_files_size = 150 << 10; // 150KB
|
||||
options.compaction_options_fifo.allow_compaction = false;
|
||||
options.ttl = 1 * 60 * 60; // 1 hour
|
||||
options.ttl = 1 * 60 * 60; // 1 hour
|
||||
options = CurrentOptions(options);
|
||||
DestroyAndReopen(options);
|
||||
|
||||
@@ -4217,7 +4280,9 @@ TEST_F(DBTest, ConcurrentFlushWAL) {
|
||||
threads.emplace_back([&] {
|
||||
for (size_t i = cnt; i < 2 * cnt; i++) {
|
||||
auto istr = std::to_string(i);
|
||||
WriteBatch batch;
|
||||
WriteBatch batch(0 /* reserved_bytes */, 0 /* max_bytes */,
|
||||
wopt.protection_bytes_per_key,
|
||||
0 /* default_cf_ts_sz */);
|
||||
ASSERT_OK(batch.Put("a" + istr, "b" + istr));
|
||||
ASSERT_OK(
|
||||
dbfull()->WriteImpl(wopt, &batch, nullptr, nullptr, 0, true));
|
||||
@@ -6014,7 +6079,6 @@ TEST_F(DBTest, DISABLED_SuggestCompactRangeTest) {
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(1));
|
||||
}
|
||||
|
||||
|
||||
TEST_F(DBTest, PromoteL0) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
@@ -6195,13 +6259,12 @@ TEST_F(DBTest, CompactFilesShouldTriggerAutoCompaction) {
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
port::Thread manual_compaction_thread([&]() {
|
||||
auto s = db_->CompactFiles(CompactionOptions(),
|
||||
db_->DefaultColumnFamily(), input_files, 0);
|
||||
ASSERT_OK(s);
|
||||
auto s = db_->CompactFiles(CompactionOptions(), db_->DefaultColumnFamily(),
|
||||
input_files, 0);
|
||||
ASSERT_OK(s);
|
||||
});
|
||||
|
||||
TEST_SYNC_POINT(
|
||||
"DBTest::CompactFilesShouldTriggerAutoCompaction:Begin");
|
||||
TEST_SYNC_POINT("DBTest::CompactFilesShouldTriggerAutoCompaction:Begin");
|
||||
// generate enough files to trigger compaction
|
||||
for (int i = 0; i < 20; ++i) {
|
||||
for (int j = 0; j < 2; ++j) {
|
||||
@@ -6211,16 +6274,15 @@ TEST_F(DBTest, CompactFilesShouldTriggerAutoCompaction) {
|
||||
}
|
||||
db_->GetColumnFamilyMetaData(db_->DefaultColumnFamily(), &cf_meta_data);
|
||||
ASSERT_GT(cf_meta_data.levels[0].files.size(),
|
||||
options.level0_file_num_compaction_trigger);
|
||||
TEST_SYNC_POINT(
|
||||
"DBTest::CompactFilesShouldTriggerAutoCompaction:End");
|
||||
options.level0_file_num_compaction_trigger);
|
||||
TEST_SYNC_POINT("DBTest::CompactFilesShouldTriggerAutoCompaction:End");
|
||||
|
||||
manual_compaction_thread.join();
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
|
||||
db_->GetColumnFamilyMetaData(db_->DefaultColumnFamily(), &cf_meta_data);
|
||||
ASSERT_LE(cf_meta_data.levels[0].files.size(),
|
||||
options.level0_file_num_compaction_trigger);
|
||||
options.level0_file_num_compaction_trigger);
|
||||
}
|
||||
#endif // ROCKSDB_LITE
|
||||
|
||||
@@ -6445,8 +6507,9 @@ class WriteStallListener : public EventListener {
|
||||
MutexLock l(&mutex_);
|
||||
return expected == condition_;
|
||||
}
|
||||
|
||||
private:
|
||||
port::Mutex mutex_;
|
||||
port::Mutex mutex_;
|
||||
WriteStallCondition condition_;
|
||||
};
|
||||
|
||||
@@ -6674,7 +6737,8 @@ TEST_F(DBTest, LastWriteBufferDelay) {
|
||||
sleeping_task.WakeUp();
|
||||
sleeping_task.WaitUntilDone();
|
||||
}
|
||||
#endif // !defined(ROCKSDB_LITE) && !defined(ROCKSDB_DISABLE_STALL_NOTIFICATION)
|
||||
#endif // !defined(ROCKSDB_LITE) &&
|
||||
// !defined(ROCKSDB_DISABLE_STALL_NOTIFICATION)
|
||||
|
||||
TEST_F(DBTest, FailWhenCompressionNotSupportedTest) {
|
||||
CompressionType compressions[] = {kZlibCompression, kBZip2Compression,
|
||||
@@ -6759,6 +6823,89 @@ TEST_F(DBTest, PinnableSliceAndRowCache) {
|
||||
1);
|
||||
}
|
||||
|
||||
TEST_F(DBTest, ReusePinnableSlice) {
|
||||
Options options = CurrentOptions();
|
||||
options.statistics = ROCKSDB_NAMESPACE::CreateDBStatistics();
|
||||
options.row_cache = NewLRUCache(8192);
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(Put("foo", "bar"));
|
||||
ASSERT_OK(Flush());
|
||||
|
||||
ASSERT_EQ(Get("foo"), "bar");
|
||||
ASSERT_EQ(
|
||||
reinterpret_cast<LRUCache*>(options.row_cache.get())->TEST_GetLRUSize(),
|
||||
1);
|
||||
|
||||
{
|
||||
PinnableSlice pin_slice;
|
||||
ASSERT_EQ(Get("foo", &pin_slice), Status::OK());
|
||||
ASSERT_EQ(Get("foo", &pin_slice), Status::OK());
|
||||
ASSERT_EQ(pin_slice.ToString(), "bar");
|
||||
|
||||
// Entry is already in cache, lookup will remove the element from lru
|
||||
ASSERT_EQ(
|
||||
reinterpret_cast<LRUCache*>(options.row_cache.get())->TEST_GetLRUSize(),
|
||||
0);
|
||||
}
|
||||
// After PinnableSlice destruction element is added back in LRU
|
||||
ASSERT_EQ(
|
||||
reinterpret_cast<LRUCache*>(options.row_cache.get())->TEST_GetLRUSize(),
|
||||
1);
|
||||
|
||||
{
|
||||
std::vector<Slice> multiget_keys;
|
||||
multiget_keys.push_back("foo");
|
||||
std::vector<PinnableSlice> multiget_values(1);
|
||||
std::vector<Status> statuses({Status::NotFound()});
|
||||
ReadOptions ropt;
|
||||
dbfull()->MultiGet(ropt, dbfull()->DefaultColumnFamily(),
|
||||
multiget_keys.size(), multiget_keys.data(),
|
||||
multiget_values.data(), statuses.data());
|
||||
ASSERT_EQ(Status::OK(), statuses[0]);
|
||||
dbfull()->MultiGet(ropt, dbfull()->DefaultColumnFamily(),
|
||||
multiget_keys.size(), multiget_keys.data(),
|
||||
multiget_values.data(), statuses.data());
|
||||
ASSERT_EQ(Status::OK(), statuses[0]);
|
||||
|
||||
// Entry is already in cache, lookup will remove the element from lru
|
||||
ASSERT_EQ(
|
||||
reinterpret_cast<LRUCache*>(options.row_cache.get())->TEST_GetLRUSize(),
|
||||
0);
|
||||
}
|
||||
// After PinnableSlice destruction element is added back in LRU
|
||||
ASSERT_EQ(
|
||||
reinterpret_cast<LRUCache*>(options.row_cache.get())->TEST_GetLRUSize(),
|
||||
1);
|
||||
|
||||
{
|
||||
std::vector<ColumnFamilyHandle*> multiget_cfs;
|
||||
multiget_cfs.push_back(dbfull()->DefaultColumnFamily());
|
||||
std::vector<Slice> multiget_keys;
|
||||
multiget_keys.push_back("foo");
|
||||
std::vector<PinnableSlice> multiget_values(1);
|
||||
std::vector<Status> statuses({Status::NotFound()});
|
||||
ReadOptions ropt;
|
||||
dbfull()->MultiGet(ropt, multiget_keys.size(), multiget_cfs.data(),
|
||||
multiget_keys.data(), multiget_values.data(),
|
||||
statuses.data());
|
||||
ASSERT_EQ(Status::OK(), statuses[0]);
|
||||
dbfull()->MultiGet(ropt, multiget_keys.size(), multiget_cfs.data(),
|
||||
multiget_keys.data(), multiget_values.data(),
|
||||
statuses.data());
|
||||
ASSERT_EQ(Status::OK(), statuses[0]);
|
||||
|
||||
// Entry is already in cache, lookup will remove the element from lru
|
||||
ASSERT_EQ(
|
||||
reinterpret_cast<LRUCache*>(options.row_cache.get())->TEST_GetLRUSize(),
|
||||
0);
|
||||
}
|
||||
// After PinnableSlice destruction element is added back in LRU
|
||||
ASSERT_EQ(
|
||||
reinterpret_cast<LRUCache*>(options.row_cache.get())->TEST_GetLRUSize(),
|
||||
1);
|
||||
}
|
||||
|
||||
#endif // ROCKSDB_LITE
|
||||
|
||||
TEST_F(DBTest, DeletingOldWalAfterDrop) {
|
||||
@@ -6838,9 +6985,7 @@ TEST_F(DBTest, PauseBackgroundWorkTest) {
|
||||
TEST_F(DBTest, ThreadLocalPtrDeadlock) {
|
||||
std::atomic<int> flushes_done{0};
|
||||
std::atomic<int> threads_destroyed{0};
|
||||
auto done = [&] {
|
||||
return flushes_done.load() > 10;
|
||||
};
|
||||
auto done = [&] { return flushes_done.load() > 10; };
|
||||
|
||||
port::Thread flushing_thread([&] {
|
||||
for (int i = 0; !done(); ++i) {
|
||||
@@ -6853,7 +6998,7 @@ TEST_F(DBTest, ThreadLocalPtrDeadlock) {
|
||||
});
|
||||
|
||||
std::vector<port::Thread> thread_spawning_threads(10);
|
||||
for (auto& t: thread_spawning_threads) {
|
||||
for (auto& t : thread_spawning_threads) {
|
||||
t = port::Thread([&] {
|
||||
while (!done()) {
|
||||
{
|
||||
@@ -6869,7 +7014,7 @@ TEST_F(DBTest, ThreadLocalPtrDeadlock) {
|
||||
});
|
||||
}
|
||||
|
||||
for (auto& t: thread_spawning_threads) {
|
||||
for (auto& t : thread_spawning_threads) {
|
||||
t.join();
|
||||
}
|
||||
flushing_thread.join();
|
||||
|
||||
Loaded 100 of 381 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user