mirror of
https://github.com/facebook/rocksdb.git
synced 2026-11-06 02:52:35 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
832966016d | ||
|
|
33767c4840 | ||
|
|
dfb1aee921 | ||
|
|
82acf110ab | ||
|
|
bbf61996a9 | ||
|
|
766c7fba67 | ||
|
|
c4535c9948 | ||
|
|
56edb7db36 | ||
|
|
e24b67e588 | ||
|
|
76e4237b92 | ||
|
|
31d9b50985 | ||
|
|
1a387d78f8 | ||
|
|
7f338c572d | ||
|
|
206fd92537 | ||
|
|
d4e458b02a | ||
|
|
eb9a80fe1f | ||
|
|
6296ac656b | ||
|
|
59495ff26a | ||
|
|
3acb421f68 | ||
|
|
01cd296b07 | ||
|
|
3c3112d33e | ||
|
|
7b491e4218 | ||
|
|
73782b260d | ||
|
|
13f9b5716d | ||
|
|
8a0c1e7ce5 | ||
|
|
d03498175d | ||
|
|
9b1817df6e | ||
|
|
50386918ff | ||
|
|
50a63bc48a | ||
|
|
01ebe8a5f7 | ||
|
|
2cc5b39560 | ||
|
|
e053ccde99 | ||
|
|
0f91c72adc | ||
|
|
5724348689 | ||
|
|
37b75e1364 | ||
|
|
088b9844d4 | ||
|
|
c206aebd0b | ||
|
|
6ce782beaf | ||
|
|
7dad485278 | ||
|
|
b418ace352 | ||
|
|
1cdc84114f | ||
|
|
849cf1bf68 | ||
|
|
ce2c11d848 | ||
|
|
be04a3b6cd | ||
|
|
832fd644fc | ||
|
|
87c8bb4bef | ||
|
|
bb9a6d4e4b | ||
|
|
7b11d48444 | ||
|
|
06ab0a8b40 | ||
|
|
f291eefb02 | ||
|
|
03fc43976d | ||
|
|
03c4ea26bb | ||
|
|
bd2ad2f9a0 | ||
|
|
f79b3d19a7 | ||
|
|
be09943fb5 | ||
|
|
7a9ecdac3c | ||
|
|
dc7d155438 | ||
|
|
4100eb3053 | ||
|
|
0148c4934d | ||
|
|
3d67d79154 | ||
|
|
fe56cb9aa0 | ||
|
|
6de7081cf3 | ||
|
|
d490bfcdb6 | ||
|
|
c8543296ca | ||
|
|
5a97e6b1d2 | ||
|
|
1ffadbe9fc | ||
|
|
76de3c85cc | ||
|
|
36dec11bc6 | ||
|
|
fe5fbe32cb | ||
|
|
30bc495c03 | ||
|
|
3770d6b74b | ||
|
|
9d5b3dabcf | ||
|
|
b07217da04 | ||
|
|
4cd16d65ae | ||
|
|
5fbcc8c54d | ||
|
|
3a75219e5d | ||
|
|
ccf822492f | ||
|
|
e7525a1fff | ||
|
|
9509003503 | ||
|
|
228f2c5bf5 | ||
|
|
01e88dfeb4 | ||
|
|
8a85946f58 | ||
|
|
6cd8133035 | ||
|
|
7c0838e65e | ||
|
|
e484b81eee | ||
|
|
7818560194 | ||
|
|
72a3fb3424 | ||
|
|
3613d862ba | ||
|
|
c5afbbfe4b | ||
|
|
5532b462c4 | ||
|
|
64e74723f7 | ||
|
|
ce529a4ce1 | ||
|
|
d17be55aab | ||
|
|
23376aa576 | ||
|
|
7670fdd690 | ||
|
|
d9e71fb2c5 | ||
|
|
3f57d84af4 | ||
|
|
418b36a9bc | ||
|
|
9f290a5d15 | ||
|
|
7ad4b38617 | ||
|
|
7fbee01f0c | ||
|
|
d95e376368 | ||
|
|
d140fbfd7d | ||
|
|
79ed4be80f | ||
|
|
78bbdef530 | ||
|
|
4915f89513 | ||
|
|
198e5d8ee9 | ||
|
|
cbe2c6d2d2 | ||
|
|
616f3bd02e | ||
|
|
38bf569ee7 | ||
|
|
6e93d24935 | ||
|
|
06f73d2575 | ||
|
|
9593fd1c82 | ||
|
|
db7606a41a | ||
|
|
7b9e970042 | ||
|
|
b16655a547 | ||
|
|
b0048b673c |
No files matched your search
+71
-7
@@ -72,6 +72,14 @@ commands:
|
||||
path: /tmp/core_dumps
|
||||
when: on_fail
|
||||
|
||||
upgrade-cmake:
|
||||
steps:
|
||||
- run:
|
||||
name: Upgrade cmake
|
||||
command: |
|
||||
sudo apt remove --purge cmake
|
||||
sudo snap install cmake --classic
|
||||
|
||||
install-gflags:
|
||||
steps:
|
||||
- run:
|
||||
@@ -93,6 +101,13 @@ commands:
|
||||
command: |
|
||||
make checkout_folly
|
||||
|
||||
build-folly:
|
||||
steps:
|
||||
- run:
|
||||
name: Build folly and dependencies
|
||||
command: |
|
||||
make build_folly
|
||||
|
||||
build-for-benchmarks:
|
||||
steps:
|
||||
- pre-steps
|
||||
@@ -105,14 +120,23 @@ commands:
|
||||
steps:
|
||||
- run:
|
||||
name: "Test low-variance benchmarks"
|
||||
command: ./tools/benchmark_ci.py --db_dir /tmp/rocksdb-benchmark-datadir --output_dir /tmp/benchmark-results --num_keys 10000000
|
||||
command: ./tools/benchmark_ci.py --db_dir /tmp/rocksdb-benchmark-datadir --output_dir /tmp/benchmark-results --num_keys 50000
|
||||
environment:
|
||||
LD_LIBRARY_PATH: /usr/local/lib
|
||||
# How long to run parts of the test(s)
|
||||
DURATION_RO: 450
|
||||
DURATION_RW: 450
|
||||
DURATION_RO: 400
|
||||
DURATION_RW: 700
|
||||
# Keep threads within physical capacity of server (much lower than default)
|
||||
NUM_THREADS: 1
|
||||
MAX_BACKGROUND_JOBS: 3
|
||||
# Don't run a couple of "optional" initial tests
|
||||
CI_TESTS_ONLY: "true"
|
||||
# Reduce configured size of levels to ensure more levels in the leveled compaction LSM tree
|
||||
WRITE_BUFFER_SIZE_MB: 16
|
||||
TARGET_FILE_SIZE_BASE_MB: 16
|
||||
MAX_BYTES_FOR_LEVEL_BASE_MB: 64
|
||||
# The benchmark host has 32GB memory
|
||||
# The folllowing values are tailored to work with that
|
||||
# The following values are tailored to work with that
|
||||
# Note, tests may not exercise the targeted issues if the memory is increased on new test hosts.
|
||||
|
||||
|
||||
@@ -149,7 +173,7 @@ executors:
|
||||
# to run the CI image environment locally:
|
||||
# $ docker run --cap-add=SYS_PTRACE --security-opt seccomp=unconfined -it zjay437/rocksdb:0.5 bash
|
||||
# option `--cap-add=SYS_PTRACE --security-opt seccomp=unconfined` is used to enable gdb to attach an existing process
|
||||
- image: zjay437/rocksdb:0.5
|
||||
- image: zjay437/rocksdb:0.6
|
||||
|
||||
jobs:
|
||||
build-macos:
|
||||
@@ -346,7 +370,17 @@ jobs:
|
||||
steps:
|
||||
- pre-steps
|
||||
- setup-folly
|
||||
- run: (mkdir build && cd build && cmake -DUSE_FOLLY=1 -DWITH_GFLAGS=1 .. && make V=1 -j20 && ctest -j20)
|
||||
- build-folly
|
||||
- run: (mkdir build && cd build && cmake -DUSE_FOLLY=1 -DWITH_GFLAGS=1 -DROCKSDB_BUILD_SHARED=0 .. && make V=1 -j20 && ctest -j20)
|
||||
- post-steps
|
||||
|
||||
build-linux-cmake-with-folly-lite-no-test:
|
||||
executor: linux-docker
|
||||
resource_class: 2xlarge
|
||||
steps:
|
||||
- pre-steps
|
||||
- setup-folly
|
||||
- run: (mkdir build && cd build && cmake -DUSE_FOLLY_LITE=1 -DWITH_GFLAGS=1 .. && make V=1 -j20)
|
||||
- post-steps
|
||||
|
||||
build-linux-cmake-with-benchmark:
|
||||
@@ -376,9 +410,19 @@ jobs:
|
||||
steps:
|
||||
- pre-steps
|
||||
- setup-folly
|
||||
- build-folly
|
||||
- run: USE_FOLLY=1 CC=gcc-7 CXX=g++-7 V=1 make -j32 check
|
||||
- post-steps
|
||||
|
||||
build-linux-gcc-7-with-folly-lite-no-test:
|
||||
executor: linux-docker
|
||||
resource_class: 2xlarge
|
||||
steps:
|
||||
- pre-steps
|
||||
- setup-folly
|
||||
- run: USE_FOLLY_LITE=1 CC=gcc-7 CXX=g++-7 V=1 make -j32 all
|
||||
- post-steps
|
||||
|
||||
build-linux-gcc-8-no_test_run:
|
||||
executor: linux-docker
|
||||
resource_class: 2xlarge
|
||||
@@ -387,6 +431,19 @@ jobs:
|
||||
- run: CC=gcc-8 CXX=g++-8 V=1 make -j32 all
|
||||
- post-steps
|
||||
|
||||
build-linux-cmake-with-folly-coroutines:
|
||||
executor: linux-docker
|
||||
resource_class: 2xlarge
|
||||
environment:
|
||||
CC: gcc-10
|
||||
CXX: g++-10
|
||||
steps:
|
||||
- pre-steps
|
||||
- setup-folly
|
||||
- build-folly
|
||||
- run: (mkdir build && cd build && cmake -DUSE_COROUTINES=1 -DWITH_GFLAGS=1 -DROCKSDB_BUILD_SHARED=0 .. && make V=1 -j20 && ctest -j20)
|
||||
- post-steps
|
||||
|
||||
build-linux-gcc-10-cxx20-no_test_run:
|
||||
executor: linux-docker
|
||||
resource_class: 2xlarge
|
||||
@@ -418,6 +475,7 @@ jobs:
|
||||
steps:
|
||||
- pre-steps
|
||||
- setup-folly
|
||||
- build-folly
|
||||
- run: CC=clang-13 CXX=clang++-13 USE_CLANG=1 USE_FOLLY=1 COMPILE_WITH_UBSAN=1 COMPILE_WITH_ASAN=1 make -j32 check
|
||||
- post-steps
|
||||
|
||||
@@ -435,7 +493,7 @@ jobs:
|
||||
resource_class: large
|
||||
steps:
|
||||
- pre-steps
|
||||
- run: ulimit -S -n `ulimit -H -n` && make V=1 -j8 CRASH_TEST_EXT_ARGS=--duration=960 blackbox_crash_test_with_atomic_flush
|
||||
- run: ulimit -S -n `ulimit -H -n` && make V=1 -j8 CRASH_TEST_EXT_ARGS='--duration=960 --max_key=2500000' blackbox_crash_test_with_atomic_flush
|
||||
- post-steps
|
||||
|
||||
build-linux-crashtest-tiered-storage-bb:
|
||||
@@ -584,6 +642,7 @@ jobs:
|
||||
- run:
|
||||
name: "Test RocksDBJava"
|
||||
command: make V=1 J=16 -j16 jtest
|
||||
no_output_timeout: 20m
|
||||
- post-steps
|
||||
|
||||
build-macos-java-static:
|
||||
@@ -608,6 +667,7 @@ jobs:
|
||||
- run:
|
||||
name: "Build RocksDBJava x86 and ARM Static Libraries"
|
||||
command: make V=1 J=16 -j16 rocksdbjavastaticosx
|
||||
no_output_timeout: 20m
|
||||
- post-steps
|
||||
|
||||
build-macos-java-static-universal:
|
||||
@@ -632,6 +692,7 @@ jobs:
|
||||
- run:
|
||||
name: "Build RocksDBJava Universal Binary Static Library"
|
||||
command: make V=1 J=16 -j16 rocksdbjavastaticosx_ub
|
||||
no_output_timeout: 20m
|
||||
- post-steps
|
||||
|
||||
build-examples:
|
||||
@@ -765,7 +826,10 @@ workflows:
|
||||
jobs:
|
||||
- build-linux
|
||||
- build-linux-cmake-with-folly
|
||||
- build-linux-cmake-with-folly-lite-no-test
|
||||
- build-linux-gcc-7-with-folly
|
||||
- build-linux-gcc-7-with-folly-lite-no-test
|
||||
- build-linux-cmake-with-folly-coroutines
|
||||
- build-linux-cmake-with-benchmark
|
||||
- build-linux-encrypted_env-no_compression
|
||||
- build-linux-lite
|
||||
|
||||
+72
-18
@@ -74,6 +74,7 @@ option(WITH_WINDOWS_UTF8_FILENAMES "use UTF8 as characterset for opening files,
|
||||
if (WITH_WINDOWS_UTF8_FILENAMES)
|
||||
add_definitions(-DROCKSDB_WINDOWS_UTF8_FILENAMES)
|
||||
endif()
|
||||
option(ROCKSDB_BUILD_SHARED "Build shared versions of the RocksDB libraries" ON)
|
||||
|
||||
if ($ENV{CIRCLECI})
|
||||
message(STATUS "Build for CircieCI env, a few tests may be disabled")
|
||||
@@ -584,10 +585,61 @@ endif()
|
||||
|
||||
include_directories(${PROJECT_SOURCE_DIR})
|
||||
include_directories(${PROJECT_SOURCE_DIR}/include)
|
||||
|
||||
if(USE_COROUTINES)
|
||||
if(USE_FOLLY OR USE_FOLLY_LITE)
|
||||
message(FATAL_ERROR "Please specify exactly one of USE_COROUTINES,"
|
||||
" USE_FOLLY, and USE_FOLLY_LITE")
|
||||
endif()
|
||||
set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fcoroutines -Wno-maybe-uninitialized")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-deprecated")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-redundant-move")
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-invalid-memory-model")
|
||||
add_compile_definitions(USE_COROUTINES)
|
||||
set(USE_FOLLY 1)
|
||||
endif()
|
||||
|
||||
if(USE_FOLLY)
|
||||
include_directories(${PROJECT_SOURCE_DIR}/third-party/folly)
|
||||
add_definitions(-DUSE_FOLLY -DFOLLY_NO_CONFIG)
|
||||
list(APPEND THIRDPARTY_LIBS glog)
|
||||
if(USE_FOLLY_LITE)
|
||||
message(FATAL_ERROR "Please specify one of USE_FOLLY or USE_FOLLY_LITE")
|
||||
endif()
|
||||
if(ROCKSDB_BUILD_SHARED)
|
||||
message(FATAL_ERROR "Cannot build RocksDB shared library with folly")
|
||||
endif()
|
||||
set(ROCKSDB_BUILD_SHARED OFF)
|
||||
set(GFLAGS_SHARED FALSE)
|
||||
find_package(folly)
|
||||
# If cmake could not find the folly-config.cmake file, fall back
|
||||
# to looking in third-party/folly for folly and its dependencies
|
||||
if(NOT FOLLY_LIBRARIES)
|
||||
exec_program(python3 ${PROJECT_SOURCE_DIR}/third-party/folly ARGS
|
||||
build/fbcode_builder/getdeps.py show-inst-dir OUTPUT_VARIABLE
|
||||
FOLLY_INST_PATH)
|
||||
exec_program(ls ARGS -d ${FOLLY_INST_PATH}/../boost* OUTPUT_VARIABLE
|
||||
BOOST_INST_PATH)
|
||||
exec_program(ls ARGS -d ${FOLLY_INST_PATH}/../fmt* OUTPUT_VARIABLE
|
||||
FMT_INST_PATH)
|
||||
exec_program(ls ARGS -d ${FOLLY_INST_PATH}/../gflags* OUTPUT_VARIABLE
|
||||
GFLAGS_INST_PATH)
|
||||
set(Boost_DIR ${BOOST_INST_PATH}/lib/cmake/Boost-1.78.0)
|
||||
if(EXISTS ${FMT_INST_PATH}/lib64)
|
||||
set(fmt_DIR ${FMT_INST_PATH}/lib64/cmake/fmt)
|
||||
else()
|
||||
set(fmt_DIR ${FMT_INST_PATH}/lib/cmake/fmt)
|
||||
endif()
|
||||
set(gflags_DIR ${GFLAGS_INST_PATH}/lib/cmake/gflags)
|
||||
|
||||
exec_program(sed ARGS -i 's/gflags_shared//g'
|
||||
${FOLLY_INST_PATH}/lib/cmake/folly/folly-targets.cmake)
|
||||
|
||||
include(${FOLLY_INST_PATH}/lib/cmake/folly/folly-config.cmake)
|
||||
endif()
|
||||
|
||||
add_compile_definitions(USE_FOLLY FOLLY_NO_CONFIG HAVE_CXX11_ATOMIC)
|
||||
list(APPEND THIRDPARTY_LIBS Folly::folly)
|
||||
set(FOLLY_LIBS Folly::folly)
|
||||
set(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -Wl,--copy-dt-needed-entries")
|
||||
endif()
|
||||
find_package(Threads REQUIRED)
|
||||
|
||||
@@ -605,6 +657,7 @@ set(SOURCES
|
||||
cache/lru_cache.cc
|
||||
cache/sharded_cache.cc
|
||||
db/arena_wrapped_db_iter.cc
|
||||
db/blob/blob_contents.cc
|
||||
db/blob/blob_fetcher.cc
|
||||
db/blob/blob_file_addition.cc
|
||||
db/blob/blob_file_builder.cc
|
||||
@@ -667,7 +720,7 @@ set(SOURCES
|
||||
db/merge_helper.cc
|
||||
db/merge_operator.cc
|
||||
db/output_validator.cc
|
||||
db/periodic_work_scheduler.cc
|
||||
db/periodic_task_scheduler.cc
|
||||
db/range_del_aggregator.cc
|
||||
db/range_tombstone_fragmenter.cc
|
||||
db/repair.cc
|
||||
@@ -827,6 +880,7 @@ set(SOURCES
|
||||
util/slice.cc
|
||||
util/file_checksum_helper.cc
|
||||
util/status.cc
|
||||
util/stderr_logger.cc
|
||||
util/string_util.cc
|
||||
util/thread_local.cc
|
||||
util/threadpool_imp.cc
|
||||
@@ -981,7 +1035,7 @@ else()
|
||||
env/io_posix.cc)
|
||||
endif()
|
||||
|
||||
if(USE_FOLLY)
|
||||
if(USE_FOLLY_LITE)
|
||||
list(APPEND SOURCES
|
||||
third-party/folly/folly/container/detail/F14Table.cpp
|
||||
third-party/folly/folly/detail/Futex.cpp
|
||||
@@ -991,13 +1045,14 @@ if(USE_FOLLY)
|
||||
third-party/folly/folly/synchronization/AtomicNotification.cpp
|
||||
third-party/folly/folly/synchronization/DistributedMutex.cpp
|
||||
third-party/folly/folly/synchronization/ParkingLot.cpp)
|
||||
include_directories(${PROJECT_SOURCE_DIR}/third-party/folly)
|
||||
add_definitions(-DUSE_FOLLY -DFOLLY_NO_CONFIG)
|
||||
list(APPEND THIRDPARTY_LIBS glog)
|
||||
endif()
|
||||
|
||||
set(ROCKSDB_STATIC_LIB rocksdb${ARTIFACT_SUFFIX})
|
||||
set(ROCKSDB_SHARED_LIB rocksdb-shared${ARTIFACT_SUFFIX})
|
||||
|
||||
option(ROCKSDB_BUILD_SHARED "Build shared versions of the RocksDB libraries" ON)
|
||||
|
||||
|
||||
if(WIN32)
|
||||
set(SYSTEM_LIBS ${SYSTEM_LIBS} shlwapi.lib rpcrt4.lib)
|
||||
@@ -1008,7 +1063,6 @@ endif()
|
||||
set(ROCKSDB_PLUGIN_EXTERNS "")
|
||||
set(ROCKSDB_PLUGIN_BUILTINS "")
|
||||
message(STATUS "ROCKSDB PLUGINS TO BUILD ${ROCKSDB_PLUGINS}")
|
||||
list(APPEND PLUGINS ${ROCKSDB_PLUGINS})
|
||||
foreach(PLUGIN IN LISTS PLUGINS)
|
||||
set(PLUGIN_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/plugin/${PLUGIN}/")
|
||||
message("including rocksb plugin ${PLUGIN_ROOT}")
|
||||
@@ -1297,7 +1351,7 @@ if(WITH_TESTS)
|
||||
db/merge_test.cc
|
||||
db/options_file_test.cc
|
||||
db/perf_context_test.cc
|
||||
db/periodic_work_scheduler_test.cc
|
||||
db/periodic_task_scheduler_test.cc
|
||||
db/plain_table_db_test.cc
|
||||
db/seqno_time_test.cc
|
||||
db/prefix_test.cc
|
||||
@@ -1424,7 +1478,7 @@ if(WITH_TESTS)
|
||||
add_custom_target(check COMMAND ${CMAKE_CTEST_COMMAND})
|
||||
set(TESTUTILLIB testutillib${ARTIFACT_SUFFIX})
|
||||
add_library(${TESTUTILLIB} STATIC ${TESTUTIL_SOURCE})
|
||||
target_link_libraries(${TESTUTILLIB} ${ROCKSDB_LIB})
|
||||
target_link_libraries(${TESTUTILLIB} ${ROCKSDB_LIB} ${FOLLY_LIBS})
|
||||
if(MSVC)
|
||||
set_target_properties(${TESTUTILLIB} PROPERTIES COMPILE_FLAGS "/Fd${CMAKE_CFG_INTDIR}/testutillib${ARTIFACT_SUFFIX}.pdb")
|
||||
endif()
|
||||
@@ -1482,32 +1536,32 @@ if(WITH_BENCHMARK_TOOLS)
|
||||
cache/cache_bench.cc
|
||||
cache/cache_bench_tool.cc)
|
||||
target_link_libraries(cache_bench${ARTIFACT_SUFFIX}
|
||||
${ROCKSDB_LIB} ${GFLAGS_LIB})
|
||||
${ROCKSDB_LIB} ${GFLAGS_LIB} ${FOLLY_LIBS})
|
||||
|
||||
add_executable(memtablerep_bench${ARTIFACT_SUFFIX}
|
||||
memtable/memtablerep_bench.cc)
|
||||
target_link_libraries(memtablerep_bench${ARTIFACT_SUFFIX}
|
||||
${ROCKSDB_LIB} ${GFLAGS_LIB})
|
||||
${ROCKSDB_LIB} ${GFLAGS_LIB} ${FOLLY_LIBS})
|
||||
|
||||
add_executable(range_del_aggregator_bench${ARTIFACT_SUFFIX}
|
||||
db/range_del_aggregator_bench.cc)
|
||||
target_link_libraries(range_del_aggregator_bench${ARTIFACT_SUFFIX}
|
||||
${ROCKSDB_LIB} ${GFLAGS_LIB})
|
||||
${ROCKSDB_LIB} ${GFLAGS_LIB} ${FOLLY_LIBS})
|
||||
|
||||
add_executable(table_reader_bench${ARTIFACT_SUFFIX}
|
||||
table/table_reader_bench.cc)
|
||||
target_link_libraries(table_reader_bench${ARTIFACT_SUFFIX}
|
||||
${ROCKSDB_LIB} testharness ${GFLAGS_LIB})
|
||||
${ROCKSDB_LIB} testharness ${GFLAGS_LIB} ${FOLLY_LIBS})
|
||||
|
||||
add_executable(filter_bench${ARTIFACT_SUFFIX}
|
||||
util/filter_bench.cc)
|
||||
target_link_libraries(filter_bench${ARTIFACT_SUFFIX}
|
||||
${ROCKSDB_LIB} ${GFLAGS_LIB})
|
||||
${ROCKSDB_LIB} ${GFLAGS_LIB} ${FOLLY_LIBS})
|
||||
|
||||
add_executable(hash_table_bench${ARTIFACT_SUFFIX}
|
||||
utilities/persistent_cache/hash_table_bench.cc)
|
||||
target_link_libraries(hash_table_bench${ARTIFACT_SUFFIX}
|
||||
${ROCKSDB_LIB} ${GFLAGS_LIB})
|
||||
${ROCKSDB_LIB} ${GFLAGS_LIB} ${FOLLY_LIBS})
|
||||
endif()
|
||||
|
||||
option(WITH_TRACE_TOOLS "build with trace tools" ON)
|
||||
@@ -1515,12 +1569,12 @@ if(WITH_TRACE_TOOLS)
|
||||
add_executable(block_cache_trace_analyzer_tool${ARTIFACT_SUFFIX}
|
||||
tools/block_cache_analyzer/block_cache_trace_analyzer_tool.cc)
|
||||
target_link_libraries(block_cache_trace_analyzer_tool${ARTIFACT_SUFFIX}
|
||||
${ROCKSDB_LIB} ${GFLAGS_LIB})
|
||||
${ROCKSDB_LIB} ${GFLAGS_LIB} ${FOLLY_LIBS})
|
||||
|
||||
add_executable(trace_analyzer${ARTIFACT_SUFFIX}
|
||||
tools/trace_analyzer.cc)
|
||||
target_link_libraries(trace_analyzer${ARTIFACT_SUFFIX}
|
||||
${ROCKSDB_LIB} ${GFLAGS_LIB})
|
||||
${ROCKSDB_LIB} ${GFLAGS_LIB} ${FOLLY_LIBS})
|
||||
|
||||
endif()
|
||||
|
||||
|
||||
+85
-4
@@ -1,5 +1,88 @@
|
||||
# Rocksdb Change Log
|
||||
## Unreleased
|
||||
## 7.7.8 (11/27/2022)
|
||||
### Bug Fixes
|
||||
* Fix failed memtable flush retry bug that could cause wrongly ordered updates, which would surface to writers as `Status::Corruption` in case of `force_consistency_checks=true` (default). It affects use cases that enable both parallel flush (`max_background_flushes > 1` or `max_background_jobs >= 8`) and non-default memtable count (`max_write_buffer_number > 2`).
|
||||
* Tiered Storage: fixed excessive keys written to penultimate level in non-debug builds.
|
||||
* Fixed a regression in iterator where range tombstones after `iterate_upper_bound` is processed.
|
||||
|
||||
## 7.7.7 (11/15/2022)
|
||||
### Bug Fixes
|
||||
* Fixed a regression in scan for async_io. During seek, valid buffers were getting cleared causing a regression.
|
||||
|
||||
## 7.7.6 (11/03/2022)
|
||||
### Bug Fixes
|
||||
* Fix memory corruption error in scans if async_io is enabled. Memory corruption happened if there is IOError while reading the data leading to empty buffer and other buffer already in progress of async read goes again for reading.
|
||||
|
||||
## 7.7.5 (10/28/2022)
|
||||
### Bug Fixes
|
||||
* Fixed an iterator performance regression for delete range users when scanning through a consecutive sequence of range tombstones (#10877).
|
||||
|
||||
## 7.7.4 (10/28/2022)
|
||||
### Bug Fixes
|
||||
* Fixed a case of calling malloc_usable_size on result of operator new[].
|
||||
|
||||
## 7.7.3 (10/11/2022)
|
||||
### Bug Fixes
|
||||
* Fixed a memory safety bug in experimental HyperClockCache (#10768)
|
||||
|
||||
## 7.7.2 (10/05/2022)
|
||||
### Bug Fixes
|
||||
* Fixed a bug in iterator refresh that was not freeing up SuperVersion, which could cause excessive resource pinniung (#10770).
|
||||
* Fixed a bug where RocksDB could be doing compaction endlessly when allow_ingest_behind is true and the bottommost level is not filled (#10767).
|
||||
|
||||
### Behavior Changes
|
||||
* Sanitize min_write_buffer_number_to_merge to 1 if atomic flush is enabled to prevent unexpected data loss when WAL is disabled in a multi-column-family setting (#10773).
|
||||
|
||||
## 7.7.1 (09/26/2022)
|
||||
### Bug Fixes
|
||||
* Fixed an optimistic transaction validation bug caused by DBImpl::GetLatestSequenceForKey() returning non-latest seq for merge (#10724).
|
||||
* Fixed a bug in iterator refresh which could segfault for DeleteRange users (#10739).
|
||||
|
||||
## 7.7.0 (09/18/2022)
|
||||
### Bug Fixes
|
||||
* Fixed a hang when an operation such as `GetLiveFiles` or `CreateNewBackup` is asked to trigger and wait for memtable flush on a read-only DB. Such indirect requests for memtable flush are now ignored on a read-only DB.
|
||||
* Fixed bug where `FlushWAL(true /* sync */)` (used by `GetLiveFilesStorageInfo()`, which is used by checkpoint and backup) could cause parallel writes at the tail of a WAL file to never be synced.
|
||||
* Fix periodic_task unable to re-register the same task type, which may cause `SetOptions()` fail to update periodical_task time like: `stats_dump_period_sec`, `stats_persist_period_sec`.
|
||||
* Fixed a bug in the rocksdb.prefetched.bytes.discarded stat. It was counting the prefetch buffer size, rather than the actual number of bytes discarded from the buffer.
|
||||
* Fix bug where the directory containing CURRENT can left unsynced after CURRENT is updated to point to the latest MANIFEST, which leads to risk of unsync data loss of CURRENT.
|
||||
* Update rocksdb.multiget.io.batch.size stat in non-async MultiGet as well.
|
||||
* Fix a bug in key range overlap checking with concurrent compactions when user-defined timestamp is enabled. User-defined timestamps should be EXCLUDED when checking if two ranges overlap.
|
||||
* Fixed a bug where the blob cache prepopulating logic did not consider the secondary cache (see #10603).
|
||||
* Fixed the rocksdb.num.sst.read.per.level, rocksdb.num.index.and.filter.blocks.read.per.level and rocksdb.num.level.read.per.multiget stats in the MultiGet coroutines
|
||||
* Fix a bug in io_uring_prep_cancel in AbortIO API for posix which expects sqe->addr to match with read request submitted and wrong paramter was being passed.
|
||||
* Fixed a regression in iterator performance when the entire DB is a single memtable introduced in #10449. The fix is in #10705 and #10716.
|
||||
* Fix a bug in io_uring_prep_cancel in AbortIO API for posix which expects sqe->addr to match with read request submitted and wrong paramter was being passed.
|
||||
* Fixed a regression in iterator performance when the entire DB is a single memtable introduced in #10449. The fix is in #10705 and #10716.
|
||||
|
||||
### Public API changes
|
||||
* Add `rocksdb_column_family_handle_get_id`, `rocksdb_column_family_handle_get_name` to get name, id of column family in C API
|
||||
* Add a new stat rocksdb.async.prefetch.abort.micros to measure time spent waiting for async prefetch reads to abort
|
||||
|
||||
### Java API Changes
|
||||
* Add CompactionPriority.RoundRobin.
|
||||
* Revert to using the default metadata charge policy when creating an LRU cache via the Java API.
|
||||
|
||||
### Behavior Change
|
||||
* DBOptions::verify_sst_unique_id_in_manifest is now an on-by-default feature that verifies SST file identity whenever they are opened by a DB, rather than only at DB::Open time.
|
||||
* Right now, when the option migration tool (OptionChangeMigration()) migrates to FIFO compaction, it compacts all the data into one single SST file and move to L0. This might create a problem for some users: the giant file may be soon deleted to satisfy max_table_files_size, and might cayse the DB to be almost empty. We change the behavior so that the files are cut to be smaller, but these files might not follow the data insertion order. With the change, after the migration, migrated data might not be dropped by insertion order by FIFO compaction.
|
||||
* When a block is firstly found from `CompressedSecondaryCache`, we just insert a dummy block into the primary cache and don’t erase the block from `CompressedSecondaryCache`. A standalone handle is returned to the caller. Only if the block is found again from `CompressedSecondaryCache` before the dummy block is evicted, we erase the block from `CompressedSecondaryCache` and insert it into the primary cache.
|
||||
* When a block is firstly evicted from the primary cache to `CompressedSecondaryCache`, we just insert a dummy block in `CompressedSecondaryCache`. Only if it is evicted again before the dummy block is evicted from the cache, it is treated as a hot block and is inserted into `CompressedSecondaryCache`.
|
||||
* Improved the estimation of memory used by cached blobs by taking into account the size of the object owning the blob value and also the allocator overhead if `malloc_usable_size` is available (see #10583).
|
||||
* Blob values now have their own category in the cache occupancy statistics, as opposed to being lumped into the "Misc" bucket (see #10601).
|
||||
* Change the optimize_multiget_for_io experimental ReadOptions flag to default on.
|
||||
|
||||
### New Features
|
||||
* RocksDB does internal auto prefetching if it notices 2 sequential reads if readahead_size is not specified. New option `num_file_reads_for_auto_readahead` is added in BlockBasedTableOptions which indicates after how many sequential reads internal auto prefetching should be start (default is 2).
|
||||
* Added new perf context counters `block_cache_standalone_handle_count`, `block_cache_real_handle_count`,`compressed_sec_cache_insert_real_count`, `compressed_sec_cache_insert_dummy_count`, `compressed_sec_cache_uncompressed_bytes`, and `compressed_sec_cache_compressed_bytes`.
|
||||
* Memory for blobs which are to be inserted into the blob cache is now allocated using the cache's allocator (see #10628 and #10647).
|
||||
* HyperClockCache is an experimental, lock-free Cache alternative for block cache that offers much improved CPU efficiency under high parallel load or high contention, with some caveats. As much as 4.5x higher ops/sec vs. LRUCache has been seen in db_bench under high parallel load.
|
||||
* `CompressedSecondaryCacheOptions::enable_custom_split_merge` is added for enabling the custom split and merge feature, which split the compressed value into chunks so that they may better fit jemalloc bins.
|
||||
|
||||
### Performance Improvements
|
||||
* Iterator performance is improved for `DeleteRange()` users. Internally, iterator will skip to the end of a range tombstone when possible, instead of looping through each key and check individually if a key is range deleted.
|
||||
* Eliminated some allocations and copies in the blob read path. Also, `PinnableSlice` now only points to the blob value and pins the backing resource (cache entry or buffer) in all cases, instead of containing a copy of the blob value. See #10625 and #10647.
|
||||
* In case of scans with async_io enabled, few optimizations have been added to issue more asynchronous requests in parallel in order to avoid synchronous prefetching.
|
||||
* `DeleteRange()` users should see improvement in get/iterator performance from mutable memtable (see #10547).
|
||||
|
||||
## 7.6.0 (08/19/2022)
|
||||
### New Features
|
||||
@@ -41,15 +124,13 @@
|
||||
* If an error is hit when writing to a file (append, sync, etc), RocksDB is more strict with not issuing more operations to it, except closing the file, with exceptions of some WAL file operations in error recovery path.
|
||||
* A `WriteBufferManager` constructed with `allow_stall == false` will no longer trigger write stall implicitly by thrashing until memtable count limit is reached. Instead, a column family can continue accumulating writes while that CF is flushing, which means memory may increase. Users who prefer stalling writes must now explicitly set `allow_stall == true`.
|
||||
* Add `CompressedSecondaryCache` into the stress tests.
|
||||
* Block cache keys have changed, which will cause any persistent caches to miss between versions.
|
||||
|
||||
### Performance Improvements
|
||||
* Instead of constructing `FragmentedRangeTombstoneList` during every read operation, it is now constructed once and stored in immutable memtables. This improves speed of querying range tombstones from immutable memtables.
|
||||
* When using iterators with the integrated BlobDB implementation, blob cache handles are now released immediately when the iterator's position changes.
|
||||
* MultiGet can now do more IO in parallel by reading data blocks from SST files in multiple levels, if the optimize_multiget_for_io ReadOption flag is set.
|
||||
|
||||
## Behavior Change
|
||||
* Block cache keys have changed, which will cause any persistent caches to miss between versions.
|
||||
|
||||
## 7.5.0 (07/15/2022)
|
||||
### New Features
|
||||
* Mempurge option flag `experimental_mempurge_threshold` is now a ColumnFamilyOptions and can now be dynamically configured using `SetOptions()`.
|
||||
|
||||
@@ -6,7 +6,9 @@ This is the list of all known third-party language bindings for RocksDB. If some
|
||||
* http://pyrocksdb.readthedocs.org/en/latest/ (unmaintained)
|
||||
* Perl - https://metacpan.org/pod/RocksDB
|
||||
* Node.js - https://npmjs.org/package/rocksdb
|
||||
* Go - https://github.com/tecbot/gorocksdb
|
||||
* Go
|
||||
* https://github.com/linxGnu/grocksdb
|
||||
* https://github.com/tecbot/gorocksdb (unmaintained)
|
||||
* Ruby - http://rubygems.org/gems/rocksdb-ruby
|
||||
* Haskell - https://hackage.haskell.org/package/rocksdb-haskell
|
||||
* PHP - https://github.com/Photonios/rocksdb-php
|
||||
|
||||
@@ -139,9 +139,14 @@ endif
|
||||
GIT_COMMAND ?= git
|
||||
ifeq ($(USE_COROUTINES), 1)
|
||||
USE_FOLLY = 1
|
||||
OPT += -DUSE_COROUTINES
|
||||
# glog/logging.h requires HAVE_CXX11_ATOMIC
|
||||
OPT += -DUSE_COROUTINES -DHAVE_CXX11_ATOMIC
|
||||
ROCKSDB_CXX_STANDARD = c++2a
|
||||
USE_RTTI = 1
|
||||
ifneq ($(USE_CLANG), 1)
|
||||
ROCKSDB_CXX_STANDARD = c++20
|
||||
PLATFORM_CXXFLAGS += -fcoroutines
|
||||
endif
|
||||
endif
|
||||
|
||||
# if we're compiling for release, compile without debug code (-DNDEBUG)
|
||||
@@ -185,6 +190,16 @@ ifeq ($(USE_LTO), 1)
|
||||
LDFLAGS += -flto -fuse-linker-plugin
|
||||
endif
|
||||
|
||||
# `COERCE_CONTEXT_SWITCH=1` will inject spurious wakeup and
|
||||
# random length of sleep or context switch at critical
|
||||
# points (e.g, before acquring db mutex) in RocksDB.
|
||||
# In this way, it coerces as many excution orders as possible in the hope of
|
||||
# exposing the problematic excution order
|
||||
COERCE_CONTEXT_SWITCH ?= 0
|
||||
ifeq ($(COERCE_CONTEXT_SWITCH), 1)
|
||||
OPT += -DCOERCE_CONTEXT_SWITCH
|
||||
endif
|
||||
|
||||
#-----------------------------------------------
|
||||
include src.mk
|
||||
|
||||
@@ -235,6 +250,7 @@ dummy := $(shell (export ROCKSDB_ROOT="$(CURDIR)"; \
|
||||
export USE_CLANG="$(USE_CLANG)"; \
|
||||
export LIB_MODE="$(LIB_MODE)"; \
|
||||
export ROCKSDB_CXX_STANDARD="$(ROCKSDB_CXX_STANDARD)"; \
|
||||
export USE_FOLLY="$(USE_FOLLY)"; \
|
||||
"$(CURDIR)/build_tools/build_detect_platform" "$(CURDIR)/make_config.mk"))
|
||||
# this file is generated by the previous line to set build flags and sources
|
||||
include make_config.mk
|
||||
@@ -415,6 +431,10 @@ ifndef DISABLE_JEMALLOC
|
||||
PLATFORM_CXXFLAGS += -DUSE_JEMALLOC
|
||||
PLATFORM_CCFLAGS += -DUSE_JEMALLOC
|
||||
endif
|
||||
ifeq ($(USE_FOLLY_LITE),1)
|
||||
PLATFORM_CXXFLAGS += -DUSE_JEMALLOC
|
||||
PLATFORM_CCFLAGS += -DUSE_JEMALLOC
|
||||
endif
|
||||
endif
|
||||
ifdef WITH_JEMALLOC_FLAG
|
||||
PLATFORM_LDFLAGS += -ljemalloc
|
||||
@@ -448,10 +468,57 @@ endif
|
||||
|
||||
# This provides a Makefile simulation of a Meta-internal folly integration.
|
||||
# It is not validated for general use.
|
||||
#
|
||||
# USE_FOLLY links the build targets with libfolly.a. The latter could be
|
||||
# built using 'make build_folly', or built externally and specified in
|
||||
# the CXXFLAGS and EXTRA_LDFLAGS env variables. The build_detect_platform
|
||||
# script tries to detect if an external folly dependency has been specified.
|
||||
# If not, it exports FOLLY_PATH to the path of the installed Folly and
|
||||
# dependency libraries.
|
||||
#
|
||||
# USE_FOLLY_LITE cherry picks source files from Folly to include in the
|
||||
# RocksDB library. Its faster and has fewer dependencies on 3rd party
|
||||
# libraries, but with limited functionality. For example, coroutine
|
||||
# functionality is not available.
|
||||
ifeq ($(USE_FOLLY),1)
|
||||
ifeq (,$(FOLLY_DIR))
|
||||
FOLLY_DIR = ./third-party/folly
|
||||
ifeq ($(USE_FOLLY_LITE),1)
|
||||
$(error Please specify only one of USE_FOLLY and USE_FOLLY_LITE)
|
||||
endif
|
||||
ifneq ($(strip $(FOLLY_PATH)),)
|
||||
BOOST_PATH = $(shell (ls -d $(FOLLY_PATH)/../boost*))
|
||||
DBL_CONV_PATH = $(shell (ls -d $(FOLLY_PATH)/../double-conversion*))
|
||||
GFLAGS_PATH = $(shell (ls -d $(FOLLY_PATH)/../gflags*))
|
||||
GLOG_PATH = $(shell (ls -d $(FOLLY_PATH)/../glog*))
|
||||
LIBEVENT_PATH = $(shell (ls -d $(FOLLY_PATH)/../libevent*))
|
||||
XZ_PATH = $(shell (ls -d $(FOLLY_PATH)/../xz*))
|
||||
LIBSODIUM_PATH = $(shell (ls -d $(FOLLY_PATH)/../libsodium*))
|
||||
FMT_PATH = $(shell (ls -d $(FOLLY_PATH)/../fmt*))
|
||||
|
||||
# For some reason, glog and fmt libraries are under either lib or lib64
|
||||
GLOG_LIB_PATH = $(shell (ls -d $(GLOG_PATH)/lib*))
|
||||
FMT_LIB_PATH = $(shell (ls -d $(FMT_PATH)/lib*))
|
||||
|
||||
# AIX: pre-defined system headers are surrounded by an extern "C" block
|
||||
ifeq ($(PLATFORM), OS_AIX)
|
||||
PLATFORM_CCFLAGS += -I$(BOOST_PATH)/include -I$(DBL_CONV_PATH)/include -I$(GLOG_PATH)/include -I$(LIBEVENT_PATH)/include -I$(XZ_PATH)/include -I$(LIBSODIUM_PATH)/include -I$(FOLLY_PATH)/include -I$(FMT_PATH)/include
|
||||
PLATFORM_CXXFLAGS += -I$(BOOST_PATH)/include -I$(DBL_CONV_PATH)/include -I$(GLOG_PATH)/include -I$(LIBEVENT_PATH)/include -I$(XZ_PATH)/include -I$(LIBSODIUM_PATH)/include -I$(FOLLY_PATH)/include -I$(FMT_PATH)/include
|
||||
else
|
||||
PLATFORM_CCFLAGS += -isystem $(BOOST_PATH)/include -isystem $(DBL_CONV_PATH)/include -isystem $(GLOG_PATH)/include -isystem $(LIBEVENT_PATH)/include -isystem $(XZ_PATH)/include -isystem $(LIBSODIUM_PATH)/include -isystem $(FOLLY_PATH)/include -isystem $(FMT_PATH)/include
|
||||
PLATFORM_CXXFLAGS += -isystem $(BOOST_PATH)/include -isystem $(DBL_CONV_PATH)/include -isystem $(GLOG_PATH)/include -isystem $(LIBEVENT_PATH)/include -isystem $(XZ_PATH)/include -isystem $(LIBSODIUM_PATH)/include -isystem $(FOLLY_PATH)/include -isystem $(FMT_PATH)/include
|
||||
endif
|
||||
|
||||
# Add -ldl at the end as gcc resolves a symbol in a library by searching only in libraries specified later
|
||||
# in the command line
|
||||
PLATFORM_LDFLAGS += $(BOOST_PATH)/lib/libboost_context.a $(BOOST_PATH)/lib/libboost_filesystem.a $(BOOST_PATH)/lib/libboost_atomic.a $(BOOST_PATH)/lib/libboost_program_options.a $(BOOST_PATH)/lib/libboost_regex.a $(BOOST_PATH)/lib/libboost_system.a $(BOOST_PATH)/lib/libboost_thread.a $(DBL_CONV_PATH)/lib/libdouble-conversion.a $(FOLLY_PATH)/lib/libfolly.a $(FMT_LIB_PATH)/libfmt.a $(GLOG_LIB_PATH)/libglog.so $(GFLAGS_PATH)/lib/libgflags.so.2.2 -ldl
|
||||
PLATFORM_LDFLAGS += -Wl,-rpath=$(GFLAGS_PATH)/lib -Wl,-rpath=$(GLOG_LIB_PATH) -Wl,-rpath=$(LIBEVENT_PATH)/lib -Wl,-rpath=$(LIBSODIUM_PATH)/lib -Wl,-rpath=$(LIBEVENT_PATH)/lib
|
||||
endif
|
||||
PLATFORM_CCFLAGS += -DUSE_FOLLY -DFOLLY_NO_CONFIG
|
||||
PLATFORM_CXXFLAGS += -DUSE_FOLLY -DFOLLY_NO_CONFIG
|
||||
endif
|
||||
|
||||
ifeq ($(USE_FOLLY_LITE),1)
|
||||
# Path to the Folly source code and include files
|
||||
FOLLY_DIR = ./third-party/folly
|
||||
# AIX: pre-defined system headers are surrounded by an extern "C" block
|
||||
ifeq ($(PLATFORM), OS_AIX)
|
||||
PLATFORM_CCFLAGS += -I$(FOLLY_DIR)
|
||||
@@ -550,7 +617,7 @@ LIB_OBJECTS += $(patsubst %.c, $(OBJ_DIR)/%.o, $(LIB_SOURCES_C))
|
||||
LIB_OBJECTS += $(patsubst %.S, $(OBJ_DIR)/%.o, $(LIB_SOURCES_ASM))
|
||||
endif
|
||||
|
||||
ifeq ($(USE_FOLLY),1)
|
||||
ifeq ($(USE_FOLLY_LITE),1)
|
||||
LIB_OBJECTS += $(patsubst %.cpp, $(OBJ_DIR)/%.o, $(FOLLY_SOURCES))
|
||||
endif
|
||||
|
||||
@@ -1882,7 +1949,7 @@ blob_garbage_meter_test: $(OBJ_DIR)/db/blob/blob_garbage_meter_test.o $(TEST_LIB
|
||||
timer_test: $(OBJ_DIR)/util/timer_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
periodic_work_scheduler_test: $(OBJ_DIR)/db/periodic_work_scheduler_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
periodic_task_scheduler_test: $(OBJ_DIR)/db/periodic_task_scheduler_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
$(AM_LINK)
|
||||
|
||||
testutil_test: $(OBJ_DIR)/test_util/testutil_test.o $(TEST_LIBRARY) $(LIBRARY)
|
||||
@@ -2371,13 +2438,26 @@ checkout_folly:
|
||||
@# need to worry about folly breaking our integration. Update periodically
|
||||
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
|
||||
@# NOTE: this hack is only needed if building using USE_FOLLY_LITE
|
||||
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_folly:
|
||||
FOLLY_INST_PATH=`cd third-party/folly; $(PYTHON) build/fbcode_builder/getdeps.py show-inst-dir`; \
|
||||
if [ "$$FOLLY_INST_PATH" ]; then \
|
||||
rm -rf $${FOLLY_INST_PATH}/../../*; \
|
||||
else \
|
||||
echo "Please run checkout_folly first"; \
|
||||
false; \
|
||||
fi
|
||||
# Restore the original version of Invoke.h with boost dependency
|
||||
cd third-party/folly && ${GIT_COMMAND} checkout folly/functional/Invoke.h
|
||||
cd third-party/folly && MAYBE_AVX2=`echo $(CXXFLAGS) | grep -o -- -DHAVE_AVX2 | sed 's/-DHAVE_AVX2/-mavx2/g' || true` && \
|
||||
CXXFLAGS=" $$MAYBE_AVX2 -DHAVE_CXX11_ATOMIC " $(PYTHON) build/fbcode_builder/getdeps.py build --no-tests
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Build size testing
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -2462,7 +2542,7 @@ endif
|
||||
ifneq ($(SKIP_DEPENDS), 1)
|
||||
DEPFILES = $(patsubst %.cc, $(OBJ_DIR)/%.cc.d, $(ALL_SOURCES))
|
||||
DEPFILES+ = $(patsubst %.c, $(OBJ_DIR)/%.c.d, $(LIB_SOURCES_C) $(TEST_MAIN_SOURCES_C))
|
||||
ifeq ($(USE_FOLLY),1)
|
||||
ifeq ($(USE_FOLLY_LITE),1)
|
||||
DEPFILES +=$(patsubst %.cpp, $(OBJ_DIR)/%.cpp.d, $(FOLLY_SOURCES))
|
||||
endif
|
||||
endif
|
||||
|
||||
@@ -20,6 +20,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"cache/lru_cache.cc",
|
||||
"cache/sharded_cache.cc",
|
||||
"db/arena_wrapped_db_iter.cc",
|
||||
"db/blob/blob_contents.cc",
|
||||
"db/blob/blob_fetcher.cc",
|
||||
"db/blob/blob_file_addition.cc",
|
||||
"db/blob/blob_file_builder.cc",
|
||||
@@ -82,7 +83,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"db/merge_helper.cc",
|
||||
"db/merge_operator.cc",
|
||||
"db/output_validator.cc",
|
||||
"db/periodic_work_scheduler.cc",
|
||||
"db/periodic_task_scheduler.cc",
|
||||
"db/range_del_aggregator.cc",
|
||||
"db/range_tombstone_fragmenter.cc",
|
||||
"db/repair.cc",
|
||||
@@ -252,6 +253,7 @@ cpp_library_wrapper(name="rocksdb_lib", srcs=[
|
||||
"util/ribbon_config.cc",
|
||||
"util/slice.cc",
|
||||
"util/status.cc",
|
||||
"util/stderr_logger.cc",
|
||||
"util/string_util.cc",
|
||||
"util/thread_local.cc",
|
||||
"util/threadpool_imp.cc",
|
||||
@@ -357,6 +359,7 @@ cpp_library_wrapper(name="rocksdb_whole_archive_lib", srcs=[
|
||||
"cache/lru_cache.cc",
|
||||
"cache/sharded_cache.cc",
|
||||
"db/arena_wrapped_db_iter.cc",
|
||||
"db/blob/blob_contents.cc",
|
||||
"db/blob/blob_fetcher.cc",
|
||||
"db/blob/blob_file_addition.cc",
|
||||
"db/blob/blob_file_builder.cc",
|
||||
@@ -419,7 +422,7 @@ cpp_library_wrapper(name="rocksdb_whole_archive_lib", srcs=[
|
||||
"db/merge_helper.cc",
|
||||
"db/merge_operator.cc",
|
||||
"db/output_validator.cc",
|
||||
"db/periodic_work_scheduler.cc",
|
||||
"db/periodic_task_scheduler.cc",
|
||||
"db/range_del_aggregator.cc",
|
||||
"db/range_tombstone_fragmenter.cc",
|
||||
"db/repair.cc",
|
||||
@@ -589,6 +592,7 @@ cpp_library_wrapper(name="rocksdb_whole_archive_lib", srcs=[
|
||||
"util/ribbon_config.cc",
|
||||
"util/slice.cc",
|
||||
"util/status.cc",
|
||||
"util/stderr_logger.cc",
|
||||
"util/string_util.cc",
|
||||
"util/thread_local.cc",
|
||||
"util/threadpool_imp.cc",
|
||||
@@ -5598,8 +5602,8 @@ cpp_unittest_wrapper(name="perf_context_test",
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
cpp_unittest_wrapper(name="periodic_work_scheduler_test",
|
||||
srcs=["db/periodic_work_scheduler_test.cc"],
|
||||
cpp_unittest_wrapper(name="periodic_task_scheduler_test",
|
||||
srcs=["db/periodic_task_scheduler_test.cc"],
|
||||
deps=[":rocksdb_test_lib"],
|
||||
extra_compiler_flags=[])
|
||||
|
||||
|
||||
@@ -277,6 +277,7 @@ if [ "$CROSS_COMPILE" = "true" -o "$FBCODE_BUILD" = "true" ]; then
|
||||
if [ "$FBCODE_BUILD" = "true" ]; then
|
||||
# Enable backtrace on fbcode since the necessary libraries are present
|
||||
COMMON_FLAGS="$COMMON_FLAGS -DROCKSDB_BACKTRACE"
|
||||
FOLLY_DIR="third-party/folly"
|
||||
fi
|
||||
true
|
||||
else
|
||||
@@ -601,6 +602,17 @@ EOF
|
||||
PLATFORM_LDFLAGS="$PLATFORM_LDFLAGS -lbenchmark"
|
||||
fi
|
||||
fi
|
||||
if test $USE_FOLLY; then
|
||||
# Test whether libfolly library is installed
|
||||
$CXX $PLATFORM_CXXFLAGS $COMMON_FLAGS -x c++ - -o /dev/null 2>/dev/null <<EOF
|
||||
#include <folly/synchronization/DistributedMutex.h>
|
||||
int main() {}
|
||||
EOF
|
||||
if [ "$?" != 0 ]; then
|
||||
FOLLY_DIR="./third-party/folly"
|
||||
fi
|
||||
fi
|
||||
|
||||
fi
|
||||
|
||||
# TODO(tec): Fix -Wshorten-64-to-32 errors on FreeBSD and enable the warning.
|
||||
@@ -827,6 +839,13 @@ fi
|
||||
|
||||
rm -f test.o test_dl.o
|
||||
|
||||
# Get the path for the folly installation dir
|
||||
if [ "$USE_FOLLY" ]; then
|
||||
if [ "$FOLLY_DIR" ]; then
|
||||
FOLLY_PATH=`cd $FOLLY_DIR && $PYTHON build/fbcode_builder/getdeps.py show-inst-dir folly`
|
||||
fi
|
||||
fi
|
||||
|
||||
PLATFORM_CCFLAGS="$PLATFORM_CCFLAGS $COMMON_FLAGS"
|
||||
PLATFORM_CXXFLAGS="$PLATFORM_CXXFLAGS $COMMON_FLAGS"
|
||||
|
||||
@@ -866,6 +885,8 @@ echo "CLANG_ANALYZER=$CLANG_ANALYZER" >> "$OUTPUT"
|
||||
echo "PROFILING_FLAGS=$PROFILING_FLAGS" >> "$OUTPUT"
|
||||
echo "FIND=$FIND" >> "$OUTPUT"
|
||||
echo "WATCH=$WATCH" >> "$OUTPUT"
|
||||
echo "FOLLY_PATH=$FOLLY_PATH" >> "$OUTPUT"
|
||||
|
||||
# This will enable some related identifiers for the preprocessor
|
||||
if test -n "$JEMALLOC"; then
|
||||
echo "JEMALLOC=1" >> "$OUTPUT"
|
||||
|
||||
@@ -19,8 +19,4 @@ BINUTILS_BASE=/mnt/gvfs/third-party2/binutils/08634589372fa5f237bfd374e8c644a836
|
||||
VALGRIND_BASE=/mnt/gvfs/third-party2/valgrind/6ae525939ad02e5e676855082fbbc7828dbafeac/3.15.0/platform009/7f3b187
|
||||
LUA_BASE=/mnt/gvfs/third-party2/lua/162efd9561a3d21f6869f4814011e9cf1b3ff4dc/5.3.4/platform009/a6271c4
|
||||
BENCHMARK_BASE=/mnt/gvfs/third-party2/benchmark/30bf49ad6414325e17f3425b0edcb64239427ae3/1.6.1/platform009/7f3b187
|
||||
BOOST_BASE=/mnt/gvfs/third-party2/boost/201b7d74941e54b436dfa364a063aa6d2cd7de4c/1.69.0/platform009/8a7ffdf
|
||||
GLOG_BASE=/mnt/gvfs/third-party2/glog/32d751bd5673375b438158717ab6a57c1cc57e3d/0.3.2_fb/platform009/10a364d/
|
||||
FMT_BASE=/mnt/gvfs/third-party2/fmt/ce0c25f67165f4d2c22a29b8ef50f5600d7873ca/6.1.1/platform009/7f3b187/
|
||||
DBL_CONV_BASE=/mnt/gvfs/third-party2/double_conversion/109b3d9696d71f1048678cd7da1e22505470543d/20141126/platform009/7f3b187/
|
||||
LIBEVENT_BASE=/mnt/gvfs/third-party2/libevent/4a4d3a79a76c2439b6bd471bf3586b3481dde75e/1.4.14b_hphp/platform009/7f3b187/
|
||||
GLOG_BASE=/mnt/gvfs/third-party2/glog/32d751bd5673375b438158717ab6a57c1cc57e3d/0.3.2_fb/platform009/10a364d
|
||||
@@ -68,20 +68,9 @@ CFLAGS+=" -DGFLAGS=gflags"
|
||||
BENCHMARK_INCLUDE=" -I $BENCHMARK_BASE/include/"
|
||||
BENCHMARK_LIBS=" $BENCHMARK_BASE/lib/libbenchmark${MAYBE_PIC}.a"
|
||||
|
||||
BOOST_INCLUDE=" -I $BOOST_BASE/include/"
|
||||
|
||||
GLOG_INCLUDE=" -I $GLOG_BASE/include/"
|
||||
GLOG_LIBS=" $GLOG_BASE/lib/libglog${MAYBE_PIC}.a"
|
||||
|
||||
FMT_INCLUDE=" -I $FMT_BASE/include/"
|
||||
FMT_LIBS=" $FMT_BASE/lib/libfmt${MAYBE_PIC}.a"
|
||||
|
||||
DBL_CONV_INCLUDE=" -I $DBL_CONV_BASE/include/"
|
||||
DBL_CONV_LIBS=" $DBL_CONV_BASE/lib/libdouble-conversion${MAYBE_PIC}.a"
|
||||
|
||||
LIBEVENT_INCLUDE=" -I $LIBEVENT_BASE/include/"
|
||||
LIBEVENT_LIBS=" $LIBEVENT_BASE/lib/libevent${MAYBE_PIC}.a"
|
||||
|
||||
# location of jemalloc
|
||||
JEMALLOC_INCLUDE=" -I $JEMALLOC_BASE/include/"
|
||||
JEMALLOC_LIB=" $JEMALLOC_BASE/lib/libjemalloc${MAYBE_PIC}.a"
|
||||
@@ -113,7 +102,7 @@ BINUTILS="$BINUTILS_BASE/bin"
|
||||
AR="$BINUTILS/ar"
|
||||
AS="$BINUTILS/as"
|
||||
|
||||
DEPS_INCLUDE="$SNAPPY_INCLUDE $ZLIB_INCLUDE $BZIP_INCLUDE $LZ4_INCLUDE $ZSTD_INCLUDE $GFLAGS_INCLUDE $NUMA_INCLUDE $TBB_INCLUDE $LIBURING_INCLUDE $BENCHMARK_INCLUDE $BOOST_INCLUDE $GLOG_INCLUDE $FMT_INCLUDE $DBL_CONV_INCLUDE $LIBEVENT_INCLUDE"
|
||||
DEPS_INCLUDE="$SNAPPY_INCLUDE $ZLIB_INCLUDE $BZIP_INCLUDE $LZ4_INCLUDE $ZSTD_INCLUDE $GFLAGS_INCLUDE $NUMA_INCLUDE $TBB_INCLUDE $LIBURING_INCLUDE $BENCHMARK_INCLUDE $GLOG_INCLUDE"
|
||||
|
||||
STDLIBS="-L $GCC_BASE/lib64"
|
||||
|
||||
|
||||
Vendored
+5
@@ -58,6 +58,11 @@ static std::unordered_map<std::string, OptionTypeInfo>
|
||||
compress_format_version),
|
||||
OptionType::kUInt32T, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kMutable}},
|
||||
{"enable_custom_split_merge",
|
||||
{offsetof(struct CompressedSecondaryCacheOptions,
|
||||
enable_custom_split_merge),
|
||||
OptionType::kBoolean, OptionVerificationType::kNormal,
|
||||
OptionTypeFlags::kMutable}},
|
||||
};
|
||||
#endif // ROCKSDB_LITE
|
||||
|
||||
|
||||
Vendored
+13
-8
@@ -13,7 +13,6 @@
|
||||
#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"
|
||||
@@ -292,13 +291,12 @@ class CacheBench {
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
fprintf(stderr, "Old clock cache implementation has been removed.\n");
|
||||
exit(1);
|
||||
} else if (FLAGS_cache_type == "hyper_clock_cache") {
|
||||
cache_ = HyperClockCacheOptions(FLAGS_cache_size, FLAGS_value_bytes,
|
||||
FLAGS_num_shard_bits)
|
||||
.MakeSharedCache();
|
||||
} else if (FLAGS_cache_type == "fast_lru_cache") {
|
||||
cache_ = NewFastLRUCache(
|
||||
FLAGS_cache_size, FLAGS_value_bytes, FLAGS_num_shard_bits,
|
||||
@@ -441,6 +439,8 @@ class CacheBench {
|
||||
uint64_t total_key_size = 0;
|
||||
uint64_t total_charge = 0;
|
||||
uint64_t total_entry_count = 0;
|
||||
uint64_t table_occupancy = 0;
|
||||
uint64_t table_size = 0;
|
||||
std::set<Cache::DeleterFn> deleters;
|
||||
StopWatchNano timer(clock);
|
||||
|
||||
@@ -456,6 +456,9 @@ class CacheBench {
|
||||
std::ostringstream ostr;
|
||||
ostr << "Most recent cache entry stats:\n"
|
||||
<< "Number of entries: " << total_entry_count << "\n"
|
||||
<< "Table occupancy: " << table_occupancy << " / "
|
||||
<< table_size << " = "
|
||||
<< (100.0 * table_occupancy / table_size) << "%\n"
|
||||
<< "Total charge: " << BytesToHumanString(total_charge) << "\n"
|
||||
<< "Average key size: "
|
||||
<< (1.0 * total_key_size / total_entry_count) << "\n"
|
||||
@@ -492,6 +495,8 @@ class CacheBench {
|
||||
Cache::ApplyToAllEntriesOptions opts;
|
||||
opts.average_entries_per_lock = FLAGS_gather_stats_entries_per_lock;
|
||||
shared->GetCacheBench()->cache_->ApplyToAllEntries(fn, opts);
|
||||
table_occupancy = shared->GetCacheBench()->cache_->GetOccupancyCount();
|
||||
table_size = shared->GetCacheBench()->cache_->GetTableAddressCount();
|
||||
stats_hist->Add(timer.ElapsedNanos() / 1000);
|
||||
}
|
||||
}
|
||||
|
||||
Vendored
+2
@@ -23,6 +23,7 @@ std::array<std::string, kNumCacheEntryRoles> kCacheEntryRoleToCamelString{{
|
||||
"FilterConstruction",
|
||||
"BlockBasedTableReader",
|
||||
"FileMetadata",
|
||||
"BlobValue",
|
||||
"BlobCache",
|
||||
"Misc",
|
||||
}};
|
||||
@@ -39,6 +40,7 @@ std::array<std::string, kNumCacheEntryRoles> kCacheEntryRoleToHyphenString{{
|
||||
"filter-construction",
|
||||
"block-based-table-reader",
|
||||
"file-metadata",
|
||||
"blob-value",
|
||||
"blob-cache",
|
||||
"misc",
|
||||
}};
|
||||
|
||||
Vendored
+116
-57
@@ -15,7 +15,6 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "cache/clock_cache.h"
|
||||
#include "cache/fast_lru_cache.h"
|
||||
#include "cache/lru_cache.h"
|
||||
#include "port/stack_trace.h"
|
||||
@@ -23,7 +22,7 @@
|
||||
#include "util/coding.h"
|
||||
#include "util/string_util.h"
|
||||
|
||||
// FastLRUCache and ClockCache only support 16-byte keys, so some of
|
||||
// FastLRUCache and HyperClockCache only support 16-byte keys, so some of
|
||||
// the tests originally wrote for LRUCache do not work on the other caches.
|
||||
// Those tests were adapted to use 16-byte keys. We kept the original ones.
|
||||
// TODO: Remove the original tests if they ever become unused.
|
||||
@@ -76,7 +75,7 @@ void EraseDeleter2(const Slice& /*key*/, void* value) {
|
||||
}
|
||||
|
||||
const std::string kLRU = "lru";
|
||||
const std::string kClock = "clock";
|
||||
const std::string kHyperClock = "hyper_clock";
|
||||
const std::string kFast = "fast";
|
||||
|
||||
} // anonymous namespace
|
||||
@@ -87,7 +86,7 @@ class CacheTest : public testing::TestWithParam<std::string> {
|
||||
static std::string type_;
|
||||
|
||||
static void Deleter(const Slice& key, void* v) {
|
||||
if (type_ == kFast || type_ == kClock) {
|
||||
if (type_ == kFast || type_ == kHyperClock) {
|
||||
current_->deleted_keys_.push_back(DecodeKey16Bytes(key));
|
||||
} else {
|
||||
current_->deleted_keys_.push_back(DecodeKey32Bits(key));
|
||||
@@ -106,6 +105,8 @@ class CacheTest : public testing::TestWithParam<std::string> {
|
||||
std::shared_ptr<Cache> cache_;
|
||||
std::shared_ptr<Cache> cache2_;
|
||||
|
||||
size_t estimated_value_size_ = 1;
|
||||
|
||||
CacheTest()
|
||||
: cache_(NewCache(kCacheSize, kNumShardBits, false)),
|
||||
cache2_(NewCache(kCacheSize2, kNumShardBits2, false)) {
|
||||
@@ -120,14 +121,14 @@ class CacheTest : public testing::TestWithParam<std::string> {
|
||||
if (type == kLRU) {
|
||||
return NewLRUCache(capacity);
|
||||
}
|
||||
if (type == kClock) {
|
||||
return ExperimentalNewClockCache(
|
||||
capacity, 1 /*estimated_value_size*/, -1 /*num_shard_bits*/,
|
||||
false /*strict_capacity_limit*/, kDefaultCacheMetadataChargePolicy);
|
||||
if (type == kHyperClock) {
|
||||
return HyperClockCacheOptions(
|
||||
capacity, estimated_value_size_ /*estimated_value_size*/)
|
||||
.MakeSharedCache();
|
||||
}
|
||||
if (type == kFast) {
|
||||
return NewFastLRUCache(
|
||||
capacity, 1 /*estimated_value_size*/, -1 /*num_shard_bits*/,
|
||||
capacity, estimated_value_size_, -1 /*num_shard_bits*/,
|
||||
false /*strict_capacity_limit*/, kDefaultCacheMetadataChargePolicy);
|
||||
}
|
||||
return nullptr;
|
||||
@@ -146,10 +147,11 @@ class CacheTest : public testing::TestWithParam<std::string> {
|
||||
co.metadata_charge_policy = charge_policy;
|
||||
return NewLRUCache(co);
|
||||
}
|
||||
if (type == kClock) {
|
||||
return ExperimentalNewClockCache(capacity, 1 /*estimated_value_size*/,
|
||||
num_shard_bits, strict_capacity_limit,
|
||||
charge_policy);
|
||||
if (type == kHyperClock) {
|
||||
return HyperClockCacheOptions(capacity, 1 /*estimated_value_size*/,
|
||||
num_shard_bits, strict_capacity_limit,
|
||||
nullptr /*allocator*/, charge_policy)
|
||||
.MakeSharedCache();
|
||||
}
|
||||
if (type == kFast) {
|
||||
return NewFastLRUCache(capacity, 1 /*estimated_value_size*/,
|
||||
@@ -161,12 +163,11 @@ class CacheTest : public testing::TestWithParam<std::string> {
|
||||
|
||||
// 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.
|
||||
// Currently, HyperClockCache requires keys to be 16B long, whereas
|
||||
// LRUCache doesn't, so the encoding depends on the cache type.
|
||||
std::string EncodeKey(int k) {
|
||||
auto type = GetParam();
|
||||
if (type == kFast || type == kClock) {
|
||||
if (type == kFast || type == kHyperClock) {
|
||||
return EncodeKey16Bytes(k);
|
||||
} else {
|
||||
return EncodeKey32Bits(k);
|
||||
@@ -175,7 +176,7 @@ class CacheTest : public testing::TestWithParam<std::string> {
|
||||
|
||||
int DecodeKey(const Slice& k) {
|
||||
auto type = GetParam();
|
||||
if (type == kFast || type == kClock) {
|
||||
if (type == kFast || type == kHyperClock) {
|
||||
return DecodeKey16Bytes(k);
|
||||
} else {
|
||||
return DecodeKey32Bits(k);
|
||||
@@ -239,7 +240,10 @@ TEST_P(CacheTest, UsageTest) {
|
||||
auto cache = NewCache(kCapacity, 8, false, kDontChargeCacheMetadata);
|
||||
auto precise_cache = NewCache(kCapacity, 0, false, kFullChargeCacheMetadata);
|
||||
ASSERT_EQ(0, cache->GetUsage());
|
||||
ASSERT_EQ(0, precise_cache->GetUsage());
|
||||
size_t baseline_meta_usage = precise_cache->GetUsage();
|
||||
if (type != kHyperClock) {
|
||||
ASSERT_EQ(0, baseline_meta_usage);
|
||||
}
|
||||
|
||||
size_t usage = 0;
|
||||
char value[10] = "abcdef";
|
||||
@@ -258,13 +262,17 @@ TEST_P(CacheTest, UsageTest) {
|
||||
kv_size, DumbDeleter));
|
||||
usage += kv_size;
|
||||
ASSERT_EQ(usage, cache->GetUsage());
|
||||
ASSERT_LT(usage, precise_cache->GetUsage());
|
||||
if (type == kHyperClock) {
|
||||
ASSERT_EQ(baseline_meta_usage + usage, precise_cache->GetUsage());
|
||||
} else {
|
||||
ASSERT_LT(usage, precise_cache->GetUsage());
|
||||
}
|
||||
}
|
||||
|
||||
cache->EraseUnRefEntries();
|
||||
precise_cache->EraseUnRefEntries();
|
||||
ASSERT_EQ(0, cache->GetUsage());
|
||||
ASSERT_EQ(0, precise_cache->GetUsage());
|
||||
ASSERT_EQ(baseline_meta_usage, precise_cache->GetUsage());
|
||||
|
||||
// make sure the cache will be overloaded
|
||||
for (size_t i = 1; i < kCapacity; ++i) {
|
||||
@@ -284,7 +292,15 @@ TEST_P(CacheTest, UsageTest) {
|
||||
ASSERT_GT(kCapacity, cache->GetUsage());
|
||||
ASSERT_GT(kCapacity, precise_cache->GetUsage());
|
||||
ASSERT_LT(kCapacity * 0.95, cache->GetUsage());
|
||||
ASSERT_LT(kCapacity * 0.95, precise_cache->GetUsage());
|
||||
if (type != kHyperClock) {
|
||||
ASSERT_LT(kCapacity * 0.95, precise_cache->GetUsage());
|
||||
} else {
|
||||
// estimated value size of 1 is weird for clock cache, because
|
||||
// almost all of the capacity will be used for metadata, and due to only
|
||||
// using power of 2 table sizes, we might hit strict occupancy limit
|
||||
// before hitting capacity limit.
|
||||
ASSERT_LT(kCapacity * 0.80, precise_cache->GetUsage());
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: This test takes longer than expected on ClockCache. This is
|
||||
@@ -301,6 +317,10 @@ TEST_P(CacheTest, PinnedUsageTest) {
|
||||
const size_t kCapacity = 200000;
|
||||
auto cache = NewCache(kCapacity, 8, false, kDontChargeCacheMetadata);
|
||||
auto precise_cache = NewCache(kCapacity, 8, false, kFullChargeCacheMetadata);
|
||||
size_t baseline_meta_usage = precise_cache->GetUsage();
|
||||
if (type != kHyperClock) {
|
||||
ASSERT_EQ(0, baseline_meta_usage);
|
||||
}
|
||||
|
||||
size_t pinned_usage = 0;
|
||||
char value[10] = "abcdef";
|
||||
@@ -390,7 +410,7 @@ TEST_P(CacheTest, PinnedUsageTest) {
|
||||
cache->EraseUnRefEntries();
|
||||
precise_cache->EraseUnRefEntries();
|
||||
ASSERT_EQ(0, cache->GetUsage());
|
||||
ASSERT_EQ(0, precise_cache->GetUsage());
|
||||
ASSERT_EQ(baseline_meta_usage, precise_cache->GetUsage());
|
||||
}
|
||||
|
||||
TEST_P(CacheTest, HitAndMiss) {
|
||||
@@ -407,16 +427,30 @@ TEST_P(CacheTest, HitAndMiss) {
|
||||
ASSERT_EQ(-1, Lookup(300));
|
||||
|
||||
Insert(100, 102);
|
||||
ASSERT_EQ(102, Lookup(100));
|
||||
if (GetParam() == kHyperClock) {
|
||||
// ClockCache usually doesn't overwrite on Insert
|
||||
ASSERT_EQ(101, Lookup(100));
|
||||
} else {
|
||||
ASSERT_EQ(102, Lookup(100));
|
||||
}
|
||||
ASSERT_EQ(201, Lookup(200));
|
||||
ASSERT_EQ(-1, Lookup(300));
|
||||
|
||||
ASSERT_EQ(1U, deleted_keys_.size());
|
||||
ASSERT_EQ(100, deleted_keys_[0]);
|
||||
ASSERT_EQ(101, deleted_values_[0]);
|
||||
if (GetParam() == kHyperClock) {
|
||||
ASSERT_EQ(102, deleted_values_[0]);
|
||||
} else {
|
||||
ASSERT_EQ(101, deleted_values_[0]);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(CacheTest, InsertSameKey) {
|
||||
if (GetParam() == kHyperClock) {
|
||||
ROCKSDB_GTEST_BYPASS(
|
||||
"ClockCache doesn't guarantee Insert overwrite same key.");
|
||||
return;
|
||||
}
|
||||
Insert(1, 1);
|
||||
Insert(1, 2);
|
||||
ASSERT_EQ(2, Lookup(1));
|
||||
@@ -442,6 +476,11 @@ TEST_P(CacheTest, Erase) {
|
||||
}
|
||||
|
||||
TEST_P(CacheTest, EntriesArePinned) {
|
||||
if (GetParam() == kHyperClock) {
|
||||
ROCKSDB_GTEST_BYPASS(
|
||||
"ClockCache doesn't guarantee Insert overwrite same key.");
|
||||
return;
|
||||
}
|
||||
Insert(100, 101);
|
||||
Cache::Handle* h1 = cache_->Lookup(EncodeKey(100));
|
||||
ASSERT_EQ(101, DecodeValue(cache_->Value(h1)));
|
||||
@@ -474,7 +513,6 @@ TEST_P(CacheTest, EntriesArePinned) {
|
||||
TEST_P(CacheTest, EvictionPolicy) {
|
||||
Insert(100, 101);
|
||||
Insert(200, 201);
|
||||
|
||||
// Frequently used entry must be kept around
|
||||
for (int i = 0; i < 2 * kCacheSize; i++) {
|
||||
Insert(1000+i, 2000+i);
|
||||
@@ -503,6 +541,12 @@ TEST_P(CacheTest, ExternalRefPinsEntries) {
|
||||
for (int j = 0; j < 2 * kCacheSize + 100; j++) {
|
||||
Insert(1000 + j, 2000 + j);
|
||||
}
|
||||
// Clock cache is even more stateful and needs more churn to evict
|
||||
if (GetParam() == kHyperClock) {
|
||||
for (int j = 0; j < kCacheSize; j++) {
|
||||
Insert(11000 + j, 11000 + j);
|
||||
}
|
||||
}
|
||||
if (i < 2) {
|
||||
ASSERT_EQ(101, Lookup(100));
|
||||
}
|
||||
@@ -697,9 +741,9 @@ TEST_P(CacheTest, ReleaseWithoutErase) {
|
||||
|
||||
TEST_P(CacheTest, SetCapacity) {
|
||||
auto type = GetParam();
|
||||
if (type == kFast || type == kClock) {
|
||||
if (type == kFast || type == kHyperClock) {
|
||||
ROCKSDB_GTEST_BYPASS(
|
||||
"FastLRUCache and ClockCache don't support arbitrary capacity "
|
||||
"FastLRUCache and HyperClockCache don't support arbitrary capacity "
|
||||
"adjustments.");
|
||||
return;
|
||||
}
|
||||
@@ -810,11 +854,6 @@ TEST_P(LRUCacheTest, SetStrictCapacityLimit) {
|
||||
}
|
||||
|
||||
TEST_P(CacheTest, OverCapacity) {
|
||||
auto type = GetParam();
|
||||
if (type == kClock) {
|
||||
ROCKSDB_GTEST_BYPASS("Requires LRU eviction policy.");
|
||||
return;
|
||||
}
|
||||
size_t n = 10;
|
||||
|
||||
// a LRUCache with n entries and one shard only
|
||||
@@ -842,23 +881,34 @@ TEST_P(CacheTest, OverCapacity) {
|
||||
for (int i = 0; i < static_cast<int>(n + 1); i++) {
|
||||
cache->Release(handles[i]);
|
||||
}
|
||||
// Make sure eviction is triggered.
|
||||
cache->SetCapacity(n);
|
||||
|
||||
// cache is under capacity now since elements were released
|
||||
ASSERT_EQ(n, cache->GetUsage());
|
||||
if (GetParam() == kHyperClock) {
|
||||
// Make sure eviction is triggered.
|
||||
ASSERT_OK(cache->Insert(EncodeKey(-1), nullptr, 1, &deleter, &handles[0]));
|
||||
|
||||
// element 0 is evicted and the rest is there
|
||||
// This is consistent with the LRU policy since the element 0
|
||||
// was released first
|
||||
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(static_cast<size_t>(i), 0U);
|
||||
cache->Release(h);
|
||||
} else {
|
||||
ASSERT_EQ(static_cast<size_t>(i), 0U);
|
||||
// cache is under capacity now since elements were released
|
||||
ASSERT_GE(n, cache->GetUsage());
|
||||
|
||||
// clean up
|
||||
cache->Release(handles[0]);
|
||||
} else {
|
||||
// LRUCache checks for over-capacity in Release.
|
||||
|
||||
// cache is exactly at capacity now with minimal eviction
|
||||
ASSERT_EQ(n, cache->GetUsage());
|
||||
|
||||
// element 0 is evicted and the rest is there
|
||||
// This is consistent with the LRU policy since the element 0
|
||||
// was released first
|
||||
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(static_cast<size_t>(i), 0U);
|
||||
cache->Release(h);
|
||||
} else {
|
||||
ASSERT_EQ(static_cast<size_t>(i), 0U);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -966,19 +1016,31 @@ TEST_P(CacheTest, ApplyToAllEntriesDuringResize) {
|
||||
}
|
||||
|
||||
TEST_P(CacheTest, DefaultShardBits) {
|
||||
// test1: set the flag to false. Insert more keys than capacity. See if they
|
||||
// all go through.
|
||||
std::shared_ptr<Cache> cache = NewCache(16 * 1024L * 1024L);
|
||||
// Prevent excessive allocation (to save time & space)
|
||||
estimated_value_size_ = 100000;
|
||||
// Implementations use different minimum shard sizes
|
||||
size_t min_shard_size =
|
||||
(GetParam() == kHyperClock ? 32U * 1024U : 512U) * 1024U;
|
||||
|
||||
std::shared_ptr<Cache> cache = NewCache(32U * min_shard_size);
|
||||
ShardedCache* sc = dynamic_cast<ShardedCache*>(cache.get());
|
||||
ASSERT_EQ(5, sc->GetNumShardBits());
|
||||
|
||||
cache = NewLRUCache(511 * 1024L, -1, true);
|
||||
cache = NewCache(min_shard_size / 1000U * 999U);
|
||||
sc = dynamic_cast<ShardedCache*>(cache.get());
|
||||
ASSERT_EQ(0, sc->GetNumShardBits());
|
||||
|
||||
cache = NewLRUCache(1024L * 1024L * 1024L, -1, true);
|
||||
cache = NewCache(3U * 1024U * 1024U * 1024U);
|
||||
sc = dynamic_cast<ShardedCache*>(cache.get());
|
||||
// current maximum of 6
|
||||
ASSERT_EQ(6, sc->GetNumShardBits());
|
||||
|
||||
if constexpr (sizeof(size_t) > 4) {
|
||||
cache = NewCache(128U * min_shard_size);
|
||||
sc = dynamic_cast<ShardedCache*>(cache.get());
|
||||
// current maximum of 6
|
||||
ASSERT_EQ(6, sc->GetNumShardBits());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(CacheTest, GetChargeAndDeleter) {
|
||||
@@ -990,11 +1052,8 @@ TEST_P(CacheTest, GetChargeAndDeleter) {
|
||||
cache_->Release(h1);
|
||||
}
|
||||
|
||||
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));
|
||||
testing::Values(kLRU, kHyperClock, kFast));
|
||||
INSTANTIATE_TEST_CASE_P(CacheTestInstance, LRUCacheTest,
|
||||
testing::Values(kLRU, kFast));
|
||||
|
||||
|
||||
Vendored
+983
-473
File diff suppressed because it is too large.
Load diff
Vendored
+419
-634
File diff suppressed because it is too large.
Load diff
Vendored
+104
-40
@@ -10,6 +10,7 @@
|
||||
#include <memory>
|
||||
|
||||
#include "memory/memory_allocator.h"
|
||||
#include "monitoring/perf_context_imp.h"
|
||||
#include "util/compression.h"
|
||||
#include "util/string_util.h"
|
||||
|
||||
@@ -20,11 +21,13 @@ CompressedSecondaryCache::CompressedSecondaryCache(
|
||||
double high_pri_pool_ratio, double low_pri_pool_ratio,
|
||||
std::shared_ptr<MemoryAllocator> memory_allocator, bool use_adaptive_mutex,
|
||||
CacheMetadataChargePolicy metadata_charge_policy,
|
||||
CompressionType compression_type, uint32_t compress_format_version)
|
||||
CompressionType compression_type, uint32_t compress_format_version,
|
||||
bool enable_custom_split_merge)
|
||||
: cache_options_(capacity, num_shard_bits, strict_capacity_limit,
|
||||
high_pri_pool_ratio, memory_allocator, use_adaptive_mutex,
|
||||
metadata_charge_policy, compression_type,
|
||||
compress_format_version, low_pri_pool_ratio) {
|
||||
high_pri_pool_ratio, low_pri_pool_ratio, memory_allocator,
|
||||
use_adaptive_mutex, metadata_charge_policy,
|
||||
compression_type, compress_format_version,
|
||||
enable_custom_split_merge) {
|
||||
cache_ =
|
||||
NewLRUCache(capacity, num_shard_bits, strict_capacity_limit,
|
||||
high_pri_pool_ratio, memory_allocator, use_adaptive_mutex,
|
||||
@@ -35,25 +38,38 @@ CompressedSecondaryCache::~CompressedSecondaryCache() { cache_.reset(); }
|
||||
|
||||
std::unique_ptr<SecondaryCacheResultHandle> CompressedSecondaryCache::Lookup(
|
||||
const Slice& key, const Cache::CreateCallback& create_cb, bool /*wait*/,
|
||||
bool& is_in_sec_cache) {
|
||||
bool advise_erase, bool& is_in_sec_cache) {
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle;
|
||||
is_in_sec_cache = false;
|
||||
Cache::Handle* lru_handle = cache_->Lookup(key);
|
||||
if (lru_handle == nullptr) {
|
||||
return handle;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
CacheValueChunk* handle_value =
|
||||
reinterpret_cast<CacheValueChunk*>(cache_->Value(lru_handle));
|
||||
void* handle_value = cache_->Value(lru_handle);
|
||||
if (handle_value == nullptr) {
|
||||
cache_->Release(lru_handle, /*erase_if_last_ref=*/false);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
CacheAllocationPtr* ptr{nullptr};
|
||||
CacheAllocationPtr merged_value;
|
||||
size_t handle_value_charge{0};
|
||||
CacheAllocationPtr merged_value =
|
||||
MergeChunksIntoValue(handle_value, handle_value_charge);
|
||||
if (cache_options_.enable_custom_split_merge) {
|
||||
CacheValueChunk* value_chunk_ptr =
|
||||
reinterpret_cast<CacheValueChunk*>(handle_value);
|
||||
merged_value = MergeChunksIntoValue(value_chunk_ptr, handle_value_charge);
|
||||
ptr = &merged_value;
|
||||
} else {
|
||||
ptr = reinterpret_cast<CacheAllocationPtr*>(handle_value);
|
||||
handle_value_charge = cache_->GetCharge(lru_handle);
|
||||
}
|
||||
|
||||
Status s;
|
||||
void* value{nullptr};
|
||||
size_t charge{0};
|
||||
if (cache_options_.compression_type == kNoCompression) {
|
||||
s = create_cb(merged_value.get(), handle_value_charge, &value, &charge);
|
||||
s = create_cb(ptr->get(), handle_value_charge, &value, &charge);
|
||||
} else {
|
||||
UncompressionContext uncompression_context(cache_options_.compression_type);
|
||||
UncompressionInfo uncompression_info(uncompression_context,
|
||||
@@ -61,32 +77,55 @@ std::unique_ptr<SecondaryCacheResultHandle> CompressedSecondaryCache::Lookup(
|
||||
cache_options_.compression_type);
|
||||
|
||||
size_t uncompressed_size{0};
|
||||
CacheAllocationPtr uncompressed;
|
||||
uncompressed = UncompressData(uncompression_info, (char*)merged_value.get(),
|
||||
handle_value_charge, &uncompressed_size,
|
||||
cache_options_.compress_format_version,
|
||||
cache_options_.memory_allocator.get());
|
||||
CacheAllocationPtr uncompressed = UncompressData(
|
||||
uncompression_info, (char*)ptr->get(), handle_value_charge,
|
||||
&uncompressed_size, cache_options_.compress_format_version,
|
||||
cache_options_.memory_allocator.get());
|
||||
|
||||
if (!uncompressed) {
|
||||
cache_->Release(lru_handle, /* erase_if_last_ref */ true);
|
||||
return handle;
|
||||
cache_->Release(lru_handle, /*erase_if_last_ref=*/true);
|
||||
return nullptr;
|
||||
}
|
||||
s = create_cb(uncompressed.get(), uncompressed_size, &value, &charge);
|
||||
}
|
||||
|
||||
if (!s.ok()) {
|
||||
cache_->Release(lru_handle, /* erase_if_last_ref */ true);
|
||||
return handle;
|
||||
cache_->Release(lru_handle, /*erase_if_last_ref=*/true);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
cache_->Release(lru_handle, /* erase_if_last_ref */ true);
|
||||
if (advise_erase) {
|
||||
cache_->Release(lru_handle, /*erase_if_last_ref=*/true);
|
||||
// Insert a dummy handle.
|
||||
cache_
|
||||
->Insert(key, /*value=*/nullptr, /*charge=*/0,
|
||||
GetDeletionCallback(cache_options_.enable_custom_split_merge))
|
||||
.PermitUncheckedError();
|
||||
} else {
|
||||
is_in_sec_cache = true;
|
||||
cache_->Release(lru_handle, /*erase_if_last_ref=*/false);
|
||||
}
|
||||
handle.reset(new CompressedSecondaryCacheResultHandle(value, charge));
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
Status CompressedSecondaryCache::Insert(const Slice& key, void* value,
|
||||
const Cache::CacheItemHelper* helper) {
|
||||
if (value == nullptr) {
|
||||
return Status::InvalidArgument();
|
||||
}
|
||||
|
||||
Cache::Handle* lru_handle = cache_->Lookup(key);
|
||||
Cache::DeleterFn del_cb =
|
||||
GetDeletionCallback(cache_options_.enable_custom_split_merge);
|
||||
if (lru_handle == nullptr) {
|
||||
PERF_COUNTER_ADD(compressed_sec_cache_insert_dummy_count, 1);
|
||||
// Insert a dummy handle if the handle is evicted for the first time.
|
||||
return cache_->Insert(key, /*value=*/nullptr, /*charge=*/0, del_cb);
|
||||
} else {
|
||||
cache_->Release(lru_handle, /*erase_if_last_ref=*/false);
|
||||
}
|
||||
|
||||
size_t size = (*helper->size_cb)(value);
|
||||
CacheAllocationPtr ptr =
|
||||
AllocateBlock(size, cache_options_.memory_allocator.get());
|
||||
@@ -99,6 +138,7 @@ Status CompressedSecondaryCache::Insert(const Slice& key, void* value,
|
||||
|
||||
std::string compressed_val;
|
||||
if (cache_options_.compression_type != kNoCompression) {
|
||||
PERF_COUNTER_ADD(compressed_sec_cache_uncompressed_bytes, size);
|
||||
CompressionOptions compression_opts;
|
||||
CompressionContext compression_context(cache_options_.compression_type);
|
||||
uint64_t sample_for_compression{0};
|
||||
@@ -115,12 +155,25 @@ Status CompressedSecondaryCache::Insert(const Slice& key, void* value,
|
||||
}
|
||||
|
||||
val = Slice(compressed_val);
|
||||
size = compressed_val.size();
|
||||
PERF_COUNTER_ADD(compressed_sec_cache_compressed_bytes, size);
|
||||
|
||||
if (!cache_options_.enable_custom_split_merge) {
|
||||
ptr = AllocateBlock(size, cache_options_.memory_allocator.get());
|
||||
memcpy(ptr.get(), compressed_val.data(), size);
|
||||
}
|
||||
}
|
||||
|
||||
size_t charge{0};
|
||||
CacheValueChunk* value_chunks_head =
|
||||
SplitValueIntoChunks(val, cache_options_.compression_type, charge);
|
||||
return cache_->Insert(key, value_chunks_head, charge, DeletionCallback);
|
||||
PERF_COUNTER_ADD(compressed_sec_cache_insert_real_count, 1);
|
||||
if (cache_options_.enable_custom_split_merge) {
|
||||
size_t charge{0};
|
||||
CacheValueChunk* value_chunks_head =
|
||||
SplitValueIntoChunks(val, cache_options_.compression_type, charge);
|
||||
return cache_->Insert(key, value_chunks_head, charge, del_cb);
|
||||
} else {
|
||||
CacheAllocationPtr* buf = new CacheAllocationPtr(std::move(ptr));
|
||||
return cache_->Insert(key, buf, size, del_cb);
|
||||
}
|
||||
}
|
||||
|
||||
void CompressedSecondaryCache::Erase(const Slice& key) { cache_->Erase(key); }
|
||||
@@ -210,28 +263,38 @@ CacheAllocationPtr CompressedSecondaryCache::MergeChunksIntoValue(
|
||||
return ptr;
|
||||
}
|
||||
|
||||
void CompressedSecondaryCache::DeletionCallback(const Slice& /*key*/,
|
||||
void* obj) {
|
||||
CacheValueChunk* chunks_head = reinterpret_cast<CacheValueChunk*>(obj);
|
||||
while (chunks_head != nullptr) {
|
||||
CacheValueChunk* tmp_chunk = chunks_head;
|
||||
chunks_head = chunks_head->next;
|
||||
tmp_chunk->Free();
|
||||
Cache::DeleterFn CompressedSecondaryCache::GetDeletionCallback(
|
||||
bool enable_custom_split_merge) {
|
||||
if (enable_custom_split_merge) {
|
||||
return [](const Slice& /*key*/, void* obj) {
|
||||
CacheValueChunk* chunks_head = reinterpret_cast<CacheValueChunk*>(obj);
|
||||
while (chunks_head != nullptr) {
|
||||
CacheValueChunk* tmp_chunk = chunks_head;
|
||||
chunks_head = chunks_head->next;
|
||||
tmp_chunk->Free();
|
||||
obj = nullptr;
|
||||
};
|
||||
};
|
||||
} else {
|
||||
return [](const Slice& /*key*/, void* obj) {
|
||||
delete reinterpret_cast<CacheAllocationPtr*>(obj);
|
||||
obj = nullptr;
|
||||
};
|
||||
}
|
||||
obj = nullptr;
|
||||
}
|
||||
|
||||
std::shared_ptr<SecondaryCache> NewCompressedSecondaryCache(
|
||||
size_t capacity, int num_shard_bits, bool strict_capacity_limit,
|
||||
double high_pri_pool_ratio,
|
||||
double high_pri_pool_ratio, double low_pri_pool_ratio,
|
||||
std::shared_ptr<MemoryAllocator> memory_allocator, bool use_adaptive_mutex,
|
||||
CacheMetadataChargePolicy metadata_charge_policy,
|
||||
CompressionType compression_type, uint32_t compress_format_version,
|
||||
double low_pri_pool_ratio) {
|
||||
bool enable_custom_split_merge) {
|
||||
return std::make_shared<CompressedSecondaryCache>(
|
||||
capacity, num_shard_bits, strict_capacity_limit, high_pri_pool_ratio,
|
||||
low_pri_pool_ratio, memory_allocator, use_adaptive_mutex,
|
||||
metadata_charge_policy, compression_type, compress_format_version);
|
||||
metadata_charge_policy, compression_type, compress_format_version,
|
||||
enable_custom_split_merge);
|
||||
}
|
||||
|
||||
std::shared_ptr<SecondaryCache> NewCompressedSecondaryCache(
|
||||
@@ -240,9 +303,10 @@ std::shared_ptr<SecondaryCache> NewCompressedSecondaryCache(
|
||||
assert(opts.secondary_cache == nullptr);
|
||||
return NewCompressedSecondaryCache(
|
||||
opts.capacity, opts.num_shard_bits, opts.strict_capacity_limit,
|
||||
opts.high_pri_pool_ratio, opts.memory_allocator, opts.use_adaptive_mutex,
|
||||
opts.metadata_charge_policy, opts.compression_type,
|
||||
opts.compress_format_version, opts.low_pri_pool_ratio);
|
||||
opts.high_pri_pool_ratio, opts.low_pri_pool_ratio, opts.memory_allocator,
|
||||
opts.use_adaptive_mutex, opts.metadata_charge_policy,
|
||||
opts.compression_type, opts.compress_format_version,
|
||||
opts.enable_custom_split_merge);
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
Vendored
+23
-8
@@ -45,7 +45,21 @@ class CompressedSecondaryCacheResultHandle : public SecondaryCacheResultHandle {
|
||||
// The CompressedSecondaryCache is a concrete implementation of
|
||||
// rocksdb::SecondaryCache.
|
||||
//
|
||||
// Users can also cast a pointer to it and call methods on
|
||||
// When a block is found from CompressedSecondaryCache::Lookup, we check whether
|
||||
// there is a dummy block with the same key in the primary cache.
|
||||
// 1. If the dummy block exits, we erase the block from
|
||||
// CompressedSecondaryCache and insert it into the primary cache.
|
||||
// 2. If not, we just insert a dummy block into the primary cache
|
||||
// (charging the actual size of the block) and don not erase the block from
|
||||
// CompressedSecondaryCache. A standalone handle is returned to the caller.
|
||||
//
|
||||
// When a block is evicted from the primary cache, we check whether
|
||||
// there is a dummy block with the same key in CompressedSecondaryCache.
|
||||
// 1. If the dummy block exits, the block is inserted into
|
||||
// CompressedSecondaryCache.
|
||||
// 2. If not, we just insert a dummy block (size 0) in CompressedSecondaryCache.
|
||||
//
|
||||
// Users can also cast a pointer to CompressedSecondaryCache and call methods on
|
||||
// it directly, especially custom methods that may be added
|
||||
// in the future. For example -
|
||||
// std::unique_ptr<rocksdb::SecondaryCache> cache =
|
||||
@@ -62,7 +76,8 @@ class CompressedSecondaryCache : public SecondaryCache {
|
||||
CacheMetadataChargePolicy metadata_charge_policy =
|
||||
kDefaultCacheMetadataChargePolicy,
|
||||
CompressionType compression_type = CompressionType::kLZ4Compression,
|
||||
uint32_t compress_format_version = 2);
|
||||
uint32_t compress_format_version = 2,
|
||||
bool enable_custom_split_merge = false);
|
||||
virtual ~CompressedSecondaryCache() override;
|
||||
|
||||
const char* Name() const override { return "CompressedSecondaryCache"; }
|
||||
@@ -72,7 +87,9 @@ class CompressedSecondaryCache : public SecondaryCache {
|
||||
|
||||
std::unique_ptr<SecondaryCacheResultHandle> Lookup(
|
||||
const Slice& key, const Cache::CreateCallback& create_cb, bool /*wait*/,
|
||||
bool& is_in_sec_cache) override;
|
||||
bool advise_erase, bool& is_in_sec_cache) override;
|
||||
|
||||
bool SupportForceErase() const override { return true; }
|
||||
|
||||
void Erase(const Slice& key) override;
|
||||
|
||||
@@ -82,10 +99,8 @@ class CompressedSecondaryCache : public SecondaryCache {
|
||||
|
||||
private:
|
||||
friend class CompressedSecondaryCacheTest;
|
||||
static constexpr std::array<uint16_t, 33> malloc_bin_sizes_{
|
||||
32, 64, 96, 128, 160, 192, 224, 256, 320, 384, 448,
|
||||
512, 640, 768, 896, 1024, 1280, 1536, 1792, 2048, 2560, 3072,
|
||||
3584, 4096, 5120, 6144, 7168, 8192, 10240, 12288, 14336, 16384, 32768};
|
||||
static constexpr std::array<uint16_t, 8> malloc_bin_sizes_{
|
||||
128, 256, 512, 1024, 2048, 4096, 8192, 16384};
|
||||
|
||||
struct CacheValueChunk {
|
||||
// TODO try "CacheAllocationPtr next;".
|
||||
@@ -110,7 +125,7 @@ class CompressedSecondaryCache : public SecondaryCache {
|
||||
size_t& charge);
|
||||
|
||||
// An implementation of Cache::DeleterFn.
|
||||
static void DeletionCallback(const Slice& /*key*/, void* obj);
|
||||
static Cache::DeleterFn GetDeletionCallback(bool enable_custom_split_merge);
|
||||
std::shared_ptr<Cache> cache_;
|
||||
CompressedSecondaryCacheOptions cache_options_;
|
||||
};
|
||||
|
||||
+351
-185
@@ -8,6 +8,7 @@
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <iterator>
|
||||
#include <tuple>
|
||||
|
||||
#include "cache/lru_cache.h"
|
||||
#include "memory/jemalloc_nodump_allocator.h"
|
||||
@@ -84,52 +85,91 @@ class CompressedSecondaryCacheTest : public testing::Test {
|
||||
|
||||
void SetFailCreate(bool fail) { fail_create_ = fail; }
|
||||
|
||||
void BasicTestHelper(std::shared_ptr<SecondaryCache> sec_cache) {
|
||||
void BasicTestHelper(std::shared_ptr<SecondaryCache> sec_cache,
|
||||
bool sec_cache_is_compressed) {
|
||||
get_perf_context()->Reset();
|
||||
bool is_in_sec_cache{true};
|
||||
// Lookup an non-existent key.
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle0 =
|
||||
sec_cache->Lookup("k0", test_item_creator, true, is_in_sec_cache);
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle0 = sec_cache->Lookup(
|
||||
"k0", test_item_creator, true, /*advise_erase=*/true, is_in_sec_cache);
|
||||
ASSERT_EQ(handle0, nullptr);
|
||||
|
||||
Random rnd(301);
|
||||
// Insert and Lookup the first item.
|
||||
std::string str1;
|
||||
test::CompressibleString(&rnd, 0.25, 1000, &str1);
|
||||
// Insert and Lookup the item k1 for the first time.
|
||||
std::string str1(rnd.RandomString(1000));
|
||||
TestItem item1(str1.data(), str1.length());
|
||||
// A dummy handle is inserted if the item is inserted for the first time.
|
||||
ASSERT_OK(sec_cache->Insert("k1", &item1,
|
||||
&CompressedSecondaryCacheTest::helper_));
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_insert_dummy_count, 1);
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_uncompressed_bytes, 0);
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_compressed_bytes, 0);
|
||||
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle1 =
|
||||
sec_cache->Lookup("k1", test_item_creator, true, is_in_sec_cache);
|
||||
ASSERT_NE(handle1, nullptr);
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle1_1 = sec_cache->Lookup(
|
||||
"k1", test_item_creator, true, /*advise_erase=*/false, is_in_sec_cache);
|
||||
ASSERT_EQ(handle1_1, nullptr);
|
||||
|
||||
// Insert and Lookup the item k1 for the second time and advise erasing it.
|
||||
ASSERT_OK(sec_cache->Insert("k1", &item1,
|
||||
&CompressedSecondaryCacheTest::helper_));
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_insert_real_count, 1);
|
||||
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle1_2 = sec_cache->Lookup(
|
||||
"k1", test_item_creator, true, /*advise_erase=*/true, is_in_sec_cache);
|
||||
ASSERT_NE(handle1_2, nullptr);
|
||||
ASSERT_FALSE(is_in_sec_cache);
|
||||
if (sec_cache_is_compressed) {
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_uncompressed_bytes,
|
||||
1000);
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_compressed_bytes,
|
||||
1007);
|
||||
} else {
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_uncompressed_bytes, 0);
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_compressed_bytes, 0);
|
||||
}
|
||||
|
||||
std::unique_ptr<TestItem> val1 =
|
||||
std::unique_ptr<TestItem>(static_cast<TestItem*>(handle1->Value()));
|
||||
std::unique_ptr<TestItem>(static_cast<TestItem*>(handle1_2->Value()));
|
||||
ASSERT_NE(val1, nullptr);
|
||||
ASSERT_EQ(memcmp(val1->Buf(), item1.Buf(), item1.Size()), 0);
|
||||
|
||||
// Lookup the first item again.
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle1_1 =
|
||||
sec_cache->Lookup("k1", test_item_creator, true, is_in_sec_cache);
|
||||
ASSERT_EQ(handle1_1, nullptr);
|
||||
// Lookup the item k1 again.
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle1_3 = sec_cache->Lookup(
|
||||
"k1", test_item_creator, true, /*advise_erase=*/true, is_in_sec_cache);
|
||||
ASSERT_EQ(handle1_3, nullptr);
|
||||
|
||||
// Insert and Lookup the second item.
|
||||
std::string str2;
|
||||
test::CompressibleString(&rnd, 0.5, 1000, &str2);
|
||||
// Insert and Lookup the item k2.
|
||||
std::string str2(rnd.RandomString(1000));
|
||||
TestItem item2(str2.data(), str2.length());
|
||||
ASSERT_OK(sec_cache->Insert("k2", &item2,
|
||||
&CompressedSecondaryCacheTest::helper_));
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle2 =
|
||||
sec_cache->Lookup("k2", test_item_creator, true, is_in_sec_cache);
|
||||
ASSERT_NE(handle2, nullptr);
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_insert_dummy_count, 2);
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle2_1 = sec_cache->Lookup(
|
||||
"k2", test_item_creator, true, /*advise_erase=*/false, is_in_sec_cache);
|
||||
ASSERT_EQ(handle2_1, nullptr);
|
||||
|
||||
ASSERT_OK(sec_cache->Insert("k2", &item2,
|
||||
&CompressedSecondaryCacheTest::helper_));
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_insert_real_count, 2);
|
||||
if (sec_cache_is_compressed) {
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_uncompressed_bytes,
|
||||
2000);
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_compressed_bytes,
|
||||
2014);
|
||||
} else {
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_uncompressed_bytes, 0);
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_compressed_bytes, 0);
|
||||
}
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle2_2 = sec_cache->Lookup(
|
||||
"k2", test_item_creator, true, /*advise_erase=*/false, is_in_sec_cache);
|
||||
ASSERT_NE(handle2_2, nullptr);
|
||||
std::unique_ptr<TestItem> val2 =
|
||||
std::unique_ptr<TestItem>(static_cast<TestItem*>(handle2->Value()));
|
||||
std::unique_ptr<TestItem>(static_cast<TestItem*>(handle2_2->Value()));
|
||||
ASSERT_NE(val2, nullptr);
|
||||
ASSERT_EQ(memcmp(val2->Buf(), item2.Buf(), item2.Size()), 0);
|
||||
|
||||
std::vector<SecondaryCacheResultHandle*> handles = {handle1.get(),
|
||||
handle2.get()};
|
||||
std::vector<SecondaryCacheResultHandle*> handles = {handle1_2.get(),
|
||||
handle2_2.get()};
|
||||
sec_cache->WaitAll(handles);
|
||||
|
||||
sec_cache.reset();
|
||||
@@ -144,6 +184,7 @@ class CompressedSecondaryCacheTest : public testing::Test {
|
||||
if (!LZ4_Supported()) {
|
||||
ROCKSDB_GTEST_SKIP("This test requires LZ4 support.");
|
||||
opts.compression_type = CompressionType::kNoCompression;
|
||||
sec_cache_is_compressed = false;
|
||||
}
|
||||
} else {
|
||||
opts.compression_type = CompressionType::kNoCompression;
|
||||
@@ -165,7 +206,7 @@ class CompressedSecondaryCacheTest : public testing::Test {
|
||||
std::shared_ptr<SecondaryCache> sec_cache =
|
||||
NewCompressedSecondaryCache(opts);
|
||||
|
||||
BasicTestHelper(sec_cache);
|
||||
BasicTestHelper(sec_cache, sec_cache_is_compressed);
|
||||
}
|
||||
|
||||
void FailsTest(bool sec_cache_is_compressed) {
|
||||
@@ -188,89 +229,152 @@ class CompressedSecondaryCacheTest : public testing::Test {
|
||||
Random rnd(301);
|
||||
std::string str1(rnd.RandomString(1000));
|
||||
TestItem item1(str1.data(), str1.length());
|
||||
// Insert a dummy handle.
|
||||
ASSERT_OK(sec_cache->Insert("k1", &item1,
|
||||
&CompressedSecondaryCacheTest::helper_));
|
||||
// Insert k1.
|
||||
ASSERT_OK(sec_cache->Insert("k1", &item1,
|
||||
&CompressedSecondaryCacheTest::helper_));
|
||||
|
||||
// Insert and Lookup the second item.
|
||||
std::string str2(rnd.RandomString(200));
|
||||
TestItem item2(str2.data(), str2.length());
|
||||
// k1 is evicted.
|
||||
// Insert a dummy handle, k1 is not evicted.
|
||||
ASSERT_OK(sec_cache->Insert("k2", &item2,
|
||||
&CompressedSecondaryCacheTest::helper_));
|
||||
bool is_in_sec_cache{false};
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle1_1 =
|
||||
sec_cache->Lookup("k1", test_item_creator, true, is_in_sec_cache);
|
||||
ASSERT_EQ(handle1_1, nullptr);
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle2 =
|
||||
sec_cache->Lookup("k2", test_item_creator, true, is_in_sec_cache);
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle1 = sec_cache->Lookup(
|
||||
"k1", test_item_creator, true, /*advise_erase=*/false, is_in_sec_cache);
|
||||
ASSERT_EQ(handle1, nullptr);
|
||||
|
||||
// Insert k2 and k1 is evicted.
|
||||
ASSERT_OK(sec_cache->Insert("k2", &item2,
|
||||
&CompressedSecondaryCacheTest::helper_));
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle2 = sec_cache->Lookup(
|
||||
"k2", test_item_creator, true, /*advise_erase=*/false, is_in_sec_cache);
|
||||
ASSERT_NE(handle2, nullptr);
|
||||
std::unique_ptr<TestItem> val2 =
|
||||
std::unique_ptr<TestItem>(static_cast<TestItem*>(handle2->Value()));
|
||||
ASSERT_NE(val2, nullptr);
|
||||
ASSERT_EQ(memcmp(val2->Buf(), item2.Buf(), item2.Size()), 0);
|
||||
|
||||
// Insert k1 again and a dummy handle is inserted.
|
||||
ASSERT_OK(sec_cache->Insert("k1", &item1,
|
||||
&CompressedSecondaryCacheTest::helper_));
|
||||
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle1_1 = sec_cache->Lookup(
|
||||
"k1", test_item_creator, true, /*advise_erase=*/false, is_in_sec_cache);
|
||||
ASSERT_EQ(handle1_1, nullptr);
|
||||
|
||||
// Create Fails.
|
||||
SetFailCreate(true);
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle2_1 =
|
||||
sec_cache->Lookup("k2", test_item_creator, true, is_in_sec_cache);
|
||||
std::unique_ptr<SecondaryCacheResultHandle> handle2_1 = sec_cache->Lookup(
|
||||
"k2", test_item_creator, true, /*advise_erase=*/true, is_in_sec_cache);
|
||||
ASSERT_EQ(handle2_1, nullptr);
|
||||
|
||||
// Save Fails.
|
||||
std::string str3 = rnd.RandomString(10);
|
||||
TestItem item3(str3.data(), str3.length());
|
||||
// The Status is OK because a dummy handle is inserted.
|
||||
ASSERT_OK(sec_cache->Insert("k3", &item3,
|
||||
&CompressedSecondaryCacheTest::helper_fail_));
|
||||
ASSERT_NOK(sec_cache->Insert("k3", &item3,
|
||||
&CompressedSecondaryCacheTest::helper_fail_));
|
||||
|
||||
sec_cache.reset();
|
||||
}
|
||||
|
||||
void BasicIntegrationTest(bool sec_cache_is_compressed) {
|
||||
void BasicIntegrationTest(bool sec_cache_is_compressed,
|
||||
bool enable_custom_split_merge) {
|
||||
CompressedSecondaryCacheOptions secondary_cache_opts;
|
||||
|
||||
if (sec_cache_is_compressed) {
|
||||
if (!LZ4_Supported()) {
|
||||
ROCKSDB_GTEST_SKIP("This test requires LZ4 support.");
|
||||
secondary_cache_opts.compression_type = CompressionType::kNoCompression;
|
||||
sec_cache_is_compressed = false;
|
||||
}
|
||||
} else {
|
||||
secondary_cache_opts.compression_type = CompressionType::kNoCompression;
|
||||
}
|
||||
|
||||
secondary_cache_opts.capacity = 2300;
|
||||
secondary_cache_opts.capacity = 6000;
|
||||
secondary_cache_opts.num_shard_bits = 0;
|
||||
secondary_cache_opts.enable_custom_split_merge = enable_custom_split_merge;
|
||||
std::shared_ptr<SecondaryCache> secondary_cache =
|
||||
NewCompressedSecondaryCache(secondary_cache_opts);
|
||||
LRUCacheOptions lru_cache_opts(
|
||||
1300 /* capacity */, 0 /* num_shard_bits */,
|
||||
false /* strict_capacity_limit */, 0.5 /* high_pri_pool_ratio */,
|
||||
nullptr /* memory_allocator */, kDefaultToAdaptiveMutex,
|
||||
kDefaultCacheMetadataChargePolicy);
|
||||
/*_capacity =*/1300, /*_num_shard_bits =*/0,
|
||||
/*_strict_capacity_limit =*/false, /*_high_pri_pool_ratio =*/0.5,
|
||||
/*_memory_allocator =*/nullptr, kDefaultToAdaptiveMutex,
|
||||
kDefaultCacheMetadataChargePolicy, /*_low_pri_pool_ratio =*/0.0);
|
||||
lru_cache_opts.secondary_cache = secondary_cache;
|
||||
std::shared_ptr<Cache> cache = NewLRUCache(lru_cache_opts);
|
||||
std::shared_ptr<Statistics> stats = CreateDBStatistics();
|
||||
|
||||
get_perf_context()->Reset();
|
||||
Random rnd(301);
|
||||
std::string str1 = rnd.RandomString(1001);
|
||||
TestItem* item1_1 = new TestItem(str1.data(), str1.length());
|
||||
ASSERT_OK(cache->Insert(
|
||||
"k1", item1_1, &CompressedSecondaryCacheTest::helper_, str1.length()));
|
||||
|
||||
std::string str1;
|
||||
test::CompressibleString(&rnd, 0.5, 1001, &str1);
|
||||
std::string str1_clone{str1};
|
||||
TestItem* item1 = new TestItem(str1.data(), str1.length());
|
||||
ASSERT_OK(cache->Insert("k1", item1, &CompressedSecondaryCacheTest::helper_,
|
||||
str1.length()));
|
||||
std::string str2 = rnd.RandomString(1012);
|
||||
TestItem* item2_1 = new TestItem(str2.data(), str2.length());
|
||||
// After this Insert, primary cache contains k2 and secondary cache contains
|
||||
// k1's dummy item.
|
||||
ASSERT_OK(cache->Insert(
|
||||
"k2", item2_1, &CompressedSecondaryCacheTest::helper_, str2.length()));
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_insert_dummy_count, 1);
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_uncompressed_bytes, 0);
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_compressed_bytes, 0);
|
||||
|
||||
std::string str2;
|
||||
test::CompressibleString(&rnd, 0.5, 1012, &str2);
|
||||
TestItem* item2 = new TestItem(str2.data(), str2.length());
|
||||
// After Insert, cache contains k2 and secondary cache contains k1.
|
||||
ASSERT_OK(cache->Insert("k2", item2, &CompressedSecondaryCacheTest::helper_,
|
||||
str2.length()));
|
||||
std::string str3 = rnd.RandomString(1024);
|
||||
TestItem* item3_1 = new TestItem(str3.data(), str3.length());
|
||||
// After this Insert, primary cache contains k3 and secondary cache contains
|
||||
// k1's dummy item and k2's dummy item.
|
||||
ASSERT_OK(cache->Insert(
|
||||
"k3", item3_1, &CompressedSecondaryCacheTest::helper_, str3.length()));
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_insert_dummy_count, 2);
|
||||
|
||||
std::string str3;
|
||||
test::CompressibleString(&rnd, 0.5, 1024, &str3);
|
||||
TestItem* item3 = new TestItem(str3.data(), str3.length());
|
||||
// After Insert, cache contains k3 and secondary cache contains k1 and k2.
|
||||
ASSERT_OK(cache->Insert("k3", item3, &CompressedSecondaryCacheTest::helper_,
|
||||
str3.length()));
|
||||
// After this Insert, primary cache contains k1 and secondary cache contains
|
||||
// k1's dummy item, k2's dummy item, and k3's dummy item.
|
||||
TestItem* item1_2 = new TestItem(str1.data(), str1.length());
|
||||
ASSERT_OK(cache->Insert(
|
||||
"k1", item1_2, &CompressedSecondaryCacheTest::helper_, str1.length()));
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_insert_dummy_count, 3);
|
||||
|
||||
// After this Insert, primary cache contains k2 and secondary cache contains
|
||||
// k1's item, k2's dummy item, and k3's dummy item.
|
||||
TestItem* item2_2 = new TestItem(str2.data(), str2.length());
|
||||
ASSERT_OK(cache->Insert(
|
||||
"k2", item2_2, &CompressedSecondaryCacheTest::helper_, str2.length()));
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_insert_real_count, 1);
|
||||
if (sec_cache_is_compressed) {
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_uncompressed_bytes,
|
||||
str1.length());
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_compressed_bytes,
|
||||
1008);
|
||||
} else {
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_uncompressed_bytes, 0);
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_compressed_bytes, 0);
|
||||
}
|
||||
|
||||
// After this Insert, primary cache contains k3 and secondary cache contains
|
||||
// k1's item and k2's item.
|
||||
TestItem* item3_2 = new TestItem(str3.data(), str3.length());
|
||||
ASSERT_OK(cache->Insert(
|
||||
"k3", item3_2, &CompressedSecondaryCacheTest::helper_, str3.length()));
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_insert_real_count, 2);
|
||||
if (sec_cache_is_compressed) {
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_uncompressed_bytes,
|
||||
str1.length() + str2.length());
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_compressed_bytes,
|
||||
2027);
|
||||
} else {
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_uncompressed_bytes, 0);
|
||||
ASSERT_EQ(get_perf_context()->compressed_sec_cache_compressed_bytes, 0);
|
||||
}
|
||||
|
||||
Cache::Handle* handle;
|
||||
handle = cache->Lookup("k3", &CompressedSecondaryCacheTest::helper_,
|
||||
@@ -279,7 +383,7 @@ class CompressedSecondaryCacheTest : public testing::Test {
|
||||
ASSERT_NE(handle, nullptr);
|
||||
TestItem* val3 = static_cast<TestItem*>(cache->Value(handle));
|
||||
ASSERT_NE(val3, nullptr);
|
||||
ASSERT_EQ(memcmp(val3->Buf(), item3->Buf(), item3->Size()), 0);
|
||||
ASSERT_EQ(memcmp(val3->Buf(), item3_2->Buf(), item3_2->Size()), 0);
|
||||
cache->Release(handle);
|
||||
|
||||
// Lookup an non-existent key.
|
||||
@@ -288,21 +392,33 @@ class CompressedSecondaryCacheTest : public testing::Test {
|
||||
stats.get());
|
||||
ASSERT_EQ(handle, nullptr);
|
||||
|
||||
// This Lookup should promote k1 and erase k1 from the secondary cache,
|
||||
// then k3 is demoted. So k2 and k3 are in the secondary cache.
|
||||
// This Lookup should just insert a dummy handle in the primary cache
|
||||
// and the k1 is still in the secondary cache.
|
||||
handle = cache->Lookup("k1", &CompressedSecondaryCacheTest::helper_,
|
||||
test_item_creator, Cache::Priority::LOW, true,
|
||||
stats.get());
|
||||
ASSERT_NE(handle, nullptr);
|
||||
ASSERT_EQ(get_perf_context()->block_cache_standalone_handle_count, 1);
|
||||
TestItem* val1_1 = static_cast<TestItem*>(cache->Value(handle));
|
||||
ASSERT_NE(val1_1, nullptr);
|
||||
ASSERT_EQ(memcmp(val1_1->Buf(), str1_clone.data(), str1_clone.size()), 0);
|
||||
ASSERT_EQ(memcmp(val1_1->Buf(), str1.data(), str1.size()), 0);
|
||||
cache->Release(handle);
|
||||
|
||||
// This Lookup should erase k1 from the secondary cache and insert
|
||||
// it into primary cache; then k3 is demoted.
|
||||
handle = cache->Lookup("k1", &CompressedSecondaryCacheTest::helper_,
|
||||
test_item_creator, Cache::Priority::LOW, true,
|
||||
stats.get());
|
||||
ASSERT_NE(handle, nullptr);
|
||||
ASSERT_EQ(get_perf_context()->block_cache_standalone_handle_count, 1);
|
||||
cache->Release(handle);
|
||||
|
||||
// k2 is still in secondary cache.
|
||||
handle = cache->Lookup("k2", &CompressedSecondaryCacheTest::helper_,
|
||||
test_item_creator, Cache::Priority::LOW, true,
|
||||
stats.get());
|
||||
ASSERT_NE(handle, nullptr);
|
||||
ASSERT_EQ(get_perf_context()->block_cache_standalone_handle_count, 2);
|
||||
cache->Release(handle);
|
||||
|
||||
cache.reset();
|
||||
@@ -321,22 +437,21 @@ class CompressedSecondaryCacheTest : public testing::Test {
|
||||
secondary_cache_opts.compression_type = CompressionType::kNoCompression;
|
||||
}
|
||||
|
||||
secondary_cache_opts.capacity = 2300;
|
||||
secondary_cache_opts.capacity = 6000;
|
||||
secondary_cache_opts.num_shard_bits = 0;
|
||||
std::shared_ptr<SecondaryCache> secondary_cache =
|
||||
NewCompressedSecondaryCache(secondary_cache_opts);
|
||||
|
||||
LRUCacheOptions opts(
|
||||
1024 /* capacity */, 0 /* num_shard_bits */,
|
||||
false /* strict_capacity_limit */, 0.5 /* high_pri_pool_ratio */,
|
||||
nullptr /* memory_allocator */, kDefaultToAdaptiveMutex,
|
||||
kDefaultCacheMetadataChargePolicy);
|
||||
/*_capacity=*/1300, /*_num_shard_bits=*/0,
|
||||
/*_strict_capacity_limit=*/false, /*_high_pri_pool_ratio=*/0.5,
|
||||
/*_memory_allocator=*/nullptr, kDefaultToAdaptiveMutex,
|
||||
kDefaultCacheMetadataChargePolicy, /*_low_pri_pool_ratio=*/0.0);
|
||||
opts.secondary_cache = secondary_cache;
|
||||
std::shared_ptr<Cache> cache = NewLRUCache(opts);
|
||||
|
||||
Random rnd(301);
|
||||
std::string str1;
|
||||
test::CompressibleString(&rnd, 0.5, 1001, &str1);
|
||||
std::string str1 = rnd.RandomString(1001);
|
||||
auto item1 =
|
||||
std::unique_ptr<TestItem>(new TestItem(str1.data(), str1.length()));
|
||||
ASSERT_NOK(cache->Insert("k1", item1.get(), nullptr, str1.length()));
|
||||
@@ -369,30 +484,28 @@ class CompressedSecondaryCacheTest : public testing::Test {
|
||||
secondary_cache_opts.compression_type = CompressionType::kNoCompression;
|
||||
}
|
||||
|
||||
secondary_cache_opts.capacity = 2300;
|
||||
secondary_cache_opts.capacity = 6000;
|
||||
secondary_cache_opts.num_shard_bits = 0;
|
||||
|
||||
std::shared_ptr<SecondaryCache> secondary_cache =
|
||||
NewCompressedSecondaryCache(secondary_cache_opts);
|
||||
|
||||
LRUCacheOptions opts(
|
||||
1200 /* capacity */, 0 /* num_shard_bits */,
|
||||
false /* strict_capacity_limit */, 0.5 /* high_pri_pool_ratio */,
|
||||
nullptr /* memory_allocator */, kDefaultToAdaptiveMutex,
|
||||
kDefaultCacheMetadataChargePolicy);
|
||||
/*_capacity=*/1300, /*_num_shard_bits=*/0,
|
||||
/*_strict_capacity_limit=*/false, /*_high_pri_pool_ratio=*/0.5,
|
||||
/*_memory_allocator=*/nullptr, kDefaultToAdaptiveMutex,
|
||||
kDefaultCacheMetadataChargePolicy, /*_low_pri_pool_ratio=*/0.0);
|
||||
opts.secondary_cache = secondary_cache;
|
||||
std::shared_ptr<Cache> cache = NewLRUCache(opts);
|
||||
|
||||
Random rnd(301);
|
||||
std::string str1;
|
||||
test::CompressibleString(&rnd, 0.5, 1001, &str1);
|
||||
std::string str1 = rnd.RandomString(1001);
|
||||
TestItem* item1 = new TestItem(str1.data(), str1.length());
|
||||
ASSERT_OK(cache->Insert("k1", item1,
|
||||
&CompressedSecondaryCacheTest::helper_fail_,
|
||||
str1.length()));
|
||||
|
||||
std::string str2;
|
||||
test::CompressibleString(&rnd, 0.5, 1002, &str2);
|
||||
std::string str2 = rnd.RandomString(1002);
|
||||
TestItem* item2 = new TestItem(str2.data(), str2.length());
|
||||
// k1 should be demoted to the secondary cache.
|
||||
ASSERT_OK(cache->Insert("k2", item2,
|
||||
@@ -404,11 +517,11 @@ class CompressedSecondaryCacheTest : public testing::Test {
|
||||
test_item_creator, Cache::Priority::LOW, true);
|
||||
ASSERT_NE(handle, nullptr);
|
||||
cache->Release(handle);
|
||||
// This lookup should fail, since k1 demotion would have failed
|
||||
// This lookup should fail, since k1 demotion would have failed.
|
||||
handle = cache->Lookup("k1", &CompressedSecondaryCacheTest::helper_fail_,
|
||||
test_item_creator, Cache::Priority::LOW, true);
|
||||
ASSERT_EQ(handle, nullptr);
|
||||
// Since k1 didn't get promoted, k2 should still be in cache
|
||||
// Since k1 was not promoted, k2 should still be in cache.
|
||||
handle = cache->Lookup("k2", &CompressedSecondaryCacheTest::helper_fail_,
|
||||
test_item_creator, Cache::Priority::LOW, true);
|
||||
ASSERT_NE(handle, nullptr);
|
||||
@@ -430,29 +543,27 @@ class CompressedSecondaryCacheTest : public testing::Test {
|
||||
secondary_cache_opts.compression_type = CompressionType::kNoCompression;
|
||||
}
|
||||
|
||||
secondary_cache_opts.capacity = 2300;
|
||||
secondary_cache_opts.capacity = 6000;
|
||||
secondary_cache_opts.num_shard_bits = 0;
|
||||
|
||||
std::shared_ptr<SecondaryCache> secondary_cache =
|
||||
NewCompressedSecondaryCache(secondary_cache_opts);
|
||||
|
||||
LRUCacheOptions opts(
|
||||
1200 /* capacity */, 0 /* num_shard_bits */,
|
||||
false /* strict_capacity_limit */, 0.5 /* high_pri_pool_ratio */,
|
||||
nullptr /* memory_allocator */, kDefaultToAdaptiveMutex,
|
||||
kDefaultCacheMetadataChargePolicy);
|
||||
/*_capacity=*/1300, /*_num_shard_bits=*/0,
|
||||
/*_strict_capacity_limit=*/false, /*_high_pri_pool_ratio=*/0.5,
|
||||
/*_memory_allocator=*/nullptr, kDefaultToAdaptiveMutex,
|
||||
kDefaultCacheMetadataChargePolicy, /*_low_pri_pool_ratio=*/0.0);
|
||||
opts.secondary_cache = secondary_cache;
|
||||
std::shared_ptr<Cache> cache = NewLRUCache(opts);
|
||||
|
||||
Random rnd(301);
|
||||
std::string str1;
|
||||
test::CompressibleString(&rnd, 0.5, 1001, &str1);
|
||||
std::string str1 = rnd.RandomString(1001);
|
||||
TestItem* item1 = new TestItem(str1.data(), str1.length());
|
||||
ASSERT_OK(cache->Insert("k1", item1, &CompressedSecondaryCacheTest::helper_,
|
||||
str1.length()));
|
||||
|
||||
std::string str2;
|
||||
test::CompressibleString(&rnd, 0.5, 1002, &str2);
|
||||
std::string str2 = rnd.RandomString(1002);
|
||||
TestItem* item2 = new TestItem(str2.data(), str2.length());
|
||||
// k1 should be demoted to the secondary cache.
|
||||
ASSERT_OK(cache->Insert("k2", item2, &CompressedSecondaryCacheTest::helper_,
|
||||
@@ -490,35 +601,46 @@ class CompressedSecondaryCacheTest : public testing::Test {
|
||||
secondary_cache_opts.compression_type = CompressionType::kNoCompression;
|
||||
}
|
||||
|
||||
secondary_cache_opts.capacity = 2300;
|
||||
secondary_cache_opts.capacity = 6000;
|
||||
secondary_cache_opts.num_shard_bits = 0;
|
||||
|
||||
std::shared_ptr<SecondaryCache> secondary_cache =
|
||||
NewCompressedSecondaryCache(secondary_cache_opts);
|
||||
|
||||
LRUCacheOptions opts(
|
||||
1200 /* capacity */, 0 /* num_shard_bits */,
|
||||
true /* strict_capacity_limit */, 0.5 /* high_pri_pool_ratio */,
|
||||
nullptr /* memory_allocator */, kDefaultToAdaptiveMutex,
|
||||
kDefaultCacheMetadataChargePolicy);
|
||||
/*_capacity=*/1300, /*_num_shard_bits=*/0,
|
||||
/*_strict_capacity_limit=*/false, /*_high_pri_pool_ratio=*/0.5,
|
||||
/*_memory_allocator=*/nullptr, kDefaultToAdaptiveMutex,
|
||||
kDefaultCacheMetadataChargePolicy, /*_low_pri_pool_ratio=*/0.0);
|
||||
opts.secondary_cache = secondary_cache;
|
||||
std::shared_ptr<Cache> cache = NewLRUCache(opts);
|
||||
|
||||
Random rnd(301);
|
||||
std::string str1;
|
||||
test::CompressibleString(&rnd, 0.5, 1001, &str1);
|
||||
TestItem* item1 = new TestItem(str1.data(), str1.length());
|
||||
ASSERT_OK(cache->Insert("k1", item1, &CompressedSecondaryCacheTest::helper_,
|
||||
str1.length()));
|
||||
std::string str1 = rnd.RandomString(1001);
|
||||
TestItem* item1_1 = new TestItem(str1.data(), str1.length());
|
||||
ASSERT_OK(cache->Insert(
|
||||
"k1", item1_1, &CompressedSecondaryCacheTest::helper_, str1.length()));
|
||||
|
||||
std::string str2;
|
||||
test::CompressibleString(&rnd, 0.5, 1002, &str2);
|
||||
std::string str2 = rnd.RandomString(1002);
|
||||
std::string str2_clone{str2};
|
||||
TestItem* item2 = new TestItem(str2.data(), str2.length());
|
||||
// k1 should be demoted to the secondary cache.
|
||||
// After this Insert, primary cache contains k2 and secondary cache contains
|
||||
// k1's dummy item.
|
||||
ASSERT_OK(cache->Insert("k2", item2, &CompressedSecondaryCacheTest::helper_,
|
||||
str2.length()));
|
||||
|
||||
// After this Insert, primary cache contains k1 and secondary cache contains
|
||||
// k1's dummy item and k2's dummy item.
|
||||
TestItem* item1_2 = new TestItem(str1.data(), str1.length());
|
||||
ASSERT_OK(cache->Insert(
|
||||
"k1", item1_2, &CompressedSecondaryCacheTest::helper_, str1.length()));
|
||||
|
||||
TestItem* item2_2 = new TestItem(str2.data(), str2.length());
|
||||
// After this Insert, primary cache contains k2 and secondary cache contains
|
||||
// k1's item and k2's dummy item.
|
||||
ASSERT_OK(cache->Insert(
|
||||
"k2", item2_2, &CompressedSecondaryCacheTest::helper_, str2.length()));
|
||||
|
||||
Cache::Handle* handle2;
|
||||
handle2 = cache->Lookup("k2", &CompressedSecondaryCacheTest::helper_,
|
||||
test_item_creator, Cache::Priority::LOW, true);
|
||||
@@ -527,6 +649,7 @@ class CompressedSecondaryCacheTest : public testing::Test {
|
||||
|
||||
// k1 promotion should fail because cache is at capacity and
|
||||
// strict_capacity_limit is true, but the lookup should still succeed.
|
||||
// A k1's dummy item is inserted into primary cache.
|
||||
Cache::Handle* handle1;
|
||||
handle1 = cache->Lookup("k1", &CompressedSecondaryCacheTest::helper_,
|
||||
test_item_creator, Cache::Priority::LOW, true);
|
||||
@@ -561,8 +684,8 @@ class CompressedSecondaryCacheTest : public testing::Test {
|
||||
std::make_unique<CompressedSecondaryCache>(1000, 0, true, 0.5, 0.0,
|
||||
allocator);
|
||||
Random rnd(301);
|
||||
// 10000 = 8169 + 1769 + 62 , so there should be 3 chunks after split.
|
||||
size_t str_size{10000};
|
||||
// 8500 = 8169 + 233 + 98, so there should be 3 chunks after split.
|
||||
size_t str_size{8500};
|
||||
std::string str = rnd.RandomString(static_cast<int>(str_size));
|
||||
size_t charge{0};
|
||||
CacheValueChunk* chunks_head =
|
||||
@@ -572,11 +695,11 @@ class CompressedSecondaryCacheTest : public testing::Test {
|
||||
CacheValueChunk* current_chunk = chunks_head;
|
||||
ASSERT_EQ(current_chunk->size, 8192 - sizeof(CacheValueChunk) + 1);
|
||||
current_chunk = current_chunk->next;
|
||||
ASSERT_EQ(current_chunk->size, 1792 - sizeof(CacheValueChunk) + 1);
|
||||
ASSERT_EQ(current_chunk->size, 256 - sizeof(CacheValueChunk) + 1);
|
||||
current_chunk = current_chunk->next;
|
||||
ASSERT_EQ(current_chunk->size, 62);
|
||||
ASSERT_EQ(current_chunk->size, 98);
|
||||
|
||||
sec_cache->DeletionCallback("dummy", chunks_head);
|
||||
sec_cache->GetDeletionCallback(true)("dummy", chunks_head);
|
||||
}
|
||||
|
||||
void MergeChunksIntoValueTest() {
|
||||
@@ -618,7 +741,11 @@ class CompressedSecondaryCacheTest : public testing::Test {
|
||||
std::string value_str{value.get(), charge};
|
||||
ASSERT_EQ(strcmp(value_str.data(), str.data()), 0);
|
||||
|
||||
sec_cache->DeletionCallback("dummy", chunks_head);
|
||||
while (chunks_head != nullptr) {
|
||||
CacheValueChunk* tmp_chunk = chunks_head;
|
||||
chunks_head = chunks_head->next;
|
||||
tmp_chunk->Free();
|
||||
}
|
||||
}
|
||||
|
||||
void SplictValueAndMergeChunksTest() {
|
||||
@@ -639,8 +766,8 @@ class CompressedSecondaryCacheTest : public testing::Test {
|
||||
std::make_unique<CompressedSecondaryCache>(1000, 0, true, 0.5, 0.0,
|
||||
allocator);
|
||||
Random rnd(301);
|
||||
// 10000 = 8169 + 1769 + 62 , so there should be 3 chunks after split.
|
||||
size_t str_size{10000};
|
||||
// 8500 = 8169 + 233 + 98, so there should be 3 chunks after split.
|
||||
size_t str_size{8500};
|
||||
std::string str = rnd.RandomString(static_cast<int>(str_size));
|
||||
size_t charge{0};
|
||||
CacheValueChunk* chunks_head =
|
||||
@@ -653,7 +780,7 @@ class CompressedSecondaryCacheTest : public testing::Test {
|
||||
std::string value_str{value.get(), charge};
|
||||
ASSERT_EQ(strcmp(value_str.data(), str.data()), 0);
|
||||
|
||||
sec_cache->DeletionCallback("dummy", chunks_head);
|
||||
sec_cache->GetDeletionCallback(true)("dummy", chunks_head);
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -670,111 +797,150 @@ Cache::CacheItemHelper CompressedSecondaryCacheTest::helper_fail_(
|
||||
CompressedSecondaryCacheTest::SaveToCallbackFail,
|
||||
CompressedSecondaryCacheTest::DeletionCallback);
|
||||
|
||||
TEST_F(CompressedSecondaryCacheTest, BasicTestWithNoCompression) {
|
||||
BasicTest(false, false);
|
||||
class CompressedSecCacheTestWithCompressAndAllocatorParam
|
||||
: public CompressedSecondaryCacheTest,
|
||||
public ::testing::WithParamInterface<std::tuple<bool, bool>> {
|
||||
public:
|
||||
CompressedSecCacheTestWithCompressAndAllocatorParam() {
|
||||
sec_cache_is_compressed_ = std::get<0>(GetParam());
|
||||
use_jemalloc_ = std::get<1>(GetParam());
|
||||
}
|
||||
bool sec_cache_is_compressed_;
|
||||
bool use_jemalloc_;
|
||||
};
|
||||
|
||||
TEST_P(CompressedSecCacheTestWithCompressAndAllocatorParam, BasicTes) {
|
||||
BasicTest(sec_cache_is_compressed_, use_jemalloc_);
|
||||
}
|
||||
|
||||
TEST_F(CompressedSecondaryCacheTest,
|
||||
BasicTestWithMemoryAllocatorAndNoCompression) {
|
||||
BasicTest(false, true);
|
||||
}
|
||||
INSTANTIATE_TEST_CASE_P(CompressedSecCacheTests,
|
||||
CompressedSecCacheTestWithCompressAndAllocatorParam,
|
||||
::testing::Combine(testing::Bool(), testing::Bool()));
|
||||
|
||||
TEST_F(CompressedSecondaryCacheTest, BasicTestWithCompression) {
|
||||
BasicTest(true, false);
|
||||
}
|
||||
|
||||
TEST_F(CompressedSecondaryCacheTest,
|
||||
BasicTestWithMemoryAllocatorAndCompression) {
|
||||
BasicTest(true, true);
|
||||
}
|
||||
class CompressedSecondaryCacheTestWithCompressionParam
|
||||
: public CompressedSecondaryCacheTest,
|
||||
public ::testing::WithParamInterface<bool> {
|
||||
public:
|
||||
CompressedSecondaryCacheTestWithCompressionParam() {
|
||||
sec_cache_is_compressed_ = GetParam();
|
||||
}
|
||||
bool sec_cache_is_compressed_;
|
||||
};
|
||||
|
||||
#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) {
|
||||
TEST_P(CompressedSecondaryCacheTestWithCompressionParam, BasicTestFromString) {
|
||||
std::shared_ptr<SecondaryCache> sec_cache{nullptr};
|
||||
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";
|
||||
if (sec_cache_is_compressed_) {
|
||||
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";
|
||||
sec_cache_is_compressed_ = false;
|
||||
}
|
||||
Status s = SecondaryCache::CreateFromString(ConfigOptions(), sec_cache_uri,
|
||||
&sec_cache);
|
||||
EXPECT_OK(s);
|
||||
} else {
|
||||
ROCKSDB_GTEST_SKIP("This test requires LZ4 support.");
|
||||
sec_cache_uri =
|
||||
"compressed_secondary_cache://"
|
||||
"capacity=2048;num_shard_bits=0;compression_type=kNoCompression";
|
||||
Status s = SecondaryCache::CreateFromString(ConfigOptions(), sec_cache_uri,
|
||||
&sec_cache);
|
||||
EXPECT_OK(s);
|
||||
}
|
||||
BasicTestHelper(sec_cache, sec_cache_is_compressed_);
|
||||
}
|
||||
|
||||
std::shared_ptr<SecondaryCache> sec_cache;
|
||||
Status s = SecondaryCache::CreateFromString(ConfigOptions(), sec_cache_uri,
|
||||
&sec_cache);
|
||||
EXPECT_OK(s);
|
||||
BasicTestHelper(sec_cache);
|
||||
TEST_P(CompressedSecondaryCacheTestWithCompressionParam,
|
||||
BasicTestFromStringWithSplit) {
|
||||
std::shared_ptr<SecondaryCache> sec_cache{nullptr};
|
||||
std::string sec_cache_uri;
|
||||
if (sec_cache_is_compressed_) {
|
||||
if (LZ4_Supported()) {
|
||||
sec_cache_uri =
|
||||
"compressed_secondary_cache://"
|
||||
"capacity=2048;num_shard_bits=0;compression_type=kLZ4Compression;"
|
||||
"compress_format_version=2;enable_custom_split_merge=true";
|
||||
} else {
|
||||
ROCKSDB_GTEST_SKIP("This test requires LZ4 support.");
|
||||
sec_cache_uri =
|
||||
"compressed_secondary_cache://"
|
||||
"capacity=2048;num_shard_bits=0;compression_type=kNoCompression;"
|
||||
"enable_custom_split_merge=true";
|
||||
sec_cache_is_compressed_ = false;
|
||||
}
|
||||
Status s = SecondaryCache::CreateFromString(ConfigOptions(), sec_cache_uri,
|
||||
&sec_cache);
|
||||
EXPECT_OK(s);
|
||||
} else {
|
||||
sec_cache_uri =
|
||||
"compressed_secondary_cache://"
|
||||
"capacity=2048;num_shard_bits=0;compression_type=kNoCompression;"
|
||||
"enable_custom_split_merge=true";
|
||||
Status s = SecondaryCache::CreateFromString(ConfigOptions(), sec_cache_uri,
|
||||
&sec_cache);
|
||||
EXPECT_OK(s);
|
||||
}
|
||||
BasicTestHelper(sec_cache, sec_cache_is_compressed_);
|
||||
}
|
||||
|
||||
#endif // ROCKSDB_LITE
|
||||
|
||||
TEST_F(CompressedSecondaryCacheTest, FailsTestWithNoCompression) {
|
||||
FailsTest(false);
|
||||
TEST_P(CompressedSecondaryCacheTestWithCompressionParam, FailsTest) {
|
||||
FailsTest(sec_cache_is_compressed_);
|
||||
}
|
||||
|
||||
TEST_F(CompressedSecondaryCacheTest, FailsTestWithCompression) {
|
||||
FailsTest(true);
|
||||
TEST_P(CompressedSecondaryCacheTestWithCompressionParam,
|
||||
BasicIntegrationFailTest) {
|
||||
BasicIntegrationFailTest(sec_cache_is_compressed_);
|
||||
}
|
||||
|
||||
TEST_F(CompressedSecondaryCacheTest, BasicIntegrationTestWithNoCompression) {
|
||||
BasicIntegrationTest(false);
|
||||
TEST_P(CompressedSecondaryCacheTestWithCompressionParam,
|
||||
IntegrationSaveFailTest) {
|
||||
IntegrationSaveFailTest(sec_cache_is_compressed_);
|
||||
}
|
||||
|
||||
TEST_F(CompressedSecondaryCacheTest, BasicIntegrationTestWithCompression) {
|
||||
BasicIntegrationTest(true);
|
||||
TEST_P(CompressedSecondaryCacheTestWithCompressionParam,
|
||||
IntegrationCreateFailTest) {
|
||||
IntegrationCreateFailTest(sec_cache_is_compressed_);
|
||||
}
|
||||
|
||||
TEST_F(CompressedSecondaryCacheTest,
|
||||
BasicIntegrationFailTestWithNoCompression) {
|
||||
BasicIntegrationFailTest(false);
|
||||
TEST_P(CompressedSecondaryCacheTestWithCompressionParam,
|
||||
IntegrationFullCapacityTest) {
|
||||
IntegrationFullCapacityTest(sec_cache_is_compressed_);
|
||||
}
|
||||
|
||||
TEST_F(CompressedSecondaryCacheTest, BasicIntegrationFailTestWithCompression) {
|
||||
BasicIntegrationFailTest(true);
|
||||
INSTANTIATE_TEST_CASE_P(CompressedSecCacheTests,
|
||||
CompressedSecondaryCacheTestWithCompressionParam,
|
||||
testing::Bool());
|
||||
|
||||
class CompressedSecCacheTestWithCompressAndSplitParam
|
||||
: public CompressedSecondaryCacheTest,
|
||||
public ::testing::WithParamInterface<std::tuple<bool, bool>> {
|
||||
public:
|
||||
CompressedSecCacheTestWithCompressAndSplitParam() {
|
||||
sec_cache_is_compressed_ = std::get<0>(GetParam());
|
||||
enable_custom_split_merge_ = std::get<1>(GetParam());
|
||||
}
|
||||
bool sec_cache_is_compressed_;
|
||||
bool enable_custom_split_merge_;
|
||||
};
|
||||
|
||||
TEST_P(CompressedSecCacheTestWithCompressAndSplitParam, BasicIntegrationTest) {
|
||||
BasicIntegrationTest(sec_cache_is_compressed_, enable_custom_split_merge_);
|
||||
}
|
||||
|
||||
TEST_F(CompressedSecondaryCacheTest, IntegrationSaveFailTestWithNoCompression) {
|
||||
IntegrationSaveFailTest(false);
|
||||
}
|
||||
|
||||
TEST_F(CompressedSecondaryCacheTest, IntegrationSaveFailTestWithCompression) {
|
||||
IntegrationSaveFailTest(true);
|
||||
}
|
||||
|
||||
TEST_F(CompressedSecondaryCacheTest,
|
||||
IntegrationCreateFailTestWithNoCompression) {
|
||||
IntegrationCreateFailTest(false);
|
||||
}
|
||||
|
||||
TEST_F(CompressedSecondaryCacheTest, IntegrationCreateFailTestWithCompression) {
|
||||
IntegrationCreateFailTest(true);
|
||||
}
|
||||
|
||||
TEST_F(CompressedSecondaryCacheTest,
|
||||
IntegrationFullCapacityTestWithNoCompression) {
|
||||
IntegrationFullCapacityTest(false);
|
||||
}
|
||||
|
||||
TEST_F(CompressedSecondaryCacheTest,
|
||||
IntegrationFullCapacityTestWithCompression) {
|
||||
IntegrationFullCapacityTest(true);
|
||||
}
|
||||
INSTANTIATE_TEST_CASE_P(CompressedSecCacheTests,
|
||||
CompressedSecCacheTestWithCompressAndSplitParam,
|
||||
::testing::Combine(testing::Bool(), testing::Bool()));
|
||||
|
||||
TEST_F(CompressedSecondaryCacheTest, SplitValueIntoChunksTest) {
|
||||
SplitValueIntoChunksTest();
|
||||
|
||||
Vendored
+12
-2
@@ -173,13 +173,13 @@ inline int LRUHandleTable::FindSlot(const Slice& key,
|
||||
LRUCacheShard::LRUCacheShard(size_t capacity, size_t estimated_value_size,
|
||||
bool strict_capacity_limit,
|
||||
CacheMetadataChargePolicy metadata_charge_policy)
|
||||
: capacity_(capacity),
|
||||
: CacheShard(metadata_charge_policy),
|
||||
capacity_(capacity),
|
||||
strict_capacity_limit_(strict_capacity_limit),
|
||||
table_(
|
||||
CalcHashBits(capacity, estimated_value_size, metadata_charge_policy)),
|
||||
usage_(0),
|
||||
lru_usage_(0) {
|
||||
set_metadata_charge_policy(metadata_charge_policy);
|
||||
// Make empty circular linked list.
|
||||
lru_.next = &lru_;
|
||||
lru_.prev = &lru_;
|
||||
@@ -525,6 +525,16 @@ size_t LRUCacheShard::GetPinnedUsage() const {
|
||||
return usage_ - lru_usage_;
|
||||
}
|
||||
|
||||
size_t LRUCacheShard::GetOccupancyCount() const {
|
||||
DMutexLock l(mutex_);
|
||||
return table_.GetOccupancy();
|
||||
}
|
||||
|
||||
size_t LRUCacheShard::GetTableAddressCount() const {
|
||||
DMutexLock l(mutex_);
|
||||
return table_.GetTableSize();
|
||||
}
|
||||
|
||||
std::string LRUCacheShard::GetPrintableOptions() const { return std::string{}; }
|
||||
|
||||
LRUCache::LRUCache(size_t capacity, size_t estimated_value_size,
|
||||
|
||||
Vendored
+2
@@ -368,6 +368,8 @@ class ALIGN_AS(CACHE_LINE_SIZE) LRUCacheShard final : public CacheShard {
|
||||
|
||||
size_t GetUsage() const override;
|
||||
size_t GetPinnedUsage() const override;
|
||||
size_t GetOccupancyCount() const override;
|
||||
size_t GetTableAddressCount() const override;
|
||||
|
||||
void ApplyToSomeEntries(
|
||||
const std::function<void(const Slice& key, void* value, size_t charge,
|
||||
|
||||
Vendored
+133
-47
@@ -12,6 +12,7 @@
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
|
||||
#include "monitoring/perf_context_imp.h"
|
||||
#include "monitoring/statistics.h"
|
||||
@@ -114,7 +115,8 @@ LRUCacheShard::LRUCacheShard(
|
||||
double low_pri_pool_ratio, bool use_adaptive_mutex,
|
||||
CacheMetadataChargePolicy metadata_charge_policy, int max_upper_hash_bits,
|
||||
const std::shared_ptr<SecondaryCache>& secondary_cache)
|
||||
: capacity_(0),
|
||||
: CacheShard(metadata_charge_policy),
|
||||
capacity_(0),
|
||||
high_pri_pool_usage_(0),
|
||||
low_pri_pool_usage_(0),
|
||||
strict_capacity_limit_(strict_capacity_limit),
|
||||
@@ -127,7 +129,6 @@ LRUCacheShard::LRUCacheShard(
|
||||
lru_usage_(0),
|
||||
mutex_(use_adaptive_mutex),
|
||||
secondary_cache_(secondary_cache) {
|
||||
set_metadata_charge_policy(metadata_charge_policy);
|
||||
// Make empty circular linked list.
|
||||
lru_.next = &lru_;
|
||||
lru_.prev = &lru_;
|
||||
@@ -329,6 +330,19 @@ void LRUCacheShard::EvictFromLRU(size_t charge,
|
||||
}
|
||||
}
|
||||
|
||||
void LRUCacheShard::TryInsertIntoSecondaryCache(
|
||||
autovector<LRUHandle*> evicted_handles) {
|
||||
for (auto entry : evicted_handles) {
|
||||
if (secondary_cache_ && entry->IsSecondaryCacheCompatible() &&
|
||||
!entry->IsInSecondaryCache()) {
|
||||
secondary_cache_->Insert(entry->key(), entry->value, entry->info_.helper)
|
||||
.PermitUncheckedError();
|
||||
}
|
||||
// Free the entries here outside of mutex for performance reasons.
|
||||
entry->Free();
|
||||
}
|
||||
}
|
||||
|
||||
void LRUCacheShard::SetCapacity(size_t capacity) {
|
||||
autovector<LRUHandle*> last_reference_list;
|
||||
{
|
||||
@@ -339,16 +353,7 @@ void LRUCacheShard::SetCapacity(size_t capacity) {
|
||||
EvictFromLRU(0, &last_reference_list);
|
||||
}
|
||||
|
||||
// Try to insert the evicted entries into tiered cache.
|
||||
// Free the entries outside of mutex for performance reasons.
|
||||
for (auto entry : last_reference_list) {
|
||||
if (secondary_cache_ && entry->IsSecondaryCacheCompatible() &&
|
||||
!entry->IsInSecondaryCache()) {
|
||||
secondary_cache_->Insert(entry->key(), entry->value, entry->info_.helper)
|
||||
.PermitUncheckedError();
|
||||
}
|
||||
entry->Free();
|
||||
}
|
||||
TryInsertIntoSecondaryCache(last_reference_list);
|
||||
}
|
||||
|
||||
void LRUCacheShard::SetStrictCapacityLimit(bool strict_capacity_limit) {
|
||||
@@ -377,7 +382,7 @@ Status LRUCacheShard::InsertItem(LRUHandle* e, Cache::Handle** handle,
|
||||
last_reference_list.push_back(e);
|
||||
} else {
|
||||
if (free_handle_on_fail) {
|
||||
delete[] reinterpret_cast<char*>(e);
|
||||
free(e);
|
||||
*handle = nullptr;
|
||||
}
|
||||
s = Status::MemoryLimit("Insert failed due to LRU cache being full.");
|
||||
@@ -411,16 +416,7 @@ Status LRUCacheShard::InsertItem(LRUHandle* e, Cache::Handle** handle,
|
||||
}
|
||||
}
|
||||
|
||||
// Try to insert the evicted entries into the secondary cache.
|
||||
// Free the entries here outside of mutex for performance reasons.
|
||||
for (auto entry : last_reference_list) {
|
||||
if (secondary_cache_ && entry->IsSecondaryCacheCompatible() &&
|
||||
!entry->IsInSecondaryCache()) {
|
||||
secondary_cache_->Insert(entry->key(), entry->value, entry->info_.helper)
|
||||
.PermitUncheckedError();
|
||||
}
|
||||
entry->Free();
|
||||
}
|
||||
TryInsertIntoSecondaryCache(last_reference_list);
|
||||
|
||||
return s;
|
||||
}
|
||||
@@ -430,23 +426,70 @@ void LRUCacheShard::Promote(LRUHandle* e) {
|
||||
|
||||
assert(secondary_handle->IsReady());
|
||||
e->SetIncomplete(false);
|
||||
e->SetInCache(true);
|
||||
e->SetInCache(false);
|
||||
e->value = secondary_handle->Value();
|
||||
e->CalcTotalCharge(secondary_handle->Size(), metadata_charge_policy_);
|
||||
delete secondary_handle;
|
||||
|
||||
// This call could fail if the cache is over capacity and
|
||||
// strict_capacity_limit_ is true. In such a case, we don't want
|
||||
// InsertItem() to free the handle, since the item is already in memory
|
||||
// and the caller will most likely just read from disk if we erase it here.
|
||||
if (e->value) {
|
||||
Cache::Handle* handle = reinterpret_cast<Cache::Handle*>(e);
|
||||
Status s = InsertItem(e, &handle, /*free_handle_on_fail=*/false);
|
||||
Status s;
|
||||
if (secondary_cache_ && secondary_cache_->SupportForceErase() &&
|
||||
e->IsStandalone()) {
|
||||
// Insert a dummy handle and return a standalone handle to caller.
|
||||
// Charge the standalone handle.
|
||||
autovector<LRUHandle*> last_reference_list;
|
||||
bool free_standalone_handle{false};
|
||||
{
|
||||
DMutexLock l(mutex_);
|
||||
|
||||
// Free the space following strict LRU policy until enough space
|
||||
// is freed or the lru list is empty.
|
||||
EvictFromLRU(e->total_charge, &last_reference_list);
|
||||
|
||||
if ((usage_ + e->total_charge) > capacity_ && strict_capacity_limit_) {
|
||||
free_standalone_handle = true;
|
||||
} else {
|
||||
usage_ += e->total_charge;
|
||||
}
|
||||
}
|
||||
|
||||
TryInsertIntoSecondaryCache(last_reference_list);
|
||||
if (free_standalone_handle) {
|
||||
e->Unref();
|
||||
e->Free();
|
||||
e = nullptr;
|
||||
} else {
|
||||
PERF_COUNTER_ADD(block_cache_standalone_handle_count, 1);
|
||||
}
|
||||
|
||||
// Insert a dummy handle into the primary cache. This dummy handle is
|
||||
// not IsSecondaryCacheCompatible().
|
||||
Cache::Priority priority =
|
||||
e->IsHighPri() ? Cache::Priority::HIGH : Cache::Priority::LOW;
|
||||
s = Insert(e->key(), e->hash, /*value=*/nullptr, 0,
|
||||
/*deleter=*/nullptr, /*helper=*/nullptr, /*handle=*/nullptr,
|
||||
priority);
|
||||
} else {
|
||||
e->SetInCache(true);
|
||||
e->SetIsStandalone(false);
|
||||
Cache::Handle* handle = reinterpret_cast<Cache::Handle*>(e);
|
||||
// This InsertItem() could fail if the cache is over capacity and
|
||||
// strict_capacity_limit_ is true. In such a case, we don't want
|
||||
// InsertItem() to free the handle, since the item is already in memory
|
||||
// and the caller will most likely just read it from disk if we erase it
|
||||
// here.
|
||||
s = InsertItem(e, &handle, /*free_handle_on_fail=*/false);
|
||||
if (s.ok()) {
|
||||
PERF_COUNTER_ADD(block_cache_real_handle_count, 1);
|
||||
}
|
||||
}
|
||||
|
||||
if (!s.ok()) {
|
||||
// Item is in memory, but not accounted against the cache capacity.
|
||||
// When the handle is released, the item should get deleted.
|
||||
assert(!e->InCache());
|
||||
}
|
||||
|
||||
} else {
|
||||
// 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
|
||||
@@ -454,6 +497,7 @@ void LRUCacheShard::Promote(LRUHandle* e) {
|
||||
// TODO
|
||||
e->CalcTotalCharge(0, metadata_charge_policy_);
|
||||
e->SetInCache(false);
|
||||
e->SetIsStandalone(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -463,37 +507,61 @@ Cache::Handle* LRUCacheShard::Lookup(
|
||||
const ShardedCache::CreateCallback& create_cb, Cache::Priority priority,
|
||||
bool wait, Statistics* stats) {
|
||||
LRUHandle* e = nullptr;
|
||||
bool found_dummy_entry{false};
|
||||
{
|
||||
DMutexLock l(mutex_);
|
||||
e = table_.Lookup(key, hash);
|
||||
if (e != nullptr) {
|
||||
assert(e->InCache());
|
||||
if (!e->HasRefs()) {
|
||||
// The entry is in LRU since it's in hash and has no external references
|
||||
// The entry is in LRU since it's in hash and has no external
|
||||
// references.
|
||||
LRU_Remove(e);
|
||||
}
|
||||
e->Ref();
|
||||
e->SetHit();
|
||||
|
||||
// For a dummy handle, if it was retrieved from secondary cache,
|
||||
// it may still exist in secondary cache.
|
||||
// If the handle exists in secondary cache, the value should be
|
||||
// erased from sec cache and be inserted into primary cache.
|
||||
if (!e->value && secondary_cache_ &&
|
||||
secondary_cache_->SupportForceErase()) {
|
||||
found_dummy_entry = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If handle table lookup failed, then allocate a handle outside the
|
||||
// mutex if we're going to lookup in the secondary cache.
|
||||
// If handle table lookup failed or the handle is a dummy one, allocate
|
||||
// a handle outside the mutex if we re going to lookup in the secondary cache.
|
||||
//
|
||||
// When a block is firstly Lookup from CompressedSecondaryCache, we just
|
||||
// insert a dummy block into the primary cache (charging the actual size of
|
||||
// the block) and don't erase the block from CompressedSecondaryCache. A
|
||||
// standalone handle is returned to the caller. Only if the block is hit
|
||||
// again, we erase it from CompressedSecondaryCache and add it into the
|
||||
// primary cache.
|
||||
//
|
||||
// Only support synchronous for now.
|
||||
// TODO: Support asynchronous lookup in secondary cache
|
||||
if (!e && secondary_cache_ && helper && helper->saveto_cb) {
|
||||
if ((!e || found_dummy_entry) && secondary_cache_ && helper &&
|
||||
helper->saveto_cb) {
|
||||
// For objects from the secondary cache, we expect the caller to provide
|
||||
// a way to create/delete the primary cache object. The only case where
|
||||
// a deleter would not be required is for dummy entries inserted for
|
||||
// accounting purposes, which we won't demote to the secondary cache
|
||||
// anyway.
|
||||
assert(create_cb && helper->del_cb);
|
||||
// Release the dummy handle.
|
||||
if (e) {
|
||||
Release(reinterpret_cast<Cache::Handle*>(e), true /*erase_if_last_ref*/);
|
||||
}
|
||||
bool is_in_sec_cache{false};
|
||||
std::unique_ptr<SecondaryCacheResultHandle> secondary_handle =
|
||||
secondary_cache_->Lookup(key, create_cb, wait, is_in_sec_cache);
|
||||
secondary_cache_->Lookup(key, create_cb, wait, found_dummy_entry,
|
||||
is_in_sec_cache);
|
||||
if (secondary_handle != nullptr) {
|
||||
e = reinterpret_cast<LRUHandle*>(
|
||||
new char[sizeof(LRUHandle) - 1 + key.size()]);
|
||||
e = static_cast<LRUHandle*>(malloc(sizeof(LRUHandle) - 1 + key.size()));
|
||||
|
||||
e->flags = 0;
|
||||
e->SetSecondaryCacheCompatible(true);
|
||||
@@ -510,16 +578,22 @@ Cache::Handle* LRUCacheShard::Lookup(
|
||||
e->Ref();
|
||||
e->SetIsInSecondaryCache(is_in_sec_cache);
|
||||
|
||||
if (secondary_cache_->SupportForceErase() && !found_dummy_entry) {
|
||||
e->SetIsStandalone(true);
|
||||
}
|
||||
|
||||
if (wait) {
|
||||
Promote(e);
|
||||
if (!e->value) {
|
||||
// The secondary cache returned a handle, but the lookup failed.
|
||||
e->Unref();
|
||||
e->Free();
|
||||
e = nullptr;
|
||||
} else {
|
||||
PERF_COUNTER_ADD(secondary_cache_hit_count, 1);
|
||||
RecordTick(stats, SECONDARY_CACHE_HITS);
|
||||
if (e) {
|
||||
if (!e->value) {
|
||||
// The secondary cache returned a handle, but the lookup failed.
|
||||
e->Unref();
|
||||
e->Free();
|
||||
e = nullptr;
|
||||
} else {
|
||||
PERF_COUNTER_ADD(secondary_cache_hit_count, 1);
|
||||
RecordTick(stats, SECONDARY_CACHE_HITS);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// If wait is false, we always return a handle and let the caller
|
||||
@@ -530,6 +604,8 @@ Cache::Handle* LRUCacheShard::Lookup(
|
||||
PERF_COUNTER_ADD(secondary_cache_hit_count, 1);
|
||||
RecordTick(stats, SECONDARY_CACHE_HITS);
|
||||
}
|
||||
} else {
|
||||
e = nullptr;
|
||||
}
|
||||
}
|
||||
return reinterpret_cast<Cache::Handle*>(e);
|
||||
@@ -607,8 +683,8 @@ Status LRUCacheShard::Insert(const Slice& key, uint32_t hash, void* value,
|
||||
// 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()]);
|
||||
LRUHandle* e =
|
||||
static_cast<LRUHandle*>(malloc(sizeof(LRUHandle) - 1 + key.size()));
|
||||
|
||||
e->value = value;
|
||||
e->flags = 0;
|
||||
@@ -682,6 +758,16 @@ size_t LRUCacheShard::GetPinnedUsage() const {
|
||||
return usage_ - lru_usage_;
|
||||
}
|
||||
|
||||
size_t LRUCacheShard::GetOccupancyCount() const {
|
||||
DMutexLock l(mutex_);
|
||||
return table_.GetOccupancyCount();
|
||||
}
|
||||
|
||||
size_t LRUCacheShard::GetTableAddressCount() const {
|
||||
DMutexLock l(mutex_);
|
||||
return size_t{1} << table_.GetLengthBits();
|
||||
}
|
||||
|
||||
std::string LRUCacheShard::GetPrintableOptions() const {
|
||||
const int kBufferSize = 200;
|
||||
char buffer[kBufferSize];
|
||||
|
||||
Vendored
+26
-3
@@ -40,8 +40,11 @@ namespace lru_cache {
|
||||
// In that case the entry is not in the LRU list and not in hash table.
|
||||
// The entry can be freed when refs becomes 0.
|
||||
// (refs >= 1 && in_cache == false)
|
||||
//
|
||||
// All newly created LRUHandles are in state 1. If you call
|
||||
// 4. The handle is never inserted into the LRUCache (both hash table and LRU
|
||||
// list) and it doesn't experience the above three states.
|
||||
// The entry can be freed when refs becomes 0.
|
||||
// (refs >= 1 && in_cache == false && IS_STANDALONE == true)
|
||||
// All newly created LRUHandles are in state 1 or 4. If you call
|
||||
// LRUCacheShard::Release on entry in state 1, it will go into state 2.
|
||||
// To move from state 1 to state 3, either call LRUCacheShard::Erase or
|
||||
// LRUCacheShard::Insert with the same key (but possibly different value).
|
||||
@@ -93,6 +96,9 @@ struct LRUHandle {
|
||||
IS_LOW_PRI = (1 << 7),
|
||||
// Whether this entry is in low-pri pool.
|
||||
IN_LOW_PRI_POOL = (1 << 8),
|
||||
// Whether this entry is not inserted into the cache (both hash table and
|
||||
// LRU list).
|
||||
IS_STANDALONE = (1 << 9),
|
||||
};
|
||||
|
||||
uint16_t flags;
|
||||
@@ -138,6 +144,7 @@ struct LRUHandle {
|
||||
}
|
||||
bool IsPending() const { return flags & IS_PENDING; }
|
||||
bool IsInSecondaryCache() const { return flags & IS_IN_SECONDARY_CACHE; }
|
||||
bool IsStandalone() const { return flags & IS_STANDALONE; }
|
||||
|
||||
void SetInCache(bool in_cache) {
|
||||
if (in_cache) {
|
||||
@@ -205,6 +212,14 @@ struct LRUHandle {
|
||||
}
|
||||
}
|
||||
|
||||
void SetIsStandalone(bool is_standalone) {
|
||||
if (is_standalone) {
|
||||
flags |= IS_STANDALONE;
|
||||
} else {
|
||||
flags &= ~IS_STANDALONE;
|
||||
}
|
||||
}
|
||||
|
||||
void Free() {
|
||||
assert(refs == 0);
|
||||
#ifdef __SANITIZE_THREAD__
|
||||
@@ -226,7 +241,8 @@ struct LRUHandle {
|
||||
(*info_.helper->del_cb)(key(), value);
|
||||
}
|
||||
}
|
||||
delete[] reinterpret_cast<char*>(this);
|
||||
|
||||
free(this);
|
||||
}
|
||||
|
||||
inline size_t CalcuMetaCharge(
|
||||
@@ -290,6 +306,8 @@ class LRUHandleTable {
|
||||
|
||||
int GetLengthBits() const { return length_bits_; }
|
||||
|
||||
size_t GetOccupancyCount() const { return elems_; }
|
||||
|
||||
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
|
||||
@@ -379,6 +397,8 @@ class ALIGN_AS(CACHE_LINE_SIZE) LRUCacheShard final : public CacheShard {
|
||||
|
||||
virtual size_t GetUsage() const override;
|
||||
virtual size_t GetPinnedUsage() const override;
|
||||
virtual size_t GetOccupancyCount() const override;
|
||||
virtual size_t GetTableAddressCount() const override;
|
||||
|
||||
virtual void ApplyToSomeEntries(
|
||||
const std::function<void(const Slice& key, void* value, size_t charge,
|
||||
@@ -435,6 +455,9 @@ class ALIGN_AS(CACHE_LINE_SIZE) LRUCacheShard final : public CacheShard {
|
||||
// holding the mutex_.
|
||||
void EvictFromLRU(size_t charge, autovector<LRUHandle*>* deleted);
|
||||
|
||||
// Try to insert the evicted handles into the secondary cache.
|
||||
void TryInsertIntoSecondaryCache(autovector<LRUHandle*> evicted_handles);
|
||||
|
||||
// Initialized before use.
|
||||
size_t capacity_;
|
||||
|
||||
|
||||
Vendored
+422
-97
@@ -46,10 +46,11 @@ class LRUCacheTest : public testing::Test {
|
||||
DeleteCache();
|
||||
cache_ = reinterpret_cast<LRUCacheShard*>(
|
||||
port::cacheline_aligned_alloc(sizeof(LRUCacheShard)));
|
||||
new (cache_) LRUCacheShard(
|
||||
capacity, false /*strict_capcity_limit*/, high_pri_pool_ratio,
|
||||
low_pri_pool_ratio, use_adaptive_mutex, kDontChargeCacheMetadata,
|
||||
24 /*max_upper_hash_bits*/, nullptr /*secondary_cache*/);
|
||||
new (cache_) LRUCacheShard(capacity, /*strict_capacity_limit=*/false,
|
||||
high_pri_pool_ratio, low_pri_pool_ratio,
|
||||
use_adaptive_mutex, kDontChargeCacheMetadata,
|
||||
/*max_upper_hash_bits=*/24,
|
||||
/*secondary_cache=*/nullptr);
|
||||
}
|
||||
|
||||
void Insert(const std::string& key,
|
||||
@@ -505,7 +506,7 @@ TEST_F(FastLRUCacheTest, CalcHashBitsTest) {
|
||||
|
||||
} // namespace fast_lru_cache
|
||||
|
||||
namespace clock_cache {
|
||||
namespace hyper_clock_cache {
|
||||
|
||||
class ClockCacheTest : public testing::Test {
|
||||
public:
|
||||
@@ -520,11 +521,11 @@ class ClockCacheTest : public testing::Test {
|
||||
}
|
||||
}
|
||||
|
||||
void NewShard(size_t capacity) {
|
||||
void NewShard(size_t capacity, bool strict_capacity_limit = true) {
|
||||
DeleteShard();
|
||||
shard_ = reinterpret_cast<ClockCacheShard*>(
|
||||
port::cacheline_aligned_alloc(sizeof(ClockCacheShard)));
|
||||
new (shard_) ClockCacheShard(capacity, 1, true /*strict_capacity_limit*/,
|
||||
new (shard_) ClockCacheShard(capacity, 1, strict_capacity_limit,
|
||||
kDontChargeCacheMetadata);
|
||||
}
|
||||
|
||||
@@ -538,21 +539,26 @@ class ClockCacheTest : public testing::Test {
|
||||
return Insert(std::string(kCacheKeySize, key), priority);
|
||||
}
|
||||
|
||||
Status Insert(char key, size_t len) { return Insert(std::string(len, key)); }
|
||||
Status InsertWithLen(char key, size_t len) {
|
||||
return Insert(std::string(len, key));
|
||||
}
|
||||
|
||||
bool Lookup(const std::string& key) {
|
||||
bool Lookup(const std::string& key, bool useful = true) {
|
||||
auto handle = shard_->Lookup(key, 0 /*hash*/);
|
||||
if (handle) {
|
||||
shard_->Release(handle);
|
||||
shard_->Release(handle, useful, /*erase_if_last_ref=*/false);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Lookup(char key) { return Lookup(std::string(kCacheKeySize, key)); }
|
||||
bool Lookup(char key, bool useful = true) {
|
||||
return Lookup(std::string(kCacheKeySize, key), useful);
|
||||
}
|
||||
|
||||
void Erase(const std::string& key) { shard_->Erase(key, 0 /*hash*/); }
|
||||
|
||||
#if 0 // FIXME
|
||||
size_t CalcEstimatedHandleChargeWrapper(
|
||||
size_t estimated_value_size,
|
||||
CacheMetadataChargePolicy metadata_charge_policy) {
|
||||
@@ -582,109 +588,426 @@ class ClockCacheTest : public testing::Test {
|
||||
(1 << (hash_bits - 1) <= max_occupancy);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
private:
|
||||
ClockCacheShard* shard_ = nullptr;
|
||||
};
|
||||
|
||||
TEST_F(ClockCacheTest, Validate) {
|
||||
TEST_F(ClockCacheTest, Misc) {
|
||||
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));
|
||||
|
||||
// Key size stuff
|
||||
EXPECT_OK(InsertWithLen('a', 16));
|
||||
EXPECT_NOK(InsertWithLen('b', 15));
|
||||
EXPECT_OK(InsertWithLen('b', 16));
|
||||
EXPECT_NOK(InsertWithLen('c', 17));
|
||||
EXPECT_NOK(InsertWithLen('d', 1000));
|
||||
EXPECT_NOK(InsertWithLen('e', 11));
|
||||
EXPECT_NOK(InsertWithLen('f', 0));
|
||||
|
||||
// Some of this is motivated by code coverage
|
||||
std::string wrong_size_key(15, 'x');
|
||||
EXPECT_FALSE(Lookup(wrong_size_key));
|
||||
EXPECT_FALSE(shard_->Ref(nullptr));
|
||||
EXPECT_FALSE(shard_->Release(nullptr));
|
||||
shard_->Erase(wrong_size_key, /*hash*/ 42); // no-op
|
||||
}
|
||||
|
||||
TEST_F(ClockCacheTest, ClockPriorityTest) {
|
||||
ClockHandle handle;
|
||||
EXPECT_EQ(handle.GetClockPriority(), ClockHandle::ClockPriority::NONE);
|
||||
handle.SetClockPriority(ClockHandle::ClockPriority::HIGH);
|
||||
EXPECT_EQ(handle.GetClockPriority(), ClockHandle::ClockPriority::HIGH);
|
||||
handle.DecreaseClockPriority();
|
||||
EXPECT_EQ(handle.GetClockPriority(), ClockHandle::ClockPriority::MEDIUM);
|
||||
handle.DecreaseClockPriority();
|
||||
EXPECT_EQ(handle.GetClockPriority(), ClockHandle::ClockPriority::LOW);
|
||||
handle.SetClockPriority(ClockHandle::ClockPriority::MEDIUM);
|
||||
EXPECT_EQ(handle.GetClockPriority(), ClockHandle::ClockPriority::MEDIUM);
|
||||
handle.SetClockPriority(ClockHandle::ClockPriority::NONE);
|
||||
EXPECT_EQ(handle.GetClockPriority(), ClockHandle::ClockPriority::NONE);
|
||||
handle.SetClockPriority(ClockHandle::ClockPriority::MEDIUM);
|
||||
EXPECT_EQ(handle.GetClockPriority(), ClockHandle::ClockPriority::MEDIUM);
|
||||
handle.DecreaseClockPriority();
|
||||
handle.DecreaseClockPriority();
|
||||
EXPECT_EQ(handle.GetClockPriority(), ClockHandle::ClockPriority::NONE);
|
||||
TEST_F(ClockCacheTest, Limits) {
|
||||
NewShard(3, false /*strict_capacity_limit*/);
|
||||
for (bool strict_capacity_limit : {false, true, false}) {
|
||||
SCOPED_TRACE("strict_capacity_limit = " +
|
||||
std::to_string(strict_capacity_limit));
|
||||
|
||||
// Also tests switching between strict limit and not
|
||||
shard_->SetStrictCapacityLimit(strict_capacity_limit);
|
||||
|
||||
std::string key(16, 'x');
|
||||
|
||||
// Single entry charge beyond capacity
|
||||
{
|
||||
Status s = shard_->Insert(key, 0 /*hash*/, nullptr /*value*/,
|
||||
5 /*charge*/, nullptr /*deleter*/,
|
||||
nullptr /*handle*/, Cache::Priority::LOW);
|
||||
if (strict_capacity_limit) {
|
||||
EXPECT_TRUE(s.IsMemoryLimit());
|
||||
} else {
|
||||
EXPECT_OK(s);
|
||||
}
|
||||
}
|
||||
|
||||
// Single entry fills capacity
|
||||
{
|
||||
Cache::Handle* h;
|
||||
ASSERT_OK(shard_->Insert(key, 0 /*hash*/, nullptr /*value*/, 3 /*charge*/,
|
||||
nullptr /*deleter*/, &h, Cache::Priority::LOW));
|
||||
// Try to insert more
|
||||
Status s = Insert('a');
|
||||
if (strict_capacity_limit) {
|
||||
EXPECT_TRUE(s.IsMemoryLimit());
|
||||
} else {
|
||||
EXPECT_OK(s);
|
||||
}
|
||||
// Release entry filling capacity.
|
||||
// Cover useful = false case.
|
||||
shard_->Release(h, false /*useful*/, false /*erase_if_last_ref*/);
|
||||
}
|
||||
|
||||
// Insert more than table size can handle (cleverly using zero-charge
|
||||
// entries) to exceed occupancy limit.
|
||||
{
|
||||
size_t n = shard_->GetTableAddressCount() + 1;
|
||||
std::unique_ptr<Cache::Handle* []> ha { new Cache::Handle* [n] {} };
|
||||
Status s;
|
||||
for (size_t i = 0; i < n && s.ok(); ++i) {
|
||||
EncodeFixed64(&key[0], i);
|
||||
s = shard_->Insert(key, 0 /*hash*/, nullptr /*value*/, 0 /*charge*/,
|
||||
nullptr /*deleter*/, &ha[i], Cache::Priority::LOW);
|
||||
if (i == 0) {
|
||||
EXPECT_OK(s);
|
||||
}
|
||||
}
|
||||
if (strict_capacity_limit) {
|
||||
EXPECT_TRUE(s.IsMemoryLimit());
|
||||
} else {
|
||||
EXPECT_OK(s);
|
||||
}
|
||||
// Same result if not keeping a reference
|
||||
s = Insert('a');
|
||||
if (strict_capacity_limit) {
|
||||
EXPECT_TRUE(s.IsMemoryLimit());
|
||||
} else {
|
||||
EXPECT_OK(s);
|
||||
}
|
||||
|
||||
// Regardless, we didn't allow table to actually get full
|
||||
EXPECT_LT(shard_->GetOccupancyCount(), shard_->GetTableAddressCount());
|
||||
|
||||
// Release handles
|
||||
for (size_t i = 0; i < n; ++i) {
|
||||
if (ha[i]) {
|
||||
shard_->Release(ha[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ClockCacheTest, CalcHashBitsTest) {
|
||||
size_t capacity;
|
||||
size_t estimated_value_size;
|
||||
double max_occupancy;
|
||||
int hash_bits;
|
||||
CacheMetadataChargePolicy metadata_charge_policy;
|
||||
TEST_F(ClockCacheTest, ClockEvictionTest) {
|
||||
for (bool strict_capacity_limit : {false, true}) {
|
||||
SCOPED_TRACE("strict_capacity_limit = " +
|
||||
std::to_string(strict_capacity_limit));
|
||||
|
||||
// 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));
|
||||
NewShard(6, strict_capacity_limit);
|
||||
EXPECT_OK(Insert('a', Cache::Priority::BOTTOM));
|
||||
EXPECT_OK(Insert('b', Cache::Priority::LOW));
|
||||
EXPECT_OK(Insert('c', Cache::Priority::HIGH));
|
||||
EXPECT_OK(Insert('d', Cache::Priority::BOTTOM));
|
||||
EXPECT_OK(Insert('e', Cache::Priority::LOW));
|
||||
EXPECT_OK(Insert('f', Cache::Priority::HIGH));
|
||||
|
||||
EXPECT_TRUE(Lookup('a', /*use*/ false));
|
||||
EXPECT_TRUE(Lookup('b', /*use*/ false));
|
||||
EXPECT_TRUE(Lookup('c', /*use*/ false));
|
||||
EXPECT_TRUE(Lookup('d', /*use*/ false));
|
||||
EXPECT_TRUE(Lookup('e', /*use*/ false));
|
||||
EXPECT_TRUE(Lookup('f', /*use*/ false));
|
||||
|
||||
// Ensure bottom are evicted first, even if new entries are low
|
||||
EXPECT_OK(Insert('g', Cache::Priority::LOW));
|
||||
EXPECT_OK(Insert('h', Cache::Priority::LOW));
|
||||
|
||||
EXPECT_FALSE(Lookup('a', /*use*/ false));
|
||||
EXPECT_TRUE(Lookup('b', /*use*/ false));
|
||||
EXPECT_TRUE(Lookup('c', /*use*/ false));
|
||||
EXPECT_FALSE(Lookup('d', /*use*/ false));
|
||||
EXPECT_TRUE(Lookup('e', /*use*/ false));
|
||||
EXPECT_TRUE(Lookup('f', /*use*/ false));
|
||||
// Mark g & h useful
|
||||
EXPECT_TRUE(Lookup('g', /*use*/ true));
|
||||
EXPECT_TRUE(Lookup('h', /*use*/ true));
|
||||
|
||||
// Then old LOW entries
|
||||
EXPECT_OK(Insert('i', Cache::Priority::LOW));
|
||||
EXPECT_OK(Insert('j', Cache::Priority::LOW));
|
||||
|
||||
EXPECT_FALSE(Lookup('b', /*use*/ false));
|
||||
EXPECT_TRUE(Lookup('c', /*use*/ false));
|
||||
EXPECT_FALSE(Lookup('e', /*use*/ false));
|
||||
EXPECT_TRUE(Lookup('f', /*use*/ false));
|
||||
// Mark g & h useful once again
|
||||
EXPECT_TRUE(Lookup('g', /*use*/ true));
|
||||
EXPECT_TRUE(Lookup('h', /*use*/ true));
|
||||
EXPECT_TRUE(Lookup('i', /*use*/ false));
|
||||
EXPECT_TRUE(Lookup('j', /*use*/ false));
|
||||
|
||||
// Then old HIGH entries
|
||||
EXPECT_OK(Insert('k', Cache::Priority::LOW));
|
||||
EXPECT_OK(Insert('l', Cache::Priority::LOW));
|
||||
|
||||
EXPECT_FALSE(Lookup('c', /*use*/ false));
|
||||
EXPECT_FALSE(Lookup('f', /*use*/ false));
|
||||
EXPECT_TRUE(Lookup('g', /*use*/ false));
|
||||
EXPECT_TRUE(Lookup('h', /*use*/ false));
|
||||
EXPECT_TRUE(Lookup('i', /*use*/ false));
|
||||
EXPECT_TRUE(Lookup('j', /*use*/ false));
|
||||
EXPECT_TRUE(Lookup('k', /*use*/ false));
|
||||
EXPECT_TRUE(Lookup('l', /*use*/ false));
|
||||
|
||||
// Then the (roughly) least recently useful
|
||||
EXPECT_OK(Insert('m', Cache::Priority::HIGH));
|
||||
EXPECT_OK(Insert('n', Cache::Priority::HIGH));
|
||||
|
||||
EXPECT_TRUE(Lookup('g', /*use*/ false));
|
||||
EXPECT_TRUE(Lookup('h', /*use*/ false));
|
||||
EXPECT_FALSE(Lookup('i', /*use*/ false));
|
||||
EXPECT_FALSE(Lookup('j', /*use*/ false));
|
||||
EXPECT_TRUE(Lookup('k', /*use*/ false));
|
||||
EXPECT_TRUE(Lookup('l', /*use*/ false));
|
||||
|
||||
// Now try changing capacity down
|
||||
shard_->SetCapacity(4);
|
||||
// Insert to ensure evictions happen
|
||||
EXPECT_OK(Insert('o', Cache::Priority::LOW));
|
||||
EXPECT_OK(Insert('p', Cache::Priority::LOW));
|
||||
|
||||
EXPECT_FALSE(Lookup('g', /*use*/ false));
|
||||
EXPECT_FALSE(Lookup('h', /*use*/ false));
|
||||
EXPECT_FALSE(Lookup('k', /*use*/ false));
|
||||
EXPECT_FALSE(Lookup('l', /*use*/ false));
|
||||
EXPECT_TRUE(Lookup('m', /*use*/ false));
|
||||
EXPECT_TRUE(Lookup('n', /*use*/ false));
|
||||
EXPECT_TRUE(Lookup('o', /*use*/ false));
|
||||
EXPECT_TRUE(Lookup('p', /*use*/ false));
|
||||
|
||||
// Now try changing capacity up
|
||||
EXPECT_TRUE(Lookup('m', /*use*/ true));
|
||||
EXPECT_TRUE(Lookup('n', /*use*/ true));
|
||||
shard_->SetCapacity(6);
|
||||
EXPECT_OK(Insert('q', Cache::Priority::HIGH));
|
||||
EXPECT_OK(Insert('r', Cache::Priority::HIGH));
|
||||
EXPECT_OK(Insert('s', Cache::Priority::HIGH));
|
||||
EXPECT_OK(Insert('t', Cache::Priority::HIGH));
|
||||
|
||||
EXPECT_FALSE(Lookup('o', /*use*/ false));
|
||||
EXPECT_FALSE(Lookup('p', /*use*/ false));
|
||||
EXPECT_TRUE(Lookup('m', /*use*/ false));
|
||||
EXPECT_TRUE(Lookup('n', /*use*/ false));
|
||||
EXPECT_TRUE(Lookup('q', /*use*/ false));
|
||||
EXPECT_TRUE(Lookup('r', /*use*/ false));
|
||||
EXPECT_TRUE(Lookup('s', /*use*/ false));
|
||||
EXPECT_TRUE(Lookup('t', /*use*/ false));
|
||||
}
|
||||
}
|
||||
|
||||
void IncrementIntDeleter(const Slice& /*key*/, void* value) {
|
||||
*reinterpret_cast<int*>(value) += 1;
|
||||
}
|
||||
|
||||
// Testing calls to CorrectNearOverflow in Release
|
||||
TEST_F(ClockCacheTest, ClockCounterOverflowTest) {
|
||||
NewShard(6, /*strict_capacity_limit*/ false);
|
||||
Cache::Handle* h;
|
||||
int deleted = 0;
|
||||
std::string my_key(kCacheKeySize, 'x');
|
||||
uint32_t my_hash = 42;
|
||||
ASSERT_OK(shard_->Insert(my_key, my_hash, &deleted, 1, IncrementIntDeleter,
|
||||
&h, Cache::Priority::HIGH));
|
||||
|
||||
// Some large number outstanding
|
||||
shard_->TEST_RefN(h, 123456789);
|
||||
// Simulate many lookup/ref + release, plenty to overflow counters
|
||||
for (int i = 0; i < 10000; ++i) {
|
||||
shard_->TEST_RefN(h, 1234567);
|
||||
shard_->TEST_ReleaseN(h, 1234567);
|
||||
}
|
||||
// Mark it invisible (to reach a different CorrectNearOverflow() in Release)
|
||||
shard_->Erase(my_key, my_hash);
|
||||
// Simulate many more lookup/ref + release (one-by-one would be too
|
||||
// expensive for unit test)
|
||||
for (int i = 0; i < 10000; ++i) {
|
||||
shard_->TEST_RefN(h, 1234567);
|
||||
shard_->TEST_ReleaseN(h, 1234567);
|
||||
}
|
||||
// Free all but last 1
|
||||
shard_->TEST_ReleaseN(h, 123456789);
|
||||
// Still alive
|
||||
ASSERT_EQ(deleted, 0);
|
||||
// Free last ref, which will finalize erasure
|
||||
shard_->Release(h);
|
||||
// Deleted
|
||||
ASSERT_EQ(deleted, 1);
|
||||
}
|
||||
|
||||
// This test is mostly to exercise some corner case logic, by forcing two
|
||||
// keys to have the same hash, and more
|
||||
TEST_F(ClockCacheTest, CollidingInsertEraseTest) {
|
||||
NewShard(6, /*strict_capacity_limit*/ false);
|
||||
int deleted = 0;
|
||||
std::string key1(kCacheKeySize, 'x');
|
||||
std::string key2(kCacheKeySize, 'y');
|
||||
std::string key3(kCacheKeySize, 'z');
|
||||
uint32_t my_hash = 42;
|
||||
Cache::Handle* h1;
|
||||
ASSERT_OK(shard_->Insert(key1, my_hash, &deleted, 1, IncrementIntDeleter, &h1,
|
||||
Cache::Priority::HIGH));
|
||||
Cache::Handle* h2;
|
||||
ASSERT_OK(shard_->Insert(key2, my_hash, &deleted, 1, IncrementIntDeleter, &h2,
|
||||
Cache::Priority::HIGH));
|
||||
Cache::Handle* h3;
|
||||
ASSERT_OK(shard_->Insert(key3, my_hash, &deleted, 1, IncrementIntDeleter, &h3,
|
||||
Cache::Priority::HIGH));
|
||||
|
||||
// Can repeatedly lookup+release despite the hash collision
|
||||
Cache::Handle* tmp_h;
|
||||
for (bool erase_if_last_ref : {true, false}) { // but not last ref
|
||||
tmp_h = shard_->Lookup(key1, my_hash);
|
||||
ASSERT_EQ(h1, tmp_h);
|
||||
ASSERT_FALSE(shard_->Release(tmp_h, erase_if_last_ref));
|
||||
|
||||
tmp_h = shard_->Lookup(key2, my_hash);
|
||||
ASSERT_EQ(h2, tmp_h);
|
||||
ASSERT_FALSE(shard_->Release(tmp_h, erase_if_last_ref));
|
||||
|
||||
tmp_h = shard_->Lookup(key3, my_hash);
|
||||
ASSERT_EQ(h3, tmp_h);
|
||||
ASSERT_FALSE(shard_->Release(tmp_h, erase_if_last_ref));
|
||||
}
|
||||
|
||||
// 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));
|
||||
// Make h1 invisible
|
||||
shard_->Erase(key1, my_hash);
|
||||
// Redundant erase
|
||||
shard_->Erase(key1, my_hash);
|
||||
|
||||
// All still alive
|
||||
ASSERT_EQ(deleted, 0);
|
||||
|
||||
// Invisible to Lookup
|
||||
tmp_h = shard_->Lookup(key1, my_hash);
|
||||
ASSERT_EQ(nullptr, tmp_h);
|
||||
|
||||
// Can still find h2, h3
|
||||
for (bool erase_if_last_ref : {true, false}) { // but not last ref
|
||||
tmp_h = shard_->Lookup(key2, my_hash);
|
||||
ASSERT_EQ(h2, tmp_h);
|
||||
ASSERT_FALSE(shard_->Release(tmp_h, erase_if_last_ref));
|
||||
|
||||
tmp_h = shard_->Lookup(key3, my_hash);
|
||||
ASSERT_EQ(h3, tmp_h);
|
||||
ASSERT_FALSE(shard_->Release(tmp_h, erase_if_last_ref));
|
||||
}
|
||||
|
||||
// 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 */));
|
||||
// Also Insert with invisible entry there
|
||||
ASSERT_OK(shard_->Insert(key1, my_hash, &deleted, 1, IncrementIntDeleter,
|
||||
nullptr, Cache::Priority::HIGH));
|
||||
tmp_h = shard_->Lookup(key1, my_hash);
|
||||
// Found but distinct handle
|
||||
ASSERT_NE(nullptr, tmp_h);
|
||||
ASSERT_NE(h1, tmp_h);
|
||||
ASSERT_TRUE(shard_->Release(tmp_h, /*erase_if_last_ref*/ true));
|
||||
|
||||
// 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 */));
|
||||
// tmp_h deleted
|
||||
ASSERT_EQ(deleted--, 1);
|
||||
|
||||
// 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 */));
|
||||
// Release last ref on h1 (already invisible)
|
||||
ASSERT_TRUE(shard_->Release(h1, /*erase_if_last_ref*/ false));
|
||||
|
||||
// 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));
|
||||
// h1 deleted
|
||||
ASSERT_EQ(deleted--, 1);
|
||||
h1 = nullptr;
|
||||
|
||||
// Can still find h2, h3
|
||||
for (bool erase_if_last_ref : {true, false}) { // but not last ref
|
||||
tmp_h = shard_->Lookup(key2, my_hash);
|
||||
ASSERT_EQ(h2, tmp_h);
|
||||
ASSERT_FALSE(shard_->Release(tmp_h, erase_if_last_ref));
|
||||
|
||||
tmp_h = shard_->Lookup(key3, my_hash);
|
||||
ASSERT_EQ(h3, tmp_h);
|
||||
ASSERT_FALSE(shard_->Release(tmp_h, erase_if_last_ref));
|
||||
}
|
||||
|
||||
// Release last ref on h2
|
||||
ASSERT_FALSE(shard_->Release(h2, /*erase_if_last_ref*/ false));
|
||||
|
||||
// h2 still not deleted (unreferenced in cache)
|
||||
ASSERT_EQ(deleted, 0);
|
||||
|
||||
// Can still find it
|
||||
tmp_h = shard_->Lookup(key2, my_hash);
|
||||
ASSERT_EQ(h2, tmp_h);
|
||||
|
||||
// Release last ref on h2, with erase
|
||||
ASSERT_TRUE(shard_->Release(h2, /*erase_if_last_ref*/ true));
|
||||
|
||||
// h2 deleted
|
||||
ASSERT_EQ(deleted--, 1);
|
||||
tmp_h = shard_->Lookup(key2, my_hash);
|
||||
ASSERT_EQ(nullptr, tmp_h);
|
||||
|
||||
// Can still find h3
|
||||
for (bool erase_if_last_ref : {true, false}) { // but not last ref
|
||||
tmp_h = shard_->Lookup(key3, my_hash);
|
||||
ASSERT_EQ(h3, tmp_h);
|
||||
ASSERT_FALSE(shard_->Release(tmp_h, erase_if_last_ref));
|
||||
}
|
||||
|
||||
// Release last ref on h3, without erase
|
||||
ASSERT_FALSE(shard_->Release(h3, /*erase_if_last_ref*/ false));
|
||||
|
||||
// h3 still not deleted (unreferenced in cache)
|
||||
ASSERT_EQ(deleted, 0);
|
||||
|
||||
// Explicit erase
|
||||
shard_->Erase(key3, my_hash);
|
||||
|
||||
// h3 deleted
|
||||
ASSERT_EQ(deleted--, 1);
|
||||
tmp_h = shard_->Lookup(key3, my_hash);
|
||||
ASSERT_EQ(nullptr, tmp_h);
|
||||
}
|
||||
|
||||
} // namespace clock_cache
|
||||
// This uses the public API to effectively test CalcHashBits etc.
|
||||
TEST_F(ClockCacheTest, TableSizesTest) {
|
||||
for (size_t est_val_size : {1U, 5U, 123U, 2345U, 345678U}) {
|
||||
SCOPED_TRACE("est_val_size = " + std::to_string(est_val_size));
|
||||
for (double est_count : {1.1, 2.2, 511.9, 512.1, 2345.0}) {
|
||||
SCOPED_TRACE("est_count = " + std::to_string(est_count));
|
||||
size_t capacity = static_cast<size_t>(est_val_size * est_count);
|
||||
// kDontChargeCacheMetadata
|
||||
auto cache = HyperClockCacheOptions(
|
||||
capacity, est_val_size, /*num shard_bits*/ -1,
|
||||
/*strict_capacity_limit*/ false,
|
||||
/*memory_allocator*/ nullptr, kDontChargeCacheMetadata)
|
||||
.MakeSharedCache();
|
||||
// Table sizes are currently only powers of two
|
||||
EXPECT_GE(cache->GetTableAddressCount(), est_count / kLoadFactor);
|
||||
EXPECT_LE(cache->GetTableAddressCount(), est_count / kLoadFactor * 2.0);
|
||||
EXPECT_EQ(cache->GetUsage(), 0);
|
||||
|
||||
// kFullChargeMetaData
|
||||
// Because table sizes are currently only powers of two, sizes get
|
||||
// really weird when metadata is a huge portion of capacity. For example,
|
||||
// doubling the table size could cut by 90% the space available to
|
||||
// values. Therefore, we omit those weird cases for now.
|
||||
if (est_val_size >= 512) {
|
||||
cache = HyperClockCacheOptions(
|
||||
capacity, est_val_size, /*num shard_bits*/ -1,
|
||||
/*strict_capacity_limit*/ false,
|
||||
/*memory_allocator*/ nullptr, kFullChargeCacheMetadata)
|
||||
.MakeSharedCache();
|
||||
double est_count_after_meta =
|
||||
(capacity - cache->GetUsage()) * 1.0 / est_val_size;
|
||||
EXPECT_GE(cache->GetTableAddressCount(),
|
||||
est_count_after_meta / kLoadFactor);
|
||||
EXPECT_LE(cache->GetTableAddressCount(),
|
||||
est_count_after_meta / kLoadFactor * 2.0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace hyper_clock_cache
|
||||
|
||||
class TestSecondaryCache : public SecondaryCache {
|
||||
public:
|
||||
@@ -742,7 +1065,7 @@ class TestSecondaryCache : public SecondaryCache {
|
||||
|
||||
std::unique_ptr<SecondaryCacheResultHandle> Lookup(
|
||||
const Slice& key, const Cache::CreateCallback& create_cb, bool /*wait*/,
|
||||
bool& is_in_sec_cache) override {
|
||||
bool /*advise_erase*/, bool& is_in_sec_cache) override {
|
||||
std::string key_str = key.ToString();
|
||||
TEST_SYNC_POINT_CALLBACK("TestSecondaryCache::Lookup", &key_str);
|
||||
|
||||
@@ -780,6 +1103,8 @@ class TestSecondaryCache : public SecondaryCache {
|
||||
return secondary_handle;
|
||||
}
|
||||
|
||||
bool SupportForceErase() const override { return false; }
|
||||
|
||||
void Erase(const Slice& /*key*/) override {}
|
||||
|
||||
void WaitAll(std::vector<SecondaryCacheResultHandle*> handles) override {
|
||||
@@ -960,7 +1285,7 @@ TEST_F(LRUCacheSecondaryCacheTest, BasicTest) {
|
||||
TestItem* item1 = new TestItem(str1.data(), str1.length());
|
||||
ASSERT_OK(cache->Insert(k1.AsSlice(), item1,
|
||||
&LRUCacheSecondaryCacheTest::helper_, str1.length()));
|
||||
std::string str2 = rnd.RandomString(1020);
|
||||
std::string str2 = rnd.RandomString(1021);
|
||||
TestItem* item2 = new TestItem(str2.data(), str2.length());
|
||||
// k1 should be demoted to NVM
|
||||
ASSERT_OK(cache->Insert(k2.AsSlice(), item2,
|
||||
|
||||
Vendored
+19
-2
@@ -213,9 +213,9 @@ std::string ShardedCache::GetPrintableOptions() const {
|
||||
ret.append(GetShard(0)->GetPrintableOptions());
|
||||
return ret;
|
||||
}
|
||||
int GetDefaultCacheShardBits(size_t capacity) {
|
||||
|
||||
int GetDefaultCacheShardBits(size_t capacity, size_t min_shard_size) {
|
||||
int num_shard_bits = 0;
|
||||
size_t min_shard_size = 512L * 1024L; // Every shard is at least 512KB.
|
||||
size_t num_shards = capacity / min_shard_size;
|
||||
while (num_shards >>= 1) {
|
||||
if (++num_shard_bits >= 6) {
|
||||
@@ -230,4 +230,21 @@ int ShardedCache::GetNumShardBits() const { return BitsSetToOne(shard_mask_); }
|
||||
|
||||
uint32_t ShardedCache::GetNumShards() const { return shard_mask_ + 1; }
|
||||
|
||||
size_t ShardedCache::GetOccupancyCount() const {
|
||||
size_t oc = 0;
|
||||
uint32_t num_shards = GetNumShards();
|
||||
for (uint32_t s = 0; s < num_shards; s++) {
|
||||
oc += GetShard(s)->GetOccupancyCount();
|
||||
}
|
||||
return oc;
|
||||
}
|
||||
size_t ShardedCache::GetTableAddressCount() const {
|
||||
size_t tac = 0;
|
||||
uint32_t num_shards = GetNumShards();
|
||||
for (uint32_t s = 0; s < num_shards; s++) {
|
||||
tac += GetShard(s)->GetTableAddressCount();
|
||||
}
|
||||
return tac;
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
Vendored
+10
-7
@@ -20,7 +20,8 @@ namespace ROCKSDB_NAMESPACE {
|
||||
// Single cache shard interface.
|
||||
class CacheShard {
|
||||
public:
|
||||
CacheShard() = default;
|
||||
explicit CacheShard(CacheMetadataChargePolicy metadata_charge_policy)
|
||||
: metadata_charge_policy_(metadata_charge_policy) {}
|
||||
virtual ~CacheShard() = default;
|
||||
|
||||
using DeleterFn = Cache::DeleterFn;
|
||||
@@ -47,6 +48,8 @@ class CacheShard {
|
||||
virtual void SetStrictCapacityLimit(bool strict_capacity_limit) = 0;
|
||||
virtual size_t GetUsage() const = 0;
|
||||
virtual size_t GetPinnedUsage() const = 0;
|
||||
virtual size_t GetOccupancyCount() const = 0;
|
||||
virtual size_t GetTableAddressCount() const = 0;
|
||||
// Handles iterating over roughly `average_entries_per_lock` entries, using
|
||||
// `state` to somehow record where it last ended up. Caller initially uses
|
||||
// *state == 0 and implementation sets *state = UINT32_MAX to indicate
|
||||
@@ -57,13 +60,9 @@ class CacheShard {
|
||||
uint32_t average_entries_per_lock, uint32_t* state) = 0;
|
||||
virtual void EraseUnRefEntries() = 0;
|
||||
virtual std::string GetPrintableOptions() const { return ""; }
|
||||
void set_metadata_charge_policy(
|
||||
CacheMetadataChargePolicy metadata_charge_policy) {
|
||||
metadata_charge_policy_ = metadata_charge_policy;
|
||||
}
|
||||
|
||||
protected:
|
||||
CacheMetadataChargePolicy metadata_charge_policy_ = kDontChargeCacheMetadata;
|
||||
const CacheMetadataChargePolicy metadata_charge_policy_;
|
||||
};
|
||||
|
||||
// Generic cache interface which shards cache by hash of keys. 2^num_shard_bits
|
||||
@@ -106,6 +105,8 @@ class ShardedCache : public Cache {
|
||||
virtual size_t GetUsage() const override;
|
||||
virtual size_t GetUsage(Handle* handle) const override;
|
||||
virtual size_t GetPinnedUsage() const override;
|
||||
virtual size_t GetOccupancyCount() const override;
|
||||
virtual size_t GetTableAddressCount() const override;
|
||||
virtual void ApplyToAllEntries(
|
||||
const std::function<void(const Slice& key, void* value, size_t charge,
|
||||
DeleterFn deleter)>& callback,
|
||||
@@ -127,6 +128,8 @@ class ShardedCache : public Cache {
|
||||
std::atomic<uint64_t> last_id_;
|
||||
};
|
||||
|
||||
extern int GetDefaultCacheShardBits(size_t capacity);
|
||||
// 512KB is traditional minimum shard size.
|
||||
int GetDefaultCacheShardBits(size_t capacity,
|
||||
size_t min_shard_size = 512U * 1024U);
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
+2
-2
@@ -78,7 +78,7 @@ blackbox_crash_test_with_multiops_wp_txn: $(DB_STRESS_CMD)
|
||||
$(CRASHTEST_PY) --test_multiops_txn --write_policy write_prepared blackbox $(CRASH_TEST_EXT_ARGS)
|
||||
|
||||
blackbox_crash_test_with_tiered_storage: $(DB_STRESS_CMD)
|
||||
$(CRASHTEST_PY) --enable_tiered_storage blackbox $(CRASH_TEST_EXT_ARGS)
|
||||
$(CRASHTEST_PY) --test_tiered_storage blackbox $(CRASH_TEST_EXT_ARGS)
|
||||
|
||||
ifeq ($(CRASH_TEST_KILL_ODD),)
|
||||
CRASH_TEST_KILL_ODD=888887
|
||||
@@ -103,5 +103,5 @@ whitebox_crash_test_with_ts: $(DB_STRESS_CMD)
|
||||
$(CRASH_TEST_KILL_ODD) $(CRASH_TEST_EXT_ARGS)
|
||||
|
||||
whitebox_crash_test_with_tiered_storage: $(DB_STRESS_CMD)
|
||||
$(CRASHTEST_PY) --enable_tiered_storage whitebox --random_kill_odd \
|
||||
$(CRASHTEST_PY) --test_tiered_storage whitebox --random_kill_odd \
|
||||
$(CRASH_TEST_KILL_ODD) $(CRASH_TEST_EXT_ARGS)
|
||||
+55
-27
@@ -45,6 +45,7 @@ void ArenaWrappedDBIter::Init(
|
||||
sv_number_ = version_number;
|
||||
read_options_ = read_options;
|
||||
allow_refresh_ = allow_refresh;
|
||||
memtable_range_tombstone_iter_ = nullptr;
|
||||
}
|
||||
|
||||
Status ArenaWrappedDBIter::Refresh() {
|
||||
@@ -58,41 +59,68 @@ Status ArenaWrappedDBIter::Refresh() {
|
||||
uint64_t cur_sv_number = cfd_->GetSuperVersionNumber();
|
||||
TEST_SYNC_POINT("ArenaWrappedDBIter::Refresh:1");
|
||||
TEST_SYNC_POINT("ArenaWrappedDBIter::Refresh:2");
|
||||
auto reinit_internal_iter = [&]() {
|
||||
Env* env = db_iter_->env();
|
||||
db_iter_->~DBIter();
|
||||
arena_.~Arena();
|
||||
new (&arena_) Arena();
|
||||
|
||||
SuperVersion* sv = cfd_->GetReferencedSuperVersion(db_impl_);
|
||||
SequenceNumber latest_seq = db_impl_->GetLatestSequenceNumber();
|
||||
if (read_callback_) {
|
||||
read_callback_->Refresh(latest_seq);
|
||||
}
|
||||
Init(env, read_options_, *(cfd_->ioptions()), sv->mutable_cf_options,
|
||||
sv->current, latest_seq,
|
||||
sv->mutable_cf_options.max_sequential_skip_in_iterations,
|
||||
cur_sv_number, read_callback_, db_impl_, cfd_, expose_blob_index_,
|
||||
allow_refresh_);
|
||||
|
||||
InternalIterator* internal_iter = db_impl_->NewInternalIterator(
|
||||
read_options_, cfd_, sv, &arena_, latest_seq,
|
||||
/* allow_unprepared_value */ true, /* db_iter */ this);
|
||||
SetIterUnderDBIter(internal_iter);
|
||||
};
|
||||
while (true) {
|
||||
if (sv_number_ != cur_sv_number) {
|
||||
Env* env = db_iter_->env();
|
||||
db_iter_->~DBIter();
|
||||
arena_.~Arena();
|
||||
new (&arena_) Arena();
|
||||
|
||||
SuperVersion* sv = cfd_->GetReferencedSuperVersion(db_impl_);
|
||||
SequenceNumber latest_seq = db_impl_->GetLatestSequenceNumber();
|
||||
if (read_callback_) {
|
||||
read_callback_->Refresh(latest_seq);
|
||||
}
|
||||
Init(env, read_options_, *(cfd_->ioptions()), sv->mutable_cf_options,
|
||||
sv->current, latest_seq,
|
||||
sv->mutable_cf_options.max_sequential_skip_in_iterations,
|
||||
cur_sv_number, read_callback_, db_impl_, cfd_, expose_blob_index_,
|
||||
allow_refresh_);
|
||||
|
||||
InternalIterator* internal_iter = db_impl_->NewInternalIterator(
|
||||
read_options_, cfd_, sv, &arena_, db_iter_->GetRangeDelAggregator(),
|
||||
latest_seq, /* allow_unprepared_value */ true);
|
||||
SetIterUnderDBIter(internal_iter);
|
||||
reinit_internal_iter();
|
||||
break;
|
||||
} else {
|
||||
SequenceNumber latest_seq = db_impl_->GetLatestSequenceNumber();
|
||||
// Refresh range-tombstones in MemTable
|
||||
if (!read_options_.ignore_range_deletions) {
|
||||
SuperVersion* sv = cfd_->GetThreadLocalSuperVersion(db_impl_);
|
||||
ReadRangeDelAggregator* range_del_agg =
|
||||
db_iter_->GetRangeDelAggregator();
|
||||
std::unique_ptr<FragmentedRangeTombstoneIterator> range_del_iter;
|
||||
range_del_iter.reset(sv->mem->NewRangeTombstoneIterator(
|
||||
read_options_, latest_seq, false /* immutable_memtable */));
|
||||
range_del_agg->AddTombstones(std::move(range_del_iter));
|
||||
cfd_->ReturnThreadLocalSuperVersion(sv);
|
||||
TEST_SYNC_POINT_CALLBACK("ArenaWrappedDBIter::Refresh:SV", nullptr);
|
||||
auto t = sv->mem->NewRangeTombstoneIterator(
|
||||
read_options_, latest_seq, false /* immutable_memtable */);
|
||||
if (!t || t->empty()) {
|
||||
// If memtable_range_tombstone_iter_ points to a non-empty tombstone
|
||||
// iterator, then it means sv->mem is not the memtable that
|
||||
// memtable_range_tombstone_iter_ points to, so SV must have changed
|
||||
// after the sv_number_ != cur_sv_number check above. We will fall
|
||||
// back to re-init the InternalIterator, and the tombstone iterator
|
||||
// will be freed during db_iter destruction there.
|
||||
if (memtable_range_tombstone_iter_) {
|
||||
assert(!*memtable_range_tombstone_iter_ ||
|
||||
sv_number_ != cfd_->GetSuperVersionNumber());
|
||||
}
|
||||
delete t;
|
||||
} else { // current mutable memtable has range tombstones
|
||||
if (!memtable_range_tombstone_iter_) {
|
||||
delete t;
|
||||
db_impl_->ReturnAndCleanupSuperVersion(cfd_, sv);
|
||||
// The memtable under DBIter did not have range tombstone before
|
||||
// refresh.
|
||||
reinit_internal_iter();
|
||||
break;
|
||||
} else {
|
||||
delete *memtable_range_tombstone_iter_;
|
||||
*memtable_range_tombstone_iter_ = new TruncatedRangeDelIterator(
|
||||
std::unique_ptr<FragmentedRangeTombstoneIterator>(t),
|
||||
&cfd_->internal_comparator(), nullptr, nullptr);
|
||||
}
|
||||
}
|
||||
db_impl_->ReturnAndCleanupSuperVersion(cfd_, sv);
|
||||
}
|
||||
// Refresh latest sequence number
|
||||
db_iter_->set_sequence(latest_seq);
|
||||
|
||||
@@ -44,9 +44,7 @@ class ArenaWrappedDBIter : public Iterator {
|
||||
// Get the arena to be used to allocate memory for DBIter to be wrapped,
|
||||
// as well as child iterators in it.
|
||||
virtual Arena* GetArena() { return &arena_; }
|
||||
virtual ReadRangeDelAggregator* GetRangeDelAggregator() {
|
||||
return db_iter_->GetRangeDelAggregator();
|
||||
}
|
||||
|
||||
const ReadOptions& GetReadOptions() { return read_options_; }
|
||||
|
||||
// Set the internal iterator wrapped inside the DB Iterator. Usually it is
|
||||
@@ -55,6 +53,10 @@ class ArenaWrappedDBIter : public Iterator {
|
||||
db_iter_->SetIter(iter);
|
||||
}
|
||||
|
||||
void SetMemtableRangetombstoneIter(TruncatedRangeDelIterator** iter) {
|
||||
memtable_range_tombstone_iter_ = iter;
|
||||
}
|
||||
|
||||
bool Valid() const override { return db_iter_->Valid(); }
|
||||
void SeekToFirst() override { db_iter_->SeekToFirst(); }
|
||||
void SeekToLast() override { db_iter_->SeekToLast(); }
|
||||
@@ -68,6 +70,7 @@ class ArenaWrappedDBIter : public Iterator {
|
||||
void Prev() override { db_iter_->Prev(); }
|
||||
Slice key() const override { return db_iter_->key(); }
|
||||
Slice value() const override { return db_iter_->value(); }
|
||||
const WideColumns& columns() const override { return db_iter_->columns(); }
|
||||
Status status() const override { return db_iter_->status(); }
|
||||
Slice timestamp() const override { return db_iter_->timestamp(); }
|
||||
bool IsBlob() const { return db_iter_->IsBlob(); }
|
||||
@@ -104,6 +107,9 @@ class ArenaWrappedDBIter : public Iterator {
|
||||
ReadCallback* read_callback_;
|
||||
bool expose_blob_index_ = false;
|
||||
bool allow_refresh_ = true;
|
||||
// If this is nullptr, it means the mutable memtable does not contain range
|
||||
// tombstone when added under this DBIter.
|
||||
TruncatedRangeDelIterator** memtable_range_tombstone_iter_ = nullptr;
|
||||
};
|
||||
|
||||
// Generate the arena wrapped iterator class.
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
// 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_contents.h"
|
||||
|
||||
#include <cassert>
|
||||
|
||||
#include "cache/cache_entry_roles.h"
|
||||
#include "cache/cache_helpers.h"
|
||||
#include "port/malloc.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
std::unique_ptr<BlobContents> BlobContents::Create(
|
||||
CacheAllocationPtr&& allocation, size_t size) {
|
||||
return std::unique_ptr<BlobContents>(
|
||||
new BlobContents(std::move(allocation), size));
|
||||
}
|
||||
|
||||
size_t BlobContents::ApproximateMemoryUsage() const {
|
||||
size_t usage = 0;
|
||||
|
||||
if (allocation_) {
|
||||
MemoryAllocator* const allocator = allocation_.get_deleter().allocator;
|
||||
|
||||
if (allocator) {
|
||||
usage += allocator->UsableSize(allocation_.get(), data_.size());
|
||||
} else {
|
||||
#ifdef ROCKSDB_MALLOC_USABLE_SIZE
|
||||
usage += malloc_usable_size(allocation_.get());
|
||||
#else
|
||||
usage += data_.size();
|
||||
#endif
|
||||
}
|
||||
}
|
||||
|
||||
#ifdef ROCKSDB_MALLOC_USABLE_SIZE
|
||||
usage += malloc_usable_size(const_cast<BlobContents*>(this));
|
||||
#else
|
||||
usage += sizeof(*this);
|
||||
#endif
|
||||
|
||||
return usage;
|
||||
}
|
||||
|
||||
size_t BlobContents::SizeCallback(void* obj) {
|
||||
assert(obj);
|
||||
|
||||
return static_cast<const BlobContents*>(obj)->size();
|
||||
}
|
||||
|
||||
Status BlobContents::SaveToCallback(void* from_obj, size_t from_offset,
|
||||
size_t length, void* out) {
|
||||
assert(from_obj);
|
||||
|
||||
const BlobContents* buf = static_cast<const BlobContents*>(from_obj);
|
||||
assert(buf->size() >= from_offset + length);
|
||||
|
||||
memcpy(out, buf->data().data() + from_offset, length);
|
||||
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Cache::CacheItemHelper* BlobContents::GetCacheItemHelper() {
|
||||
static Cache::CacheItemHelper cache_helper(
|
||||
&SizeCallback, &SaveToCallback,
|
||||
GetCacheEntryDeleterForRole<BlobContents, CacheEntryRole::kBlobValue>());
|
||||
|
||||
return &cache_helper;
|
||||
}
|
||||
|
||||
Status BlobContents::CreateCallback(CacheAllocationPtr&& allocation,
|
||||
const void* buf, size_t size,
|
||||
void** out_obj, size_t* charge) {
|
||||
assert(allocation);
|
||||
|
||||
memcpy(allocation.get(), buf, size);
|
||||
|
||||
std::unique_ptr<BlobContents> obj = Create(std::move(allocation), size);
|
||||
BlobContents* const contents = obj.release();
|
||||
|
||||
*out_obj = contents;
|
||||
*charge = contents->ApproximateMemoryUsage();
|
||||
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -0,0 +1,56 @@
|
||||
// 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 <memory>
|
||||
|
||||
#include "memory/memory_allocator.h"
|
||||
#include "rocksdb/cache.h"
|
||||
#include "rocksdb/rocksdb_namespace.h"
|
||||
#include "rocksdb/slice.h"
|
||||
#include "rocksdb/status.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
// A class representing a single uncompressed value read from a blob file.
|
||||
class BlobContents {
|
||||
public:
|
||||
static std::unique_ptr<BlobContents> Create(CacheAllocationPtr&& allocation,
|
||||
size_t size);
|
||||
|
||||
BlobContents(const BlobContents&) = delete;
|
||||
BlobContents& operator=(const BlobContents&) = delete;
|
||||
|
||||
BlobContents(BlobContents&&) = default;
|
||||
BlobContents& operator=(BlobContents&&) = default;
|
||||
|
||||
~BlobContents() = default;
|
||||
|
||||
const Slice& data() const { return data_; }
|
||||
size_t size() const { return data_.size(); }
|
||||
|
||||
size_t ApproximateMemoryUsage() const;
|
||||
|
||||
// Callbacks for secondary cache
|
||||
static size_t SizeCallback(void* obj);
|
||||
|
||||
static Status SaveToCallback(void* from_obj, size_t from_offset,
|
||||
size_t length, void* out);
|
||||
|
||||
static Cache::CacheItemHelper* GetCacheItemHelper();
|
||||
|
||||
static Status CreateCallback(CacheAllocationPtr&& allocation, const void* buf,
|
||||
size_t size, void** out_obj, size_t* charge);
|
||||
|
||||
private:
|
||||
BlobContents(CacheAllocationPtr&& allocation, size_t size)
|
||||
: allocation_(std::move(allocation)), data_(allocation_.get(), size) {}
|
||||
|
||||
CacheAllocationPtr allocation_;
|
||||
Slice data_;
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include <cassert>
|
||||
|
||||
#include "db/blob/blob_contents.h"
|
||||
#include "db/blob/blob_file_addition.h"
|
||||
#include "db/blob/blob_file_completion_callback.h"
|
||||
#include "db/blob/blob_index.h"
|
||||
@@ -408,16 +409,28 @@ Status BlobFileBuilder::PutBlobIntoCacheIfNeeded(const Slice& blob,
|
||||
|
||||
// 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::unique_ptr<std::string> buf = std::make_unique<std::string>();
|
||||
buf->assign(blob.data(), blob.size());
|
||||
// take unique ownership of them.
|
||||
CacheAllocationPtr allocation =
|
||||
AllocateBlock(blob.size(), blob_cache->memory_allocator());
|
||||
memcpy(allocation.get(), blob.data(), blob.size());
|
||||
std::unique_ptr<BlobContents> buf =
|
||||
BlobContents::Create(std::move(allocation), blob.size());
|
||||
|
||||
Cache::CacheItemHelper* const cache_item_helper =
|
||||
BlobContents::GetCacheItemHelper();
|
||||
assert(cache_item_helper);
|
||||
|
||||
if (immutable_options_->lowest_used_cache_tier ==
|
||||
CacheTier::kNonVolatileBlockTier) {
|
||||
s = blob_cache->Insert(key, buf.get(), cache_item_helper,
|
||||
buf->ApproximateMemoryUsage(),
|
||||
nullptr /* cache_handle */, priority);
|
||||
} else {
|
||||
s = blob_cache->Insert(key, buf.get(), buf->ApproximateMemoryUsage(),
|
||||
cache_item_helper->del_cb,
|
||||
nullptr /* cache_handle */, priority);
|
||||
}
|
||||
|
||||
// TODO: support custom allocators and provide a better estimated memory
|
||||
// usage using malloc_usable_size.
|
||||
s = blob_cache->Insert(key, buf.get(), buf->size(),
|
||||
&DeleteCacheEntry<std::string>,
|
||||
nullptr /* cache_handle */, priority);
|
||||
if (s.ok()) {
|
||||
RecordTick(statistics, BLOB_DB_CACHE_ADD);
|
||||
RecordTick(statistics, BLOB_DB_CACHE_BYTES_WRITE, buf->size());
|
||||
|
||||
+64
-59
@@ -8,6 +8,7 @@
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
|
||||
#include "db/blob/blob_contents.h"
|
||||
#include "db/blob/blob_log_format.h"
|
||||
#include "file/file_prefetch_buffer.h"
|
||||
#include "file/filename.h"
|
||||
@@ -283,14 +284,12 @@ BlobFileReader::BlobFileReader(
|
||||
|
||||
BlobFileReader::~BlobFileReader() = default;
|
||||
|
||||
Status BlobFileReader::GetBlob(const ReadOptions& read_options,
|
||||
const Slice& user_key, uint64_t offset,
|
||||
uint64_t value_size,
|
||||
CompressionType compression_type,
|
||||
FilePrefetchBuffer* prefetch_buffer,
|
||||
PinnableSlice* value,
|
||||
uint64_t* bytes_read) const {
|
||||
assert(value);
|
||||
Status BlobFileReader::GetBlob(
|
||||
const ReadOptions& read_options, const Slice& user_key, uint64_t offset,
|
||||
uint64_t value_size, CompressionType compression_type,
|
||||
FilePrefetchBuffer* prefetch_buffer, MemoryAllocator* allocator,
|
||||
std::unique_ptr<BlobContents>* result, uint64_t* bytes_read) const {
|
||||
assert(result);
|
||||
|
||||
const uint64_t key_size = user_key.size();
|
||||
|
||||
@@ -361,8 +360,8 @@ Status BlobFileReader::GetBlob(const ReadOptions& read_options,
|
||||
const Slice value_slice(record_slice.data() + adjustment, value_size);
|
||||
|
||||
{
|
||||
const Status s = UncompressBlobIfNeeded(value_slice, compression_type,
|
||||
clock_, statistics_, value);
|
||||
const Status s = UncompressBlobIfNeeded(
|
||||
value_slice, compression_type, allocator, clock_, statistics_, result);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -375,16 +374,18 @@ Status BlobFileReader::GetBlob(const ReadOptions& read_options,
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
void BlobFileReader::MultiGetBlob(const ReadOptions& read_options,
|
||||
autovector<BlobReadRequest*>& blob_reqs,
|
||||
uint64_t* bytes_read) const {
|
||||
void BlobFileReader::MultiGetBlob(
|
||||
const ReadOptions& read_options, MemoryAllocator* allocator,
|
||||
autovector<std::pair<BlobReadRequest*, std::unique_ptr<BlobContents>>>&
|
||||
blob_reqs,
|
||||
uint64_t* bytes_read) const {
|
||||
const size_t num_blobs = blob_reqs.size();
|
||||
assert(num_blobs > 0);
|
||||
assert(num_blobs <= MultiGetContext::MAX_BATCH_SIZE);
|
||||
|
||||
#ifndef NDEBUG
|
||||
for (size_t i = 0; i < num_blobs - 1; ++i) {
|
||||
assert(blob_reqs[i]->offset <= blob_reqs[i + 1]->offset);
|
||||
assert(blob_reqs[i].first->offset <= blob_reqs[i + 1].first->offset);
|
||||
}
|
||||
#endif // !NDEBUG
|
||||
|
||||
@@ -393,16 +394,21 @@ void BlobFileReader::MultiGetBlob(const ReadOptions& read_options,
|
||||
uint64_t total_len = 0;
|
||||
read_reqs.reserve(num_blobs);
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
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;
|
||||
BlobReadRequest* const req = blob_reqs[i].first;
|
||||
assert(req);
|
||||
assert(req->user_key);
|
||||
assert(req->status);
|
||||
|
||||
const size_t key_size = req->user_key->size();
|
||||
const uint64_t offset = req->offset;
|
||||
const uint64_t value_size = req->len;
|
||||
|
||||
if (!IsValidBlobOffset(offset, key_size, value_size, file_size_)) {
|
||||
*blob_reqs[i]->status = Status::Corruption("Invalid blob offset");
|
||||
*req->status = Status::Corruption("Invalid blob offset");
|
||||
continue;
|
||||
}
|
||||
if (blob_reqs[i]->compression != compression_type_) {
|
||||
*blob_reqs[i]->status =
|
||||
if (req->compression != compression_type_) {
|
||||
*req->status =
|
||||
Status::Corruption("Compression type mismatch when reading a blob");
|
||||
continue;
|
||||
}
|
||||
@@ -411,12 +417,12 @@ void BlobFileReader::MultiGetBlob(const ReadOptions& read_options,
|
||||
read_options.verify_checksums
|
||||
? BlobLogRecord::CalculateAdjustmentForRecordHeader(key_size)
|
||||
: 0;
|
||||
assert(blob_reqs[i]->offset >= adjustment);
|
||||
assert(req->offset >= adjustment);
|
||||
adjustments.push_back(adjustment);
|
||||
|
||||
FSReadRequest read_req = {};
|
||||
read_req.offset = blob_reqs[i]->offset - adjustment;
|
||||
read_req.len = blob_reqs[i]->len + adjustment;
|
||||
read_req.offset = req->offset - adjustment;
|
||||
read_req.len = req->len + adjustment;
|
||||
read_reqs.emplace_back(read_req);
|
||||
total_len += read_req.len;
|
||||
}
|
||||
@@ -450,8 +456,11 @@ void BlobFileReader::MultiGetBlob(const ReadOptions& read_options,
|
||||
for (auto& req : read_reqs) {
|
||||
req.status.PermitUncheckedError();
|
||||
}
|
||||
for (auto& req : blob_reqs) {
|
||||
for (auto& blob_req : blob_reqs) {
|
||||
BlobReadRequest* const req = blob_req.first;
|
||||
assert(req);
|
||||
assert(req->status);
|
||||
|
||||
if (!req->status->IsCorruption()) {
|
||||
// Avoid overwriting corruption status.
|
||||
*req->status = s;
|
||||
@@ -464,38 +473,42 @@ void BlobFileReader::MultiGetBlob(const ReadOptions& read_options,
|
||||
|
||||
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()) {
|
||||
BlobReadRequest* const req = blob_reqs[i].first;
|
||||
assert(req);
|
||||
assert(req->user_key);
|
||||
assert(req->status);
|
||||
|
||||
if (!req->status->ok()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
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");
|
||||
auto& read_req = read_reqs[j++];
|
||||
const auto& record_slice = read_req.result;
|
||||
if (read_req.status.ok() && record_slice.size() != read_req.len) {
|
||||
read_req.status =
|
||||
IOStatus::Corruption("Failed to read data from blob file");
|
||||
}
|
||||
|
||||
*blob_reqs[i]->status = req.status;
|
||||
if (!blob_reqs[i]->status->ok()) {
|
||||
*req->status = read_req.status;
|
||||
if (!req->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()) {
|
||||
*req->status = VerifyBlob(record_slice, *req->user_key, req->len);
|
||||
if (!req->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()) {
|
||||
Slice value_slice(record_slice.data() + adjustments[i], req->len);
|
||||
*req->status =
|
||||
UncompressBlobIfNeeded(value_slice, compression_type_, allocator,
|
||||
clock_, statistics_, &blob_reqs[i].second);
|
||||
if (req->status->ok()) {
|
||||
total_bytes += record_slice.size();
|
||||
}
|
||||
}
|
||||
@@ -549,15 +562,18 @@ Status BlobFileReader::VerifyBlob(const Slice& record_slice,
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status BlobFileReader::UncompressBlobIfNeeded(const Slice& value_slice,
|
||||
CompressionType compression_type,
|
||||
SystemClock* clock,
|
||||
Statistics* statistics,
|
||||
PinnableSlice* value) {
|
||||
assert(value);
|
||||
Status BlobFileReader::UncompressBlobIfNeeded(
|
||||
const Slice& value_slice, CompressionType compression_type,
|
||||
MemoryAllocator* allocator, SystemClock* clock, Statistics* statistics,
|
||||
std::unique_ptr<BlobContents>* result) {
|
||||
assert(result);
|
||||
|
||||
if (compression_type == kNoCompression) {
|
||||
SaveValue(value_slice, value);
|
||||
CacheAllocationPtr allocation =
|
||||
AllocateBlock(value_slice.size(), allocator);
|
||||
memcpy(allocation.get(), value_slice.data(), value_slice.size());
|
||||
|
||||
*result = BlobContents::Create(std::move(allocation), value_slice.size());
|
||||
|
||||
return Status::OK();
|
||||
}
|
||||
@@ -568,7 +584,6 @@ Status BlobFileReader::UncompressBlobIfNeeded(const Slice& value_slice,
|
||||
|
||||
size_t uncompressed_size = 0;
|
||||
constexpr uint32_t compression_format_version = 2;
|
||||
constexpr MemoryAllocator* allocator = nullptr;
|
||||
|
||||
CacheAllocationPtr output;
|
||||
|
||||
@@ -587,19 +602,9 @@ Status BlobFileReader::UncompressBlobIfNeeded(const Slice& value_slice,
|
||||
return Status::Corruption("Unable to uncompress blob");
|
||||
}
|
||||
|
||||
SaveValue(Slice(output.get(), uncompressed_size), value);
|
||||
*result = BlobContents::Create(std::move(output), uncompressed_size);
|
||||
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
void BlobFileReader::SaveValue(const Slice& src, PinnableSlice* dst) {
|
||||
assert(dst);
|
||||
|
||||
if (dst->IsPinned()) {
|
||||
dst->Reset();
|
||||
}
|
||||
|
||||
dst->PinSelf(src);
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -23,7 +23,7 @@ class HistogramImpl;
|
||||
struct ReadOptions;
|
||||
class Slice;
|
||||
class FilePrefetchBuffer;
|
||||
class PinnableSlice;
|
||||
class BlobContents;
|
||||
class Statistics;
|
||||
|
||||
class BlobFileReader {
|
||||
@@ -44,13 +44,17 @@ class BlobFileReader {
|
||||
Status GetBlob(const ReadOptions& read_options, const Slice& user_key,
|
||||
uint64_t offset, uint64_t value_size,
|
||||
CompressionType compression_type,
|
||||
FilePrefetchBuffer* prefetch_buffer, PinnableSlice* value,
|
||||
FilePrefetchBuffer* prefetch_buffer,
|
||||
MemoryAllocator* allocator,
|
||||
std::unique_ptr<BlobContents>* result,
|
||||
uint64_t* bytes_read) const;
|
||||
|
||||
// offsets must be sorted in ascending order by caller.
|
||||
void MultiGetBlob(const ReadOptions& read_options,
|
||||
autovector<BlobReadRequest*>& blob_reqs,
|
||||
uint64_t* bytes_read) const;
|
||||
void MultiGetBlob(
|
||||
const ReadOptions& read_options, MemoryAllocator* allocator,
|
||||
autovector<std::pair<BlobReadRequest*, std::unique_ptr<BlobContents>>>&
|
||||
blob_reqs,
|
||||
uint64_t* bytes_read) const;
|
||||
|
||||
CompressionType GetCompressionType() const { return compression_type_; }
|
||||
|
||||
@@ -89,11 +93,10 @@ class BlobFileReader {
|
||||
|
||||
static Status UncompressBlobIfNeeded(const Slice& value_slice,
|
||||
CompressionType compression_type,
|
||||
MemoryAllocator* allocator,
|
||||
SystemClock* clock,
|
||||
Statistics* statistics,
|
||||
PinnableSlice* value);
|
||||
|
||||
static void SaveValue(const Slice& src, PinnableSlice* dst);
|
||||
std::unique_ptr<BlobContents>* result);
|
||||
|
||||
std::unique_ptr<RandomAccessFileReader> file_reader_;
|
||||
uint64_t file_size_;
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
|
||||
#include "db/blob/blob_contents.h"
|
||||
#include "db/blob/blob_log_format.h"
|
||||
#include "db/blob/blob_log_writer.h"
|
||||
#include "env/mock_env.h"
|
||||
@@ -180,15 +181,17 @@ TEST_F(BlobFileReaderTest, CreateReaderAndGetBlob) {
|
||||
read_options.verify_checksums = false;
|
||||
|
||||
constexpr FilePrefetchBuffer* prefetch_buffer = nullptr;
|
||||
constexpr MemoryAllocator* allocator = nullptr;
|
||||
|
||||
{
|
||||
PinnableSlice value;
|
||||
std::unique_ptr<BlobContents> value;
|
||||
uint64_t bytes_read = 0;
|
||||
|
||||
ASSERT_OK(reader->GetBlob(read_options, keys[0], blob_offsets[0],
|
||||
blob_sizes[0], kNoCompression, prefetch_buffer,
|
||||
&value, &bytes_read));
|
||||
ASSERT_EQ(value, blobs[0]);
|
||||
allocator, &value, &bytes_read));
|
||||
ASSERT_NE(value, nullptr);
|
||||
ASSERT_EQ(value->data(), blobs[0]);
|
||||
ASSERT_EQ(bytes_read, blob_sizes[0]);
|
||||
|
||||
// MultiGetBlob
|
||||
@@ -196,22 +199,25 @@ TEST_F(BlobFileReaderTest, CreateReaderAndGetBlob) {
|
||||
size_t total_size = 0;
|
||||
|
||||
std::array<Status, num_blobs> statuses_buf;
|
||||
std::array<PinnableSlice, num_blobs> value_buf;
|
||||
std::array<BlobReadRequest, num_blobs> requests_buf;
|
||||
autovector<BlobReadRequest*> blob_reqs;
|
||||
autovector<std::pair<BlobReadRequest*, std::unique_ptr<BlobContents>>>
|
||||
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]);
|
||||
kNoCompression, nullptr, &statuses_buf[i]);
|
||||
blob_reqs.emplace_back(&requests_buf[i], std::unique_ptr<BlobContents>());
|
||||
}
|
||||
|
||||
reader->MultiGetBlob(read_options, blob_reqs, &bytes_read);
|
||||
reader->MultiGetBlob(read_options, allocator, blob_reqs, &bytes_read);
|
||||
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
const auto& result = blob_reqs[i].second;
|
||||
|
||||
ASSERT_OK(statuses_buf[i]);
|
||||
ASSERT_EQ(value_buf[i], blobs[i]);
|
||||
ASSERT_NE(result, nullptr);
|
||||
ASSERT_EQ(result->data(), blobs[i]);
|
||||
total_size += blob_sizes[i];
|
||||
}
|
||||
ASSERT_EQ(bytes_read, total_size);
|
||||
@@ -220,13 +226,14 @@ TEST_F(BlobFileReaderTest, CreateReaderAndGetBlob) {
|
||||
read_options.verify_checksums = true;
|
||||
|
||||
{
|
||||
PinnableSlice value;
|
||||
std::unique_ptr<BlobContents> value;
|
||||
uint64_t bytes_read = 0;
|
||||
|
||||
ASSERT_OK(reader->GetBlob(read_options, keys[1], blob_offsets[1],
|
||||
blob_sizes[1], kNoCompression, prefetch_buffer,
|
||||
&value, &bytes_read));
|
||||
ASSERT_EQ(value, blobs[1]);
|
||||
allocator, &value, &bytes_read));
|
||||
ASSERT_NE(value, nullptr);
|
||||
ASSERT_EQ(value->data(), blobs[1]);
|
||||
|
||||
const uint64_t key_size = keys[1].size();
|
||||
ASSERT_EQ(bytes_read,
|
||||
@@ -236,47 +243,50 @@ TEST_F(BlobFileReaderTest, CreateReaderAndGetBlob) {
|
||||
|
||||
// Invalid offset (too close to start of file)
|
||||
{
|
||||
PinnableSlice value;
|
||||
std::unique_ptr<BlobContents> value;
|
||||
uint64_t bytes_read = 0;
|
||||
|
||||
ASSERT_TRUE(reader
|
||||
->GetBlob(read_options, keys[0], blob_offsets[0] - 1,
|
||||
blob_sizes[0], kNoCompression, prefetch_buffer,
|
||||
&value, &bytes_read)
|
||||
allocator, &value, &bytes_read)
|
||||
.IsCorruption());
|
||||
ASSERT_EQ(value, nullptr);
|
||||
ASSERT_EQ(bytes_read, 0);
|
||||
}
|
||||
|
||||
// Invalid offset (too close to end of file)
|
||||
{
|
||||
PinnableSlice value;
|
||||
std::unique_ptr<BlobContents> value;
|
||||
uint64_t bytes_read = 0;
|
||||
|
||||
ASSERT_TRUE(reader
|
||||
->GetBlob(read_options, keys[2], blob_offsets[2] + 1,
|
||||
blob_sizes[2], kNoCompression, prefetch_buffer,
|
||||
&value, &bytes_read)
|
||||
allocator, &value, &bytes_read)
|
||||
.IsCorruption());
|
||||
ASSERT_EQ(value, nullptr);
|
||||
ASSERT_EQ(bytes_read, 0);
|
||||
}
|
||||
|
||||
// Incorrect compression type
|
||||
{
|
||||
PinnableSlice value;
|
||||
std::unique_ptr<BlobContents> value;
|
||||
uint64_t bytes_read = 0;
|
||||
|
||||
ASSERT_TRUE(reader
|
||||
->GetBlob(read_options, keys[0], blob_offsets[0],
|
||||
blob_sizes[0], kZSTD, prefetch_buffer, &value,
|
||||
&bytes_read)
|
||||
blob_sizes[0], kZSTD, prefetch_buffer, allocator,
|
||||
&value, &bytes_read)
|
||||
.IsCorruption());
|
||||
ASSERT_EQ(value, nullptr);
|
||||
ASSERT_EQ(bytes_read, 0);
|
||||
}
|
||||
|
||||
// Incorrect key size
|
||||
{
|
||||
constexpr char shorter_key[] = "k";
|
||||
PinnableSlice value;
|
||||
std::unique_ptr<BlobContents> value;
|
||||
uint64_t bytes_read = 0;
|
||||
|
||||
ASSERT_TRUE(reader
|
||||
@@ -284,8 +294,9 @@ TEST_F(BlobFileReaderTest, CreateReaderAndGetBlob) {
|
||||
blob_offsets[0] -
|
||||
(keys[0].size() - sizeof(shorter_key) + 1),
|
||||
blob_sizes[0], kNoCompression, prefetch_buffer,
|
||||
&value, &bytes_read)
|
||||
allocator, &value, &bytes_read)
|
||||
.IsCorruption());
|
||||
ASSERT_EQ(value, nullptr);
|
||||
ASSERT_EQ(bytes_read, 0);
|
||||
|
||||
// MultiGetBlob
|
||||
@@ -302,18 +313,18 @@ TEST_F(BlobFileReaderTest, CreateReaderAndGetBlob) {
|
||||
blob_offsets[2]};
|
||||
|
||||
std::array<Status, num_blobs> statuses_buf;
|
||||
std::array<PinnableSlice, num_blobs> value_buf;
|
||||
std::array<BlobReadRequest, num_blobs> requests_buf;
|
||||
autovector<BlobReadRequest*> blob_reqs;
|
||||
autovector<std::pair<BlobReadRequest*, std::unique_ptr<BlobContents>>>
|
||||
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]);
|
||||
kNoCompression, nullptr, &statuses_buf[i]);
|
||||
blob_reqs.emplace_back(&requests_buf[i], std::unique_ptr<BlobContents>());
|
||||
}
|
||||
|
||||
reader->MultiGetBlob(read_options, blob_reqs, &bytes_read);
|
||||
reader->MultiGetBlob(read_options, allocator, blob_reqs, &bytes_read);
|
||||
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
if (i == 1) {
|
||||
@@ -327,14 +338,15 @@ TEST_F(BlobFileReaderTest, CreateReaderAndGetBlob) {
|
||||
// Incorrect key
|
||||
{
|
||||
constexpr char incorrect_key[] = "foo1";
|
||||
PinnableSlice value;
|
||||
std::unique_ptr<BlobContents> value;
|
||||
uint64_t bytes_read = 0;
|
||||
|
||||
ASSERT_TRUE(reader
|
||||
->GetBlob(read_options, incorrect_key, blob_offsets[0],
|
||||
blob_sizes[0], kNoCompression, prefetch_buffer,
|
||||
&value, &bytes_read)
|
||||
allocator, &value, &bytes_read)
|
||||
.IsCorruption());
|
||||
ASSERT_EQ(value, nullptr);
|
||||
ASSERT_EQ(bytes_read, 0);
|
||||
|
||||
// MultiGetBlob
|
||||
@@ -346,19 +358,18 @@ TEST_F(BlobFileReaderTest, CreateReaderAndGetBlob) {
|
||||
key_refs[2] = std::cref(wrong_key_slice);
|
||||
|
||||
std::array<Status, num_blobs> statuses_buf;
|
||||
std::array<PinnableSlice, num_blobs> value_buf;
|
||||
std::array<BlobReadRequest, num_blobs> requests_buf;
|
||||
autovector<std::pair<BlobReadRequest*, std::unique_ptr<BlobContents>>>
|
||||
blob_reqs;
|
||||
|
||||
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]);
|
||||
kNoCompression, nullptr, &statuses_buf[i]);
|
||||
blob_reqs.emplace_back(&requests_buf[i], std::unique_ptr<BlobContents>());
|
||||
}
|
||||
|
||||
autovector<BlobReadRequest*> blob_reqs = {
|
||||
&requests_buf[0], &requests_buf[1], &requests_buf[2]};
|
||||
|
||||
reader->MultiGetBlob(read_options, blob_reqs, &bytes_read);
|
||||
reader->MultiGetBlob(read_options, allocator, blob_reqs, &bytes_read);
|
||||
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
if (i == num_blobs - 1) {
|
||||
@@ -371,14 +382,15 @@ TEST_F(BlobFileReaderTest, CreateReaderAndGetBlob) {
|
||||
|
||||
// Incorrect value size
|
||||
{
|
||||
PinnableSlice value;
|
||||
std::unique_ptr<BlobContents> value;
|
||||
uint64_t bytes_read = 0;
|
||||
|
||||
ASSERT_TRUE(reader
|
||||
->GetBlob(read_options, keys[1], blob_offsets[1],
|
||||
blob_sizes[1] + 1, kNoCompression,
|
||||
prefetch_buffer, &value, &bytes_read)
|
||||
prefetch_buffer, allocator, &value, &bytes_read)
|
||||
.IsCorruption());
|
||||
ASSERT_EQ(value, nullptr);
|
||||
ASSERT_EQ(bytes_read, 0);
|
||||
|
||||
// MultiGetBlob
|
||||
@@ -388,23 +400,26 @@ TEST_F(BlobFileReaderTest, CreateReaderAndGetBlob) {
|
||||
}
|
||||
|
||||
std::array<Status, num_blobs> statuses_buf;
|
||||
std::array<PinnableSlice, num_blobs> value_buf;
|
||||
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]);
|
||||
kNoCompression, nullptr, &statuses_buf[0]);
|
||||
requests_buf[1] =
|
||||
BlobReadRequest(key_refs[1], blob_offsets[1], blob_sizes[1] + 1,
|
||||
kNoCompression, &value_buf[1], &statuses_buf[1]);
|
||||
kNoCompression, nullptr, &statuses_buf[1]);
|
||||
requests_buf[2] =
|
||||
BlobReadRequest(key_refs[2], blob_offsets[2], blob_sizes[2],
|
||||
kNoCompression, &value_buf[2], &statuses_buf[2]);
|
||||
kNoCompression, nullptr, &statuses_buf[2]);
|
||||
|
||||
autovector<BlobReadRequest*> blob_reqs = {
|
||||
&requests_buf[0], &requests_buf[1], &requests_buf[2]};
|
||||
autovector<std::pair<BlobReadRequest*, std::unique_ptr<BlobContents>>>
|
||||
blob_reqs;
|
||||
|
||||
reader->MultiGetBlob(read_options, blob_reqs, &bytes_read);
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
blob_reqs.emplace_back(&requests_buf[i], std::unique_ptr<BlobContents>());
|
||||
}
|
||||
|
||||
reader->MultiGetBlob(read_options, allocator, blob_reqs, &bytes_read);
|
||||
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
if (i != 1) {
|
||||
@@ -665,14 +680,17 @@ TEST_F(BlobFileReaderTest, BlobCRCError) {
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
constexpr FilePrefetchBuffer* prefetch_buffer = nullptr;
|
||||
PinnableSlice value;
|
||||
constexpr MemoryAllocator* allocator = nullptr;
|
||||
|
||||
std::unique_ptr<BlobContents> value;
|
||||
uint64_t bytes_read = 0;
|
||||
|
||||
ASSERT_TRUE(reader
|
||||
->GetBlob(ReadOptions(), key, blob_offset, blob_size,
|
||||
kNoCompression, prefetch_buffer, &value,
|
||||
kNoCompression, prefetch_buffer, allocator, &value,
|
||||
&bytes_read)
|
||||
.IsCorruption());
|
||||
ASSERT_EQ(value, nullptr);
|
||||
ASSERT_EQ(bytes_read, 0);
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
@@ -720,28 +738,31 @@ TEST_F(BlobFileReaderTest, Compression) {
|
||||
read_options.verify_checksums = false;
|
||||
|
||||
constexpr FilePrefetchBuffer* prefetch_buffer = nullptr;
|
||||
constexpr MemoryAllocator* allocator = nullptr;
|
||||
|
||||
{
|
||||
PinnableSlice value;
|
||||
std::unique_ptr<BlobContents> value;
|
||||
uint64_t bytes_read = 0;
|
||||
|
||||
ASSERT_OK(reader->GetBlob(read_options, key, blob_offset, blob_size,
|
||||
kSnappyCompression, prefetch_buffer, &value,
|
||||
&bytes_read));
|
||||
ASSERT_EQ(value, blob);
|
||||
kSnappyCompression, prefetch_buffer, allocator,
|
||||
&value, &bytes_read));
|
||||
ASSERT_NE(value, nullptr);
|
||||
ASSERT_EQ(value->data(), blob);
|
||||
ASSERT_EQ(bytes_read, blob_size);
|
||||
}
|
||||
|
||||
read_options.verify_checksums = true;
|
||||
|
||||
{
|
||||
PinnableSlice value;
|
||||
std::unique_ptr<BlobContents> value;
|
||||
uint64_t bytes_read = 0;
|
||||
|
||||
ASSERT_OK(reader->GetBlob(read_options, key, blob_offset, blob_size,
|
||||
kSnappyCompression, prefetch_buffer, &value,
|
||||
&bytes_read));
|
||||
ASSERT_EQ(value, blob);
|
||||
kSnappyCompression, prefetch_buffer, allocator,
|
||||
&value, &bytes_read));
|
||||
ASSERT_NE(value, nullptr);
|
||||
ASSERT_EQ(value->data(), blob);
|
||||
|
||||
constexpr uint64_t key_size = sizeof(key) - 1;
|
||||
ASSERT_EQ(bytes_read,
|
||||
@@ -799,14 +820,17 @@ TEST_F(BlobFileReaderTest, UncompressionError) {
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
constexpr FilePrefetchBuffer* prefetch_buffer = nullptr;
|
||||
PinnableSlice value;
|
||||
constexpr MemoryAllocator* allocator = nullptr;
|
||||
|
||||
std::unique_ptr<BlobContents> value;
|
||||
uint64_t bytes_read = 0;
|
||||
|
||||
ASSERT_TRUE(reader
|
||||
->GetBlob(ReadOptions(), key, blob_offset, blob_size,
|
||||
kSnappyCompression, prefetch_buffer, &value,
|
||||
&bytes_read)
|
||||
kSnappyCompression, prefetch_buffer, allocator,
|
||||
&value, &bytes_read)
|
||||
.IsCorruption());
|
||||
ASSERT_EQ(value, nullptr);
|
||||
ASSERT_EQ(bytes_read, 0);
|
||||
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
@@ -885,14 +909,17 @@ TEST_P(BlobFileReaderIOErrorTest, IOError) {
|
||||
ASSERT_OK(s);
|
||||
|
||||
constexpr FilePrefetchBuffer* prefetch_buffer = nullptr;
|
||||
PinnableSlice value;
|
||||
constexpr MemoryAllocator* allocator = nullptr;
|
||||
|
||||
std::unique_ptr<BlobContents> value;
|
||||
uint64_t bytes_read = 0;
|
||||
|
||||
ASSERT_TRUE(reader
|
||||
->GetBlob(ReadOptions(), key, blob_offset, blob_size,
|
||||
kNoCompression, prefetch_buffer, &value,
|
||||
&bytes_read)
|
||||
kNoCompression, prefetch_buffer, allocator,
|
||||
&value, &bytes_read)
|
||||
.IsIOError());
|
||||
ASSERT_EQ(value, nullptr);
|
||||
ASSERT_EQ(bytes_read, 0);
|
||||
}
|
||||
|
||||
@@ -970,14 +997,17 @@ TEST_P(BlobFileReaderDecodingErrorTest, DecodingError) {
|
||||
ASSERT_OK(s);
|
||||
|
||||
constexpr FilePrefetchBuffer* prefetch_buffer = nullptr;
|
||||
PinnableSlice value;
|
||||
constexpr MemoryAllocator* allocator = nullptr;
|
||||
|
||||
std::unique_ptr<BlobContents> value;
|
||||
uint64_t bytes_read = 0;
|
||||
|
||||
ASSERT_TRUE(reader
|
||||
->GetBlob(ReadOptions(), key, blob_offset, blob_size,
|
||||
kNoCompression, prefetch_buffer, &value,
|
||||
&bytes_read)
|
||||
kNoCompression, prefetch_buffer, allocator,
|
||||
&value, &bytes_read)
|
||||
.IsCorruption());
|
||||
ASSERT_EQ(value, nullptr);
|
||||
ASSERT_EQ(bytes_read, 0);
|
||||
}
|
||||
|
||||
|
||||
+160
-137
@@ -10,6 +10,7 @@
|
||||
|
||||
#include "cache/cache_reservation_manager.h"
|
||||
#include "cache/charged_cache.h"
|
||||
#include "db/blob/blob_contents.h"
|
||||
#include "db/blob/blob_file_reader.h"
|
||||
#include "db/blob/blob_log_format.h"
|
||||
#include "monitoring/statistics.h"
|
||||
@@ -43,52 +44,63 @@ BlobSource::BlobSource(const ImmutableOptions* immutable_options,
|
||||
|
||||
BlobSource::~BlobSource() = default;
|
||||
|
||||
Status BlobSource::GetBlobFromCache(const Slice& cache_key,
|
||||
CacheHandleGuard<std::string>* blob) const {
|
||||
assert(blob);
|
||||
assert(blob->IsEmpty());
|
||||
Status BlobSource::GetBlobFromCache(
|
||||
const Slice& cache_key, CacheHandleGuard<BlobContents>* cached_blob) const {
|
||||
assert(blob_cache_);
|
||||
assert(!cache_key.empty());
|
||||
assert(cached_blob);
|
||||
assert(cached_blob->IsEmpty());
|
||||
|
||||
Cache::Handle* cache_handle = nullptr;
|
||||
cache_handle = GetEntryFromCache(cache_key);
|
||||
if (cache_handle != nullptr) {
|
||||
*blob = CacheHandleGuard<std::string>(blob_cache_.get(), cache_handle);
|
||||
*cached_blob =
|
||||
CacheHandleGuard<BlobContents>(blob_cache_.get(), cache_handle);
|
||||
|
||||
assert(cached_blob->GetValue());
|
||||
|
||||
PERF_COUNTER_ADD(blob_cache_hit_count, 1);
|
||||
RecordTick(statistics_, BLOB_DB_CACHE_HIT);
|
||||
RecordTick(statistics_, BLOB_DB_CACHE_BYTES_READ,
|
||||
cached_blob->GetValue()->size());
|
||||
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
assert(blob->IsEmpty());
|
||||
RecordTick(statistics_, BLOB_DB_CACHE_MISS);
|
||||
|
||||
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());
|
||||
Status BlobSource::PutBlobIntoCache(
|
||||
const Slice& cache_key, std::unique_ptr<BlobContents>* blob,
|
||||
CacheHandleGuard<BlobContents>* cached_blob) const {
|
||||
assert(blob_cache_);
|
||||
assert(!cache_key.empty());
|
||||
assert(blob);
|
||||
assert(*blob);
|
||||
assert(cached_blob);
|
||||
assert(cached_blob->IsEmpty());
|
||||
|
||||
Status s;
|
||||
const Cache::Priority priority = Cache::Priority::BOTTOM;
|
||||
|
||||
// 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::unique_ptr<std::string> buf = std::make_unique<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.get(), buf->size(), &cache_handle,
|
||||
priority);
|
||||
const Status s = InsertEntryIntoCache(cache_key, blob->get(),
|
||||
(*blob)->ApproximateMemoryUsage(),
|
||||
&cache_handle, Cache::Priority::BOTTOM);
|
||||
if (s.ok()) {
|
||||
buf.release();
|
||||
blob->release();
|
||||
|
||||
assert(cache_handle != nullptr);
|
||||
*cached_blob =
|
||||
CacheHandleGuard<std::string>(blob_cache_.get(), cache_handle);
|
||||
CacheHandleGuard<BlobContents>(blob_cache_.get(), cache_handle);
|
||||
|
||||
assert(cached_blob->GetValue());
|
||||
|
||||
RecordTick(statistics_, BLOB_DB_CACHE_ADD);
|
||||
RecordTick(statistics_, BLOB_DB_CACHE_BYTES_WRITE,
|
||||
cached_blob->GetValue()->size());
|
||||
|
||||
} else {
|
||||
RecordTick(statistics_, BLOB_DB_CACHE_ADD_FAILURES);
|
||||
}
|
||||
|
||||
return s;
|
||||
@@ -98,54 +110,77 @@ Cache::Handle* BlobSource::GetEntryFromCache(const Slice& key) const {
|
||||
Cache::Handle* cache_handle = nullptr;
|
||||
|
||||
if (lowest_used_cache_tier_ == CacheTier::kNonVolatileBlockTier) {
|
||||
Cache::CreateCallback create_cb = [&](const void* buf, size_t size,
|
||||
void** out_obj,
|
||||
size_t* charge) -> Status {
|
||||
std::string* blob = new std::string();
|
||||
blob->assign(static_cast<const char*>(buf), size);
|
||||
*out_obj = blob;
|
||||
*charge = size;
|
||||
return Status::OK();
|
||||
Cache::CreateCallback create_cb =
|
||||
[allocator = blob_cache_->memory_allocator()](
|
||||
const void* buf, size_t size, void** out_obj,
|
||||
size_t* charge) -> Status {
|
||||
return BlobContents::CreateCallback(AllocateBlock(size, allocator), buf,
|
||||
size, out_obj, charge);
|
||||
};
|
||||
cache_handle = blob_cache_->Lookup(key, GetCacheItemHelper(), create_cb,
|
||||
Cache::Priority::BOTTOM,
|
||||
|
||||
cache_handle = blob_cache_->Lookup(key, BlobContents::GetCacheItemHelper(),
|
||||
create_cb, Cache::Priority::BOTTOM,
|
||||
true /* wait_for_cache */, statistics_);
|
||||
} else {
|
||||
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,
|
||||
void BlobSource::PinCachedBlob(CacheHandleGuard<BlobContents>* cached_blob,
|
||||
PinnableSlice* value) {
|
||||
assert(cached_blob);
|
||||
assert(cached_blob->GetValue());
|
||||
assert(value);
|
||||
|
||||
// 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->Reset();
|
||||
|
||||
constexpr Cleanable* cleanable = nullptr;
|
||||
value->PinSlice(cached_blob->GetValue()->data(), cleanable);
|
||||
|
||||
cached_blob->TransferTo(value);
|
||||
}
|
||||
|
||||
void BlobSource::PinOwnedBlob(std::unique_ptr<BlobContents>* owned_blob,
|
||||
PinnableSlice* value) {
|
||||
assert(owned_blob);
|
||||
assert(*owned_blob);
|
||||
assert(value);
|
||||
|
||||
BlobContents* const blob = owned_blob->release();
|
||||
assert(blob);
|
||||
|
||||
value->Reset();
|
||||
value->PinSlice(
|
||||
blob->data(),
|
||||
[](void* arg1, void* /* arg2 */) {
|
||||
delete static_cast<BlobContents*>(arg1);
|
||||
},
|
||||
blob, nullptr);
|
||||
}
|
||||
|
||||
Status BlobSource::InsertEntryIntoCache(const Slice& key, BlobContents* value,
|
||||
size_t charge,
|
||||
Cache::Handle** cache_handle,
|
||||
Cache::Priority priority) const {
|
||||
Status s;
|
||||
|
||||
if (lowest_used_cache_tier_ == CacheTier::kNonVolatileBlockTier) {
|
||||
s = blob_cache_->Insert(key, value, GetCacheItemHelper(), charge,
|
||||
cache_handle, priority);
|
||||
} else {
|
||||
s = blob_cache_->Insert(key, value, charge, &DeleteCacheEntry<std::string>,
|
||||
cache_handle, priority);
|
||||
}
|
||||
Cache::CacheItemHelper* const cache_item_helper =
|
||||
BlobContents::GetCacheItemHelper();
|
||||
assert(cache_item_helper);
|
||||
|
||||
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));
|
||||
if (lowest_used_cache_tier_ == CacheTier::kNonVolatileBlockTier) {
|
||||
s = blob_cache_->Insert(key, value, cache_item_helper, charge, cache_handle,
|
||||
priority);
|
||||
} else {
|
||||
RecordTick(statistics_, BLOB_DB_CACHE_ADD_FAILURES);
|
||||
s = blob_cache_->Insert(key, value, charge, cache_item_helper->del_cb,
|
||||
cache_handle, priority);
|
||||
}
|
||||
|
||||
return s;
|
||||
@@ -164,7 +199,7 @@ Status BlobSource::GetBlob(const ReadOptions& read_options,
|
||||
|
||||
const CacheKey cache_key = GetCacheKey(file_number, file_size, offset);
|
||||
|
||||
CacheHandleGuard<std::string> blob_handle;
|
||||
CacheHandleGuard<BlobContents> blob_handle;
|
||||
|
||||
// First, try to get the blob from the cache
|
||||
//
|
||||
@@ -172,24 +207,8 @@ Status BlobSource::GetBlob(const ReadOptions& read_options,
|
||||
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);
|
||||
}
|
||||
if (s.ok()) {
|
||||
PinCachedBlob(&blob_handle, value);
|
||||
|
||||
// For consistency, the size of on-disk (possibly compressed) blob record
|
||||
// is assigned to bytes_read.
|
||||
@@ -218,6 +237,8 @@ Status BlobSource::GetBlob(const ReadOptions& read_options,
|
||||
|
||||
// Can't find the blob from the cache. Since I/O is allowed, read from the
|
||||
// file.
|
||||
std::unique_ptr<BlobContents> blob_contents;
|
||||
|
||||
{
|
||||
CacheHandleGuard<BlobFileReader> blob_file_reader;
|
||||
s = blob_file_cache_->GetBlobFileReader(file_number, &blob_file_reader);
|
||||
@@ -231,10 +252,14 @@ Status BlobSource::GetBlob(const ReadOptions& read_options,
|
||||
return Status::Corruption("Compression type mismatch when reading blob");
|
||||
}
|
||||
|
||||
MemoryAllocator* const allocator = (blob_cache_ && read_options.fill_cache)
|
||||
? blob_cache_->memory_allocator()
|
||||
: nullptr;
|
||||
|
||||
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);
|
||||
prefetch_buffer, allocator, &blob_contents, &read_size);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -247,10 +272,14 @@ Status BlobSource::GetBlob(const ReadOptions& read_options,
|
||||
// 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);
|
||||
s = PutBlobIntoCache(key, &blob_contents, &blob_handle);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
PinCachedBlob(&blob_handle, value);
|
||||
} else {
|
||||
PinOwnedBlob(&blob_contents, value);
|
||||
}
|
||||
|
||||
assert(s.ok());
|
||||
@@ -310,33 +339,17 @@ void BlobSource::MultiGetBlobFromOneFile(const ReadOptions& read_options,
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
auto& req = blob_reqs[i];
|
||||
|
||||
CacheHandleGuard<std::string> blob_handle;
|
||||
CacheHandleGuard<BlobContents> 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()) {
|
||||
if (s.ok()) {
|
||||
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);
|
||||
}
|
||||
PinCachedBlob(&blob_handle, req.result);
|
||||
|
||||
// Update the counter for the number of valid blobs read from the cache.
|
||||
++cached_blob_count;
|
||||
@@ -367,8 +380,10 @@ void BlobSource::MultiGetBlobFromOneFile(const ReadOptions& read_options,
|
||||
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 =
|
||||
BlobReadRequest& req = blob_reqs[i];
|
||||
assert(req.status);
|
||||
|
||||
*req.status =
|
||||
Status::Incomplete("Cannot read blob(s): no disk I/O allowed");
|
||||
}
|
||||
}
|
||||
@@ -377,12 +392,13 @@ void BlobSource::MultiGetBlobFromOneFile(const ReadOptions& read_options,
|
||||
|
||||
{
|
||||
// Find the rest of blobs from the file since I/O is allowed.
|
||||
autovector<BlobReadRequest*> _blob_reqs;
|
||||
autovector<std::pair<BlobReadRequest*, std::unique_ptr<BlobContents>>>
|
||||
_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]);
|
||||
_blob_reqs.emplace_back(&blob_reqs[i], std::unique_ptr<BlobContents>());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -391,32 +407,50 @@ void BlobSource::MultiGetBlobFromOneFile(const ReadOptions& read_options,
|
||||
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;
|
||||
BlobReadRequest* const req = _blob_reqs[i].first;
|
||||
assert(req);
|
||||
assert(req->status);
|
||||
|
||||
*req->status = s;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
assert(blob_file_reader.GetValue());
|
||||
|
||||
blob_file_reader.GetValue()->MultiGetBlob(read_options, _blob_reqs,
|
||||
&_bytes_read);
|
||||
MemoryAllocator* const allocator = (blob_cache_ && read_options.fill_cache)
|
||||
? blob_cache_->memory_allocator()
|
||||
: nullptr;
|
||||
|
||||
blob_file_reader.GetValue()->MultiGetBlob(read_options, allocator,
|
||||
_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);
|
||||
for (auto& [req, blob_contents] : _blob_reqs) {
|
||||
assert(req);
|
||||
|
||||
if (req->status->ok()) {
|
||||
CacheHandleGuard<BlobContents> blob_handle;
|
||||
const CacheKey cache_key = base_cache_key.WithOffset(req->offset);
|
||||
const Slice key = cache_key.AsSlice();
|
||||
s = PutBlobIntoCache(key, &blob_handle, _blob_reqs[i]->result);
|
||||
s = PutBlobIntoCache(key, &blob_contents, &blob_handle);
|
||||
if (!s.ok()) {
|
||||
*_blob_reqs[i]->status = s;
|
||||
*req->status = s;
|
||||
} else {
|
||||
PinCachedBlob(&blob_handle, req->result);
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (auto& [req, blob_contents] : _blob_reqs) {
|
||||
assert(req);
|
||||
|
||||
if (req->status->ok()) {
|
||||
PinOwnedBlob(&blob_contents, req->result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
total_bytes += _bytes_read;
|
||||
@@ -427,39 +461,28 @@ void BlobSource::MultiGetBlobFromOneFile(const ReadOptions& read_options,
|
||||
}
|
||||
|
||||
bool BlobSource::TEST_BlobInCache(uint64_t file_number, uint64_t file_size,
|
||||
uint64_t offset) const {
|
||||
uint64_t offset, size_t* charge) const {
|
||||
const CacheKey cache_key = GetCacheKey(file_number, file_size, offset);
|
||||
const Slice key = cache_key.AsSlice();
|
||||
|
||||
CacheHandleGuard<std::string> blob_handle;
|
||||
CacheHandleGuard<BlobContents> blob_handle;
|
||||
const Status s = GetBlobFromCache(key, &blob_handle);
|
||||
|
||||
if (s.ok() && blob_handle.GetValue() != nullptr) {
|
||||
if (charge) {
|
||||
const Cache* const cache = blob_handle.GetCache();
|
||||
assert(cache);
|
||||
|
||||
Cache::Handle* const handle = blob_handle.GetCacheHandle();
|
||||
assert(handle);
|
||||
|
||||
*charge = cache->GetUsage(handle);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
// Callbacks for secondary blob cache
|
||||
size_t BlobSource::SizeCallback(void* obj) {
|
||||
assert(obj != nullptr);
|
||||
return static_cast<const std::string*>(obj)->size();
|
||||
}
|
||||
|
||||
Status BlobSource::SaveToCallback(void* from_obj, size_t from_offset,
|
||||
size_t length, void* out) {
|
||||
assert(from_obj != nullptr);
|
||||
const std::string* buf = static_cast<const std::string*>(from_obj);
|
||||
assert(buf->size() >= from_offset + length);
|
||||
memcpy(out, buf->data() + from_offset, length);
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Cache::CacheItemHelper* BlobSource::GetCacheItemHelper() {
|
||||
static Cache::CacheItemHelper cache_helper(SizeCallback, SaveToCallback,
|
||||
&DeleteCacheEntry<std::string>);
|
||||
return &cache_helper;
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
+13
-13
@@ -6,6 +6,7 @@
|
||||
#pragma once
|
||||
|
||||
#include <cinttypes>
|
||||
#include <memory>
|
||||
|
||||
#include "cache/cache_helpers.h"
|
||||
#include "cache/cache_key.h"
|
||||
@@ -22,6 +23,7 @@ struct ImmutableOptions;
|
||||
class Status;
|
||||
class FilePrefetchBuffer;
|
||||
class Slice;
|
||||
class BlobContents;
|
||||
|
||||
// BlobSource is a class that provides universal access to blobs, regardless of
|
||||
// whether they are in the blob cache, secondary cache, or (remote) storage.
|
||||
@@ -102,19 +104,25 @@ class BlobSource {
|
||||
inline Cache* GetBlobCache() const { return blob_cache_.get(); }
|
||||
|
||||
bool TEST_BlobInCache(uint64_t file_number, uint64_t file_size,
|
||||
uint64_t offset) const;
|
||||
uint64_t offset, size_t* charge = nullptr) const;
|
||||
|
||||
private:
|
||||
Status GetBlobFromCache(const Slice& cache_key,
|
||||
CacheHandleGuard<std::string>* blob) const;
|
||||
CacheHandleGuard<BlobContents>* cached_blob) const;
|
||||
|
||||
Status PutBlobIntoCache(const Slice& cache_key,
|
||||
CacheHandleGuard<std::string>* cached_blob,
|
||||
PinnableSlice* blob) const;
|
||||
std::unique_ptr<BlobContents>* blob,
|
||||
CacheHandleGuard<BlobContents>* cached_blob) const;
|
||||
|
||||
static void PinCachedBlob(CacheHandleGuard<BlobContents>* cached_blob,
|
||||
PinnableSlice* value);
|
||||
|
||||
static void PinOwnedBlob(std::unique_ptr<BlobContents>* owned_blob,
|
||||
PinnableSlice* value);
|
||||
|
||||
Cache::Handle* GetEntryFromCache(const Slice& key) const;
|
||||
|
||||
Status InsertEntryIntoCache(const Slice& key, std::string* value,
|
||||
Status InsertEntryIntoCache(const Slice& key, BlobContents* value,
|
||||
size_t charge, Cache::Handle** cache_handle,
|
||||
Cache::Priority priority) const;
|
||||
|
||||
@@ -124,14 +132,6 @@ class BlobSource {
|
||||
return base_cache_key.WithOffset(offset);
|
||||
}
|
||||
|
||||
// Callbacks for secondary blob cache
|
||||
static size_t SizeCallback(void* obj);
|
||||
|
||||
static Status SaveToCallback(void* from_obj, size_t from_offset,
|
||||
size_t length, void* out);
|
||||
|
||||
static Cache::CacheItemHelper* GetCacheItemHelper();
|
||||
|
||||
const std::string& db_id_;
|
||||
const std::string& db_session_id_;
|
||||
|
||||
|
||||
+128
-56
@@ -13,6 +13,7 @@
|
||||
|
||||
#include "cache/charged_cache.h"
|
||||
#include "cache/compressed_secondary_cache.h"
|
||||
#include "db/blob/blob_contents.h"
|
||||
#include "db/blob/blob_file_cache.h"
|
||||
#include "db/blob/blob_file_reader.h"
|
||||
#include "db/blob/blob_log_format.h"
|
||||
@@ -218,6 +219,7 @@ TEST_F(BlobSourceTest, GetBlobsFromCache) {
|
||||
kNoCompression, prefetch_buffer, &values[i],
|
||||
&bytes_read));
|
||||
ASSERT_EQ(values[i], blobs[i]);
|
||||
ASSERT_TRUE(values[i].IsPinned());
|
||||
ASSERT_EQ(bytes_read,
|
||||
BlobLogRecord::kHeaderSize + keys[i].size() + blob_sizes[i]);
|
||||
|
||||
@@ -255,6 +257,7 @@ TEST_F(BlobSourceTest, GetBlobsFromCache) {
|
||||
kNoCompression, prefetch_buffer, &values[i],
|
||||
&bytes_read));
|
||||
ASSERT_EQ(values[i], blobs[i]);
|
||||
ASSERT_TRUE(values[i].IsPinned());
|
||||
ASSERT_EQ(bytes_read,
|
||||
BlobLogRecord::kHeaderSize + keys[i].size() + blob_sizes[i]);
|
||||
|
||||
@@ -298,6 +301,7 @@ TEST_F(BlobSourceTest, GetBlobsFromCache) {
|
||||
kNoCompression, prefetch_buffer, &values[i],
|
||||
&bytes_read));
|
||||
ASSERT_EQ(values[i], blobs[i]);
|
||||
ASSERT_TRUE(values[i].IsPinned());
|
||||
ASSERT_EQ(bytes_read,
|
||||
BlobLogRecord::kHeaderSize + keys[i].size() + blob_sizes[i]);
|
||||
|
||||
@@ -336,6 +340,7 @@ TEST_F(BlobSourceTest, GetBlobsFromCache) {
|
||||
kNoCompression, prefetch_buffer, &values[i],
|
||||
&bytes_read));
|
||||
ASSERT_EQ(values[i], blobs[i]);
|
||||
ASSERT_TRUE(values[i].IsPinned());
|
||||
ASSERT_EQ(bytes_read,
|
||||
BlobLogRecord::kHeaderSize + keys[i].size() + blob_sizes[i]);
|
||||
|
||||
@@ -382,6 +387,7 @@ TEST_F(BlobSourceTest, GetBlobsFromCache) {
|
||||
&bytes_read)
|
||||
.IsIncomplete());
|
||||
ASSERT_TRUE(values[i].empty());
|
||||
ASSERT_FALSE(values[i].IsPinned());
|
||||
ASSERT_EQ(bytes_read, 0);
|
||||
|
||||
ASSERT_FALSE(blob_source.TEST_BlobInCache(blob_file_number, file_size,
|
||||
@@ -423,6 +429,7 @@ TEST_F(BlobSourceTest, GetBlobsFromCache) {
|
||||
&bytes_read)
|
||||
.IsIOError());
|
||||
ASSERT_TRUE(values[i].empty());
|
||||
ASSERT_FALSE(values[i].IsPinned());
|
||||
ASSERT_EQ(bytes_read, 0);
|
||||
|
||||
ASSERT_FALSE(blob_source.TEST_BlobInCache(file_number, file_size,
|
||||
@@ -855,6 +862,7 @@ TEST_F(BlobSourceTest, MultiGetBlobsFromCache) {
|
||||
if (i % 2 == 0) {
|
||||
ASSERT_OK(statuses_buf[i]);
|
||||
ASSERT_EQ(value_buf[i], blobs[i]);
|
||||
ASSERT_TRUE(value_buf[i].IsPinned());
|
||||
fs_read_bytes +=
|
||||
blob_sizes[i] + keys[i].size() + BlobLogRecord::kHeaderSize;
|
||||
ASSERT_TRUE(blob_source.TEST_BlobInCache(blob_file_number, file_size,
|
||||
@@ -863,6 +871,7 @@ TEST_F(BlobSourceTest, MultiGetBlobsFromCache) {
|
||||
} else {
|
||||
statuses_buf[i].PermitUncheckedError();
|
||||
ASSERT_TRUE(value_buf[i].empty());
|
||||
ASSERT_FALSE(value_buf[i].IsPinned());
|
||||
ASSERT_FALSE(blob_source.TEST_BlobInCache(blob_file_number, file_size,
|
||||
blob_offsets[i]));
|
||||
}
|
||||
@@ -895,6 +904,7 @@ TEST_F(BlobSourceTest, MultiGetBlobsFromCache) {
|
||||
kNoCompression, prefetch_buffer,
|
||||
&value_buf[i], &bytes_read));
|
||||
ASSERT_EQ(value_buf[i], blobs[i]);
|
||||
ASSERT_TRUE(value_buf[i].IsPinned());
|
||||
ASSERT_EQ(bytes_read,
|
||||
BlobLogRecord::kHeaderSize + keys[i].size() + blob_sizes[i]);
|
||||
|
||||
@@ -920,6 +930,7 @@ TEST_F(BlobSourceTest, MultiGetBlobsFromCache) {
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
ASSERT_OK(statuses_buf[i]);
|
||||
ASSERT_EQ(value_buf[i], blobs[i]);
|
||||
ASSERT_TRUE(value_buf[i].IsPinned());
|
||||
ASSERT_TRUE(blob_source.TEST_BlobInCache(blob_file_number, file_size,
|
||||
blob_offsets[i]));
|
||||
blob_bytes += blob_sizes[i];
|
||||
@@ -968,6 +979,7 @@ TEST_F(BlobSourceTest, MultiGetBlobsFromCache) {
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
ASSERT_TRUE(statuses_buf[i].IsIncomplete());
|
||||
ASSERT_TRUE(value_buf[i].empty());
|
||||
ASSERT_FALSE(value_buf[i].IsPinned());
|
||||
ASSERT_FALSE(blob_source.TEST_BlobInCache(blob_file_number, file_size,
|
||||
blob_offsets[i]));
|
||||
}
|
||||
@@ -1011,6 +1023,7 @@ TEST_F(BlobSourceTest, MultiGetBlobsFromCache) {
|
||||
for (size_t i = 0; i < num_blobs; ++i) {
|
||||
ASSERT_TRUE(statuses_buf[i].IsIOError());
|
||||
ASSERT_TRUE(value_buf[i].empty());
|
||||
ASSERT_FALSE(value_buf[i].IsPinned());
|
||||
ASSERT_FALSE(blob_source.TEST_BlobInCache(non_existing_file_number,
|
||||
file_size, blob_offsets[i]));
|
||||
}
|
||||
@@ -1040,16 +1053,17 @@ class BlobSecondaryCacheTest : public DBTestBase {
|
||||
|
||||
// Set a small cache capacity to evict entries from the cache, and to test
|
||||
// that secondary cache is used properly.
|
||||
lru_cache_ops_.capacity = 1024;
|
||||
lru_cache_ops_.num_shard_bits = 0;
|
||||
lru_cache_ops_.strict_capacity_limit = true;
|
||||
lru_cache_ops_.metadata_charge_policy = kDontChargeCacheMetadata;
|
||||
lru_cache_ops_.high_pri_pool_ratio = 0.2;
|
||||
lru_cache_ops_.low_pri_pool_ratio = 0.2;
|
||||
lru_cache_opts_.capacity = 1024;
|
||||
lru_cache_opts_.num_shard_bits = 0;
|
||||
lru_cache_opts_.strict_capacity_limit = true;
|
||||
lru_cache_opts_.metadata_charge_policy = kDontChargeCacheMetadata;
|
||||
lru_cache_opts_.high_pri_pool_ratio = 0.2;
|
||||
lru_cache_opts_.low_pri_pool_ratio = 0.2;
|
||||
|
||||
secondary_cache_opts_.capacity = 8 << 20; // 8 MB
|
||||
secondary_cache_opts_.num_shard_bits = 0;
|
||||
secondary_cache_opts_.metadata_charge_policy = kDontChargeCacheMetadata;
|
||||
secondary_cache_opts_.metadata_charge_policy =
|
||||
kDefaultCacheMetadataChargePolicy;
|
||||
|
||||
// Read blobs from the secondary cache if they are not in the primary cache
|
||||
options_.lowest_used_cache_tier = CacheTier::kNonVolatileBlockTier;
|
||||
@@ -1060,7 +1074,7 @@ class BlobSecondaryCacheTest : public DBTestBase {
|
||||
|
||||
Options options_;
|
||||
|
||||
LRUCacheOptions lru_cache_ops_;
|
||||
LRUCacheOptions lru_cache_opts_;
|
||||
CompressedSecondaryCacheOptions secondary_cache_opts_;
|
||||
|
||||
std::string db_id_;
|
||||
@@ -1073,9 +1087,9 @@ TEST_F(BlobSecondaryCacheTest, GetBlobsFromSecondaryCache) {
|
||||
}
|
||||
|
||||
secondary_cache_opts_.compression_type = kSnappyCompression;
|
||||
lru_cache_ops_.secondary_cache =
|
||||
lru_cache_opts_.secondary_cache =
|
||||
NewCompressedSecondaryCache(secondary_cache_opts_);
|
||||
options_.blob_cache = NewLRUCache(lru_cache_ops_);
|
||||
options_.blob_cache = NewLRUCache(lru_cache_opts_);
|
||||
|
||||
options_.cf_paths.emplace_back(
|
||||
test::PerThreadDBPath(
|
||||
@@ -1098,8 +1112,8 @@ TEST_F(BlobSecondaryCacheTest, GetBlobsFromSecondaryCache) {
|
||||
Random rnd(301);
|
||||
|
||||
std::vector<std::string> key_strs{"key0", "key1"};
|
||||
std::vector<std::string> blob_strs{rnd.RandomString(1010),
|
||||
rnd.RandomString(1020)};
|
||||
std::vector<std::string> blob_strs{rnd.RandomString(512),
|
||||
rnd.RandomString(768)};
|
||||
|
||||
std::vector<Slice> keys{key_strs[0], key_strs[1]};
|
||||
std::vector<Slice> blobs{blob_strs[0], blob_strs[1]};
|
||||
@@ -1134,16 +1148,15 @@ TEST_F(BlobSecondaryCacheTest, GetBlobsFromSecondaryCache) {
|
||||
read_options.verify_checksums = true;
|
||||
|
||||
auto blob_cache = options_.blob_cache;
|
||||
auto secondary_cache = lru_cache_ops_.secondary_cache;
|
||||
auto secondary_cache = lru_cache_opts_.secondary_cache;
|
||||
|
||||
Cache::CreateCallback create_cb = [&](const void* buf, size_t size,
|
||||
void** out_obj,
|
||||
size_t* charge) -> Status {
|
||||
std::string* blob = new std::string();
|
||||
blob->assign(static_cast<const char*>(buf), size);
|
||||
*out_obj = blob;
|
||||
*charge = size;
|
||||
return Status::OK();
|
||||
Cache::CreateCallback create_cb = [](const void* buf, size_t size,
|
||||
void** out_obj,
|
||||
size_t* charge) -> Status {
|
||||
CacheAllocationPtr allocation(new char[size]);
|
||||
|
||||
return BlobContents::CreateCallback(std::move(allocation), buf, size,
|
||||
out_obj, charge);
|
||||
};
|
||||
|
||||
{
|
||||
@@ -1154,6 +1167,25 @@ TEST_F(BlobSecondaryCacheTest, GetBlobsFromSecondaryCache) {
|
||||
get_perf_context()->Reset();
|
||||
|
||||
// key0 should be filled to the primary cache from the blob file.
|
||||
ASSERT_OK(blob_source.GetBlob(read_options, keys[0], file_number,
|
||||
blob_offsets[0], file_size, blob_sizes[0],
|
||||
kNoCompression, nullptr /* prefetch_buffer */,
|
||||
&values[0], nullptr /* bytes_read */));
|
||||
// Release cache handle
|
||||
values[0].Reset();
|
||||
|
||||
// key0 should be evicted and key0's dummy item is inserted into secondary
|
||||
// cache. key1 should be filled to the primary cache from the blob file.
|
||||
ASSERT_OK(blob_source.GetBlob(read_options, keys[1], file_number,
|
||||
blob_offsets[1], file_size, blob_sizes[1],
|
||||
kNoCompression, nullptr /* prefetch_buffer */,
|
||||
&values[1], nullptr /* bytes_read */));
|
||||
|
||||
// Release cache handle
|
||||
values[1].Reset();
|
||||
|
||||
// key0 should be filled to the primary cache from the blob file. key1
|
||||
// should be evicted and key1's dummy item is inserted into secondary cache.
|
||||
ASSERT_OK(blob_source.GetBlob(read_options, keys[0], file_number,
|
||||
blob_offsets[0], file_size, blob_sizes[0],
|
||||
kNoCompression, nullptr /* prefetch_buffer */,
|
||||
@@ -1162,8 +1194,11 @@ TEST_F(BlobSecondaryCacheTest, GetBlobsFromSecondaryCache) {
|
||||
ASSERT_TRUE(
|
||||
blob_source.TEST_BlobInCache(file_number, file_size, blob_offsets[0]));
|
||||
|
||||
// key0 should be demoted to the secondary cache, and key1 should be filled
|
||||
// to the primary cache from the blob file.
|
||||
// Release cache handle
|
||||
values[0].Reset();
|
||||
|
||||
// key0 should be evicted and is inserted into secondary cache.
|
||||
// key1 should be filled to the primary cache from the blob file.
|
||||
ASSERT_OK(blob_source.GetBlob(read_options, keys[1], file_number,
|
||||
blob_offsets[1], file_size, blob_sizes[1],
|
||||
kNoCompression, nullptr /* prefetch_buffer */,
|
||||
@@ -1172,6 +1207,9 @@ TEST_F(BlobSecondaryCacheTest, GetBlobsFromSecondaryCache) {
|
||||
ASSERT_TRUE(
|
||||
blob_source.TEST_BlobInCache(file_number, file_size, blob_offsets[1]));
|
||||
|
||||
// Release cache handle
|
||||
values[1].Reset();
|
||||
|
||||
OffsetableCacheKey base_cache_key(db_id_, db_session_id_, file_number);
|
||||
|
||||
// blob_cache here only looks at the primary cache since we didn't provide
|
||||
@@ -1184,24 +1222,27 @@ TEST_F(BlobSecondaryCacheTest, GetBlobsFromSecondaryCache) {
|
||||
auto handle0 = blob_cache->Lookup(key0, statistics);
|
||||
ASSERT_EQ(handle0, nullptr);
|
||||
|
||||
// key0 should be in the secondary cache. After looking up key0 in the
|
||||
// secondary cache, it will be erased from the secondary cache.
|
||||
// key0's item should be in the secondary cache.
|
||||
bool is_in_sec_cache = false;
|
||||
auto sec_handle0 =
|
||||
secondary_cache->Lookup(key0, create_cb, true, is_in_sec_cache);
|
||||
secondary_cache->Lookup(key0, create_cb, true,
|
||||
/*advise_erase=*/true, is_in_sec_cache);
|
||||
ASSERT_FALSE(is_in_sec_cache);
|
||||
ASSERT_NE(sec_handle0, nullptr);
|
||||
ASSERT_TRUE(sec_handle0->IsReady());
|
||||
auto value = static_cast<std::string*>(sec_handle0->Value());
|
||||
ASSERT_EQ(*value, blobs[0]);
|
||||
auto value = static_cast<BlobContents*>(sec_handle0->Value());
|
||||
ASSERT_NE(value, nullptr);
|
||||
ASSERT_EQ(value->data(), blobs[0]);
|
||||
delete value;
|
||||
|
||||
// key0 doesn't exist in the blob cache
|
||||
// key0 doesn't exist in the blob cache although key0's dummy
|
||||
// item exist in the secondary cache.
|
||||
ASSERT_FALSE(blob_source.TEST_BlobInCache(file_number, file_size,
|
||||
blob_offsets[0]));
|
||||
}
|
||||
|
||||
// key1 should exist in the primary cache.
|
||||
// key1 should exists in the primary cache. key1's dummy item exists
|
||||
// in the secondary cache.
|
||||
{
|
||||
CacheKey cache_key = base_cache_key.WithOffset(blob_offsets[1]);
|
||||
const Slice key1 = cache_key.AsSlice();
|
||||
@@ -1211,7 +1252,8 @@ TEST_F(BlobSecondaryCacheTest, GetBlobsFromSecondaryCache) {
|
||||
|
||||
bool is_in_sec_cache = false;
|
||||
auto sec_handle1 =
|
||||
secondary_cache->Lookup(key1, create_cb, true, is_in_sec_cache);
|
||||
secondary_cache->Lookup(key1, create_cb, true,
|
||||
/*advise_erase=*/true, is_in_sec_cache);
|
||||
ASSERT_FALSE(is_in_sec_cache);
|
||||
ASSERT_EQ(sec_handle1, nullptr);
|
||||
|
||||
@@ -1221,23 +1263,27 @@ TEST_F(BlobSecondaryCacheTest, GetBlobsFromSecondaryCache) {
|
||||
|
||||
{
|
||||
// fetch key0 from the blob file to the primary cache.
|
||||
// key1 is evicted and inserted into the secondary cache.
|
||||
ASSERT_OK(blob_source.GetBlob(
|
||||
read_options, keys[0], file_number, blob_offsets[0], file_size,
|
||||
blob_sizes[0], kNoCompression, nullptr /* prefetch_buffer */,
|
||||
&values[0], nullptr /* bytes_read */));
|
||||
ASSERT_EQ(values[0], blobs[0]);
|
||||
|
||||
// Release cache handle
|
||||
values[0].Reset();
|
||||
|
||||
// key0 should be in the primary cache.
|
||||
CacheKey cache_key0 = base_cache_key.WithOffset(blob_offsets[0]);
|
||||
const Slice key0 = cache_key0.AsSlice();
|
||||
auto handle0 = blob_cache->Lookup(key0, statistics);
|
||||
ASSERT_NE(handle0, nullptr);
|
||||
auto value = static_cast<std::string*>(blob_cache->Value(handle0));
|
||||
ASSERT_EQ(*value, blobs[0]);
|
||||
auto value = static_cast<BlobContents*>(blob_cache->Value(handle0));
|
||||
ASSERT_NE(value, nullptr);
|
||||
ASSERT_EQ(value->data(), blobs[0]);
|
||||
blob_cache->Release(handle0);
|
||||
|
||||
// key1 is not in the primary cache, and it should be demoted to the
|
||||
// secondary cache.
|
||||
// key1 is not in the primary cache and is in the secondary cache.
|
||||
CacheKey cache_key1 = base_cache_key.WithOffset(blob_offsets[1]);
|
||||
const Slice key1 = cache_key1.AsSlice();
|
||||
auto handle1 = blob_cache->Lookup(key1, statistics);
|
||||
@@ -1251,16 +1297,19 @@ TEST_F(BlobSecondaryCacheTest, GetBlobsFromSecondaryCache) {
|
||||
// key1 promotion should succeed due to the primary cache being empty. we
|
||||
// did't call secondary cache's Lookup() here, because it will remove the
|
||||
// key but it won't be able to promote the key to the primary cache.
|
||||
// Instead we use the end-to-end blob source API to promote the key to
|
||||
// the primary cache.
|
||||
// Instead we use the end-to-end blob source API to read key1.
|
||||
// In function TEST_BlobInCache, key1's dummy item is inserted into the
|
||||
// primary cache and a standalone handle is checked by GetValue().
|
||||
ASSERT_TRUE(blob_source.TEST_BlobInCache(file_number, file_size,
|
||||
blob_offsets[1]));
|
||||
|
||||
// key1 should be in the primary cache.
|
||||
// key1's dummy handle is in the primary cache and key1's item is still
|
||||
// in the secondary cache. So, the primary cache's Lookup() can only
|
||||
// get a dummy handle.
|
||||
handle1 = blob_cache->Lookup(key1, statistics);
|
||||
ASSERT_NE(handle1, nullptr);
|
||||
value = static_cast<std::string*>(blob_cache->Value(handle1));
|
||||
ASSERT_EQ(*value, blobs[1]);
|
||||
// handl1 is a dummy handle.
|
||||
ASSERT_EQ(blob_cache->Value(handle1), nullptr);
|
||||
blob_cache->Release(handle1);
|
||||
}
|
||||
}
|
||||
@@ -1385,11 +1434,11 @@ TEST_F(BlobSourceCacheReservationTest, SimpleCacheReservation) {
|
||||
ReadOptions read_options;
|
||||
read_options.verify_checksums = true;
|
||||
|
||||
std::vector<PinnableSlice> values(keys_.size());
|
||||
|
||||
{
|
||||
read_options.fill_cache = false;
|
||||
|
||||
std::vector<PinnableSlice> values(keys_.size());
|
||||
|
||||
for (size_t i = 0; i < kNumBlobs; ++i) {
|
||||
ASSERT_OK(blob_source.GetBlob(
|
||||
read_options, keys_[i], kBlobFileNumber, blob_offsets[i],
|
||||
@@ -1403,6 +1452,8 @@ TEST_F(BlobSourceCacheReservationTest, SimpleCacheReservation) {
|
||||
{
|
||||
read_options.fill_cache = true;
|
||||
|
||||
std::vector<PinnableSlice> values(keys_.size());
|
||||
|
||||
// num_blobs is 16, so the total blob cache usage is less than a single
|
||||
// dummy entry. Therefore, cache reservation manager only reserves one dummy
|
||||
// entry here.
|
||||
@@ -1412,7 +1463,12 @@ TEST_F(BlobSourceCacheReservationTest, SimpleCacheReservation) {
|
||||
read_options, keys_[i], kBlobFileNumber, blob_offsets[i],
|
||||
blob_file_size_, blob_sizes[i], kNoCompression,
|
||||
nullptr /* prefetch_buffer */, &values[i], nullptr /* bytes_read */));
|
||||
blob_bytes += blob_sizes[i];
|
||||
|
||||
size_t charge = 0;
|
||||
ASSERT_TRUE(blob_source.TEST_BlobInCache(kBlobFileNumber, blob_file_size_,
|
||||
blob_offsets[i], &charge));
|
||||
|
||||
blob_bytes += charge;
|
||||
ASSERT_EQ(cache_res_mgr->GetTotalReservedCacheSize(), kSizeDummyEntry);
|
||||
ASSERT_EQ(cache_res_mgr->GetTotalMemoryUsed(), blob_bytes);
|
||||
ASSERT_EQ(cache_res_mgr->GetTotalMemoryUsed(),
|
||||
@@ -1425,19 +1481,23 @@ TEST_F(BlobSourceCacheReservationTest, SimpleCacheReservation) {
|
||||
size_t blob_bytes = options_.blob_cache->GetUsage();
|
||||
|
||||
for (size_t i = 0; i < kNumBlobs; ++i) {
|
||||
size_t charge = 0;
|
||||
ASSERT_TRUE(blob_source.TEST_BlobInCache(kBlobFileNumber, blob_file_size_,
|
||||
blob_offsets[i], &charge));
|
||||
|
||||
CacheKey cache_key = base_cache_key.WithOffset(blob_offsets[i]);
|
||||
// We didn't call options_.blob_cache->Erase() here, this is because
|
||||
// the cache wrapper's Erase() method must be called to update the
|
||||
// cache usage after erasing the cache entry.
|
||||
blob_source.GetBlobCache()->Erase(cache_key.AsSlice());
|
||||
if (i == kNumBlobs - 1) {
|
||||
// The last blob is not in the cache. cache_res_mgr should not reserve
|
||||
// any space for it.
|
||||
// All the blobs got removed from the cache. cache_res_mgr should not
|
||||
// reserve any space for them.
|
||||
ASSERT_EQ(cache_res_mgr->GetTotalReservedCacheSize(), 0);
|
||||
} else {
|
||||
ASSERT_EQ(cache_res_mgr->GetTotalReservedCacheSize(), kSizeDummyEntry);
|
||||
}
|
||||
blob_bytes -= blob_sizes[i];
|
||||
blob_bytes -= charge;
|
||||
ASSERT_EQ(cache_res_mgr->GetTotalMemoryUsed(), blob_bytes);
|
||||
ASSERT_EQ(cache_res_mgr->GetTotalMemoryUsed(),
|
||||
options_.blob_cache->GetUsage());
|
||||
@@ -1495,11 +1555,11 @@ TEST_F(BlobSourceCacheReservationTest, IncreaseCacheReservationOnFullCache) {
|
||||
ReadOptions read_options;
|
||||
read_options.verify_checksums = true;
|
||||
|
||||
std::vector<PinnableSlice> values(keys_.size());
|
||||
|
||||
{
|
||||
read_options.fill_cache = false;
|
||||
|
||||
std::vector<PinnableSlice> values(keys_.size());
|
||||
|
||||
for (size_t i = 0; i < kNumBlobs; ++i) {
|
||||
ASSERT_OK(blob_source.GetBlob(
|
||||
read_options, keys_[i], kBlobFileNumber, blob_offsets[i],
|
||||
@@ -1513,21 +1573,33 @@ TEST_F(BlobSourceCacheReservationTest, IncreaseCacheReservationOnFullCache) {
|
||||
{
|
||||
read_options.fill_cache = true;
|
||||
|
||||
// Since we resized each blob to be kSizeDummyEntry / (num_blobs/ 2), we
|
||||
// should observe cache eviction for the second half blobs.
|
||||
std::vector<PinnableSlice> values(keys_.size());
|
||||
|
||||
// Since we resized each blob to be kSizeDummyEntry / (num_blobs / 2), we
|
||||
// can't fit all the blobs in the cache at the same time, which means we
|
||||
// should observe cache evictions once we reach the cache's capacity.
|
||||
// Due to the overhead of the cache and the BlobContents objects, as well as
|
||||
// jemalloc bin sizes, this happens after inserting seven blobs.
|
||||
uint64_t blob_bytes = 0;
|
||||
for (size_t i = 0; i < kNumBlobs; ++i) {
|
||||
ASSERT_OK(blob_source.GetBlob(
|
||||
read_options, keys_[i], kBlobFileNumber, blob_offsets[i],
|
||||
blob_file_size_, blob_sizes[i], kNoCompression,
|
||||
nullptr /* prefetch_buffer */, &values[i], nullptr /* bytes_read */));
|
||||
blob_bytes += blob_sizes[i];
|
||||
ASSERT_EQ(cache_res_mgr->GetTotalReservedCacheSize(), kSizeDummyEntry);
|
||||
if (i >= kNumBlobs / 2) {
|
||||
ASSERT_EQ(cache_res_mgr->GetTotalMemoryUsed(), kSizeDummyEntry);
|
||||
} else {
|
||||
ASSERT_EQ(cache_res_mgr->GetTotalMemoryUsed(), blob_bytes);
|
||||
|
||||
// Release cache handle
|
||||
values[i].Reset();
|
||||
|
||||
if (i < kNumBlobs / 2 - 1) {
|
||||
size_t charge = 0;
|
||||
ASSERT_TRUE(blob_source.TEST_BlobInCache(
|
||||
kBlobFileNumber, blob_file_size_, blob_offsets[i], &charge));
|
||||
|
||||
blob_bytes += charge;
|
||||
}
|
||||
|
||||
ASSERT_EQ(cache_res_mgr->GetTotalReservedCacheSize(), kSizeDummyEntry);
|
||||
ASSERT_EQ(cache_res_mgr->GetTotalMemoryUsed(), blob_bytes);
|
||||
ASSERT_EQ(cache_res_mgr->GetTotalMemoryUsed(),
|
||||
options_.blob_cache->GetUsage());
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
|
||||
#include "cache/compressed_secondary_cache.h"
|
||||
#include "db/blob/blob_index.h"
|
||||
#include "db/blob/blob_log_format.h"
|
||||
#include "db/db_test_util.h"
|
||||
@@ -53,7 +54,7 @@ TEST_F(DBBlobBasicTest, GetBlobFromCache) {
|
||||
Options options = GetDefaultOptions();
|
||||
|
||||
LRUCacheOptions co;
|
||||
co.capacity = 2048;
|
||||
co.capacity = 2 << 20; // 2MB
|
||||
co.num_shard_bits = 2;
|
||||
co.metadata_charge_policy = kDontChargeCacheMetadata;
|
||||
auto backing_cache = NewLRUCache(co);
|
||||
@@ -121,7 +122,7 @@ TEST_F(DBBlobBasicTest, IterateBlobsFromCache) {
|
||||
Options options = GetDefaultOptions();
|
||||
|
||||
LRUCacheOptions co;
|
||||
co.capacity = 2048;
|
||||
co.capacity = 2 << 20; // 2MB
|
||||
co.num_shard_bits = 2;
|
||||
co.metadata_charge_policy = kDontChargeCacheMetadata;
|
||||
auto backing_cache = NewLRUCache(co);
|
||||
@@ -452,7 +453,7 @@ TEST_F(DBBlobBasicTest, MultiGetBlobsFromCache) {
|
||||
Options options = GetDefaultOptions();
|
||||
|
||||
LRUCacheOptions co;
|
||||
co.capacity = 2048;
|
||||
co.capacity = 2 << 20; // 2MB
|
||||
co.num_shard_bits = 2;
|
||||
co.metadata_charge_policy = kDontChargeCacheMetadata;
|
||||
auto backing_cache = NewLRUCache(co);
|
||||
@@ -1701,6 +1702,83 @@ TEST_F(DBBlobBasicTest, DynamicallyWarmCacheDuringFlush) {
|
||||
}
|
||||
#endif // !ROCKSDB_LITE
|
||||
|
||||
TEST_F(DBBlobBasicTest, WarmCacheWithBlobsSecondary) {
|
||||
CompressedSecondaryCacheOptions secondary_cache_opts;
|
||||
secondary_cache_opts.capacity = 1 << 20;
|
||||
secondary_cache_opts.num_shard_bits = 0;
|
||||
secondary_cache_opts.metadata_charge_policy = kDontChargeCacheMetadata;
|
||||
secondary_cache_opts.compression_type = kNoCompression;
|
||||
|
||||
LRUCacheOptions primary_cache_opts;
|
||||
primary_cache_opts.capacity = 1024;
|
||||
primary_cache_opts.num_shard_bits = 0;
|
||||
primary_cache_opts.metadata_charge_policy = kDontChargeCacheMetadata;
|
||||
primary_cache_opts.secondary_cache =
|
||||
NewCompressedSecondaryCache(secondary_cache_opts);
|
||||
|
||||
Options options = GetDefaultOptions();
|
||||
options.create_if_missing = true;
|
||||
options.statistics = CreateDBStatistics();
|
||||
options.enable_blob_files = true;
|
||||
options.blob_cache = NewLRUCache(primary_cache_opts);
|
||||
options.prepopulate_blob_cache = PrepopulateBlobCache::kFlushOnly;
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
// Note: only one of the two blobs fit in the primary cache at any given time.
|
||||
constexpr char first_key[] = "foo";
|
||||
constexpr size_t first_blob_size = 512;
|
||||
const std::string first_blob(first_blob_size, 'a');
|
||||
|
||||
constexpr char second_key[] = "bar";
|
||||
constexpr size_t second_blob_size = 768;
|
||||
const std::string second_blob(second_blob_size, 'b');
|
||||
|
||||
// First blob is inserted into primary cache during flush.
|
||||
ASSERT_OK(Put(first_key, first_blob));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_EQ(options.statistics->getAndResetTickerCount(BLOB_DB_CACHE_ADD), 1);
|
||||
|
||||
// Second blob is inserted into primary cache during flush,
|
||||
// First blob is evicted but only a dummy handle is inserted into secondary
|
||||
// cache.
|
||||
ASSERT_OK(Put(second_key, second_blob));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_EQ(options.statistics->getAndResetTickerCount(BLOB_DB_CACHE_ADD), 1);
|
||||
|
||||
// First blob is inserted into primary cache.
|
||||
// Second blob is evicted but only a dummy handle is inserted into secondary
|
||||
// cache.
|
||||
ASSERT_EQ(Get(first_key), first_blob);
|
||||
ASSERT_EQ(options.statistics->getAndResetTickerCount(BLOB_DB_CACHE_MISS), 1);
|
||||
ASSERT_EQ(options.statistics->getAndResetTickerCount(BLOB_DB_CACHE_HIT), 0);
|
||||
ASSERT_EQ(options.statistics->getAndResetTickerCount(SECONDARY_CACHE_HITS),
|
||||
0);
|
||||
// Second blob is inserted into primary cache,
|
||||
// First blob is evicted and is inserted into secondary cache.
|
||||
ASSERT_EQ(Get(second_key), second_blob);
|
||||
ASSERT_EQ(options.statistics->getAndResetTickerCount(BLOB_DB_CACHE_MISS), 1);
|
||||
ASSERT_EQ(options.statistics->getAndResetTickerCount(BLOB_DB_CACHE_HIT), 0);
|
||||
ASSERT_EQ(options.statistics->getAndResetTickerCount(SECONDARY_CACHE_HITS),
|
||||
0);
|
||||
|
||||
// First blob's dummy item is inserted into primary cache b/c of lookup.
|
||||
// Second blob is still in primary cache.
|
||||
ASSERT_EQ(Get(first_key), first_blob);
|
||||
ASSERT_EQ(options.statistics->getAndResetTickerCount(BLOB_DB_CACHE_MISS), 0);
|
||||
ASSERT_EQ(options.statistics->getAndResetTickerCount(BLOB_DB_CACHE_HIT), 1);
|
||||
ASSERT_EQ(options.statistics->getAndResetTickerCount(SECONDARY_CACHE_HITS),
|
||||
1);
|
||||
|
||||
// First blob's item is inserted into primary cache b/c of lookup.
|
||||
// Second blob is evicted and inserted into secondary cache.
|
||||
ASSERT_EQ(Get(first_key), first_blob);
|
||||
ASSERT_EQ(options.statistics->getAndResetTickerCount(BLOB_DB_CACHE_MISS), 0);
|
||||
ASSERT_EQ(options.statistics->getAndResetTickerCount(BLOB_DB_CACHE_HIT), 1);
|
||||
ASSERT_EQ(options.statistics->getAndResetTickerCount(SECONDARY_CACHE_HITS),
|
||||
1);
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
@@ -1054,6 +1054,18 @@ void rocksdb_drop_column_family(
|
||||
SaveError(errptr, db->rep->DropColumnFamily(handle->rep));
|
||||
}
|
||||
|
||||
uint32_t rocksdb_column_family_handle_get_id(
|
||||
rocksdb_column_family_handle_t* handle) {
|
||||
return handle->rep->GetID();
|
||||
}
|
||||
|
||||
char* rocksdb_column_family_handle_get_name(
|
||||
rocksdb_column_family_handle_t* handle, size_t* name_len) {
|
||||
auto name = handle->rep->GetName();
|
||||
*name_len = name.size();
|
||||
return CopyString(name);
|
||||
}
|
||||
|
||||
void rocksdb_column_family_handle_destroy(rocksdb_column_family_handle_t* handle) {
|
||||
delete handle->rep;
|
||||
delete handle;
|
||||
@@ -2606,6 +2618,11 @@ void rocksdb_block_based_options_destroy(
|
||||
delete options;
|
||||
}
|
||||
|
||||
void rocksdb_block_based_options_set_checksum(
|
||||
rocksdb_block_based_table_options_t* opt, char v) {
|
||||
opt->rep.checksum = static_cast<ROCKSDB_NAMESPACE::ChecksumType>(v);
|
||||
}
|
||||
|
||||
void rocksdb_block_based_options_set_block_size(
|
||||
rocksdb_block_based_table_options_t* options, size_t block_size) {
|
||||
options->rep.block_size = block_size;
|
||||
@@ -4099,6 +4116,8 @@ uint64_t rocksdb_perfcontext_metric(rocksdb_perfcontext_t* context,
|
||||
return rep->blob_checksum_time;
|
||||
case rocksdb_blob_decompress_time:
|
||||
return rep->blob_decompress_time;
|
||||
case rocksdb_internal_range_del_reseek_count:
|
||||
return rep->internal_range_del_reseek_count;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
+25
-1
@@ -233,6 +233,21 @@ ColumnFamilyOptions SanitizeOptions(const ImmutableDBOptions& db_options,
|
||||
result.min_write_buffer_number_to_merge = 1;
|
||||
}
|
||||
|
||||
if (db_options.atomic_flush && result.min_write_buffer_number_to_merge > 1) {
|
||||
ROCKS_LOG_WARN(
|
||||
db_options.logger,
|
||||
"Currently, if atomic_flush is true, then triggering flush for any "
|
||||
"column family internally (non-manual flush) will trigger flushing "
|
||||
"all column families even if the number of memtables is smaller "
|
||||
"min_write_buffer_number_to_merge. Therefore, configuring "
|
||||
"min_write_buffer_number_to_merge > 1 is not compatible and should "
|
||||
"be satinized to 1. Not doing so will lead to data loss and "
|
||||
"inconsistent state across multiple column families when WAL is "
|
||||
"disabled, which is a common setting for atomic flush");
|
||||
|
||||
result.min_write_buffer_number_to_merge = 1;
|
||||
}
|
||||
|
||||
if (result.num_levels < 1) {
|
||||
result.num_levels = 1;
|
||||
}
|
||||
@@ -1144,7 +1159,8 @@ Status ColumnFamilyData::RangesOverlapWithMemtables(
|
||||
MergeIteratorBuilder merge_iter_builder(&internal_comparator_, &arena);
|
||||
merge_iter_builder.AddIterator(
|
||||
super_version->mem->NewIterator(read_opts, &arena));
|
||||
super_version->imm->AddIterators(read_opts, &merge_iter_builder);
|
||||
super_version->imm->AddIterators(read_opts, &merge_iter_builder,
|
||||
false /* add_range_tombstone_iter */);
|
||||
ScopedArenaIterator memtable_iter(merge_iter_builder.Finish());
|
||||
|
||||
auto read_seq = super_version->current->version_set()->LastSequence();
|
||||
@@ -1415,6 +1431,14 @@ Status ColumnFamilyData::ValidateOptions(
|
||||
"FIFO compaction only supported with max_open_files = -1.");
|
||||
}
|
||||
|
||||
std::vector<uint32_t> supported{0, 1, 2, 4, 8};
|
||||
if (std::find(supported.begin(), supported.end(),
|
||||
cf_options.memtable_protection_bytes_per_key) ==
|
||||
supported.end()) {
|
||||
return Status::NotSupported(
|
||||
"Memtable per key-value checksum protection only supports 0, 1, 2, 4 "
|
||||
"or 8 bytes per key.");
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
@@ -3433,6 +3433,27 @@ TEST(ColumnFamilyTest, ValidateBlobGCForceThreshold) {
|
||||
.IsInvalidArgument());
|
||||
}
|
||||
|
||||
TEST(ColumnFamilyTest, ValidateMemtableKVChecksumOption) {
|
||||
DBOptions db_options;
|
||||
|
||||
ColumnFamilyOptions cf_options;
|
||||
ASSERT_OK(ColumnFamilyData::ValidateOptions(db_options, cf_options));
|
||||
|
||||
cf_options.memtable_protection_bytes_per_key = 5;
|
||||
ASSERT_TRUE(ColumnFamilyData::ValidateOptions(db_options, cf_options)
|
||||
.IsNotSupported());
|
||||
|
||||
cf_options.memtable_protection_bytes_per_key = 1;
|
||||
ASSERT_OK(ColumnFamilyData::ValidateOptions(db_options, cf_options));
|
||||
|
||||
cf_options.memtable_protection_bytes_per_key = 16;
|
||||
ASSERT_TRUE(ColumnFamilyData::ValidateOptions(db_options, cf_options)
|
||||
.IsNotSupported());
|
||||
|
||||
cf_options.memtable_protection_bytes_per_key = 0;
|
||||
ASSERT_OK(ColumnFamilyData::ValidateOptions(db_options, cf_options));
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
@@ -1086,7 +1086,9 @@ void CompactionIterator::DecideOutputLevel() {
|
||||
TEST_SYNC_POINT_CALLBACK("CompactionIterator::PrepareOutput.context",
|
||||
&context);
|
||||
output_to_penultimate_level_ = context.output_to_penultimate_level;
|
||||
#endif /* !NDEBUG */
|
||||
#else
|
||||
output_to_penultimate_level_ = false;
|
||||
#endif // NDEBUG
|
||||
|
||||
// if the key is newer than the cutoff sequence or within the earliest
|
||||
// snapshot, it should output to the penultimate level.
|
||||
|
||||
@@ -254,7 +254,7 @@ void CompactionJob::Prepare() {
|
||||
// timestamp part).
|
||||
assert(i == 0 || i == boundaries_.size() ||
|
||||
cfd->user_comparator()->CompareWithoutTimestamp(
|
||||
boundaries_[i - 1], true, boundaries_[i], true) < 0);
|
||||
boundaries_[i - 1], boundaries_[i]) < 0);
|
||||
}
|
||||
RecordInHistogram(stats_, NUM_SUBCOMPACTIONS_SCHEDULED,
|
||||
compact_->sub_compact_states.size());
|
||||
@@ -321,6 +321,7 @@ void CompactionJob::AcquireSubcompactionResources(
|
||||
->write_controller()
|
||||
->NeedSpeedupCompaction())
|
||||
.max_compactions;
|
||||
InstrumentedMutexLock l(db_mutex_);
|
||||
// Apply min function first since We need to compute the extra subcompaction
|
||||
// against compaction limits. And then try to reserve threads for extra
|
||||
// subcompactions. The actual number of reserved threads could be less than
|
||||
@@ -329,7 +330,6 @@ void CompactionJob::AcquireSubcompactionResources(
|
||||
std::max(max_db_compactions - *bg_compaction_scheduled_ -
|
||||
*bg_bottom_compaction_scheduled_,
|
||||
0);
|
||||
db_mutex_->Lock();
|
||||
// Reservation only supports backgrdoun threads of which the priority is
|
||||
// between BOTTOM and HIGH. Need to degrade the priority to HIGH if the
|
||||
// origin thread_pri_ is higher than that. Similar to ReleaseThreads().
|
||||
@@ -346,7 +346,6 @@ void CompactionJob::AcquireSubcompactionResources(
|
||||
} else {
|
||||
*bg_compaction_scheduled_ += extra_num_subcompaction_threads_reserved_;
|
||||
}
|
||||
db_mutex_->Unlock();
|
||||
}
|
||||
|
||||
void CompactionJob::ShrinkSubcompactionResources(uint64_t num_extra_resources) {
|
||||
@@ -380,17 +379,20 @@ void CompactionJob::ReleaseSubcompactionResources() {
|
||||
if (extra_num_subcompaction_threads_reserved_ == 0) {
|
||||
return;
|
||||
}
|
||||
// The number of reserved threads becomes larger than 0 only if the
|
||||
// compaction prioity is round robin and there is no sufficient
|
||||
// sub-compactions available
|
||||
{
|
||||
InstrumentedMutexLock l(db_mutex_);
|
||||
// The number of reserved threads becomes larger than 0 only if the
|
||||
// compaction prioity is round robin and there is no sufficient
|
||||
// sub-compactions available
|
||||
|
||||
// The scheduled compaction must be no less than 1 + extra number
|
||||
// subcompactions using acquired resources since this compaction job has not
|
||||
// finished yet
|
||||
assert(*bg_bottom_compaction_scheduled_ >=
|
||||
1 + extra_num_subcompaction_threads_reserved_ ||
|
||||
*bg_compaction_scheduled_ >=
|
||||
1 + extra_num_subcompaction_threads_reserved_);
|
||||
// The scheduled compaction must be no less than 1 + extra number
|
||||
// subcompactions using acquired resources since this compaction job has not
|
||||
// finished yet
|
||||
assert(*bg_bottom_compaction_scheduled_ >=
|
||||
1 + extra_num_subcompaction_threads_reserved_ ||
|
||||
*bg_compaction_scheduled_ >=
|
||||
1 + extra_num_subcompaction_threads_reserved_);
|
||||
}
|
||||
ShrinkSubcompactionResources(extra_num_subcompaction_threads_reserved_);
|
||||
}
|
||||
|
||||
@@ -462,7 +464,7 @@ void CompactionJob::GenSubcompactionBoundaries() {
|
||||
FileMetaData* f = flevel->files[i].file_metadata;
|
||||
std::vector<TableReader::Anchor> my_anchors;
|
||||
Status s = cfd->table_cache()->ApproximateKeyAnchors(
|
||||
ReadOptions(), icomp, f->fd, my_anchors);
|
||||
ReadOptions(), icomp, *f, my_anchors);
|
||||
if (!s.ok() || my_anchors.empty()) {
|
||||
my_anchors.emplace_back(f->largest.user_key(), f->fd.GetFileSize());
|
||||
}
|
||||
@@ -484,8 +486,8 @@ void CompactionJob::GenSubcompactionBoundaries() {
|
||||
std::sort(
|
||||
all_anchors.begin(), all_anchors.end(),
|
||||
[cfd_comparator](TableReader::Anchor& a, TableReader::Anchor& b) -> bool {
|
||||
return cfd_comparator->CompareWithoutTimestamp(a.user_key, true,
|
||||
b.user_key, true) < 0;
|
||||
return cfd_comparator->CompareWithoutTimestamp(a.user_key, b.user_key) <
|
||||
0;
|
||||
});
|
||||
|
||||
// Remove duplicated entries from boundaries.
|
||||
@@ -494,7 +496,7 @@ void CompactionJob::GenSubcompactionBoundaries() {
|
||||
[cfd_comparator](TableReader::Anchor& a,
|
||||
TableReader::Anchor& b) -> bool {
|
||||
return cfd_comparator->CompareWithoutTimestamp(
|
||||
a.user_key, true, b.user_key, true) == 0;
|
||||
a.user_key, b.user_key) == 0;
|
||||
}),
|
||||
all_anchors.end());
|
||||
|
||||
@@ -1083,13 +1085,34 @@ void CompactionJob::ProcessKeyValueCompaction(SubcompactionState* sub_compact) {
|
||||
Slice start_slice;
|
||||
Slice end_slice;
|
||||
|
||||
static constexpr char kMaxTs[] =
|
||||
"\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff";
|
||||
Slice ts_slice;
|
||||
std::string max_ts;
|
||||
if (ts_sz > 0) {
|
||||
if (ts_sz <= strlen(kMaxTs)) {
|
||||
ts_slice = Slice(kMaxTs, ts_sz);
|
||||
} else {
|
||||
max_ts = std::string(ts_sz, '\xff');
|
||||
ts_slice = Slice(max_ts);
|
||||
}
|
||||
}
|
||||
|
||||
if (start.has_value()) {
|
||||
start_ikey.SetInternalKey(start.value(), kMaxSequenceNumber,
|
||||
kValueTypeForSeek);
|
||||
if (ts_sz > 0) {
|
||||
start_ikey.UpdateInternalKey(kMaxSequenceNumber, kValueTypeForSeek,
|
||||
&ts_slice);
|
||||
}
|
||||
start_slice = start_ikey.GetInternalKey();
|
||||
}
|
||||
if (end.has_value()) {
|
||||
end_ikey.SetInternalKey(end.value(), kMaxSequenceNumber, kValueTypeForSeek);
|
||||
if (ts_sz > 0) {
|
||||
end_ikey.UpdateInternalKey(kMaxSequenceNumber, kValueTypeForSeek,
|
||||
&ts_slice);
|
||||
}
|
||||
end_slice = end_ikey.GetInternalKey();
|
||||
}
|
||||
|
||||
@@ -1110,7 +1133,7 @@ void CompactionJob::ProcessKeyValueCompaction(SubcompactionState* sub_compact) {
|
||||
}
|
||||
|
||||
std::unique_ptr<InternalIterator> trim_history_iter;
|
||||
if (cfd->user_comparator()->timestamp_size() > 0 && !trim_ts_.empty()) {
|
||||
if (ts_sz > 0 && !trim_ts_.empty()) {
|
||||
trim_history_iter = std::make_unique<HistoryTrimmingIterator>(
|
||||
input, cfd->user_comparator(), trim_ts_);
|
||||
input = trim_history_iter.get();
|
||||
@@ -1198,13 +1221,6 @@ void CompactionJob::ProcessKeyValueCompaction(SubcompactionState* sub_compact) {
|
||||
// it only output to single level
|
||||
sub_compact->AssignRangeDelAggregator(std::move(range_del_agg));
|
||||
|
||||
if (c_iter->Valid() && sub_compact->compaction->output_level() != 0) {
|
||||
sub_compact->FillFilesToCutForTtl();
|
||||
// ShouldStopBefore() maintains state based on keys processed so far. The
|
||||
// compaction loop always calls it on the "next" key, thus won't tell it the
|
||||
// first key. So we do that here.
|
||||
sub_compact->ShouldStopBefore(c_iter->key());
|
||||
}
|
||||
const auto& c_iter_stats = c_iter->iter_stats();
|
||||
|
||||
// define the open and close functions for the compaction files, which will be
|
||||
@@ -1253,16 +1269,6 @@ void CompactionJob::ProcessKeyValueCompaction(SubcompactionState* sub_compact) {
|
||||
if (c_iter->status().IsManualCompactionPaused()) {
|
||||
break;
|
||||
}
|
||||
|
||||
// TODO: Support earlier file cut for the penultimate level files. Maybe by
|
||||
// moving `ShouldStopBefore()` to `CompactionOutputs` class. Currently
|
||||
// the penultimate level output is only cut when it reaches the size limit.
|
||||
if (!sub_compact->Current().IsPendingClose() &&
|
||||
sub_compact->compaction->output_level() != 0 &&
|
||||
!sub_compact->compaction->SupportsPerKeyPlacement() &&
|
||||
sub_compact->ShouldStopBefore(c_iter->key())) {
|
||||
sub_compact->Current().SetPendingClose();
|
||||
}
|
||||
}
|
||||
|
||||
sub_compact->compaction_job_stats.num_blobs_read =
|
||||
|
||||
@@ -19,7 +19,9 @@
|
||||
#include "db/db_impl/db_impl.h"
|
||||
#include "db/error_handler.h"
|
||||
#include "db/version_set.h"
|
||||
#include "file/random_access_file_reader.h"
|
||||
#include "file/writable_file_writer.h"
|
||||
#include "options/options_helper.h"
|
||||
#include "rocksdb/cache.h"
|
||||
#include "rocksdb/convenience.h"
|
||||
#include "rocksdb/db.h"
|
||||
@@ -194,13 +196,15 @@ class MockTestFileSystem : public FileSystemWrapper {
|
||||
Env::IOPriority write_io_priority_;
|
||||
};
|
||||
|
||||
enum TableTypeForTest : uint8_t { kMockTable = 0, kBlockBasedTable = 1 };
|
||||
|
||||
} // namespace
|
||||
|
||||
class CompactionJobTestBase : public testing::Test {
|
||||
protected:
|
||||
CompactionJobTestBase(std::string dbname, const Comparator* ucmp,
|
||||
std::function<std::string(uint64_t)> encode_u64_ts,
|
||||
bool test_io_priority)
|
||||
bool test_io_priority, TableTypeForTest table_type)
|
||||
: dbname_(std::move(dbname)),
|
||||
ucmp_(ucmp),
|
||||
db_options_(),
|
||||
@@ -217,7 +221,8 @@ class CompactionJobTestBase : public testing::Test {
|
||||
mock_table_factory_(new mock::MockTableFactory()),
|
||||
error_handler_(nullptr, db_options_, &mutex_),
|
||||
encode_u64_ts_(std::move(encode_u64_ts)),
|
||||
test_io_priority_(test_io_priority) {
|
||||
test_io_priority_(test_io_priority),
|
||||
table_type_(table_type) {
|
||||
Env* base_env = Env::Default();
|
||||
EXPECT_OK(
|
||||
test::CreateEnvFromSystem(ConfigOptions(), &base_env, &env_guard_));
|
||||
@@ -232,11 +237,13 @@ class CompactionJobTestBase : public testing::Test {
|
||||
db_options_.db_paths.emplace_back(dbname_,
|
||||
std::numeric_limits<uint64_t>::max());
|
||||
cf_options_.comparator = ucmp_;
|
||||
if (test_io_priority_) {
|
||||
if (table_type_ == TableTypeForTest::kBlockBasedTable) {
|
||||
BlockBasedTableOptions table_options;
|
||||
cf_options_.table_factory.reset(NewBlockBasedTableFactory(table_options));
|
||||
} else {
|
||||
} else if (table_type_ == TableTypeForTest::kMockTable) {
|
||||
cf_options_.table_factory = mock_table_factory_;
|
||||
} else {
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -358,13 +365,15 @@ class CompactionJobTestBase : public testing::Test {
|
||||
|
||||
uint64_t file_number = versions_->NewFileNumber();
|
||||
|
||||
uint64_t file_size;
|
||||
if (test_io_priority_) {
|
||||
uint64_t file_size = 0;
|
||||
if (table_type_ == TableTypeForTest::kBlockBasedTable) {
|
||||
CreateTable(GenerateFileName(file_number), contents, file_size);
|
||||
} else {
|
||||
} else if (table_type_ == TableTypeForTest::kMockTable) {
|
||||
file_size = 10;
|
||||
EXPECT_OK(mock_table_factory_->CreateMockTable(
|
||||
env_, GenerateFileName(file_number), std::move(contents)));
|
||||
} else {
|
||||
assert(false);
|
||||
}
|
||||
|
||||
VersionEdit edit;
|
||||
@@ -377,10 +386,87 @@ class CompactionJobTestBase : public testing::Test {
|
||||
mutex_.Lock();
|
||||
EXPECT_OK(
|
||||
versions_->LogAndApply(versions_->GetColumnFamilySet()->GetDefault(),
|
||||
mutable_cf_options_, &edit, &mutex_));
|
||||
mutable_cf_options_, &edit, &mutex_, nullptr));
|
||||
mutex_.Unlock();
|
||||
}
|
||||
|
||||
void VerifyTables(int output_level,
|
||||
const std::vector<mock::KVVector>& expected_results,
|
||||
std::vector<uint64_t> expected_oldest_blob_file_numbers) {
|
||||
if (expected_results.empty()) {
|
||||
ASSERT_EQ(compaction_job_stats_.num_output_files, 0U);
|
||||
return;
|
||||
}
|
||||
int expected_output_file_num = 0;
|
||||
for (const auto& e : expected_results) {
|
||||
if (!e.empty()) {
|
||||
++expected_output_file_num;
|
||||
}
|
||||
}
|
||||
ASSERT_EQ(expected_output_file_num, compaction_job_stats_.num_output_files);
|
||||
if (expected_output_file_num == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (expected_oldest_blob_file_numbers.empty()) {
|
||||
expected_oldest_blob_file_numbers.resize(expected_output_file_num,
|
||||
kInvalidBlobFileNumber);
|
||||
}
|
||||
|
||||
auto cfd = versions_->GetColumnFamilySet()->GetDefault();
|
||||
if (table_type_ == TableTypeForTest::kMockTable) {
|
||||
assert(expected_results.size() == 1);
|
||||
mock_table_factory_->AssertLatestFile(expected_results[0]);
|
||||
} else {
|
||||
assert(table_type_ == TableTypeForTest::kBlockBasedTable);
|
||||
}
|
||||
|
||||
auto output_files =
|
||||
cfd->current()->storage_info()->LevelFiles(output_level);
|
||||
ASSERT_EQ(expected_output_file_num, output_files.size());
|
||||
|
||||
if (table_type_ == TableTypeForTest::kMockTable) {
|
||||
assert(output_files.size() == 1);
|
||||
const FileMetaData* const output_file = output_files[0];
|
||||
ASSERT_EQ(output_file->oldest_blob_file_number,
|
||||
expected_oldest_blob_file_numbers[0]);
|
||||
return;
|
||||
}
|
||||
|
||||
for (size_t i = 0; i < expected_results.size(); ++i) {
|
||||
const FileMetaData* const output_file = output_files[i];
|
||||
std::string file_name = GenerateFileName(output_file->fd.GetNumber());
|
||||
const auto& fs = env_->GetFileSystem();
|
||||
std::unique_ptr<RandomAccessFileReader> freader;
|
||||
IOStatus ios = RandomAccessFileReader::Create(
|
||||
fs, file_name, FileOptions(), &freader, nullptr);
|
||||
ASSERT_OK(ios);
|
||||
std::unique_ptr<TableReader> table_reader;
|
||||
uint64_t file_size = output_file->fd.GetFileSize();
|
||||
ReadOptions read_opts;
|
||||
Status s = cf_options_.table_factory->NewTableReader(
|
||||
read_opts,
|
||||
TableReaderOptions(*cfd->ioptions(), nullptr, FileOptions(),
|
||||
cfd_->internal_comparator()),
|
||||
std::move(freader), file_size, &table_reader, false);
|
||||
ASSERT_OK(s);
|
||||
assert(table_reader);
|
||||
std::unique_ptr<InternalIterator> iiter(
|
||||
table_reader->NewIterator(read_opts, nullptr, nullptr, true,
|
||||
TableReaderCaller::kUncategorized));
|
||||
assert(iiter);
|
||||
|
||||
mock::KVVector from_db;
|
||||
for (iiter->SeekToFirst(); iiter->Valid(); iiter->Next()) {
|
||||
const Slice key = iiter->key();
|
||||
const Slice value = iiter->value();
|
||||
from_db.emplace_back(
|
||||
make_pair(key.ToString(false), value.ToString(false)));
|
||||
}
|
||||
ASSERT_EQ(expected_results[i], from_db);
|
||||
}
|
||||
}
|
||||
|
||||
void SetLastSequence(const SequenceNumber sequence_number) {
|
||||
versions_->SetLastAllocatedSequence(sequence_number + 1);
|
||||
versions_->SetLastPublishedSequence(sequence_number + 1);
|
||||
@@ -439,6 +525,13 @@ class CompactionJobTestBase : public testing::Test {
|
||||
void NewDB() {
|
||||
EXPECT_OK(DestroyDB(dbname_, Options()));
|
||||
EXPECT_OK(env_->CreateDirIfMissing(dbname_));
|
||||
|
||||
std::shared_ptr<Logger> info_log;
|
||||
DBOptions db_opts = BuildDBOptions(db_options_, mutable_db_options_);
|
||||
Status s = CreateLoggerFromOptions(dbname_, db_opts, &info_log);
|
||||
ASSERT_OK(s);
|
||||
db_options_.info_log = info_log;
|
||||
|
||||
versions_.reset(
|
||||
new VersionSet(dbname_, &db_options_, env_options_, table_cache_.get(),
|
||||
&write_buffer_manager_, &write_controller_,
|
||||
@@ -455,9 +548,9 @@ class CompactionJobTestBase : public testing::Test {
|
||||
const std::string manifest = DescriptorFileName(dbname_, 1);
|
||||
std::unique_ptr<WritableFileWriter> file_writer;
|
||||
const auto& fs = env_->GetFileSystem();
|
||||
Status s = WritableFileWriter::Create(
|
||||
fs, manifest, fs->OptimizeForManifestWrite(env_options_), &file_writer,
|
||||
nullptr);
|
||||
s = WritableFileWriter::Create(fs, manifest,
|
||||
fs->OptimizeForManifestWrite(env_options_),
|
||||
&file_writer, nullptr);
|
||||
|
||||
ASSERT_OK(s);
|
||||
{
|
||||
@@ -481,27 +574,32 @@ class CompactionJobTestBase : public testing::Test {
|
||||
cfd_ = versions_->GetColumnFamilySet()->GetDefault();
|
||||
}
|
||||
|
||||
// input_files[i] on input_levels[i]
|
||||
void RunLastLevelCompaction(
|
||||
const std::vector<std::vector<FileMetaData*>>& input_files,
|
||||
const std::vector<int> input_levels,
|
||||
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);
|
||||
RunCompaction(input_files, input_levels, {empty_map}, snapshots,
|
||||
kMaxSequenceNumber, kLastLevel, false);
|
||||
}
|
||||
|
||||
// input_files[i] on input_levels[i]
|
||||
void RunCompaction(
|
||||
const std::vector<std::vector<FileMetaData*>>& input_files,
|
||||
const mock::KVVector& expected_results,
|
||||
const std::vector<int>& input_levels,
|
||||
const std::vector<mock::KVVector>& expected_results,
|
||||
const std::vector<SequenceNumber>& snapshots = {},
|
||||
SequenceNumber earliest_write_conflict_snapshot = kMaxSequenceNumber,
|
||||
int output_level = 1, bool verify = true,
|
||||
uint64_t expected_oldest_blob_file_number = kInvalidBlobFileNumber,
|
||||
std::vector<uint64_t> expected_oldest_blob_file_numbers = {},
|
||||
bool check_get_priority = false,
|
||||
Env::IOPriority read_io_priority = Env::IO_TOTAL,
|
||||
Env::IOPriority write_io_priority = Env::IO_TOTAL) {
|
||||
Env::IOPriority write_io_priority = Env::IO_TOTAL,
|
||||
int max_subcompactions = 0) {
|
||||
// For compaction, set fs as MockTestFileSystem to check the io_priority.
|
||||
if (test_io_priority_) {
|
||||
db_options_.fs.reset(
|
||||
@@ -512,10 +610,10 @@ class CompactionJobTestBase : public testing::Test {
|
||||
|
||||
size_t num_input_files = 0;
|
||||
std::vector<CompactionInputFiles> compaction_input_files;
|
||||
for (size_t level = 0; level < input_files.size(); level++) {
|
||||
auto level_files = input_files[level];
|
||||
for (size_t i = 0; i < input_files.size(); ++i) {
|
||||
auto level_files = input_files[i];
|
||||
CompactionInputFiles compaction_level;
|
||||
compaction_level.level = static_cast<int>(level);
|
||||
compaction_level.level = input_levels[i];
|
||||
compaction_level.files.insert(compaction_level.files.end(),
|
||||
level_files.begin(), level_files.end());
|
||||
compaction_input_files.push_back(compaction_level);
|
||||
@@ -527,9 +625,10 @@ class CompactionJobTestBase : public testing::Test {
|
||||
*cfd->GetLatestMutableCFOptions(), mutable_db_options_,
|
||||
compaction_input_files, output_level, 1024 * 1024, 10 * 1024 * 1024, 0,
|
||||
kNoCompression, cfd->GetLatestMutableCFOptions()->compression_opts,
|
||||
Temperature::kUnknown, 0, {}, true);
|
||||
Temperature::kUnknown, max_subcompactions, {}, true);
|
||||
compaction.SetInputVersion(cfd->current());
|
||||
|
||||
assert(db_options_.info_log);
|
||||
LogBuffer log_buffer(InfoLogLevel::INFO_LEVEL, db_options_.info_log.get());
|
||||
mutex_.Lock();
|
||||
EventLogger event_logger(db_options_.info_log.get());
|
||||
@@ -559,23 +658,14 @@ class CompactionJobTestBase : public testing::Test {
|
||||
ASSERT_OK(compaction_job.Install(*cfd->GetLatestMutableCFOptions()));
|
||||
ASSERT_OK(compaction_job.io_status());
|
||||
mutex_.Unlock();
|
||||
log_buffer.FlushBufferToLog();
|
||||
|
||||
if (verify) {
|
||||
ASSERT_GE(compaction_job_stats_.elapsed_micros, 0U);
|
||||
ASSERT_EQ(compaction_job_stats_.num_input_files, num_input_files);
|
||||
|
||||
if (expected_results.empty()) {
|
||||
ASSERT_EQ(compaction_job_stats_.num_output_files, 0U);
|
||||
} else {
|
||||
ASSERT_EQ(compaction_job_stats_.num_output_files, 1U);
|
||||
mock_table_factory_->AssertLatestFile(expected_results);
|
||||
|
||||
auto output_files =
|
||||
cfd->current()->storage_info()->LevelFiles(output_level);
|
||||
ASSERT_EQ(output_files.size(), 1);
|
||||
ASSERT_EQ(output_files[0]->oldest_blob_file_number,
|
||||
expected_oldest_blob_file_number);
|
||||
}
|
||||
VerifyTables(output_level, expected_results,
|
||||
expected_oldest_blob_file_numbers);
|
||||
}
|
||||
|
||||
if (check_get_priority) {
|
||||
@@ -635,8 +725,9 @@ 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_;
|
||||
const bool test_io_priority_;
|
||||
std::function<void(Compaction& comp)> verify_per_key_placement_;
|
||||
const TableTypeForTest table_type_ = kMockTable;
|
||||
};
|
||||
|
||||
// TODO(icanadi) Make it simpler once we mock out VersionSet
|
||||
@@ -645,7 +736,8 @@ class CompactionJobTest : public CompactionJobTestBase {
|
||||
CompactionJobTest()
|
||||
: CompactionJobTestBase(
|
||||
test::PerThreadDBPath("compaction_job_test"), BytewiseComparator(),
|
||||
[](uint64_t /*ts*/) { return ""; }, false) {}
|
||||
[](uint64_t /*ts*/) { return ""; }, /*test_io_priority=*/false,
|
||||
TableTypeForTest::kMockTable) {}
|
||||
};
|
||||
|
||||
TEST_F(CompactionJobTest, Simple) {
|
||||
@@ -653,9 +745,10 @@ TEST_F(CompactionJobTest, Simple) {
|
||||
|
||||
auto expected_results = CreateTwoFiles(false);
|
||||
auto cfd = versions_->GetColumnFamilySet()->GetDefault();
|
||||
auto files = cfd->current()->storage_info()->LevelFiles(0);
|
||||
constexpr int input_level = 0;
|
||||
auto files = cfd->current()->storage_info()->LevelFiles(input_level);
|
||||
ASSERT_EQ(2U, files.size());
|
||||
RunCompaction({ files }, expected_results);
|
||||
RunCompaction({files}, {input_level}, {expected_results});
|
||||
}
|
||||
|
||||
TEST_F(CompactionJobTest, DISABLED_SimpleCorrupted) {
|
||||
@@ -663,8 +756,9 @@ TEST_F(CompactionJobTest, DISABLED_SimpleCorrupted) {
|
||||
|
||||
auto expected_results = CreateTwoFiles(true);
|
||||
auto cfd = versions_->GetColumnFamilySet()->GetDefault();
|
||||
auto files = cfd->current()->storage_info()->LevelFiles(0);
|
||||
RunCompaction({files}, expected_results);
|
||||
constexpr int input_level = 0;
|
||||
auto files = cfd->current()->storage_info()->LevelFiles(input_level);
|
||||
RunCompaction({files}, {input_level}, {expected_results});
|
||||
ASSERT_EQ(compaction_job_stats_.num_corrupt_keys, 400U);
|
||||
}
|
||||
|
||||
@@ -683,8 +777,9 @@ TEST_F(CompactionJobTest, SimpleDeletion) {
|
||||
mock::MakeMockFile({{KeyStr("b", 0U, kTypeValue), "val"}});
|
||||
|
||||
SetLastSequence(4U);
|
||||
auto files = cfd_->current()->storage_info()->LevelFiles(0);
|
||||
RunCompaction({files}, expected_results);
|
||||
constexpr int input_level = 0;
|
||||
auto files = cfd_->current()->storage_info()->LevelFiles(input_level);
|
||||
RunCompaction({files}, {input_level}, {expected_results});
|
||||
}
|
||||
|
||||
TEST_F(CompactionJobTest, OutputNothing) {
|
||||
@@ -701,8 +796,10 @@ TEST_F(CompactionJobTest, OutputNothing) {
|
||||
auto expected_results = mock::MakeMockFile();
|
||||
|
||||
SetLastSequence(4U);
|
||||
auto files = cfd_->current()->storage_info()->LevelFiles(0);
|
||||
RunCompaction({files}, expected_results);
|
||||
|
||||
constexpr int input_level = 0;
|
||||
auto files = cfd_->current()->storage_info()->LevelFiles(input_level);
|
||||
RunCompaction({files}, {input_level}, {expected_results});
|
||||
}
|
||||
|
||||
TEST_F(CompactionJobTest, SimpleOverwrite) {
|
||||
@@ -723,8 +820,9 @@ TEST_F(CompactionJobTest, SimpleOverwrite) {
|
||||
{KeyStr("b", 0U, kTypeValue), "val3"}});
|
||||
|
||||
SetLastSequence(4U);
|
||||
auto files = cfd_->current()->storage_info()->LevelFiles(0);
|
||||
RunCompaction({files}, expected_results);
|
||||
constexpr int input_level = 0;
|
||||
auto files = cfd_->current()->storage_info()->LevelFiles(input_level);
|
||||
RunCompaction({files}, {input_level}, {expected_results});
|
||||
}
|
||||
|
||||
TEST_F(CompactionJobTest, SimpleNonLastLevel) {
|
||||
@@ -751,9 +849,12 @@ TEST_F(CompactionJobTest, SimpleNonLastLevel) {
|
||||
{KeyStr("b", 6U, kTypeValue), "val3"}});
|
||||
|
||||
SetLastSequence(6U);
|
||||
auto lvl0_files = cfd_->current()->storage_info()->LevelFiles(0);
|
||||
auto lvl1_files = cfd_->current()->storage_info()->LevelFiles(1);
|
||||
RunCompaction({lvl0_files, lvl1_files}, expected_results);
|
||||
const std::vector<int> input_levels = {0, 1};
|
||||
auto lvl0_files =
|
||||
cfd_->current()->storage_info()->LevelFiles(input_levels[0]);
|
||||
auto lvl1_files =
|
||||
cfd_->current()->storage_info()->LevelFiles(input_levels[1]);
|
||||
RunCompaction({lvl0_files, lvl1_files}, input_levels, {expected_results});
|
||||
}
|
||||
|
||||
TEST_F(CompactionJobTest, SimpleMerge) {
|
||||
@@ -776,8 +877,9 @@ TEST_F(CompactionJobTest, SimpleMerge) {
|
||||
{KeyStr("b", 0U, kTypeValue), "1,2"}});
|
||||
|
||||
SetLastSequence(5U);
|
||||
auto files = cfd_->current()->storage_info()->LevelFiles(0);
|
||||
RunCompaction({files}, expected_results);
|
||||
constexpr int input_level = 0;
|
||||
auto files = cfd_->current()->storage_info()->LevelFiles(input_level);
|
||||
RunCompaction({files}, {input_level}, {expected_results});
|
||||
}
|
||||
|
||||
TEST_F(CompactionJobTest, NonAssocMerge) {
|
||||
@@ -800,8 +902,9 @@ TEST_F(CompactionJobTest, NonAssocMerge) {
|
||||
{KeyStr("b", 0U, kTypeValue), "1,2"}});
|
||||
|
||||
SetLastSequence(5U);
|
||||
auto files = cfd_->current()->storage_info()->LevelFiles(0);
|
||||
RunCompaction({files}, expected_results);
|
||||
constexpr int input_level = 0;
|
||||
auto files = cfd_->current()->storage_info()->LevelFiles(input_level);
|
||||
RunCompaction({files}, {input_level}, {expected_results});
|
||||
}
|
||||
|
||||
// Filters merge operands with value 10.
|
||||
@@ -827,8 +930,9 @@ TEST_F(CompactionJobTest, MergeOperandFilter) {
|
||||
{KeyStr("b", 0U, kTypeValue), test::EncodeInt(2U)}});
|
||||
|
||||
SetLastSequence(5U);
|
||||
auto files = cfd_->current()->storage_info()->LevelFiles(0);
|
||||
RunCompaction({files}, expected_results);
|
||||
constexpr int input_level = 0;
|
||||
auto files = cfd_->current()->storage_info()->LevelFiles(input_level);
|
||||
RunCompaction({files}, {input_level}, {expected_results});
|
||||
}
|
||||
|
||||
TEST_F(CompactionJobTest, FilterSomeMergeOperands) {
|
||||
@@ -863,8 +967,9 @@ TEST_F(CompactionJobTest, FilterSomeMergeOperands) {
|
||||
});
|
||||
|
||||
SetLastSequence(5U);
|
||||
auto files = cfd_->current()->storage_info()->LevelFiles(0);
|
||||
RunCompaction({files}, expected_results);
|
||||
constexpr int input_level = 0;
|
||||
auto files = cfd_->current()->storage_info()->LevelFiles(input_level);
|
||||
RunCompaction({files}, {input_level}, {expected_results});
|
||||
}
|
||||
|
||||
// Test where all operands/merge results are filtered out.
|
||||
@@ -897,10 +1002,11 @@ TEST_F(CompactionJobTest, FilterAllMergeOperands) {
|
||||
AddMockFile(file3, 2);
|
||||
|
||||
SetLastSequence(11U);
|
||||
auto files = cfd_->current()->storage_info()->LevelFiles(0);
|
||||
constexpr int input_level = 0;
|
||||
auto files = cfd_->current()->storage_info()->LevelFiles(input_level);
|
||||
|
||||
mock::KVVector empty_map;
|
||||
RunCompaction({files}, empty_map);
|
||||
RunCompaction({files}, {input_level}, {empty_map});
|
||||
}
|
||||
|
||||
TEST_F(CompactionJobTest, SimpleSingleDelete) {
|
||||
@@ -925,8 +1031,9 @@ TEST_F(CompactionJobTest, SimpleSingleDelete) {
|
||||
mock::MakeMockFile({{KeyStr("a", 5U, kTypeDeletion), ""}});
|
||||
|
||||
SetLastSequence(6U);
|
||||
auto files = cfd_->current()->storage_info()->LevelFiles(0);
|
||||
RunCompaction({files}, expected_results);
|
||||
constexpr int input_level = 0;
|
||||
auto files = cfd_->current()->storage_info()->LevelFiles(input_level);
|
||||
RunCompaction({files}, {input_level}, {expected_results});
|
||||
}
|
||||
|
||||
TEST_F(CompactionJobTest, SingleDeleteSnapshots) {
|
||||
@@ -990,8 +1097,9 @@ TEST_F(CompactionJobTest, SingleDeleteSnapshots) {
|
||||
});
|
||||
|
||||
SetLastSequence(22U);
|
||||
auto files = cfd_->current()->storage_info()->LevelFiles(0);
|
||||
RunCompaction({files}, expected_results, {10U, 20U}, 10U);
|
||||
constexpr int input_level = 0;
|
||||
auto files = cfd_->current()->storage_info()->LevelFiles(input_level);
|
||||
RunCompaction({files}, {input_level}, {expected_results}, {10U, 20U}, 10U);
|
||||
}
|
||||
|
||||
TEST_F(CompactionJobTest, EarliestWriteConflictSnapshot) {
|
||||
@@ -1068,8 +1176,10 @@ TEST_F(CompactionJobTest, EarliestWriteConflictSnapshot) {
|
||||
});
|
||||
|
||||
SetLastSequence(24U);
|
||||
auto files = cfd_->current()->storage_info()->LevelFiles(0);
|
||||
RunCompaction({files}, expected_results, {10U, 20U, 30U}, 20U);
|
||||
constexpr int input_level = 0;
|
||||
auto files = cfd_->current()->storage_info()->LevelFiles(input_level);
|
||||
RunCompaction({files}, {input_level}, {expected_results}, {10U, 20U, 30U},
|
||||
20U);
|
||||
}
|
||||
|
||||
TEST_F(CompactionJobTest, SingleDeleteZeroSeq) {
|
||||
@@ -1091,8 +1201,9 @@ TEST_F(CompactionJobTest, SingleDeleteZeroSeq) {
|
||||
});
|
||||
|
||||
SetLastSequence(22U);
|
||||
auto files = cfd_->current()->storage_info()->LevelFiles(0);
|
||||
RunCompaction({files}, expected_results, {});
|
||||
constexpr int input_level = 0;
|
||||
auto files = cfd_->current()->storage_info()->LevelFiles(input_level);
|
||||
RunCompaction({files}, {input_level}, {expected_results}, {});
|
||||
}
|
||||
|
||||
TEST_F(CompactionJobTest, MultiSingleDelete) {
|
||||
@@ -1246,8 +1357,9 @@ TEST_F(CompactionJobTest, MultiSingleDelete) {
|
||||
{KeyStr("M", 3U, kTypeSingleDeletion), ""}});
|
||||
|
||||
SetLastSequence(22U);
|
||||
auto files = cfd_->current()->storage_info()->LevelFiles(0);
|
||||
RunCompaction({files}, expected_results, {10U}, 10U);
|
||||
constexpr int input_level = 0;
|
||||
auto files = cfd_->current()->storage_info()->LevelFiles(input_level);
|
||||
RunCompaction({files}, {input_level}, {expected_results}, {10U}, 10U);
|
||||
}
|
||||
|
||||
// This test documents the behavior where a corrupt key follows a deletion or a
|
||||
@@ -1276,8 +1388,9 @@ TEST_F(CompactionJobTest, DISABLED_CorruptionAfterDeletion) {
|
||||
{test::KeyStr("c", 0U, kTypeValue), "val2"}});
|
||||
|
||||
SetLastSequence(6U);
|
||||
auto files = cfd_->current()->storage_info()->LevelFiles(0);
|
||||
RunCompaction({files}, expected_results);
|
||||
constexpr int input_level = 0;
|
||||
auto files = cfd_->current()->storage_info()->LevelFiles(input_level);
|
||||
RunCompaction({files}, {input_level}, {expected_results});
|
||||
}
|
||||
|
||||
TEST_F(CompactionJobTest, OldestBlobFileNumber) {
|
||||
@@ -1322,10 +1435,12 @@ TEST_F(CompactionJobTest, OldestBlobFileNumber) {
|
||||
expected_blob4, expected_blob5, expected_blob6});
|
||||
|
||||
SetLastSequence(6U);
|
||||
auto files = cfd_->current()->storage_info()->LevelFiles(0);
|
||||
RunCompaction({files}, expected_results, std::vector<SequenceNumber>(),
|
||||
kMaxSequenceNumber, /* output_level */ 1, /* verify */ true,
|
||||
/* expected_oldest_blob_file_number */ 19);
|
||||
constexpr int input_level = 0;
|
||||
auto files = cfd_->current()->storage_info()->LevelFiles(input_level);
|
||||
RunCompaction({files}, {input_level}, {expected_results},
|
||||
std::vector<SequenceNumber>(), kMaxSequenceNumber,
|
||||
/* output_level */ 1, /* verify */ true,
|
||||
/* expected_oldest_blob_file_numbers */ {19});
|
||||
}
|
||||
|
||||
TEST_F(CompactionJobTest, VerifyPenultimateLevelOutput) {
|
||||
@@ -1377,13 +1492,15 @@ TEST_F(CompactionJobTest, VerifyPenultimateLevelOutput) {
|
||||
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);
|
||||
const std::vector<int> input_levels = {0, 1, 2, 3};
|
||||
auto files0 = cfd->current()->storage_info()->LevelFiles(input_levels[0]);
|
||||
auto files1 = cfd->current()->storage_info()->LevelFiles(input_levels[1]);
|
||||
auto files2 = cfd->current()->storage_info()->LevelFiles(input_levels[2]);
|
||||
auto files3 = cfd->current()->storage_info()->LevelFiles(input_levels[3]);
|
||||
|
||||
RunLastLevelCompaction(
|
||||
{files0, files1, files2, files3}, /*verify_func=*/[&](Compaction& comp) {
|
||||
{files0, files1, files2, files3}, input_levels,
|
||||
/*verify_func=*/[&](Compaction& comp) {
|
||||
for (char c = 'a'; c <= 'z'; c++) {
|
||||
std::string c_str;
|
||||
c_str = c;
|
||||
@@ -1408,8 +1525,9 @@ TEST_F(CompactionJobTest, NoEnforceSingleDeleteContract) {
|
||||
SetLastSequence(4U);
|
||||
|
||||
auto expected_results = mock::MakeMockFile();
|
||||
auto files = cfd_->current()->storage_info()->LevelFiles(0);
|
||||
RunCompaction({files}, expected_results);
|
||||
constexpr int input_level = 0;
|
||||
auto files = cfd_->current()->storage_info()->LevelFiles(input_level);
|
||||
RunCompaction({files}, {input_level}, {expected_results});
|
||||
}
|
||||
|
||||
TEST_F(CompactionJobTest, InputSerialization) {
|
||||
@@ -1609,7 +1727,8 @@ class CompactionJobTimestampTest : public CompactionJobTestBase {
|
||||
CompactionJobTimestampTest()
|
||||
: CompactionJobTestBase(test::PerThreadDBPath("compaction_job_ts_test"),
|
||||
test::BytewiseComparatorWithU64TsWrapper(),
|
||||
test::EncodeInt, false) {}
|
||||
test::EncodeInt, /*test_io_priority=*/false,
|
||||
TableTypeForTest::kMockTable) {}
|
||||
};
|
||||
|
||||
TEST_F(CompactionJobTimestampTest, GCDisabled) {
|
||||
@@ -1641,8 +1760,9 @@ TEST_F(CompactionJobTimestampTest, GCDisabled) {
|
||||
{KeyStr("c", 4, ValueType::kTypeValue, 94), "c5"},
|
||||
{KeyStr("d", 7, ValueType::kTypeValue, 97), "d7"},
|
||||
{KeyStr("d", 3, ValueType::kTypeSingleDeletion, 93), ""}});
|
||||
const auto& files = cfd_->current()->storage_info()->LevelFiles(0);
|
||||
RunCompaction({files}, expected_results);
|
||||
constexpr int input_level = 0;
|
||||
const auto& files = cfd_->current()->storage_info()->LevelFiles(input_level);
|
||||
RunCompaction({files}, {input_level}, {expected_results});
|
||||
}
|
||||
|
||||
TEST_F(CompactionJobTimestampTest, NoKeyExpired) {
|
||||
@@ -1667,10 +1787,11 @@ TEST_F(CompactionJobTimestampTest, NoKeyExpired) {
|
||||
{KeyStr("b", 7, ValueType::kTypeValue, 101), "b7"},
|
||||
{KeyStr("c", 5, ValueType::kTypeValue, 99), "c5"},
|
||||
{KeyStr("c", 3, ValueType::kTypeValue, 97), "c3"}});
|
||||
const auto& files = cfd_->current()->storage_info()->LevelFiles(0);
|
||||
constexpr int input_level = 0;
|
||||
const auto& files = cfd_->current()->storage_info()->LevelFiles(input_level);
|
||||
|
||||
full_history_ts_low_ = encode_u64_ts_(0);
|
||||
RunCompaction({files}, expected_results);
|
||||
RunCompaction({files}, {input_level}, {expected_results});
|
||||
}
|
||||
|
||||
TEST_F(CompactionJobTimestampTest, AllKeysExpired) {
|
||||
@@ -1693,10 +1814,11 @@ TEST_F(CompactionJobTimestampTest, AllKeysExpired) {
|
||||
|
||||
auto expected_results =
|
||||
mock::MakeMockFile({{KeyStr("c", 0, ValueType::kTypeValue, 0), "c7"}});
|
||||
const auto& files = cfd_->current()->storage_info()->LevelFiles(0);
|
||||
constexpr int input_level = 0;
|
||||
const auto& files = cfd_->current()->storage_info()->LevelFiles(input_level);
|
||||
|
||||
full_history_ts_low_ = encode_u64_ts_(std::numeric_limits<uint64_t>::max());
|
||||
RunCompaction({files}, expected_results);
|
||||
RunCompaction({files}, {input_level}, {expected_results});
|
||||
}
|
||||
|
||||
TEST_F(CompactionJobTimestampTest, SomeKeysExpired) {
|
||||
@@ -1719,10 +1841,121 @@ TEST_F(CompactionJobTimestampTest, SomeKeysExpired) {
|
||||
mock::MakeMockFile({{KeyStr("a", 5, ValueType::kTypeValue, 50), "a5"},
|
||||
{KeyStr("a", 0, ValueType::kTypeValue, 0), "a3"},
|
||||
{KeyStr("b", 6, ValueType::kTypeValue, 49), "b6"}});
|
||||
const auto& files = cfd_->current()->storage_info()->LevelFiles(0);
|
||||
constexpr int input_level = 0;
|
||||
const auto& files = cfd_->current()->storage_info()->LevelFiles(input_level);
|
||||
|
||||
full_history_ts_low_ = encode_u64_ts_(49);
|
||||
RunCompaction({files}, expected_results);
|
||||
RunCompaction({files}, {input_level}, {expected_results});
|
||||
}
|
||||
|
||||
class CompactionJobTimestampTestWithBbTable : public CompactionJobTestBase {
|
||||
public:
|
||||
// Block-based table is needed if we want to test subcompaction partitioning
|
||||
// with anchors.
|
||||
explicit CompactionJobTimestampTestWithBbTable()
|
||||
: CompactionJobTestBase(
|
||||
test::PerThreadDBPath("compaction_job_ts_bbt_test"),
|
||||
test::BytewiseComparatorWithU64TsWrapper(), test::EncodeInt,
|
||||
/*test_io_priority=*/false, TableTypeForTest::kBlockBasedTable) {}
|
||||
};
|
||||
|
||||
TEST_F(CompactionJobTimestampTestWithBbTable, SubcompactionAnchorL1) {
|
||||
cf_options_.target_file_size_base = 20;
|
||||
mutable_cf_options_.target_file_size_base = 20;
|
||||
NewDB();
|
||||
|
||||
const std::vector<std::string> keys = {
|
||||
KeyStr("a", 20, ValueType::kTypeValue, 200),
|
||||
KeyStr("b", 21, ValueType::kTypeValue, 210),
|
||||
KeyStr("b", 20, ValueType::kTypeValue, 200),
|
||||
KeyStr("b", 18, ValueType::kTypeValue, 180),
|
||||
KeyStr("c", 17, ValueType::kTypeValue, 170),
|
||||
KeyStr("c", 16, ValueType::kTypeValue, 160),
|
||||
KeyStr("c", 15, ValueType::kTypeValue, 150)};
|
||||
const std::vector<std::string> values = {"a20", "b21", "b20", "b18",
|
||||
"c17", "c16", "c15"};
|
||||
|
||||
constexpr int input_level = 1;
|
||||
|
||||
auto file1 = mock::MakeMockFile(
|
||||
{{keys[0], values[0]}, {keys[1], values[1]}, {keys[2], values[2]}});
|
||||
AddMockFile(file1, input_level);
|
||||
|
||||
auto file2 = mock::MakeMockFile(
|
||||
{{keys[3], values[3]}, {keys[4], values[4]}, {keys[5], values[5]}});
|
||||
AddMockFile(file2, input_level);
|
||||
|
||||
auto file3 = mock::MakeMockFile({{keys[6], values[6]}});
|
||||
AddMockFile(file3, input_level);
|
||||
|
||||
SetLastSequence(20);
|
||||
|
||||
auto output1 = mock::MakeMockFile({{keys[0], values[0]}});
|
||||
auto output2 = mock::MakeMockFile(
|
||||
{{keys[1], values[1]}, {keys[2], values[2]}, {keys[3], values[3]}});
|
||||
auto output3 = mock::MakeMockFile(
|
||||
{{keys[4], values[4]}, {keys[5], values[5]}, {keys[6], values[6]}});
|
||||
|
||||
auto expected_results =
|
||||
std::vector<mock::KVVector>{output1, output2, output3};
|
||||
const auto& files = cfd_->current()->storage_info()->LevelFiles(input_level);
|
||||
|
||||
constexpr int output_level = 2;
|
||||
constexpr int max_subcompactions = 4;
|
||||
RunCompaction({files}, {input_level}, expected_results, /*snapshots=*/{},
|
||||
/*earliest_write_conflict_snapshot=*/kMaxSequenceNumber,
|
||||
output_level, /*verify=*/true, {kInvalidBlobFileNumber},
|
||||
/*check_get_priority=*/false, Env::IO_TOTAL, Env::IO_TOTAL,
|
||||
max_subcompactions);
|
||||
}
|
||||
|
||||
TEST_F(CompactionJobTimestampTestWithBbTable, SubcompactionL0) {
|
||||
cf_options_.target_file_size_base = 20;
|
||||
mutable_cf_options_.target_file_size_base = 20;
|
||||
NewDB();
|
||||
|
||||
const std::vector<std::string> keys = {
|
||||
KeyStr("a", 20, ValueType::kTypeValue, 200),
|
||||
KeyStr("b", 20, ValueType::kTypeValue, 200),
|
||||
KeyStr("b", 19, ValueType::kTypeValue, 190),
|
||||
KeyStr("b", 18, ValueType::kTypeValue, 180),
|
||||
KeyStr("c", 17, ValueType::kTypeValue, 170),
|
||||
KeyStr("c", 16, ValueType::kTypeValue, 160),
|
||||
KeyStr("c", 15, ValueType::kTypeValue, 150)};
|
||||
const std::vector<std::string> values = {"a20", "b20", "b19", "b18",
|
||||
"c17", "c16", "c15"};
|
||||
|
||||
constexpr int input_level = 0;
|
||||
|
||||
auto file1 = mock::MakeMockFile({{keys[5], values[5]}, {keys[6], values[6]}});
|
||||
AddMockFile(file1, input_level);
|
||||
|
||||
auto file2 = mock::MakeMockFile({{keys[3], values[3]}, {keys[4], values[4]}});
|
||||
AddMockFile(file2, input_level);
|
||||
|
||||
auto file3 = mock::MakeMockFile(
|
||||
{{keys[0], values[0]}, {keys[1], values[1]}, {keys[2], values[2]}});
|
||||
AddMockFile(file3, input_level);
|
||||
|
||||
SetLastSequence(20);
|
||||
|
||||
auto output1 = mock::MakeMockFile({{keys[0], values[0]}});
|
||||
auto output2 = mock::MakeMockFile(
|
||||
{{keys[1], values[1]}, {keys[2], values[2]}, {keys[3], values[3]}});
|
||||
auto output3 = mock::MakeMockFile(
|
||||
{{keys[4], values[4]}, {keys[5], values[5]}, {keys[6], values[6]}});
|
||||
|
||||
auto expected_results =
|
||||
std::vector<mock::KVVector>{output1, output2, output3};
|
||||
const auto& files = cfd_->current()->storage_info()->LevelFiles(input_level);
|
||||
|
||||
constexpr int output_level = 1;
|
||||
constexpr int max_subcompactions = 4;
|
||||
RunCompaction({files}, {input_level}, expected_results, /*snapshots=*/{},
|
||||
/*earliest_write_conflict_snapshot=*/kMaxSequenceNumber,
|
||||
output_level, /*verify=*/true, {kInvalidBlobFileNumber},
|
||||
/*check_get_priority=*/false, Env::IO_TOTAL, Env::IO_TOTAL,
|
||||
max_subcompactions);
|
||||
}
|
||||
|
||||
// The io priority of the compaction reads and writes are different from
|
||||
@@ -1734,7 +1967,8 @@ class CompactionJobIOPriorityTest : public CompactionJobTestBase {
|
||||
CompactionJobIOPriorityTest()
|
||||
: CompactionJobTestBase(
|
||||
test::PerThreadDBPath("compaction_job_io_priority_test"),
|
||||
BytewiseComparator(), [](uint64_t /*ts*/) { return ""; }, true) {}
|
||||
BytewiseComparator(), [](uint64_t /*ts*/) { return ""; },
|
||||
/*test_io_priority=*/true, TableTypeForTest::kBlockBasedTable) {}
|
||||
};
|
||||
|
||||
TEST_F(CompactionJobIOPriorityTest, WriteControllerStateNormal) {
|
||||
@@ -1742,10 +1976,12 @@ TEST_F(CompactionJobIOPriorityTest, WriteControllerStateNormal) {
|
||||
NewDB();
|
||||
mock::KVVector expected_results = CreateTwoFiles(false);
|
||||
auto cfd = versions_->GetColumnFamilySet()->GetDefault();
|
||||
auto files = cfd->current()->storage_info()->LevelFiles(0);
|
||||
constexpr int input_level = 0;
|
||||
auto files = cfd->current()->storage_info()->LevelFiles(input_level);
|
||||
ASSERT_EQ(2U, files.size());
|
||||
RunCompaction({files}, expected_results, {}, kMaxSequenceNumber, 1, false,
|
||||
kInvalidBlobFileNumber, false, Env::IO_LOW, Env::IO_LOW);
|
||||
RunCompaction({files}, {input_level}, {expected_results}, {},
|
||||
kMaxSequenceNumber, 1, false, {kInvalidBlobFileNumber}, false,
|
||||
Env::IO_LOW, Env::IO_LOW);
|
||||
}
|
||||
|
||||
TEST_F(CompactionJobIOPriorityTest, WriteControllerStateDelayed) {
|
||||
@@ -1753,13 +1989,15 @@ TEST_F(CompactionJobIOPriorityTest, WriteControllerStateDelayed) {
|
||||
NewDB();
|
||||
mock::KVVector expected_results = CreateTwoFiles(false);
|
||||
auto cfd = versions_->GetColumnFamilySet()->GetDefault();
|
||||
auto files = cfd->current()->storage_info()->LevelFiles(0);
|
||||
constexpr int input_level = 0;
|
||||
auto files = cfd->current()->storage_info()->LevelFiles(input_level);
|
||||
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);
|
||||
RunCompaction({files}, {input_level}, {expected_results}, {},
|
||||
kMaxSequenceNumber, 1, false, {kInvalidBlobFileNumber}, false,
|
||||
Env::IO_USER, Env::IO_USER);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1768,13 +2006,15 @@ TEST_F(CompactionJobIOPriorityTest, WriteControllerStateStalled) {
|
||||
NewDB();
|
||||
mock::KVVector expected_results = CreateTwoFiles(false);
|
||||
auto cfd = versions_->GetColumnFamilySet()->GetDefault();
|
||||
auto files = cfd->current()->storage_info()->LevelFiles(0);
|
||||
constexpr int input_level = 0;
|
||||
auto files = cfd->current()->storage_info()->LevelFiles(input_level);
|
||||
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);
|
||||
RunCompaction({files}, {input_level}, {expected_results}, {},
|
||||
kMaxSequenceNumber, 1, false, {kInvalidBlobFileNumber}, false,
|
||||
Env::IO_USER, Env::IO_USER);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1782,10 +2022,12 @@ TEST_F(CompactionJobIOPriorityTest, GetRateLimiterPriority) {
|
||||
NewDB();
|
||||
mock::KVVector expected_results = CreateTwoFiles(false);
|
||||
auto cfd = versions_->GetColumnFamilySet()->GetDefault();
|
||||
auto files = cfd->current()->storage_info()->LevelFiles(0);
|
||||
constexpr int input_level = 0;
|
||||
auto files = cfd->current()->storage_info()->LevelFiles(input_level);
|
||||
ASSERT_EQ(2U, files.size());
|
||||
RunCompaction({files}, expected_results, {}, kMaxSequenceNumber, 1, false,
|
||||
kInvalidBlobFileNumber, true, Env::IO_LOW, Env::IO_LOW);
|
||||
RunCompaction({files}, {input_level}, {expected_results}, {},
|
||||
kMaxSequenceNumber, 1, false, {kInvalidBlobFileNumber}, true,
|
||||
Env::IO_LOW, Env::IO_LOW);
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
@@ -76,6 +76,107 @@ IOStatus CompactionOutputs::WriterSyncClose(const Status& input_status,
|
||||
return io_s;
|
||||
}
|
||||
|
||||
bool CompactionOutputs::ShouldStopBefore(const CompactionIterator& c_iter) {
|
||||
assert(c_iter.Valid());
|
||||
|
||||
// If there's user defined partitioner, check that first
|
||||
if (HasBuilder() && partitioner_ &&
|
||||
partitioner_->ShouldPartition(
|
||||
PartitionerRequest(last_key_for_partitioner_, c_iter.user_key(),
|
||||
current_output_file_size_)) == kRequired) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// files output to Level 0 won't be split
|
||||
if (compaction_->output_level() == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// reach the target file size
|
||||
if (current_output_file_size_ >= compaction_->max_output_file_size()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const Slice& internal_key = c_iter.key();
|
||||
const InternalKeyComparator* icmp =
|
||||
&compaction_->column_family_data()->internal_comparator();
|
||||
|
||||
// Check if it needs to split for RoundRobin
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
|
||||
// Update grandparent information
|
||||
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_ + current_output_file_size_ >
|
||||
compaction_->max_compaction_bytes()) {
|
||||
// Too much overlap for current output; start new output
|
||||
overlapped_bytes_ = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
// check ttl file boundaries if there's any
|
||||
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 CompactionOutputs::AddToOutput(
|
||||
const CompactionIterator& c_iter,
|
||||
const CompactionFileOpenFunc& open_file_func,
|
||||
@@ -83,27 +184,19 @@ Status CompactionOutputs::AddToOutput(
|
||||
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_) {
|
||||
if (ShouldStopBefore(c_iter) && HasBuilder()) {
|
||||
s = close_file_func(*this, c_iter.InputStatus(), key);
|
||||
pending_close_ = false;
|
||||
}
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
// Open output file if necessary
|
||||
if (!HasBuilder()) {
|
||||
s = open_file_func(*this);
|
||||
}
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
Output& curr = current_output();
|
||||
@@ -130,19 +223,6 @@ Status CompactionOutputs::AddToOutput(
|
||||
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_);
|
||||
@@ -318,4 +398,59 @@ Status CompactionOutputs::AddRangeDels(
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
void CompactionOutputs::FillFilesToCutForTtl() {
|
||||
if (compaction_->immutable_options()->compaction_style !=
|
||||
kCompactionStyleLevel ||
|
||||
compaction_->immutable_options()->compaction_pri !=
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
CompactionOutputs::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();
|
||||
|
||||
if (compaction->output_level() != 0) {
|
||||
FillFilesToCutForTtl();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -45,12 +45,7 @@ class CompactionOutputs {
|
||||
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();
|
||||
}
|
||||
const bool is_penultimate_level);
|
||||
|
||||
// Add generated output to the list
|
||||
void AddOutput(FileMetaData&& meta, const InternalKeyComparator& icmp,
|
||||
@@ -179,12 +174,6 @@ class CompactionOutputs {
|
||||
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();
|
||||
@@ -193,6 +182,32 @@ class CompactionOutputs {
|
||||
private:
|
||||
friend class SubcompactionState;
|
||||
|
||||
void FillFilesToCutForTtl();
|
||||
|
||||
void SetOutputSlitKey(const std::optional<Slice> start,
|
||||
const std::optional<Slice> end) {
|
||||
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.has_value() ||
|
||||
icmp->user_comparator()->Compare(
|
||||
ExtractUserKey(output_split_key->Encode()), end.value()) < 0) &&
|
||||
(!start.has_value() || icmp->user_comparator()->Compare(
|
||||
ExtractUserKey(output_split_key->Encode()),
|
||||
start.value()) > 0)) {
|
||||
local_output_split_key_ = output_split_key;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns true iff we should stop building the current output
|
||||
// before processing the current key in compaction iterator.
|
||||
bool ShouldStopBefore(const CompactionIterator& c_iter);
|
||||
|
||||
void Cleanup() {
|
||||
if (builder_ != nullptr) {
|
||||
// May happen if we get a shutdown call in the middle of compaction
|
||||
@@ -205,7 +220,7 @@ class CompactionOutputs {
|
||||
return current_output_file_size_;
|
||||
}
|
||||
|
||||
// Add curent key from compaction_iterator to the output file. If needed
|
||||
// Add current 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,
|
||||
@@ -255,10 +270,6 @@ class CompactionOutputs {
|
||||
|
||||
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_;
|
||||
@@ -282,6 +293,30 @@ class CompactionOutputs {
|
||||
// partitioner information
|
||||
std::string last_key_for_partitioner_;
|
||||
std::unique_ptr<SstPartitioner> partitioner_;
|
||||
|
||||
// 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;
|
||||
|
||||
// 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;
|
||||
};
|
||||
|
||||
// helper struct to concatenate the last level and penultimate level outputs
|
||||
|
||||
@@ -288,8 +288,10 @@ bool CompactionPicker::RangeOverlapWithCompaction(
|
||||
const Comparator* ucmp = icmp_->user_comparator();
|
||||
for (Compaction* c : compactions_in_progress_) {
|
||||
if (c->output_level() == level &&
|
||||
ucmp->Compare(smallest_user_key, c->GetLargestUserKey()) <= 0 &&
|
||||
ucmp->Compare(largest_user_key, c->GetSmallestUserKey()) >= 0) {
|
||||
ucmp->CompareWithoutTimestamp(smallest_user_key,
|
||||
c->GetLargestUserKey()) <= 0 &&
|
||||
ucmp->CompareWithoutTimestamp(largest_user_key,
|
||||
c->GetSmallestUserKey()) >= 0) {
|
||||
// Overlap
|
||||
return true;
|
||||
}
|
||||
@@ -878,21 +880,21 @@ namespace {
|
||||
// Test whether two files have overlapping key-ranges.
|
||||
bool HaveOverlappingKeyRanges(const Comparator* c, const SstFileMetaData& a,
|
||||
const SstFileMetaData& b) {
|
||||
if (c->Compare(a.smallestkey, b.smallestkey) >= 0) {
|
||||
if (c->Compare(a.smallestkey, b.largestkey) <= 0) {
|
||||
if (c->CompareWithoutTimestamp(a.smallestkey, b.smallestkey) >= 0) {
|
||||
if (c->CompareWithoutTimestamp(a.smallestkey, b.largestkey) <= 0) {
|
||||
// b.smallestkey <= a.smallestkey <= b.largestkey
|
||||
return true;
|
||||
}
|
||||
} else if (c->Compare(a.largestkey, b.smallestkey) >= 0) {
|
||||
} else if (c->CompareWithoutTimestamp(a.largestkey, b.smallestkey) >= 0) {
|
||||
// a.smallestkey < b.smallestkey <= a.largestkey
|
||||
return true;
|
||||
}
|
||||
if (c->Compare(a.largestkey, b.largestkey) <= 0) {
|
||||
if (c->Compare(a.largestkey, b.smallestkey) >= 0) {
|
||||
if (c->CompareWithoutTimestamp(a.largestkey, b.largestkey) <= 0) {
|
||||
if (c->CompareWithoutTimestamp(a.largestkey, b.smallestkey) >= 0) {
|
||||
// b.smallestkey <= a.largestkey <= b.largestkey
|
||||
return true;
|
||||
}
|
||||
} else if (c->Compare(a.smallestkey, b.largestkey) <= 0) {
|
||||
} else if (c->CompareWithoutTimestamp(a.smallestkey, b.largestkey) <= 0) {
|
||||
// a.smallestkey <= b.largestkey < a.largestkey
|
||||
return true;
|
||||
}
|
||||
@@ -931,15 +933,16 @@ Status CompactionPicker::SanitizeCompactionInputFilesForAllLevels(
|
||||
// identify the first and the last compaction input files
|
||||
// in the current level.
|
||||
for (size_t f = 0; f < current_files.size(); ++f) {
|
||||
if (input_files->find(TableFileNameToNumber(current_files[f].name)) !=
|
||||
input_files->end()) {
|
||||
first_included = std::min(first_included, static_cast<int>(f));
|
||||
last_included = std::max(last_included, static_cast<int>(f));
|
||||
if (is_first == false) {
|
||||
smallestkey = current_files[f].smallestkey;
|
||||
largestkey = current_files[f].largestkey;
|
||||
is_first = true;
|
||||
}
|
||||
const uint64_t file_number = TableFileNameToNumber(current_files[f].name);
|
||||
if (input_files->find(file_number) == input_files->end()) {
|
||||
continue;
|
||||
}
|
||||
first_included = std::min(first_included, static_cast<int>(f));
|
||||
last_included = std::max(last_included, static_cast<int>(f));
|
||||
if (is_first == false) {
|
||||
smallestkey = current_files[f].smallestkey;
|
||||
largestkey = current_files[f].largestkey;
|
||||
is_first = true;
|
||||
}
|
||||
}
|
||||
if (last_included == kNotFound) {
|
||||
@@ -947,21 +950,22 @@ Status CompactionPicker::SanitizeCompactionInputFilesForAllLevels(
|
||||
}
|
||||
|
||||
if (l != 0) {
|
||||
// expend the compaction input of the current level if it
|
||||
// expand the compaction input of the current level if it
|
||||
// has overlapping key-range with other non-compaction input
|
||||
// files in the same level.
|
||||
while (first_included > 0) {
|
||||
if (comparator->Compare(current_files[first_included - 1].largestkey,
|
||||
current_files[first_included].smallestkey) <
|
||||
0) {
|
||||
if (comparator->CompareWithoutTimestamp(
|
||||
current_files[first_included - 1].largestkey,
|
||||
current_files[first_included].smallestkey) < 0) {
|
||||
break;
|
||||
}
|
||||
first_included--;
|
||||
}
|
||||
|
||||
while (last_included < static_cast<int>(current_files.size()) - 1) {
|
||||
if (comparator->Compare(current_files[last_included + 1].smallestkey,
|
||||
current_files[last_included].largestkey) > 0) {
|
||||
if (comparator->CompareWithoutTimestamp(
|
||||
current_files[last_included + 1].smallestkey,
|
||||
current_files[last_included].largestkey) > 0) {
|
||||
break;
|
||||
}
|
||||
last_included++;
|
||||
@@ -983,21 +987,22 @@ Status CompactionPicker::SanitizeCompactionInputFilesForAllLevels(
|
||||
// update smallest and largest key
|
||||
if (l == 0) {
|
||||
for (int f = first_included; f <= last_included; ++f) {
|
||||
if (comparator->Compare(smallestkey, current_files[f].smallestkey) >
|
||||
0) {
|
||||
if (comparator->CompareWithoutTimestamp(
|
||||
smallestkey, current_files[f].smallestkey) > 0) {
|
||||
smallestkey = current_files[f].smallestkey;
|
||||
}
|
||||
if (comparator->Compare(largestkey, current_files[f].largestkey) < 0) {
|
||||
if (comparator->CompareWithoutTimestamp(
|
||||
largestkey, current_files[f].largestkey) < 0) {
|
||||
largestkey = current_files[f].largestkey;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (comparator->Compare(smallestkey,
|
||||
current_files[first_included].smallestkey) > 0) {
|
||||
if (comparator->CompareWithoutTimestamp(
|
||||
smallestkey, current_files[first_included].smallestkey) > 0) {
|
||||
smallestkey = current_files[first_included].smallestkey;
|
||||
}
|
||||
if (comparator->Compare(largestkey,
|
||||
current_files[last_included].largestkey) < 0) {
|
||||
if (comparator->CompareWithoutTimestamp(
|
||||
largestkey, current_files[last_included].largestkey) < 0) {
|
||||
largestkey = current_files[last_included].largestkey;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -662,9 +662,14 @@ bool LevelCompactionBuilder::TryExtendNonL0TrivialMove(int start_index) {
|
||||
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) {
|
||||
compaction_picker_->icmp()
|
||||
->user_comparator()
|
||||
->CompareWithoutTimestamp(
|
||||
next_file->largest.user_key(),
|
||||
level_files[i + 1]->smallest.user_key()) == 0) {
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"LevelCompactionBuilder::TryExtendNonL0TrivialMove:NoCleanCut",
|
||||
nullptr);
|
||||
// Not a clean up after adding the next file. Skip.
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ class CountingLogger : public Logger {
|
||||
size_t log_count;
|
||||
};
|
||||
|
||||
class CompactionPickerTest : public testing::Test {
|
||||
class CompactionPickerTestBase : public testing::Test {
|
||||
public:
|
||||
const Comparator* ucmp_;
|
||||
InternalKeyComparator icmp_;
|
||||
@@ -49,9 +49,10 @@ class CompactionPickerTest : public testing::Test {
|
||||
std::vector<CompactionInputFiles> input_files_;
|
||||
int compaction_level_start_;
|
||||
|
||||
CompactionPickerTest()
|
||||
: ucmp_(BytewiseComparator()),
|
||||
explicit CompactionPickerTestBase(const Comparator* _ucmp)
|
||||
: ucmp_(_ucmp),
|
||||
icmp_(ucmp_),
|
||||
options_(CreateOptions(ucmp_)),
|
||||
ioptions_(options_),
|
||||
mutable_cf_options_(options_),
|
||||
mutable_db_options_(),
|
||||
@@ -71,7 +72,7 @@ class CompactionPickerTest : public testing::Test {
|
||||
std::numeric_limits<uint64_t>::max());
|
||||
}
|
||||
|
||||
~CompactionPickerTest() override {}
|
||||
~CompactionPickerTestBase() override {}
|
||||
|
||||
void NewVersionStorage(int num_levels, CompactionStyle style) {
|
||||
DeleteVersionStorage();
|
||||
@@ -97,12 +98,17 @@ class CompactionPickerTest : public testing::Test {
|
||||
input_files_.clear();
|
||||
}
|
||||
|
||||
// REQUIRES: smallest and largest are c-style strings ending with '\0'
|
||||
void Add(int level, uint32_t file_number, const char* smallest,
|
||||
const char* largest, uint64_t file_size = 1, uint32_t path_id = 0,
|
||||
SequenceNumber smallest_seq = 100, SequenceNumber largest_seq = 100,
|
||||
size_t compensated_file_size = 0, bool marked_for_compact = false,
|
||||
Temperature temperature = Temperature::kUnknown,
|
||||
uint64_t oldest_ancestor_time = kUnknownOldestAncesterTime) {
|
||||
uint64_t oldest_ancestor_time = kUnknownOldestAncesterTime,
|
||||
Slice ts_of_smallest = Slice(), Slice ts_of_largest = Slice()) {
|
||||
assert(ts_of_smallest.size() == ucmp_->timestamp_size());
|
||||
assert(ts_of_largest.size() == ucmp_->timestamp_size());
|
||||
|
||||
VersionStorageInfo* vstorage;
|
||||
if (temp_vstorage_) {
|
||||
vstorage = temp_vstorage_.get();
|
||||
@@ -110,19 +116,46 @@ class CompactionPickerTest : public testing::Test {
|
||||
vstorage = vstorage_.get();
|
||||
}
|
||||
assert(level < vstorage->num_levels());
|
||||
char* smallest_key_buf = nullptr;
|
||||
char* largest_key_buf = nullptr;
|
||||
|
||||
if (!ts_of_smallest.empty()) {
|
||||
smallest_key_buf = new char[strlen(smallest) + ucmp_->timestamp_size()];
|
||||
memcpy(smallest_key_buf, smallest, strlen(smallest));
|
||||
memcpy(smallest_key_buf + strlen(smallest), ts_of_smallest.data(),
|
||||
ucmp_->timestamp_size());
|
||||
largest_key_buf = new char[strlen(largest) + ucmp_->timestamp_size()];
|
||||
memcpy(largest_key_buf, largest, strlen(largest));
|
||||
memcpy(largest_key_buf + strlen(largest), ts_of_largest.data(),
|
||||
ucmp_->timestamp_size());
|
||||
}
|
||||
|
||||
InternalKey smallest_ikey = InternalKey(
|
||||
smallest_key_buf ? Slice(smallest_key_buf,
|
||||
ucmp_->timestamp_size() + strlen(smallest))
|
||||
: smallest,
|
||||
smallest_seq, kTypeValue);
|
||||
InternalKey largest_ikey = InternalKey(
|
||||
largest_key_buf
|
||||
? Slice(largest_key_buf, ucmp_->timestamp_size() + strlen(largest))
|
||||
: largest,
|
||||
largest_seq, kTypeValue);
|
||||
|
||||
FileMetaData* f = new FileMetaData(
|
||||
file_number, path_id, file_size,
|
||||
InternalKey(smallest, smallest_seq, kTypeValue),
|
||||
InternalKey(largest, largest_seq, kTypeValue), smallest_seq,
|
||||
largest_seq, marked_for_compact, temperature, kInvalidBlobFileNumber,
|
||||
kUnknownOldestAncesterTime, kUnknownFileCreationTime,
|
||||
kUnknownFileChecksum, kUnknownFileChecksumFuncName, kNullUniqueId64x2);
|
||||
file_number, path_id, file_size, smallest_ikey, largest_ikey,
|
||||
smallest_seq, largest_seq, marked_for_compact, temperature,
|
||||
kInvalidBlobFileNumber, kUnknownOldestAncesterTime,
|
||||
kUnknownFileCreationTime, kUnknownFileChecksum,
|
||||
kUnknownFileChecksumFuncName, kNullUniqueId64x2);
|
||||
f->compensated_file_size =
|
||||
(compensated_file_size != 0) ? compensated_file_size : file_size;
|
||||
f->oldest_ancester_time = oldest_ancestor_time;
|
||||
vstorage->AddFile(level, f);
|
||||
files_.emplace_back(f);
|
||||
file_map_.insert({file_number, {f, level}});
|
||||
|
||||
delete[] smallest_key_buf;
|
||||
delete[] largest_key_buf;
|
||||
}
|
||||
|
||||
void SetCompactionInputFilesLevels(int level_count, int start_level) {
|
||||
@@ -155,9 +188,31 @@ class CompactionPickerTest : public testing::Test {
|
||||
}
|
||||
|
||||
private:
|
||||
Options CreateOptions(const Comparator* ucmp) const {
|
||||
Options opts;
|
||||
opts.comparator = ucmp;
|
||||
return opts;
|
||||
}
|
||||
|
||||
std::unique_ptr<VersionStorageInfo> temp_vstorage_;
|
||||
};
|
||||
|
||||
class CompactionPickerTest : public CompactionPickerTestBase {
|
||||
public:
|
||||
explicit CompactionPickerTest()
|
||||
: CompactionPickerTestBase(BytewiseComparator()) {}
|
||||
|
||||
~CompactionPickerTest() override {}
|
||||
};
|
||||
|
||||
class CompactionPickerU64TsTest : public CompactionPickerTestBase {
|
||||
public:
|
||||
explicit CompactionPickerU64TsTest()
|
||||
: CompactionPickerTestBase(test::BytewiseComparatorWithU64TsWrapper()) {}
|
||||
|
||||
~CompactionPickerU64TsTest() override {}
|
||||
};
|
||||
|
||||
TEST_F(CompactionPickerTest, Empty) {
|
||||
NewVersionStorage(6, kCompactionStyleLevel);
|
||||
UpdateVersionStorageInfo();
|
||||
@@ -3317,6 +3372,107 @@ TEST_F(CompactionPickerTest, UniversalSizeAmpTierCompactionLastLevel) {
|
||||
ASSERT_EQ(compaction->input_levels(6)->num_files, 0);
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerU64TsTest, Overlap) {
|
||||
int num_levels = ioptions_.num_levels;
|
||||
NewVersionStorage(num_levels, kCompactionStyleLevel);
|
||||
|
||||
constexpr int level = 0;
|
||||
constexpr uint64_t file_number = 20ULL;
|
||||
constexpr char smallest[4] = "500";
|
||||
constexpr char largest[4] = "600";
|
||||
constexpr uint64_t ts_of_smallest = 12345ULL;
|
||||
constexpr uint64_t ts_of_largest = 56789ULL;
|
||||
|
||||
{
|
||||
std::string ts1;
|
||||
PutFixed64(&ts1, ts_of_smallest);
|
||||
std::string ts2;
|
||||
PutFixed64(&ts2, ts_of_largest);
|
||||
Add(level, file_number, smallest, largest,
|
||||
/*file_size=*/1U, /*path_id=*/0,
|
||||
/*smallest_seq=*/100, /*largest_seq=*/100, /*compensated_file_size=*/0,
|
||||
/*marked_for_compact=*/false, /*temperature=*/Temperature::kUnknown,
|
||||
/*oldest_ancestor_time=*/kUnknownOldestAncesterTime, ts1, ts2);
|
||||
UpdateVersionStorageInfo();
|
||||
}
|
||||
|
||||
std::unordered_set<uint64_t> input{file_number};
|
||||
|
||||
std::vector<CompactionInputFiles> input_files;
|
||||
ASSERT_OK(level_compaction_picker.GetCompactionInputsFromFileNumbers(
|
||||
&input_files, &input, vstorage_.get(), CompactionOptions()));
|
||||
std::unique_ptr<Compaction> comp1(level_compaction_picker.CompactFiles(
|
||||
CompactionOptions(), input_files, level, vstorage_.get(),
|
||||
mutable_cf_options_, mutable_db_options_, /*output_path_id=*/0));
|
||||
|
||||
{
|
||||
// [600, ts=50000] to [600, ts=50000] is the range to check.
|
||||
// ucmp->Compare(smallest_user_key, c->GetLargestUserKey()) > 0, but
|
||||
// ucmp->CompareWithoutTimestamp(smallest_user_key,
|
||||
// c->GetLargestUserKey()) == 0.
|
||||
// Should still be considered overlapping.
|
||||
std::string user_key_with_ts1(largest);
|
||||
PutFixed64(&user_key_with_ts1, ts_of_largest - 1);
|
||||
std::string user_key_with_ts2(largest);
|
||||
PutFixed64(&user_key_with_ts2, ts_of_largest - 1);
|
||||
ASSERT_TRUE(level_compaction_picker.RangeOverlapWithCompaction(
|
||||
user_key_with_ts1, user_key_with_ts2, level));
|
||||
}
|
||||
{
|
||||
// [500, ts=60000] to [500, ts=60000] is the range to check.
|
||||
// ucmp->Compare(largest_user_key, c->GetSmallestUserKey()) < 0, but
|
||||
// ucmp->CompareWithoutTimestamp(largest_user_key,
|
||||
// c->GetSmallestUserKey()) == 0.
|
||||
// Should still be considered overlapping.
|
||||
std::string user_key_with_ts1(smallest);
|
||||
PutFixed64(&user_key_with_ts1, ts_of_smallest + 1);
|
||||
std::string user_key_with_ts2(smallest);
|
||||
PutFixed64(&user_key_with_ts2, ts_of_smallest + 1);
|
||||
ASSERT_TRUE(level_compaction_picker.RangeOverlapWithCompaction(
|
||||
user_key_with_ts1, user_key_with_ts2, level));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(CompactionPickerU64TsTest, CannotTrivialMoveUniversal) {
|
||||
constexpr uint64_t kFileSize = 100000;
|
||||
|
||||
mutable_cf_options_.level0_file_num_compaction_trigger = 2;
|
||||
mutable_cf_options_.compaction_options_universal.allow_trivial_move = true;
|
||||
NewVersionStorage(1, kCompactionStyleUniversal);
|
||||
UniversalCompactionPicker universal_compaction_picker(ioptions_, &icmp_);
|
||||
UpdateVersionStorageInfo();
|
||||
// must return false when there's no files.
|
||||
ASSERT_FALSE(universal_compaction_picker.NeedsCompaction(vstorage_.get()));
|
||||
|
||||
std::string ts1;
|
||||
PutFixed64(&ts1, 9000);
|
||||
std::string ts2;
|
||||
PutFixed64(&ts2, 8000);
|
||||
std::string ts3;
|
||||
PutFixed64(&ts3, 7000);
|
||||
std::string ts4;
|
||||
PutFixed64(&ts4, 6000);
|
||||
|
||||
NewVersionStorage(3, kCompactionStyleUniversal);
|
||||
// A compaction should be triggered and pick file 2
|
||||
Add(1, 1U, "150", "150", kFileSize, /*path_id=*/0, /*smallest_seq=*/100,
|
||||
/*largest_seq=*/100, /*compensated_file_size=*/kFileSize,
|
||||
/*marked_for_compact=*/false, Temperature::kUnknown,
|
||||
kUnknownOldestAncesterTime, ts1, ts2);
|
||||
Add(2, 2U, "150", "150", kFileSize, /*path_id=*/0, /*smallest_seq=*/100,
|
||||
/*largest_seq=*/100, /*compensated_file_size=*/kFileSize,
|
||||
/*marked_for_compact=*/false, Temperature::kUnknown,
|
||||
kUnknownOldestAncesterTime, ts3, ts4);
|
||||
UpdateVersionStorageInfo();
|
||||
|
||||
std::unique_ptr<Compaction> compaction(
|
||||
universal_compaction_picker.PickCompaction(
|
||||
cf_name_, mutable_cf_options_, mutable_db_options_, vstorage_.get(),
|
||||
&log_buffer_));
|
||||
assert(compaction);
|
||||
ASSERT_TRUE(!compaction->is_trivial_move());
|
||||
}
|
||||
|
||||
class PerKeyPlacementCompactionPickerTest
|
||||
: public CompactionPickerTest,
|
||||
public testing::WithParamInterface<bool> {
|
||||
|
||||
@@ -164,8 +164,8 @@ struct SmallestKeyHeapComparator {
|
||||
explicit SmallestKeyHeapComparator(const Comparator* ucmp) { ucmp_ = ucmp; }
|
||||
|
||||
bool operator()(InputFileInfo i1, InputFileInfo i2) const {
|
||||
return (ucmp_->Compare(i1.f->smallest.user_key(),
|
||||
i2.f->smallest.user_key()) > 0);
|
||||
return (ucmp_->CompareWithoutTimestamp(i1.f->smallest.user_key(),
|
||||
i2.f->smallest.user_key()) > 0);
|
||||
}
|
||||
|
||||
private:
|
||||
@@ -249,13 +249,13 @@ bool UniversalCompactionBuilder::IsInputFilesNonOverlapping(Compaction* c) {
|
||||
prev = curr;
|
||||
first_iter = 0;
|
||||
} else {
|
||||
if (comparator->Compare(prev.f->largest.user_key(),
|
||||
curr.f->smallest.user_key()) >= 0) {
|
||||
if (comparator->CompareWithoutTimestamp(
|
||||
prev.f->largest.user_key(), curr.f->smallest.user_key()) >= 0) {
|
||||
// found overlapping files, return false
|
||||
return false;
|
||||
}
|
||||
assert(comparator->Compare(curr.f->largest.user_key(),
|
||||
prev.f->largest.user_key()) > 0);
|
||||
assert(comparator->CompareWithoutTimestamp(
|
||||
curr.f->largest.user_key(), prev.f->largest.user_key()) > 0);
|
||||
prev = curr;
|
||||
}
|
||||
|
||||
@@ -755,9 +755,9 @@ Compaction* UniversalCompactionBuilder::PickCompactionToReduceSortedRuns(
|
||||
kCompactionStyleUniversal),
|
||||
GetMaxOverlappingBytes(), path_id,
|
||||
GetCompressionType(vstorage_, mutable_cf_options_,
|
||||
start_level, 1, enable_compression),
|
||||
output_level, 1, enable_compression),
|
||||
GetCompressionOptions(mutable_cf_options_, vstorage_,
|
||||
start_level, enable_compression),
|
||||
output_level, enable_compression),
|
||||
Temperature::kUnknown,
|
||||
/* max_subcompactions */ 0, grandparents,
|
||||
/* is manual */ false, /* trim_ts */ "", score_,
|
||||
|
||||
@@ -289,16 +289,6 @@ TEST_F(CompactionServiceTest, BasicCompactions) {
|
||||
auto s = static_cast<Status*>(status);
|
||||
*s = Status::Aborted("MyTestCompactionService failed to compact!");
|
||||
});
|
||||
|
||||
// tracking success unique id verification
|
||||
std::atomic_int verify_passed{0};
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"Version::VerifySstUniqueIds::Passed", [&](void* arg) {
|
||||
// override job status
|
||||
auto id = static_cast<UniqueId64x2*>(arg);
|
||||
assert(*id != kNullUniqueId64x2);
|
||||
verify_passed++;
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
Status s;
|
||||
@@ -324,9 +314,15 @@ TEST_F(CompactionServiceTest, BasicCompactions) {
|
||||
}
|
||||
ASSERT_TRUE(s.IsAborted());
|
||||
|
||||
// Test verification
|
||||
ASSERT_EQ(verify_passed, 0);
|
||||
options.verify_sst_unique_id_in_manifest = true;
|
||||
// Test re-open and successful unique id verification
|
||||
std::atomic_int verify_passed{0};
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"BlockBasedTable::Open::PassedVerifyUniqueId", [&](void* arg) {
|
||||
// override job status
|
||||
auto id = static_cast<UniqueId64x2*>(arg);
|
||||
assert(*id != kNullUniqueId64x2);
|
||||
verify_passed++;
|
||||
});
|
||||
Reopen(options);
|
||||
ASSERT_GT(verify_passed, 0);
|
||||
}
|
||||
|
||||
@@ -23,46 +23,6 @@ void SubcompactionState::AggregateCompactionStats(
|
||||
}
|
||||
}
|
||||
|
||||
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_);
|
||||
@@ -128,83 +88,6 @@ Slice SubcompactionState::LargestUserKey() const {
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@@ -128,21 +128,12 @@ class SubcompactionState {
|
||||
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.has_value() ||
|
||||
icmp->user_comparator()->Compare(
|
||||
ExtractUserKey(output_split_key->Encode()), end.value()) < 0) &&
|
||||
(!start.has_value() || icmp->user_comparator()->Compare(
|
||||
ExtractUserKey(output_split_key->Encode()),
|
||||
start.value()) > 0)) {
|
||||
local_output_split_key_ = output_split_key;
|
||||
}
|
||||
}
|
||||
// Set output split key (used for RoundRobin feature) only for normal
|
||||
// compaction_outputs, output to penultimate_level feature doesn't support
|
||||
// RoundRobin feature (and may never going to be supported, because for
|
||||
// RoundRobin, the data time is mostly naturally sorted, no need to have
|
||||
// per-key placement with output_to_penultimate_level).
|
||||
compaction_outputs_.SetOutputSlitKey(start, end);
|
||||
}
|
||||
|
||||
SubcompactionState(SubcompactionState&& state) noexcept
|
||||
@@ -155,12 +146,6 @@ class SubcompactionState {
|
||||
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_),
|
||||
@@ -175,12 +160,6 @@ class SubcompactionState {
|
||||
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_;
|
||||
}
|
||||
@@ -224,28 +203,6 @@ class SubcompactionState {
|
||||
}
|
||||
|
||||
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_;
|
||||
|
||||
+29
-4
@@ -253,6 +253,7 @@ TEST_F(DBBasicTest, CompactedDB) {
|
||||
ASSERT_OK(Put("bbb", DummyString(kFileSize / 2, 'b')));
|
||||
ASSERT_OK(Put("eee", DummyString(kFileSize / 2, 'e')));
|
||||
ASSERT_OK(Flush());
|
||||
ASSERT_OK(Put("something_not_flushed", "x"));
|
||||
Close();
|
||||
|
||||
ASSERT_OK(ReadOnlyReopen(options));
|
||||
@@ -260,6 +261,20 @@ TEST_F(DBBasicTest, CompactedDB) {
|
||||
s = Put("new", "value");
|
||||
ASSERT_EQ(s.ToString(),
|
||||
"Not implemented: Not supported operation in read only mode.");
|
||||
|
||||
// TODO: validate that other write ops return NotImplemented
|
||||
// (DBImplReadOnly is missing some overrides)
|
||||
|
||||
// Ensure no deadlock on flush triggered by another API function
|
||||
// (Old deadlock bug depends on something_not_flushed above.)
|
||||
std::vector<std::string> files;
|
||||
uint64_t manifest_file_size;
|
||||
ASSERT_OK(db_->GetLiveFiles(files, &manifest_file_size, /*flush*/ true));
|
||||
LiveFilesStorageInfoOptions lfsi_opts;
|
||||
lfsi_opts.wal_size_for_flush = 0; // always
|
||||
std::vector<LiveFileStorageInfo> files2;
|
||||
ASSERT_OK(db_->GetLiveFilesStorageInfo(lfsi_opts, &files2));
|
||||
|
||||
Close();
|
||||
|
||||
// Full compaction
|
||||
@@ -290,6 +305,13 @@ TEST_F(DBBasicTest, CompactedDB) {
|
||||
ASSERT_EQ(DummyString(kFileSize / 2, 'j'), Get("jjj"));
|
||||
ASSERT_EQ("NOT_FOUND", Get("kkk"));
|
||||
|
||||
// TODO: validate that other write ops return NotImplemented
|
||||
// (CompactedDB is missing some overrides)
|
||||
|
||||
// Ensure no deadlock on flush triggered by another API function
|
||||
ASSERT_OK(db_->GetLiveFiles(files, &manifest_file_size, /*flush*/ true));
|
||||
ASSERT_OK(db_->GetLiveFilesStorageInfo(lfsi_opts, &files2));
|
||||
|
||||
// MultiGet
|
||||
std::vector<std::string> values;
|
||||
std::vector<Status> status_list = dbfull()->MultiGet(
|
||||
@@ -2014,7 +2036,7 @@ TEST_P(DBMultiGetTestWithParam, MultiGetBatchedValueSize) {
|
||||
|
||||
TEST_P(DBMultiGetTestWithParam, MultiGetBatchedValueSizeMultiLevelMerge) {
|
||||
if (std::get<1>(GetParam())) {
|
||||
ROCKSDB_GTEST_SKIP("This test needs to be fixed for async IO");
|
||||
ROCKSDB_GTEST_BYPASS("This test needs to be fixed for async IO");
|
||||
return;
|
||||
}
|
||||
// Skip for unbatched MultiGet
|
||||
@@ -2239,7 +2261,9 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL0) {
|
||||
ASSERT_EQ(multiget_io_batch_size.max, 3);
|
||||
ASSERT_EQ(statistics()->getTickerCount(MULTIGET_COROUTINE_COUNT), 3);
|
||||
} else {
|
||||
ASSERT_EQ(multiget_io_batch_size.count, 0);
|
||||
// Without Async IO, MultiGet will call MultiRead 3 times, once for each
|
||||
// L0 file
|
||||
ASSERT_EQ(multiget_io_batch_size.count, 3);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2356,8 +2380,9 @@ TEST_P(DBMultiGetAsyncIOTest, GetFromL1AndL2) {
|
||||
statistics()->histogramData(MULTIGET_IO_BATCH_SIZE, &multiget_io_batch_size);
|
||||
|
||||
// There are 2 keys in L1 in twp separate files, and 1 in L2. With
|
||||
// async IO, all three lookups will happen in parallel
|
||||
ASSERT_EQ(multiget_io_batch_size.count, 1);
|
||||
// optimize_multiget_for_io, all three lookups will happen in parallel.
|
||||
// Otherwise, the L2 lookup will happen after L1.
|
||||
ASSERT_EQ(multiget_io_batch_size.count, GetParam() ? 1 : 2);
|
||||
ASSERT_EQ(multiget_io_batch_size.max, GetParam() ? 3 : 2);
|
||||
}
|
||||
|
||||
|
||||
+18
-11
@@ -13,7 +13,6 @@
|
||||
|
||||
#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"
|
||||
@@ -678,8 +677,8 @@ INSTANTIATE_TEST_CASE_P(DBBlockCacheTest1, DBBlockCacheTest1,
|
||||
TEST_P(DBBlockCacheTest1, WarmCacheWithBlocksDuringFlush) {
|
||||
Options options = CurrentOptions();
|
||||
options.create_if_missing = true;
|
||||
options.disable_auto_compactions = true;
|
||||
options.statistics = ROCKSDB_NAMESPACE::CreateDBStatistics();
|
||||
options.max_compaction_bytes = 2000;
|
||||
|
||||
BlockBasedTableOptions table_options;
|
||||
table_options.block_cache = NewLRUCache(1 << 25, 0, false);
|
||||
@@ -737,8 +736,10 @@ TEST_P(DBBlockCacheTest1, WarmCacheWithBlocksDuringFlush) {
|
||||
}
|
||||
|
||||
// Verify compaction not counted
|
||||
ASSERT_OK(db_->CompactRange(CompactRangeOptions(), /*begin=*/nullptr,
|
||||
/*end=*/nullptr));
|
||||
CompactRangeOptions cro;
|
||||
// Ensure files are rewritten, not just trivially moved.
|
||||
cro.bottommost_level_compaction = BottommostLevelCompaction::kForceOptimized;
|
||||
ASSERT_OK(db_->CompactRange(cro, /*begin=*/nullptr, /*end=*/nullptr));
|
||||
EXPECT_EQ(kNumBlocks,
|
||||
options.statistics->getTickerCount(BLOCK_CACHE_DATA_ADD));
|
||||
// Index and filter blocks are automatically warmed when the new table file
|
||||
@@ -936,9 +937,11 @@ TEST_F(DBBlockCacheTest, AddRedundantStats) {
|
||||
int iterations_tested = 0;
|
||||
for (std::shared_ptr<Cache> base_cache :
|
||||
{NewLRUCache(capacity, num_shard_bits),
|
||||
ExperimentalNewClockCache(
|
||||
capacity, 1 /*estimated_value_size*/, num_shard_bits,
|
||||
false /*strict_capacity_limit*/, kDefaultCacheMetadataChargePolicy),
|
||||
HyperClockCacheOptions(
|
||||
capacity,
|
||||
BlockBasedTableOptions().block_size /*estimated_value_size*/,
|
||||
num_shard_bits)
|
||||
.MakeSharedCache(),
|
||||
NewFastLRUCache(capacity, 1 /*estimated_value_size*/, num_shard_bits,
|
||||
false /*strict_capacity_limit*/,
|
||||
kDefaultCacheMetadataChargePolicy)}) {
|
||||
@@ -1296,10 +1299,10 @@ TEST_F(DBBlockCacheTest, CacheEntryRoleStats) {
|
||||
for (bool partition : {false, true}) {
|
||||
for (std::shared_ptr<Cache> cache :
|
||||
{NewLRUCache(capacity),
|
||||
ExperimentalNewClockCache(capacity, 1 /*estimated_value_size*/,
|
||||
-1 /*num_shard_bits*/,
|
||||
false /*strict_capacity_limit*/,
|
||||
kDefaultCacheMetadataChargePolicy)}) {
|
||||
HyperClockCacheOptions(
|
||||
capacity,
|
||||
BlockBasedTableOptions().block_size /*estimated_value_size*/)
|
||||
.MakeSharedCache()}) {
|
||||
if (!cache) {
|
||||
// Skip clock cache when not supported
|
||||
continue;
|
||||
@@ -1568,6 +1571,10 @@ TEST_P(DBBlockCacheKeyTest, StableCacheKeys) {
|
||||
options.statistics = ROCKSDB_NAMESPACE::CreateDBStatistics();
|
||||
options.env = test_env.get();
|
||||
|
||||
// Corrupting the table properties corrupts the unique id.
|
||||
// Ignore the unique id recorded in the manifest.
|
||||
options.verify_sst_unique_id_in_manifest = false;
|
||||
|
||||
BlockBasedTableOptions table_options;
|
||||
|
||||
int key_count = 0;
|
||||
|
||||
@@ -328,11 +328,9 @@ TEST_F(DBTestCompactionFilter, CompactionFilter) {
|
||||
Arena arena;
|
||||
{
|
||||
InternalKeyComparator icmp(options.comparator);
|
||||
ReadRangeDelAggregator range_del_agg(&icmp,
|
||||
kMaxSequenceNumber /* upper_bound */);
|
||||
ReadOptions read_options;
|
||||
ScopedArenaIterator iter(dbfull()->NewInternalIterator(
|
||||
read_options, &arena, &range_del_agg, kMaxSequenceNumber, handles_[1]));
|
||||
read_options, &arena, kMaxSequenceNumber, handles_[1]));
|
||||
iter->SeekToFirst();
|
||||
ASSERT_OK(iter->status());
|
||||
while (iter->Valid()) {
|
||||
@@ -422,11 +420,9 @@ TEST_F(DBTestCompactionFilter, CompactionFilter) {
|
||||
count = 0;
|
||||
{
|
||||
InternalKeyComparator icmp(options.comparator);
|
||||
ReadRangeDelAggregator range_del_agg(&icmp,
|
||||
kMaxSequenceNumber /* upper_bound */);
|
||||
ReadOptions read_options;
|
||||
ScopedArenaIterator iter(dbfull()->NewInternalIterator(
|
||||
read_options, &arena, &range_del_agg, kMaxSequenceNumber, handles_[1]));
|
||||
read_options, &arena, kMaxSequenceNumber, handles_[1]));
|
||||
iter->SeekToFirst();
|
||||
ASSERT_OK(iter->status());
|
||||
while (iter->Valid()) {
|
||||
@@ -701,11 +697,9 @@ TEST_F(DBTestCompactionFilter, CompactionFilterContextManual) {
|
||||
int total = 0;
|
||||
Arena arena;
|
||||
InternalKeyComparator icmp(options.comparator);
|
||||
ReadRangeDelAggregator range_del_agg(&icmp,
|
||||
kMaxSequenceNumber /* snapshots */);
|
||||
ReadOptions read_options;
|
||||
ScopedArenaIterator iter(dbfull()->NewInternalIterator(
|
||||
read_options, &arena, &range_del_agg, kMaxSequenceNumber));
|
||||
ScopedArenaIterator iter(dbfull()->NewInternalIterator(read_options, &arena,
|
||||
kMaxSequenceNumber));
|
||||
iter->SeekToFirst();
|
||||
ASSERT_OK(iter->status());
|
||||
while (iter->Valid()) {
|
||||
|
||||
@@ -7958,6 +7958,42 @@ TEST_F(DBCompactionTest, BottomPriCompactionCountsTowardConcurrencyLimit) {
|
||||
compact_range_thread.join();
|
||||
}
|
||||
|
||||
TEST_F(DBCompactionTest, BottommostFileCompactionAllowIngestBehind) {
|
||||
// allow_ingest_behind prevents seqnum zeroing, and could cause
|
||||
// compaction loop with reason kBottommostFiles.
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
options.compaction_style = kCompactionStyleLevel;
|
||||
options.allow_ingest_behind = true;
|
||||
options.comparator = BytewiseComparator();
|
||||
DestroyAndReopen(options);
|
||||
|
||||
WriteOptions write_opts;
|
||||
ASSERT_OK(db_->Put(write_opts, "infinite", "compaction loop"));
|
||||
ASSERT_OK(db_->Put(write_opts, "infinite", "loop"));
|
||||
|
||||
ASSERT_OK(Flush());
|
||||
MoveFilesToLevel(1);
|
||||
ASSERT_OK(db_->Put(write_opts, "bumpseqnum", ""));
|
||||
ASSERT_OK(Flush());
|
||||
auto snapshot = db_->GetSnapshot();
|
||||
// Bump up oldest_snapshot_seqnum_ in VersionStorageInfo.
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
bool compacted = false;
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"LevelCompactionPicker::PickCompaction:Return", [&](void* /* arg */) {
|
||||
// There should not be a compaction.
|
||||
compacted = true;
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
// Wait for compaction to be scheduled.
|
||||
env_->SleepForMicroseconds(2000000);
|
||||
ASSERT_FALSE(compacted);
|
||||
// The following assert can be used to check for compaction loop:
|
||||
// it used to wait forever before the fix.
|
||||
// ASSERT_OK(dbfull()->TEST_WaitForCompact(true /* wait_unscheduled */));
|
||||
}
|
||||
|
||||
#endif // !defined(ROCKSDB_LITE)
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
@@ -242,7 +242,7 @@ Status CompactedDBImpl::Open(const Options& options,
|
||||
std::unique_ptr<CompactedDBImpl> db(new CompactedDBImpl(db_options, dbname));
|
||||
Status s = db->Init(options);
|
||||
if (s.ok()) {
|
||||
s = db->StartPeriodicWorkScheduler();
|
||||
s = db->StartPeriodicTaskScheduler();
|
||||
}
|
||||
if (s.ok()) {
|
||||
ROCKS_LOG_INFO(db->immutable_db_options_.info_log,
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
// TODO: Share common structure with DBImplSecondary and DBImplReadOnly
|
||||
class CompactedDBImpl : public DBImpl {
|
||||
public:
|
||||
CompactedDBImpl(const DBOptions& options, const std::string& dbname);
|
||||
@@ -127,6 +128,17 @@ class CompactedDBImpl : public DBImpl {
|
||||
return Status::NotSupported("Not supported in compacted db mode.");
|
||||
}
|
||||
|
||||
// FIXME: some missing overrides for more "write" functions
|
||||
// Share with DBImplReadOnly?
|
||||
|
||||
protected:
|
||||
#ifndef ROCKSDB_LITE
|
||||
Status FlushForGetLiveFiles() override {
|
||||
// No-op for read-only DB
|
||||
return Status::OK();
|
||||
}
|
||||
#endif // !ROCKSDB_LITE
|
||||
|
||||
private:
|
||||
friend class DB;
|
||||
inline size_t FindFile(const Slice& key);
|
||||
|
||||
+154
-99
@@ -45,7 +45,7 @@
|
||||
#include "db/memtable_list.h"
|
||||
#include "db/merge_context.h"
|
||||
#include "db/merge_helper.h"
|
||||
#include "db/periodic_work_scheduler.h"
|
||||
#include "db/periodic_task_scheduler.h"
|
||||
#include "db/range_tombstone_fragmenter.h"
|
||||
#include "db/table_cache.h"
|
||||
#include "db/table_properties_collector.h"
|
||||
@@ -165,8 +165,13 @@ DBImpl::DBImpl(const DBOptions& options, const std::string& dbname,
|
||||
fs_(immutable_db_options_.fs, io_tracer_),
|
||||
mutable_db_options_(initial_db_options_),
|
||||
stats_(immutable_db_options_.stats),
|
||||
#ifdef COERCE_CONTEXT_SWITCH
|
||||
mutex_(stats_, immutable_db_options_.clock, DB_MUTEX_WAIT_MICROS, &bg_cv_,
|
||||
immutable_db_options_.use_adaptive_mutex),
|
||||
#else // COERCE_CONTEXT_SWITCH
|
||||
mutex_(stats_, immutable_db_options_.clock, DB_MUTEX_WAIT_MICROS,
|
||||
immutable_db_options_.use_adaptive_mutex),
|
||||
#endif // COERCE_CONTEXT_SWITCH
|
||||
default_cf_handle_(nullptr),
|
||||
error_handler_(this, immutable_db_options_, &mutex_),
|
||||
event_logger_(immutable_db_options_.info_log.get()),
|
||||
@@ -217,7 +222,7 @@ DBImpl::DBImpl(const DBOptions& options, const std::string& dbname,
|
||||
refitting_level_(false),
|
||||
opened_successfully_(false),
|
||||
#ifndef ROCKSDB_LITE
|
||||
periodic_work_scheduler_(nullptr),
|
||||
periodic_task_scheduler_(),
|
||||
#endif // ROCKSDB_LITE
|
||||
two_write_queues_(options.two_write_queues),
|
||||
manual_wal_flush_(options.manual_wal_flush),
|
||||
@@ -260,6 +265,18 @@ DBImpl::DBImpl(const DBOptions& options, const std::string& dbname,
|
||||
SetDbSessionId();
|
||||
assert(!db_session_id_.empty());
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
periodic_task_functions_.emplace(PeriodicTaskType::kDumpStats,
|
||||
[this]() { this->DumpStats(); });
|
||||
periodic_task_functions_.emplace(PeriodicTaskType::kPersistStats,
|
||||
[this]() { this->PersistStats(); });
|
||||
periodic_task_functions_.emplace(PeriodicTaskType::kFlushInfoLog,
|
||||
[this]() { this->FlushInfoLog(); });
|
||||
periodic_task_functions_.emplace(
|
||||
PeriodicTaskType::kRecordSeqnoTime,
|
||||
[this]() { this->RecordSeqnoToTimeMapping(); });
|
||||
#endif // ROCKSDB_LITE
|
||||
|
||||
versions_.reset(new VersionSet(dbname_, &immutable_db_options_, file_options_,
|
||||
table_cache_.get(), write_buffer_manager_,
|
||||
&write_controller_, &block_cache_tracer_,
|
||||
@@ -480,9 +497,15 @@ void DBImpl::CancelAllBackgroundWork(bool wait) {
|
||||
"Shutdown: canceling all background work");
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
if (periodic_work_scheduler_ != nullptr) {
|
||||
periodic_work_scheduler_->Unregister(this);
|
||||
periodic_work_scheduler_->UnregisterRecordSeqnoTimeWorker(this);
|
||||
for (uint8_t task_type = 0;
|
||||
task_type < static_cast<uint8_t>(PeriodicTaskType::kMax); task_type++) {
|
||||
Status s = periodic_task_scheduler_.Unregister(
|
||||
static_cast<PeriodicTaskType>(task_type));
|
||||
if (!s.ok()) {
|
||||
ROCKS_LOG_WARN(immutable_db_options_.info_log,
|
||||
"Failed to unregister periodic task %d, status: %s",
|
||||
task_type, s.ToString().c_str());
|
||||
}
|
||||
}
|
||||
#endif // !ROCKSDB_LITE
|
||||
|
||||
@@ -767,30 +790,50 @@ void DBImpl::PrintStatistics() {
|
||||
}
|
||||
}
|
||||
|
||||
Status DBImpl::StartPeriodicWorkScheduler() {
|
||||
Status DBImpl::StartPeriodicTaskScheduler() {
|
||||
#ifndef ROCKSDB_LITE
|
||||
|
||||
#ifndef NDEBUG
|
||||
// It only used by test to disable scheduler
|
||||
bool disable_scheduler = false;
|
||||
TEST_SYNC_POINT_CALLBACK(
|
||||
"DBImpl::StartPeriodicWorkScheduler:DisableScheduler",
|
||||
"DBImpl::StartPeriodicTaskScheduler:DisableScheduler",
|
||||
&disable_scheduler);
|
||||
if (disable_scheduler) {
|
||||
return Status::OK();
|
||||
}
|
||||
#endif // !NDEBUG
|
||||
|
||||
{
|
||||
InstrumentedMutexLock l(&mutex_);
|
||||
periodic_work_scheduler_ = PeriodicWorkScheduler::Default();
|
||||
TEST_SYNC_POINT_CALLBACK("DBImpl::StartPeriodicWorkScheduler:Init",
|
||||
&periodic_work_scheduler_);
|
||||
TEST_SYNC_POINT_CALLBACK("DBImpl::StartPeriodicTaskScheduler:Init",
|
||||
&periodic_task_scheduler_);
|
||||
}
|
||||
|
||||
return periodic_work_scheduler_->Register(
|
||||
this, mutable_db_options_.stats_dump_period_sec,
|
||||
mutable_db_options_.stats_persist_period_sec);
|
||||
#endif // !NDEBUG
|
||||
if (mutable_db_options_.stats_dump_period_sec > 0) {
|
||||
Status s = periodic_task_scheduler_.Register(
|
||||
PeriodicTaskType::kDumpStats,
|
||||
periodic_task_functions_.at(PeriodicTaskType::kDumpStats),
|
||||
mutable_db_options_.stats_dump_period_sec);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
if (mutable_db_options_.stats_persist_period_sec > 0) {
|
||||
Status s = periodic_task_scheduler_.Register(
|
||||
PeriodicTaskType::kPersistStats,
|
||||
periodic_task_functions_.at(PeriodicTaskType::kPersistStats),
|
||||
mutable_db_options_.stats_persist_period_sec);
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
Status s = periodic_task_scheduler_.Register(
|
||||
PeriodicTaskType::kFlushInfoLog,
|
||||
periodic_task_functions_.at(PeriodicTaskType::kFlushInfoLog));
|
||||
|
||||
return s;
|
||||
#else
|
||||
return Status::OK();
|
||||
#endif // !ROCKSDB_LITE
|
||||
@@ -798,9 +841,6 @@ Status DBImpl::StartPeriodicWorkScheduler() {
|
||||
|
||||
Status DBImpl::RegisterRecordSeqnoTimeWorker() {
|
||||
#ifndef ROCKSDB_LITE
|
||||
if (!periodic_work_scheduler_) {
|
||||
return Status::OK();
|
||||
}
|
||||
uint64_t min_time_duration = std::numeric_limits<uint64_t>::max();
|
||||
uint64_t max_time_duration = std::numeric_limits<uint64_t>::min();
|
||||
{
|
||||
@@ -828,26 +868,13 @@ Status DBImpl::RegisterRecordSeqnoTimeWorker() {
|
||||
}
|
||||
|
||||
Status s;
|
||||
if (seqno_time_cadence != record_seqno_time_cadence_) {
|
||||
if (seqno_time_cadence == 0) {
|
||||
periodic_work_scheduler_->UnregisterRecordSeqnoTimeWorker(this);
|
||||
} else {
|
||||
s = periodic_work_scheduler_->RegisterRecordSeqnoTimeWorker(
|
||||
this, seqno_time_cadence);
|
||||
}
|
||||
|
||||
if (s.ok()) {
|
||||
record_seqno_time_cadence_ = seqno_time_cadence;
|
||||
}
|
||||
|
||||
if (s.IsNotSupported()) {
|
||||
// TODO: Fix the timer cannot cancel and re-add the same task
|
||||
ROCKS_LOG_WARN(
|
||||
immutable_db_options_.info_log,
|
||||
"Updating seqno to time worker cadence is not supported yet, to make "
|
||||
"the change effective, please reopen the DB instance.");
|
||||
s = Status::OK();
|
||||
}
|
||||
if (seqno_time_cadence == 0) {
|
||||
s = periodic_task_scheduler_.Unregister(PeriodicTaskType::kRecordSeqnoTime);
|
||||
} else {
|
||||
s = periodic_task_scheduler_.Register(
|
||||
PeriodicTaskType::kRecordSeqnoTime,
|
||||
periodic_task_functions_.at(PeriodicTaskType::kRecordSeqnoTime),
|
||||
seqno_time_cadence);
|
||||
}
|
||||
|
||||
return s;
|
||||
@@ -1087,6 +1114,10 @@ void DBImpl::DumpStats() {
|
||||
PrintStatistics();
|
||||
}
|
||||
|
||||
// Periodically flush info log out of application buffer at a low frequency.
|
||||
// This improves debuggability in case of RocksDB hanging since it ensures the
|
||||
// log messages leading up to the hang will eventually become visible in the
|
||||
// log.
|
||||
void DBImpl::FlushInfoLog() {
|
||||
if (shutdown_initiated_) {
|
||||
return;
|
||||
@@ -1279,22 +1310,36 @@ Status DBImpl::SetDBOptions(
|
||||
MaybeScheduleFlushOrCompaction();
|
||||
}
|
||||
|
||||
if (new_options.stats_dump_period_sec !=
|
||||
mutable_db_options_.stats_dump_period_sec ||
|
||||
new_options.stats_persist_period_sec !=
|
||||
mutable_db_options_.stats_persist_period_sec) {
|
||||
mutex_.Unlock();
|
||||
periodic_work_scheduler_->Unregister(this);
|
||||
s = periodic_work_scheduler_->Register(
|
||||
this, new_options.stats_dump_period_sec,
|
||||
new_options.stats_persist_period_sec);
|
||||
mutex_.Lock();
|
||||
mutex_.Unlock();
|
||||
if (new_options.stats_dump_period_sec == 0) {
|
||||
s = periodic_task_scheduler_.Unregister(PeriodicTaskType::kDumpStats);
|
||||
} else {
|
||||
s = periodic_task_scheduler_.Register(
|
||||
PeriodicTaskType::kDumpStats,
|
||||
periodic_task_functions_.at(PeriodicTaskType::kDumpStats),
|
||||
new_options.stats_dump_period_sec);
|
||||
}
|
||||
if (new_options.max_total_wal_size !=
|
||||
mutable_db_options_.max_total_wal_size) {
|
||||
max_total_wal_size_.store(new_options.max_total_wal_size,
|
||||
std::memory_order_release);
|
||||
}
|
||||
if (s.ok()) {
|
||||
if (new_options.stats_persist_period_sec == 0) {
|
||||
s = periodic_task_scheduler_.Unregister(
|
||||
PeriodicTaskType::kPersistStats);
|
||||
} else {
|
||||
s = periodic_task_scheduler_.Register(
|
||||
PeriodicTaskType::kPersistStats,
|
||||
periodic_task_functions_.at(PeriodicTaskType::kPersistStats),
|
||||
new_options.stats_persist_period_sec);
|
||||
}
|
||||
}
|
||||
mutex_.Lock();
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
|
||||
write_controller_.set_max_delayed_write_rate(
|
||||
new_options.delayed_write_rate);
|
||||
table_cache_.get()->SetCapacity(new_options.max_open_files == -1
|
||||
@@ -1494,8 +1539,8 @@ Status DBImpl::SyncWAL() {
|
||||
Status DBImpl::ApplyWALToManifest(VersionEdit* synced_wals) {
|
||||
// not empty, write to MANIFEST.
|
||||
mutex_.AssertHeld();
|
||||
Status status =
|
||||
versions_->LogAndApplyToDefaultColumnFamily(synced_wals, &mutex_);
|
||||
Status status = versions_->LogAndApplyToDefaultColumnFamily(
|
||||
synced_wals, &mutex_, directories_.GetDbDir());
|
||||
if (!status.ok() && versions_->io_status().IsIOError()) {
|
||||
status = error_handler_.SetBGError(versions_->io_status(),
|
||||
BackgroundErrorReason::kManifestWrite);
|
||||
@@ -1532,20 +1577,28 @@ void DBImpl::MarkLogsSynced(uint64_t up_to, bool synced_dir,
|
||||
auto& wal = *it;
|
||||
assert(wal.IsSyncing());
|
||||
|
||||
if (logs_.size() > 1) {
|
||||
if (wal.number < logs_.back().number) {
|
||||
// Inactive WAL
|
||||
if (immutable_db_options_.track_and_verify_wals_in_manifest &&
|
||||
wal.GetPreSyncSize() > 0) {
|
||||
synced_wals->AddWal(wal.number, WalMetadata(wal.GetPreSyncSize()));
|
||||
}
|
||||
logs_to_free_.push_back(wal.ReleaseWriter());
|
||||
it = logs_.erase(it);
|
||||
if (wal.GetPreSyncSize() == wal.writer->file()->GetFlushedSize()) {
|
||||
// Fully synced
|
||||
logs_to_free_.push_back(wal.ReleaseWriter());
|
||||
it = logs_.erase(it);
|
||||
} else {
|
||||
assert(wal.GetPreSyncSize() < wal.writer->file()->GetFlushedSize());
|
||||
wal.FinishSync();
|
||||
++it;
|
||||
}
|
||||
} else {
|
||||
assert(wal.number == logs_.back().number);
|
||||
// Active WAL
|
||||
wal.FinishSync();
|
||||
++it;
|
||||
}
|
||||
}
|
||||
assert(logs_.empty() || logs_[0].number > up_to ||
|
||||
(logs_.size() == 1 && !logs_[0].IsSyncing()));
|
||||
log_sync_cv_.SignalAll();
|
||||
}
|
||||
|
||||
@@ -1593,7 +1646,6 @@ Status DBImpl::GetFullHistoryTsLow(ColumnFamilyHandle* column_family,
|
||||
|
||||
InternalIterator* DBImpl::NewInternalIterator(const ReadOptions& read_options,
|
||||
Arena* arena,
|
||||
RangeDelAggregator* range_del_agg,
|
||||
SequenceNumber sequence,
|
||||
ColumnFamilyHandle* column_family,
|
||||
bool allow_unprepared_value) {
|
||||
@@ -1608,8 +1660,8 @@ InternalIterator* DBImpl::NewInternalIterator(const ReadOptions& read_options,
|
||||
mutex_.Lock();
|
||||
SuperVersion* super_version = cfd->GetSuperVersion()->Ref();
|
||||
mutex_.Unlock();
|
||||
return NewInternalIterator(read_options, cfd, super_version, arena,
|
||||
range_del_agg, sequence, allow_unprepared_value);
|
||||
return NewInternalIterator(read_options, cfd, super_version, arena, sequence,
|
||||
allow_unprepared_value);
|
||||
}
|
||||
|
||||
void DBImpl::SchedulePurge() {
|
||||
@@ -1740,48 +1792,54 @@ static void CleanupGetMergeOperandsState(void* arg1, void* /*arg2*/) {
|
||||
|
||||
} // namespace
|
||||
|
||||
InternalIterator* DBImpl::NewInternalIterator(const ReadOptions& read_options,
|
||||
ColumnFamilyData* cfd,
|
||||
SuperVersion* super_version,
|
||||
Arena* arena,
|
||||
RangeDelAggregator* range_del_agg,
|
||||
SequenceNumber sequence,
|
||||
bool allow_unprepared_value) {
|
||||
InternalIterator* DBImpl::NewInternalIterator(
|
||||
const ReadOptions& read_options, ColumnFamilyData* cfd,
|
||||
SuperVersion* super_version, Arena* arena, SequenceNumber sequence,
|
||||
bool allow_unprepared_value, ArenaWrappedDBIter* db_iter) {
|
||||
InternalIterator* internal_iter;
|
||||
assert(arena != nullptr);
|
||||
assert(range_del_agg != nullptr);
|
||||
// Need to create internal iterator from the arena.
|
||||
MergeIteratorBuilder merge_iter_builder(
|
||||
&cfd->internal_comparator(), arena,
|
||||
!read_options.total_order_seek &&
|
||||
super_version->mutable_cf_options.prefix_extractor != nullptr);
|
||||
// Collect iterator for mutable mem
|
||||
merge_iter_builder.AddIterator(
|
||||
super_version->mem->NewIterator(read_options, arena));
|
||||
std::unique_ptr<FragmentedRangeTombstoneIterator> range_del_iter;
|
||||
super_version->mutable_cf_options.prefix_extractor != nullptr,
|
||||
read_options.iterate_upper_bound);
|
||||
// Collect iterator for mutable memtable
|
||||
auto mem_iter = super_version->mem->NewIterator(read_options, arena);
|
||||
Status s;
|
||||
if (!read_options.ignore_range_deletions) {
|
||||
range_del_iter.reset(super_version->mem->NewRangeTombstoneIterator(
|
||||
read_options, sequence, false /* immutable_memtable */));
|
||||
range_del_agg->AddTombstones(std::move(range_del_iter));
|
||||
TruncatedRangeDelIterator* mem_tombstone_iter = nullptr;
|
||||
auto range_del_iter = super_version->mem->NewRangeTombstoneIterator(
|
||||
read_options, sequence, false /* immutable_memtable */);
|
||||
if (range_del_iter == nullptr || range_del_iter->empty()) {
|
||||
delete range_del_iter;
|
||||
} else {
|
||||
mem_tombstone_iter = new TruncatedRangeDelIterator(
|
||||
std::unique_ptr<FragmentedRangeTombstoneIterator>(range_del_iter),
|
||||
&cfd->ioptions()->internal_comparator, nullptr /* smallest */,
|
||||
nullptr /* largest */);
|
||||
}
|
||||
merge_iter_builder.AddPointAndTombstoneIterator(mem_iter,
|
||||
mem_tombstone_iter);
|
||||
} else {
|
||||
merge_iter_builder.AddIterator(mem_iter);
|
||||
}
|
||||
|
||||
// Collect all needed child iterators for immutable memtables
|
||||
if (s.ok()) {
|
||||
super_version->imm->AddIterators(read_options, &merge_iter_builder);
|
||||
if (!read_options.ignore_range_deletions) {
|
||||
s = super_version->imm->AddRangeTombstoneIterators(read_options, arena,
|
||||
range_del_agg);
|
||||
}
|
||||
super_version->imm->AddIterators(read_options, &merge_iter_builder,
|
||||
!read_options.ignore_range_deletions);
|
||||
}
|
||||
TEST_SYNC_POINT_CALLBACK("DBImpl::NewInternalIterator:StatusCallback", &s);
|
||||
if (s.ok()) {
|
||||
// Collect iterators for files in L0 - Ln
|
||||
if (read_options.read_tier != kMemtableTier) {
|
||||
super_version->current->AddIterators(read_options, file_options_,
|
||||
&merge_iter_builder, range_del_agg,
|
||||
&merge_iter_builder,
|
||||
allow_unprepared_value);
|
||||
}
|
||||
internal_iter = merge_iter_builder.Finish();
|
||||
internal_iter = merge_iter_builder.Finish(
|
||||
read_options.ignore_range_deletions ? nullptr : db_iter);
|
||||
SuperVersionHandle* cleanup = new SuperVersionHandle(
|
||||
this, &mutex_, super_version,
|
||||
read_options.background_purge_on_iterator_cleanup ||
|
||||
@@ -3035,12 +3093,7 @@ Status DBImpl::CreateColumnFamilyImpl(const ColumnFamilyOptions& cf_options,
|
||||
} // InstrumentedMutexLock l(&mutex_)
|
||||
|
||||
if (cf_options.preclude_last_level_data_seconds > 0) {
|
||||
// TODO(zjay): Fix the timer issue and re-enable this.
|
||||
ROCKS_LOG_ERROR(
|
||||
immutable_db_options_.info_log,
|
||||
"Creating column family with `preclude_last_level_data_seconds` needs "
|
||||
"to restart DB to take effect");
|
||||
// s = RegisterRecordSeqnoTimeWorker();
|
||||
s = RegisterRecordSeqnoTimeWorker();
|
||||
}
|
||||
sv_context.Clean();
|
||||
// this is outside the mutex
|
||||
@@ -3106,7 +3159,7 @@ Status DBImpl::DropColumnFamilyImpl(ColumnFamilyHandle* column_family) {
|
||||
WriteThread::Writer w;
|
||||
write_thread_.EnterUnbatched(&w, &mutex_);
|
||||
s = versions_->LogAndApply(cfd, *cfd->GetLatestMutableCFOptions(), &edit,
|
||||
&mutex_);
|
||||
&mutex_, directories_.GetDbDir());
|
||||
write_thread_.ExitUnbatched(&w);
|
||||
}
|
||||
if (s.ok()) {
|
||||
@@ -3311,9 +3364,8 @@ ArenaWrappedDBIter* DBImpl::NewIteratorImpl(const ReadOptions& read_options,
|
||||
read_options.snapshot != nullptr ? false : allow_refresh);
|
||||
|
||||
InternalIterator* internal_iter = NewInternalIterator(
|
||||
db_iter->GetReadOptions(), cfd, sv, db_iter->GetArena(),
|
||||
db_iter->GetRangeDelAggregator(), snapshot,
|
||||
/* allow_unprepared_value */ true);
|
||||
db_iter->GetReadOptions(), cfd, sv, db_iter->GetArena(), snapshot,
|
||||
/* allow_unprepared_value */ true, db_iter);
|
||||
db_iter->SetIterUnderDBIter(internal_iter);
|
||||
|
||||
return db_iter;
|
||||
@@ -3611,14 +3663,16 @@ void DBImpl::ReleaseSnapshot(const Snapshot* s) {
|
||||
if (oldest_snapshot > bottommost_files_mark_threshold_) {
|
||||
CfdList cf_scheduled;
|
||||
for (auto* cfd : *versions_->GetColumnFamilySet()) {
|
||||
cfd->current()->storage_info()->UpdateOldestSnapshot(oldest_snapshot);
|
||||
if (!cfd->current()
|
||||
->storage_info()
|
||||
->BottommostFilesMarkedForCompaction()
|
||||
.empty()) {
|
||||
SchedulePendingCompaction(cfd);
|
||||
MaybeScheduleFlushOrCompaction();
|
||||
cf_scheduled.push_back(cfd);
|
||||
if (!cfd->ioptions()->allow_ingest_behind) {
|
||||
cfd->current()->storage_info()->UpdateOldestSnapshot(oldest_snapshot);
|
||||
if (!cfd->current()
|
||||
->storage_info()
|
||||
->BottommostFilesMarkedForCompaction()
|
||||
.empty()) {
|
||||
SchedulePendingCompaction(cfd);
|
||||
MaybeScheduleFlushOrCompaction();
|
||||
cf_scheduled.push_back(cfd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3627,7 +3681,8 @@ void DBImpl::ReleaseSnapshot(const Snapshot* s) {
|
||||
// mutex might be unlocked during the loop, making the result inaccurate.
|
||||
SequenceNumber new_bottommost_files_mark_threshold = kMaxSequenceNumber;
|
||||
for (auto* cfd : *versions_->GetColumnFamilySet()) {
|
||||
if (CfdListContains(cf_scheduled, cfd)) {
|
||||
if (CfdListContains(cf_scheduled, cfd) ||
|
||||
cfd->ioptions()->allow_ingest_behind) {
|
||||
continue;
|
||||
}
|
||||
new_bottommost_files_mark_threshold = std::min(
|
||||
|
||||
+27
-31
@@ -32,6 +32,7 @@
|
||||
#include "db/log_writer.h"
|
||||
#include "db/logs_with_prep_tracker.h"
|
||||
#include "db/memtable_list.h"
|
||||
#include "db/periodic_task_scheduler.h"
|
||||
#include "db/post_memtable_callback.h"
|
||||
#include "db/pre_release_callback.h"
|
||||
#include "db/range_del_aggregator.h"
|
||||
@@ -75,10 +76,6 @@ class ArenaWrappedDBIter;
|
||||
class InMemoryStatsHistoryIterator;
|
||||
class MemTable;
|
||||
class PersistentStatsHistoryIterator;
|
||||
class PeriodicWorkScheduler;
|
||||
#ifndef NDEBUG
|
||||
class PeriodicWorkTestScheduler;
|
||||
#endif // !NDEBUG
|
||||
class TableCache;
|
||||
class TaskLimiterToken;
|
||||
class Version;
|
||||
@@ -742,13 +739,29 @@ class DBImpl : public DB {
|
||||
// the value and so will require PrepareValue() to be called before value();
|
||||
// allow_unprepared_value = false is convenient when this optimization is not
|
||||
// useful, e.g. when reading the whole column family.
|
||||
//
|
||||
// read_options.ignore_range_deletions determines whether range tombstones are
|
||||
// processed in the returned interator internally, i.e., whether range
|
||||
// tombstone covered keys are in this iterator's output.
|
||||
// @param read_options Must outlive the returned iterator.
|
||||
InternalIterator* NewInternalIterator(
|
||||
const ReadOptions& read_options, Arena* arena,
|
||||
RangeDelAggregator* range_del_agg, SequenceNumber sequence,
|
||||
const ReadOptions& read_options, Arena* arena, SequenceNumber sequence,
|
||||
ColumnFamilyHandle* column_family = nullptr,
|
||||
bool allow_unprepared_value = false);
|
||||
|
||||
// Note: to support DB iterator refresh, memtable range tombstones in the
|
||||
// underlying merging iterator needs to be refreshed. If db_iter is not
|
||||
// nullptr, db_iter->SetMemtableRangetombstoneIter() is called with the
|
||||
// memtable range tombstone iterator used by the underlying merging iterator.
|
||||
// This range tombstone iterator can be refreshed later by db_iter.
|
||||
// @param read_options Must outlive the returned iterator.
|
||||
InternalIterator* NewInternalIterator(const ReadOptions& read_options,
|
||||
ColumnFamilyData* cfd,
|
||||
SuperVersion* super_version,
|
||||
Arena* arena, SequenceNumber sequence,
|
||||
bool allow_unprepared_value,
|
||||
ArenaWrappedDBIter* db_iter = nullptr);
|
||||
|
||||
LogsWithPrepTracker* logs_with_prep_tracker() {
|
||||
return &logs_with_prep_tracker_;
|
||||
}
|
||||
@@ -871,15 +884,6 @@ class DBImpl : public DB {
|
||||
|
||||
const WriteController& write_controller() { return write_controller_; }
|
||||
|
||||
// @param read_options Must outlive the returned iterator.
|
||||
InternalIterator* NewInternalIterator(const ReadOptions& read_options,
|
||||
ColumnFamilyData* cfd,
|
||||
SuperVersion* super_version,
|
||||
Arena* arena,
|
||||
RangeDelAggregator* range_del_agg,
|
||||
SequenceNumber sequence,
|
||||
bool allow_unprepared_value);
|
||||
|
||||
// hollow transactions shell used for recovery.
|
||||
// these will then be passed to TransactionDB so that
|
||||
// locks can be reacquired before writing can resume.
|
||||
@@ -1147,7 +1151,7 @@ class DBImpl : public DB {
|
||||
int TEST_BGCompactionsAllowed() const;
|
||||
int TEST_BGFlushesAllowed() const;
|
||||
size_t TEST_GetWalPreallocateBlockSize(uint64_t write_buffer_size) const;
|
||||
void TEST_WaitForPeridicWorkerRun(std::function<void()> callback) const;
|
||||
void TEST_WaitForPeridicTaskRun(std::function<void()> callback) const;
|
||||
SeqnoToTimeMapping TEST_GetSeqnoToTimeMapping() const;
|
||||
size_t TEST_EstimateInMemoryStatsHistorySize() const;
|
||||
|
||||
@@ -1162,7 +1166,7 @@ class DBImpl : public DB {
|
||||
}
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
PeriodicWorkTestScheduler* TEST_GetPeriodicWorkScheduler() const;
|
||||
const PeriodicTaskScheduler& TEST_GetPeriodicTaskScheduler() const;
|
||||
#endif // !ROCKSDB_LITE
|
||||
|
||||
#endif // NDEBUG
|
||||
@@ -1390,7 +1394,7 @@ class DBImpl : public DB {
|
||||
void NotifyOnExternalFileIngested(
|
||||
ColumnFamilyData* cfd, const ExternalSstFileIngestionJob& ingestion_job);
|
||||
|
||||
Status FlushForGetLiveFiles();
|
||||
virtual Status FlushForGetLiveFiles();
|
||||
#endif // !ROCKSDB_LITE
|
||||
|
||||
void NewThreadStatusCfInfo(ColumnFamilyData* cfd) const;
|
||||
@@ -2069,7 +2073,7 @@ class DBImpl : public DB {
|
||||
LogBuffer* log_buffer);
|
||||
|
||||
// Schedule background tasks
|
||||
Status StartPeriodicWorkScheduler();
|
||||
Status StartPeriodicTaskScheduler();
|
||||
|
||||
Status RegisterRecordSeqnoTimeWorker();
|
||||
|
||||
@@ -2175,11 +2179,6 @@ class DBImpl : public DB {
|
||||
IOStatus CreateWAL(uint64_t log_file_num, uint64_t recycle_log_number,
|
||||
size_t preallocate_block_size, log::Writer** new_log);
|
||||
|
||||
// Verify SST file unique id between Manifest and table properties to make
|
||||
// sure they're the same. Currently only used during DB open when
|
||||
// `verify_sst_unique_id_in_manifest = true`.
|
||||
Status VerifySstUniqueIdInManifest();
|
||||
|
||||
// Validate self-consistency of DB options
|
||||
static Status ValidateOptions(const DBOptions& db_options);
|
||||
// Validate self-consistency of DB options and its consistency with cf options
|
||||
@@ -2611,14 +2610,11 @@ class DBImpl : public DB {
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
// Scheduler to run DumpStats(), PersistStats(), and FlushInfoLog().
|
||||
// Currently, it always use a global instance from
|
||||
// PeriodicWorkScheduler::Default(). Only in unittest, it can be overrided by
|
||||
// PeriodicWorkTestScheduler.
|
||||
PeriodicWorkScheduler* periodic_work_scheduler_;
|
||||
// Currently, internally it has a global timer instance for running the tasks.
|
||||
PeriodicTaskScheduler periodic_task_scheduler_;
|
||||
|
||||
// Current cadence of the periodic worker for recording sequence number to
|
||||
// time.
|
||||
uint64_t record_seqno_time_cadence_ = 0;
|
||||
// It contains the implementations for each periodic task.
|
||||
std::map<PeriodicTaskType, const PeriodicTaskFunc> periodic_task_functions_;
|
||||
#endif
|
||||
|
||||
// When set, we use a separate queue for writes that don't write to memtable.
|
||||
|
||||
@@ -988,7 +988,7 @@ Status DBImpl::IncreaseFullHistoryTsLowImpl(ColumnFamilyData* cfd,
|
||||
}
|
||||
|
||||
Status s = versions_->LogAndApply(cfd, *cfd->GetLatestMutableCFOptions(),
|
||||
&edit, &mutex_);
|
||||
&edit, &mutex_, directories_.GetDbDir());
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
@@ -3756,9 +3756,11 @@ void DBImpl::InstallSuperVersionAndScheduleWork(
|
||||
// triggered soon anyway.
|
||||
bottommost_files_mark_threshold_ = kMaxSequenceNumber;
|
||||
for (auto* my_cfd : *versions_->GetColumnFamilySet()) {
|
||||
bottommost_files_mark_threshold_ = std::min(
|
||||
bottommost_files_mark_threshold_,
|
||||
my_cfd->current()->storage_info()->bottommost_files_mark_threshold());
|
||||
if (!my_cfd->ioptions()->allow_ingest_behind) {
|
||||
bottommost_files_mark_threshold_ = std::min(
|
||||
bottommost_files_mark_threshold_,
|
||||
my_cfd->current()->storage_info()->bottommost_files_mark_threshold());
|
||||
}
|
||||
}
|
||||
|
||||
// Whenever we install new SuperVersion, we might need to issue new flushes or
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
#include "db/column_family.h"
|
||||
#include "db/db_impl/db_impl.h"
|
||||
#include "db/error_handler.h"
|
||||
#include "db/periodic_work_scheduler.h"
|
||||
#include "db/periodic_task_scheduler.h"
|
||||
#include "monitoring/thread_status_updater.h"
|
||||
#include "util/cast_util.h"
|
||||
|
||||
@@ -302,16 +302,12 @@ size_t DBImpl::TEST_GetWalPreallocateBlockSize(
|
||||
}
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
void DBImpl::TEST_WaitForPeridicWorkerRun(
|
||||
std::function<void()> callback) const {
|
||||
if (periodic_work_scheduler_ != nullptr) {
|
||||
static_cast<PeriodicWorkTestScheduler*>(periodic_work_scheduler_)
|
||||
->TEST_WaitForRun(callback);
|
||||
}
|
||||
void DBImpl::TEST_WaitForPeridicTaskRun(std::function<void()> callback) const {
|
||||
periodic_task_scheduler_.TEST_WaitForRun(callback);
|
||||
}
|
||||
|
||||
PeriodicWorkTestScheduler* DBImpl::TEST_GetPeriodicWorkScheduler() const {
|
||||
return static_cast<PeriodicWorkTestScheduler*>(periodic_work_scheduler_);
|
||||
const PeriodicTaskScheduler& DBImpl::TEST_GetPeriodicTaskScheduler() const {
|
||||
return periodic_task_scheduler_;
|
||||
}
|
||||
|
||||
SeqnoToTimeMapping DBImpl::TEST_GetSeqnoToTimeMapping() const {
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
#include "db/builder.h"
|
||||
#include "db/db_impl/db_impl.h"
|
||||
#include "db/error_handler.h"
|
||||
#include "db/periodic_work_scheduler.h"
|
||||
#include "db/periodic_task_scheduler.h"
|
||||
#include "env/composite_env_wrapper.h"
|
||||
#include "file/filename.h"
|
||||
#include "file/read_write_util.h"
|
||||
@@ -525,12 +525,6 @@ Status DBImpl::Recover(
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
if (immutable_db_options_.verify_sst_unique_id_in_manifest) {
|
||||
s = VerifySstUniqueIdInManifest();
|
||||
if (!s.ok()) {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
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) {
|
||||
@@ -708,31 +702,6 @@ Status DBImpl::Recover(
|
||||
return s;
|
||||
}
|
||||
|
||||
Status DBImpl::VerifySstUniqueIdInManifest() {
|
||||
mutex_.AssertHeld();
|
||||
ROCKS_LOG_INFO(
|
||||
immutable_db_options_.info_log,
|
||||
"Verifying SST unique id between MANIFEST and SST file table properties");
|
||||
Status status;
|
||||
for (auto cfd : *versions_->GetColumnFamilySet()) {
|
||||
if (!cfd->IsDropped()) {
|
||||
auto version = cfd->current();
|
||||
version->Ref();
|
||||
mutex_.Unlock();
|
||||
status = version->VerifySstUniqueIds();
|
||||
mutex_.Lock();
|
||||
version->Unref();
|
||||
if (!status.ok()) {
|
||||
ROCKS_LOG_WARN(immutable_db_options_.info_log,
|
||||
"SST unique id mismatch in column family \"%s\": %s",
|
||||
cfd->GetName().c_str(), status.ToString().c_str());
|
||||
return status;
|
||||
}
|
||||
}
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
Status DBImpl::PersistentStatsProcessFormatVersion() {
|
||||
mutex_.AssertHeld();
|
||||
Status s;
|
||||
@@ -2105,7 +2074,7 @@ Status DBImpl::Open(const DBOptions& db_options, const std::string& dbname,
|
||||
"DB::Open() failed: %s", s.ToString().c_str());
|
||||
}
|
||||
if (s.ok()) {
|
||||
s = impl->StartPeriodicWorkScheduler();
|
||||
s = impl->StartPeriodicTaskScheduler();
|
||||
}
|
||||
|
||||
if (s.ok()) {
|
||||
|
||||
@@ -143,8 +143,7 @@ Iterator* DBImplReadOnly::NewIterator(const ReadOptions& read_options,
|
||||
super_version->version_number, read_callback);
|
||||
auto internal_iter = NewInternalIterator(
|
||||
db_iter->GetReadOptions(), cfd, super_version, db_iter->GetArena(),
|
||||
db_iter->GetRangeDelAggregator(), read_seq,
|
||||
/* allow_unprepared_value */ true);
|
||||
read_seq, /* allow_unprepared_value */ true, db_iter);
|
||||
db_iter->SetIterUnderDBIter(internal_iter);
|
||||
return db_iter;
|
||||
}
|
||||
@@ -194,9 +193,8 @@ Status DBImplReadOnly::NewIterators(
|
||||
sv->mutable_cf_options.max_sequential_skip_in_iterations,
|
||||
sv->version_number, read_callback);
|
||||
auto* internal_iter = NewInternalIterator(
|
||||
db_iter->GetReadOptions(), cfd, sv, db_iter->GetArena(),
|
||||
db_iter->GetRangeDelAggregator(), read_seq,
|
||||
/* allow_unprepared_value */ true);
|
||||
db_iter->GetReadOptions(), cfd, sv, db_iter->GetArena(), read_seq,
|
||||
/* allow_unprepared_value */ true, db_iter);
|
||||
db_iter->SetIterUnderDBIter(internal_iter);
|
||||
iterators->push_back(db_iter);
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
// TODO: Share common structure with CompactedDBImpl and DBImplSecondary
|
||||
class DBImplReadOnly : public DBImpl {
|
||||
public:
|
||||
DBImplReadOnly(const DBOptions& options, const std::string& dbname);
|
||||
@@ -141,6 +142,16 @@ class DBImplReadOnly : public DBImpl {
|
||||
return Status::NotSupported("Not supported operation in read only mode.");
|
||||
}
|
||||
|
||||
// FIXME: some missing overrides for more "write" functions
|
||||
|
||||
protected:
|
||||
#ifndef ROCKSDB_LITE
|
||||
Status FlushForGetLiveFiles() override {
|
||||
// No-op for read-only DB
|
||||
return Status::OK();
|
||||
}
|
||||
#endif // !ROCKSDB_LITE
|
||||
|
||||
private:
|
||||
// A "helper" function for DB::OpenForReadOnly without column families
|
||||
// to reduce unnecessary I/O
|
||||
|
||||
@@ -493,8 +493,7 @@ ArenaWrappedDBIter* DBImplSecondary::NewIteratorImpl(
|
||||
expose_blob_index, read_options.snapshot ? false : allow_refresh);
|
||||
auto internal_iter = NewInternalIterator(
|
||||
db_iter->GetReadOptions(), cfd, super_version, db_iter->GetArena(),
|
||||
db_iter->GetRangeDelAggregator(), snapshot,
|
||||
/* allow_unprepared_value */ true);
|
||||
snapshot, /* allow_unprepared_value */ true, db_iter);
|
||||
db_iter->SetIterUnderDBIter(internal_iter);
|
||||
return db_iter;
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ class LogReaderContainer {
|
||||
// The secondary instance can be opened using `DB::OpenAsSecondary`. After
|
||||
// that, it can call `DBImplSecondary::TryCatchUpWithPrimary` to make best
|
||||
// effort attempts to catch up with the primary.
|
||||
// TODO: Share common structure with CompactedDBImpl and DBImplReadOnly
|
||||
class DBImplSecondary : public DBImpl {
|
||||
public:
|
||||
DBImplSecondary(const DBOptions& options, const std::string& dbname,
|
||||
@@ -268,6 +269,13 @@ class DBImplSecondary : public DBImpl {
|
||||
#endif // NDEBUG
|
||||
|
||||
protected:
|
||||
#ifndef ROCKSDB_LITE
|
||||
Status FlushForGetLiveFiles() override {
|
||||
// No-op for read-only DB
|
||||
return Status::OK();
|
||||
}
|
||||
#endif // !ROCKSDB_LITE
|
||||
|
||||
// ColumnFamilyCollector is a write batch handler which does nothing
|
||||
// except recording unique column family IDs
|
||||
class ColumnFamilyCollector : public WriteBatch::Handler {
|
||||
|
||||
@@ -2163,7 +2163,8 @@ Status DBImpl::SwitchMemtable(ColumnFamilyData* cfd, WriteContext* context) {
|
||||
|
||||
VersionEdit wal_deletion;
|
||||
wal_deletion.DeleteWalsBefore(min_wal_number_to_keep);
|
||||
s = versions_->LogAndApplyToDefaultColumnFamily(&wal_deletion, &mutex_);
|
||||
s = versions_->LogAndApplyToDefaultColumnFamily(&wal_deletion, &mutex_,
|
||||
directories_.GetDbDir());
|
||||
if (!s.ok() && versions_->io_status().IsIOError()) {
|
||||
s = error_handler_.SetBGError(versions_->io_status(),
|
||||
BackgroundErrorReason::kManifestWrite);
|
||||
|
||||
+78
-139
@@ -76,9 +76,7 @@ DBIter::DBIter(Env* _env, const ReadOptions& read_options,
|
||||
verify_checksums_(read_options.verify_checksums),
|
||||
expose_blob_index_(expose_blob_index),
|
||||
is_blob_(false),
|
||||
is_wide_(false),
|
||||
arena_mode_(arena_mode),
|
||||
range_del_agg_(&ioptions.internal_comparator, s),
|
||||
db_impl_(db_impl),
|
||||
cfd_(cfd),
|
||||
timestamp_ub_(read_options.timestamp),
|
||||
@@ -91,6 +89,7 @@ DBIter::DBIter(Env* _env, const ReadOptions& read_options,
|
||||
if (iter_.iter()) {
|
||||
iter_.iter()->SetPinnedItersMgr(&pinned_iters_mgr_);
|
||||
}
|
||||
status_.PermitUncheckedError();
|
||||
assert(timestamp_size_ == user_comparator_.timestamp_size());
|
||||
}
|
||||
|
||||
@@ -135,7 +134,7 @@ void DBIter::Next() {
|
||||
// Release temporarily pinned blocks from last operation
|
||||
ReleaseTempPinnedData();
|
||||
ResetBlobValue();
|
||||
ResetWideColumnValue();
|
||||
ResetValueAndColumns();
|
||||
local_stats_.skip_count_ += num_internal_keys_skipped_;
|
||||
local_stats_.skip_count_--;
|
||||
num_internal_keys_skipped_ = 0;
|
||||
@@ -179,8 +178,6 @@ bool DBIter::SetBlobValueIfNeeded(const Slice& user_key,
|
||||
const Slice& blob_index) {
|
||||
assert(!is_blob_);
|
||||
assert(blob_value_.empty());
|
||||
assert(!is_wide_);
|
||||
assert(value_of_default_column_.empty());
|
||||
|
||||
if (expose_blob_index_) { // Stacked BlobDB implementation
|
||||
is_blob_ = true;
|
||||
@@ -216,16 +213,11 @@ bool DBIter::SetBlobValueIfNeeded(const Slice& user_key,
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DBIter::SetWideColumnValueIfNeeded(const Slice& wide_columns_slice) {
|
||||
assert(!is_blob_);
|
||||
assert(blob_value_.empty());
|
||||
assert(!is_wide_);
|
||||
assert(value_of_default_column_.empty());
|
||||
bool DBIter::SetValueAndColumnsFromEntity(Slice slice) {
|
||||
assert(value_.empty());
|
||||
assert(wide_columns_.empty());
|
||||
|
||||
Slice wide_columns_copy = wide_columns_slice;
|
||||
|
||||
const Status s = WideColumnSerialization::GetValueOfDefaultColumn(
|
||||
wide_columns_copy, value_of_default_column_);
|
||||
const Status s = WideColumnSerialization::Deserialize(slice, wide_columns_);
|
||||
|
||||
if (!s.ok()) {
|
||||
status_ = s;
|
||||
@@ -233,7 +225,11 @@ bool DBIter::SetWideColumnValueIfNeeded(const Slice& wide_columns_slice) {
|
||||
return false;
|
||||
}
|
||||
|
||||
is_wide_ = true;
|
||||
if (!wide_columns_.empty() &&
|
||||
wide_columns_[0].name() == kDefaultWideColumnName) {
|
||||
value_ = wide_columns_[0].value();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -283,11 +279,6 @@ bool DBIter::FindNextUserEntryInternal(bool skipping_saved_key,
|
||||
// to one.
|
||||
bool reseek_done = false;
|
||||
|
||||
assert(!is_blob_);
|
||||
assert(blob_value_.empty());
|
||||
assert(!is_wide_);
|
||||
assert(value_of_default_column_.empty());
|
||||
|
||||
do {
|
||||
// Will update is_key_seqnum_zero_ as soon as we parsed the current key
|
||||
// but we need to save the previous value to be used in the loop.
|
||||
@@ -377,66 +368,39 @@ bool DBIter::FindNextUserEntryInternal(bool skipping_saved_key,
|
||||
case kTypeWideColumnEntity:
|
||||
if (timestamp_lb_) {
|
||||
saved_key_.SetInternalKey(ikey_);
|
||||
|
||||
if (ikey_.type == kTypeBlobIndex) {
|
||||
if (!SetBlobValueIfNeeded(ikey_.user_key, iter_.value())) {
|
||||
return false;
|
||||
}
|
||||
} else if (ikey_.type == kTypeWideColumnEntity) {
|
||||
if (!SetWideColumnValueIfNeeded(iter_.value())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
valid_ = true;
|
||||
return true;
|
||||
} else {
|
||||
saved_key_.SetUserKey(
|
||||
ikey_.user_key, !pin_thru_lifetime_ ||
|
||||
!iter_.iter()->IsKeyPinned() /* copy */);
|
||||
if (range_del_agg_.ShouldDelete(
|
||||
ikey_, RangeDelPositioningMode::kForwardTraversal)) {
|
||||
// Arrange to skip all upcoming entries for this key since
|
||||
// they are hidden by this deletion.
|
||||
skipping_saved_key = true;
|
||||
num_skipped = 0;
|
||||
reseek_done = false;
|
||||
PERF_COUNTER_ADD(internal_delete_skipped_count, 1);
|
||||
} else {
|
||||
if (ikey_.type == kTypeBlobIndex) {
|
||||
if (!SetBlobValueIfNeeded(ikey_.user_key, iter_.value())) {
|
||||
return false;
|
||||
}
|
||||
} else if (ikey_.type == kTypeWideColumnEntity) {
|
||||
if (!SetWideColumnValueIfNeeded(iter_.value())) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
valid_ = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (ikey_.type == kTypeBlobIndex) {
|
||||
if (!SetBlobValueIfNeeded(ikey_.user_key, iter_.value())) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SetValueAndColumnsFromPlain(expose_blob_index_ ? iter_.value()
|
||||
: blob_value_);
|
||||
} else if (ikey_.type == kTypeWideColumnEntity) {
|
||||
if (!SetValueAndColumnsFromEntity(iter_.value())) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
assert(ikey_.type == kTypeValue);
|
||||
SetValueAndColumnsFromPlain(iter_.value());
|
||||
}
|
||||
|
||||
valid_ = true;
|
||||
return true;
|
||||
break;
|
||||
case kTypeMerge:
|
||||
saved_key_.SetUserKey(
|
||||
ikey_.user_key,
|
||||
!pin_thru_lifetime_ || !iter_.iter()->IsKeyPinned() /* copy */);
|
||||
if (range_del_agg_.ShouldDelete(
|
||||
ikey_, RangeDelPositioningMode::kForwardTraversal)) {
|
||||
// Arrange to skip all upcoming entries for this key since
|
||||
// they are hidden by this deletion.
|
||||
skipping_saved_key = true;
|
||||
num_skipped = 0;
|
||||
reseek_done = false;
|
||||
PERF_COUNTER_ADD(internal_delete_skipped_count, 1);
|
||||
} else {
|
||||
// By now, we are sure the current ikey is going to yield a
|
||||
// value
|
||||
current_entry_is_merged_ = true;
|
||||
valid_ = true;
|
||||
return MergeValuesNewToOld(); // Go to a different state machine
|
||||
}
|
||||
// By now, we are sure the current ikey is going to yield a value
|
||||
current_entry_is_merged_ = true;
|
||||
valid_ = true;
|
||||
return MergeValuesNewToOld(); // Go to a different state machine
|
||||
break;
|
||||
default:
|
||||
valid_ = false;
|
||||
@@ -562,9 +526,7 @@ bool DBIter::MergeValuesNewToOld() {
|
||||
// hit the next user key, stop right here
|
||||
break;
|
||||
}
|
||||
if (kTypeDeletion == ikey.type || kTypeSingleDeletion == ikey.type ||
|
||||
range_del_agg_.ShouldDelete(
|
||||
ikey, RangeDelPositioningMode::kForwardTraversal)) {
|
||||
if (kTypeDeletion == ikey.type || kTypeSingleDeletion == ikey.type) {
|
||||
// hit a delete with the same user key, stop right here
|
||||
// iter_ is positioned after delete
|
||||
iter_.Next();
|
||||
@@ -609,15 +571,12 @@ bool DBIter::MergeValuesNewToOld() {
|
||||
return false;
|
||||
}
|
||||
valid_ = true;
|
||||
const Slice blob_value = value();
|
||||
Status s = Merge(&blob_value, ikey.user_key);
|
||||
Status s = Merge(&blob_value_, ikey.user_key);
|
||||
if (!s.ok()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ResetBlobValue();
|
||||
assert(!is_wide_);
|
||||
assert(value_of_default_column_.empty());
|
||||
|
||||
// iter_ is positioned after put
|
||||
iter_.Next();
|
||||
@@ -665,7 +624,7 @@ void DBIter::Prev() {
|
||||
PERF_CPU_TIMER_GUARD(iter_prev_cpu_nanos, clock_);
|
||||
ReleaseTempPinnedData();
|
||||
ResetBlobValue();
|
||||
ResetWideColumnValue();
|
||||
ResetValueAndColumns();
|
||||
ResetInternalKeysSkippedCounter();
|
||||
bool ok = true;
|
||||
if (direction_ == kForward) {
|
||||
@@ -913,11 +872,7 @@ bool DBIter::FindValueForCurrentKey() {
|
||||
case kTypeValue:
|
||||
case kTypeBlobIndex:
|
||||
case kTypeWideColumnEntity:
|
||||
if (range_del_agg_.ShouldDelete(
|
||||
ikey, RangeDelPositioningMode::kBackwardTraversal)) {
|
||||
last_key_entry_type = kTypeRangeDeletion;
|
||||
PERF_COUNTER_ADD(internal_delete_skipped_count, 1);
|
||||
} else if (iter_.iter()->IsValuePinned()) {
|
||||
if (iter_.iter()->IsValuePinned()) {
|
||||
pinned_value_ = iter_.value();
|
||||
} else {
|
||||
valid_ = false;
|
||||
@@ -938,21 +893,12 @@ bool DBIter::FindValueForCurrentKey() {
|
||||
last_not_merge_type = last_key_entry_type;
|
||||
PERF_COUNTER_ADD(internal_delete_skipped_count, 1);
|
||||
break;
|
||||
case kTypeMerge:
|
||||
if (range_del_agg_.ShouldDelete(
|
||||
ikey, RangeDelPositioningMode::kBackwardTraversal)) {
|
||||
merge_context_.Clear();
|
||||
last_key_entry_type = kTypeRangeDeletion;
|
||||
last_not_merge_type = last_key_entry_type;
|
||||
PERF_COUNTER_ADD(internal_delete_skipped_count, 1);
|
||||
} else {
|
||||
assert(merge_operator_ != nullptr);
|
||||
merge_context_.PushOperandBack(
|
||||
iter_.value(),
|
||||
iter_.iter()->IsValuePinned() /* operand_pinned */);
|
||||
PERF_COUNTER_ADD(internal_merge_count, 1);
|
||||
}
|
||||
break;
|
||||
case kTypeMerge: {
|
||||
assert(merge_operator_ != nullptr);
|
||||
merge_context_.PushOperandBack(
|
||||
iter_.value(), iter_.iter()->IsValuePinned() /* operand_pinned */);
|
||||
PERF_COUNTER_ADD(internal_merge_count, 1);
|
||||
} break;
|
||||
default:
|
||||
valid_ = false;
|
||||
status_ = Status::Corruption(
|
||||
@@ -989,23 +935,16 @@ bool DBIter::FindValueForCurrentKey() {
|
||||
}
|
||||
|
||||
if (timestamp_lb_ != nullptr) {
|
||||
assert(last_key_entry_type == ikey_.type ||
|
||||
last_key_entry_type == kTypeRangeDeletion);
|
||||
assert(last_key_entry_type == ikey_.type);
|
||||
}
|
||||
|
||||
Status s;
|
||||
s.PermitUncheckedError();
|
||||
|
||||
assert(!is_blob_);
|
||||
assert(blob_value_.empty());
|
||||
assert(!is_wide_);
|
||||
assert(value_of_default_column_.empty());
|
||||
|
||||
switch (last_key_entry_type) {
|
||||
case kTypeDeletion:
|
||||
case kTypeDeletionWithTimestamp:
|
||||
case kTypeSingleDeletion:
|
||||
case kTypeRangeDeletion:
|
||||
if (timestamp_lb_ == nullptr) {
|
||||
valid_ = false;
|
||||
} else {
|
||||
@@ -1016,8 +955,7 @@ bool DBIter::FindValueForCurrentKey() {
|
||||
case kTypeMerge:
|
||||
current_entry_is_merged_ = true;
|
||||
if (last_not_merge_type == kTypeDeletion ||
|
||||
last_not_merge_type == kTypeSingleDeletion ||
|
||||
last_not_merge_type == kTypeRangeDeletion) {
|
||||
last_not_merge_type == kTypeSingleDeletion) {
|
||||
s = Merge(nullptr, saved_key_.GetUserKey());
|
||||
if (!s.ok()) {
|
||||
return false;
|
||||
@@ -1034,15 +972,12 @@ bool DBIter::FindValueForCurrentKey() {
|
||||
return false;
|
||||
}
|
||||
valid_ = true;
|
||||
const Slice blob_value = value();
|
||||
s = Merge(&blob_value, saved_key_.GetUserKey());
|
||||
s = Merge(&blob_value_, saved_key_.GetUserKey());
|
||||
if (!s.ok()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ResetBlobValue();
|
||||
assert(!is_wide_);
|
||||
assert(value_of_default_column_.empty());
|
||||
|
||||
return true;
|
||||
} else if (last_not_merge_type == kTypeWideColumnEntity) {
|
||||
@@ -1061,18 +996,24 @@ 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_);
|
||||
}
|
||||
|
||||
SetValueAndColumnsFromPlain(pinned_value_);
|
||||
|
||||
break;
|
||||
case kTypeBlobIndex:
|
||||
if (!SetBlobValueIfNeeded(saved_key_.GetUserKey(), pinned_value_)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
SetValueAndColumnsFromPlain(expose_blob_index_ ? pinned_value_
|
||||
: blob_value_);
|
||||
|
||||
break;
|
||||
case kTypeWideColumnEntity:
|
||||
if (!SetWideColumnValueIfNeeded(pinned_value_)) {
|
||||
if (!SetValueAndColumnsFromEntity(pinned_value_)) {
|
||||
return false;
|
||||
}
|
||||
break;
|
||||
@@ -1119,11 +1060,6 @@ bool DBIter::FindValueForCurrentKeyUsingSeek() {
|
||||
// Find the next value that's visible.
|
||||
ParsedInternalKey ikey;
|
||||
|
||||
assert(!is_blob_);
|
||||
assert(blob_value_.empty());
|
||||
assert(!is_wide_);
|
||||
assert(value_of_default_column_.empty());
|
||||
|
||||
while (true) {
|
||||
if (!iter_.Valid()) {
|
||||
valid_ = false;
|
||||
@@ -1157,8 +1093,6 @@ bool DBIter::FindValueForCurrentKeyUsingSeek() {
|
||||
}
|
||||
|
||||
if (ikey.type == kTypeDeletion || ikey.type == kTypeSingleDeletion ||
|
||||
range_del_agg_.ShouldDelete(
|
||||
ikey, RangeDelPositioningMode::kBackwardTraversal) ||
|
||||
kTypeDeletionWithTimestamp == ikey.type) {
|
||||
if (timestamp_lb_ == nullptr) {
|
||||
valid_ = false;
|
||||
@@ -1184,10 +1118,16 @@ bool DBIter::FindValueForCurrentKeyUsingSeek() {
|
||||
if (!SetBlobValueIfNeeded(ikey.user_key, pinned_value_)) {
|
||||
return false;
|
||||
}
|
||||
} else if (ikey_.type == kTypeWideColumnEntity) {
|
||||
if (!SetWideColumnValueIfNeeded(pinned_value_)) {
|
||||
|
||||
SetValueAndColumnsFromPlain(expose_blob_index_ ? pinned_value_
|
||||
: blob_value_);
|
||||
} else if (ikey.type == kTypeWideColumnEntity) {
|
||||
if (!SetValueAndColumnsFromEntity(pinned_value_)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
assert(ikey.type == kTypeValue);
|
||||
SetValueAndColumnsFromPlain(pinned_value_);
|
||||
}
|
||||
|
||||
if (timestamp_lb_ != nullptr) {
|
||||
@@ -1221,9 +1161,7 @@ bool DBIter::FindValueForCurrentKeyUsingSeek() {
|
||||
if (!user_comparator_.Equal(ikey.user_key, saved_key_.GetUserKey())) {
|
||||
break;
|
||||
}
|
||||
if (ikey.type == kTypeDeletion || ikey.type == kTypeSingleDeletion ||
|
||||
range_del_agg_.ShouldDelete(
|
||||
ikey, RangeDelPositioningMode::kForwardTraversal)) {
|
||||
if (ikey.type == kTypeDeletion || ikey.type == kTypeSingleDeletion) {
|
||||
break;
|
||||
}
|
||||
if (!iter_.PrepareValue()) {
|
||||
@@ -1253,15 +1191,12 @@ bool DBIter::FindValueForCurrentKeyUsingSeek() {
|
||||
return false;
|
||||
}
|
||||
valid_ = true;
|
||||
const Slice blob_value = value();
|
||||
Status s = Merge(&blob_value, saved_key_.GetUserKey());
|
||||
Status s = Merge(&blob_value_, saved_key_.GetUserKey());
|
||||
if (!s.ok()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
ResetBlobValue();
|
||||
assert(!is_wide_);
|
||||
assert(value_of_default_column_.empty());
|
||||
|
||||
return true;
|
||||
} else if (ikey.type == kTypeWideColumnEntity) {
|
||||
@@ -1312,6 +1247,10 @@ Status DBIter::Merge(const Slice* val, const Slice& user_key) {
|
||||
status_ = s;
|
||||
return s;
|
||||
}
|
||||
|
||||
SetValueAndColumnsFromPlain(pinned_value_.data() ? pinned_value_
|
||||
: saved_value_);
|
||||
|
||||
valid_ = true;
|
||||
return s;
|
||||
}
|
||||
@@ -1488,7 +1427,7 @@ void DBIter::Seek(const Slice& target) {
|
||||
status_ = Status::OK();
|
||||
ReleaseTempPinnedData();
|
||||
ResetBlobValue();
|
||||
ResetWideColumnValue();
|
||||
ResetValueAndColumns();
|
||||
ResetInternalKeysSkippedCounter();
|
||||
|
||||
// Seek the inner iterator based on the target key.
|
||||
@@ -1498,7 +1437,6 @@ void DBIter::Seek(const Slice& target) {
|
||||
SetSavedKeyToSeekTarget(target);
|
||||
iter_.Seek(saved_key_.GetInternalKey());
|
||||
|
||||
range_del_agg_.InvalidateRangeDelMapPositions();
|
||||
RecordTick(statistics_, NUMBER_DB_SEEK);
|
||||
}
|
||||
if (!iter_.Valid()) {
|
||||
@@ -1566,7 +1504,7 @@ void DBIter::SeekForPrev(const Slice& target) {
|
||||
status_ = Status::OK();
|
||||
ReleaseTempPinnedData();
|
||||
ResetBlobValue();
|
||||
ResetWideColumnValue();
|
||||
ResetValueAndColumns();
|
||||
ResetInternalKeysSkippedCounter();
|
||||
|
||||
// Seek the inner iterator based on the target key.
|
||||
@@ -1574,7 +1512,6 @@ void DBIter::SeekForPrev(const Slice& target) {
|
||||
PERF_TIMER_GUARD(seek_internal_seek_time);
|
||||
SetSavedKeyToSeekForPrevTarget(target);
|
||||
iter_.SeekForPrev(saved_key_.GetInternalKey());
|
||||
range_del_agg_.InvalidateRangeDelMapPositions();
|
||||
RecordTick(statistics_, NUMBER_DB_SEEK);
|
||||
}
|
||||
if (!iter_.Valid()) {
|
||||
@@ -1622,10 +1559,12 @@ void DBIter::SeekToFirst() {
|
||||
max_skip_ = std::numeric_limits<uint64_t>::max();
|
||||
}
|
||||
status_ = Status::OK();
|
||||
// if iterator is empty, this status_ could be unchecked.
|
||||
status_.PermitUncheckedError();
|
||||
direction_ = kForward;
|
||||
ReleaseTempPinnedData();
|
||||
ResetBlobValue();
|
||||
ResetWideColumnValue();
|
||||
ResetValueAndColumns();
|
||||
ResetInternalKeysSkippedCounter();
|
||||
ClearSavedValue();
|
||||
is_key_seqnum_zero_ = false;
|
||||
@@ -1633,7 +1572,6 @@ void DBIter::SeekToFirst() {
|
||||
{
|
||||
PERF_TIMER_GUARD(seek_internal_seek_time);
|
||||
iter_.SeekToFirst();
|
||||
range_del_agg_.InvalidateRangeDelMapPositions();
|
||||
}
|
||||
|
||||
RecordTick(statistics_, NUMBER_DB_SEEK);
|
||||
@@ -1674,7 +1612,7 @@ void DBIter::SeekToLast() {
|
||||
/*b_has_ts=*/false)) {
|
||||
ReleaseTempPinnedData();
|
||||
ResetBlobValue();
|
||||
ResetWideColumnValue();
|
||||
ResetValueAndColumns();
|
||||
PrevInternal(nullptr);
|
||||
|
||||
k = key();
|
||||
@@ -1692,10 +1630,12 @@ void DBIter::SeekToLast() {
|
||||
max_skip_ = std::numeric_limits<uint64_t>::max();
|
||||
}
|
||||
status_ = Status::OK();
|
||||
// if iterator is empty, this status_ could be unchecked.
|
||||
status_.PermitUncheckedError();
|
||||
direction_ = kReverse;
|
||||
ReleaseTempPinnedData();
|
||||
ResetBlobValue();
|
||||
ResetWideColumnValue();
|
||||
ResetValueAndColumns();
|
||||
ResetInternalKeysSkippedCounter();
|
||||
ClearSavedValue();
|
||||
is_key_seqnum_zero_ = false;
|
||||
@@ -1703,7 +1643,6 @@ void DBIter::SeekToLast() {
|
||||
{
|
||||
PERF_TIMER_GUARD(seek_internal_seek_time);
|
||||
iter_.SeekToLast();
|
||||
range_del_agg_.InvalidateRangeDelMapPositions();
|
||||
}
|
||||
PrevInternal(nullptr);
|
||||
if (statistics_ != nullptr) {
|
||||
|
||||
+25
-22
@@ -17,6 +17,7 @@
|
||||
#include "options/cf_options.h"
|
||||
#include "rocksdb/db.h"
|
||||
#include "rocksdb/iterator.h"
|
||||
#include "rocksdb/wide_columns.h"
|
||||
#include "table/iterator_wrapper.h"
|
||||
#include "util/autovector.h"
|
||||
|
||||
@@ -139,7 +140,6 @@ class DBIter final : public Iterator {
|
||||
iter_.Set(iter);
|
||||
iter_.iter()->SetPinnedItersMgr(&pinned_iters_mgr_);
|
||||
}
|
||||
ReadRangeDelAggregator* GetRangeDelAggregator() { return &range_del_agg_; }
|
||||
|
||||
bool Valid() const override {
|
||||
#ifdef ROCKSDB_ASSERT_STATUS_CHECKED
|
||||
@@ -160,22 +160,16 @@ class DBIter final : public Iterator {
|
||||
}
|
||||
Slice value() const override {
|
||||
assert(valid_);
|
||||
assert(!is_blob_ || !is_wide_);
|
||||
|
||||
if (!expose_blob_index_ && is_blob_) {
|
||||
return blob_value_;
|
||||
} else if (is_wide_) {
|
||||
return value_of_default_column_;
|
||||
} else if (current_entry_is_merged_) {
|
||||
// If pinned_value_ is set then the result of merge operator is one of
|
||||
// the merge operands and we should return it.
|
||||
return pinned_value_.data() ? pinned_value_ : saved_value_;
|
||||
} else if (direction_ == kReverse) {
|
||||
return pinned_value_;
|
||||
} else {
|
||||
return iter_.value();
|
||||
}
|
||||
return value_;
|
||||
}
|
||||
|
||||
const WideColumns& columns() const override {
|
||||
assert(valid_);
|
||||
|
||||
return wide_columns_;
|
||||
}
|
||||
|
||||
Status status() const override {
|
||||
if (status_.ok()) {
|
||||
return iter_.status();
|
||||
@@ -308,11 +302,19 @@ class DBIter final : public Iterator {
|
||||
blob_value_.Reset();
|
||||
}
|
||||
|
||||
bool SetWideColumnValueIfNeeded(const Slice& wide_columns_slice);
|
||||
void SetValueAndColumnsFromPlain(const Slice& slice) {
|
||||
assert(value_.empty());
|
||||
assert(wide_columns_.empty());
|
||||
|
||||
void ResetWideColumnValue() {
|
||||
is_wide_ = false;
|
||||
value_of_default_column_.clear();
|
||||
value_ = slice;
|
||||
wide_columns_.emplace_back(kDefaultWideColumnName, slice);
|
||||
}
|
||||
|
||||
bool SetValueAndColumnsFromEntity(Slice slice);
|
||||
|
||||
void ResetValueAndColumns() {
|
||||
value_.clear();
|
||||
wide_columns_.clear();
|
||||
}
|
||||
|
||||
Status Merge(const Slice* val, const Slice& user_key);
|
||||
@@ -339,7 +341,10 @@ class DBIter final : public Iterator {
|
||||
Slice pinned_value_;
|
||||
// for prefix seek mode to support prev()
|
||||
PinnableSlice blob_value_;
|
||||
Slice value_of_default_column_;
|
||||
// Value of the default column
|
||||
Slice value_;
|
||||
// All columns (i.e. name-value pairs)
|
||||
WideColumns wide_columns_;
|
||||
Statistics* statistics_;
|
||||
uint64_t max_skip_;
|
||||
uint64_t max_skippable_internal_keys_;
|
||||
@@ -376,11 +381,9 @@ class DBIter final : public Iterator {
|
||||
// the stacked BlobDB implementation is used, false otherwise.
|
||||
bool expose_blob_index_;
|
||||
bool is_blob_;
|
||||
bool is_wide_;
|
||||
bool arena_mode_;
|
||||
// List of operands for merge operator.
|
||||
MergeContext merge_context_;
|
||||
ReadRangeDelAggregator range_del_agg_;
|
||||
LocalStatistics local_stats_;
|
||||
PinnedIteratorsManager pinned_iters_mgr_;
|
||||
#ifdef ROCKSDB_LITE
|
||||
|
||||
@@ -3232,6 +3232,28 @@ TEST_F(DBIteratorTest, BackwardIterationOnInplaceUpdateMemtable) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBIteratorTest, IteratorRefreshReturnSV) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(
|
||||
db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), "a", "z"));
|
||||
std::unique_ptr<Iterator> iter{db_->NewIterator(ReadOptions())};
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"ArenaWrappedDBIter::Refresh:SV", [&](void*) {
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "dummy", "new SV"));
|
||||
// This makes the local SV obselete.
|
||||
ASSERT_OK(Flush());
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
ASSERT_OK(iter->Refresh());
|
||||
iter.reset();
|
||||
// iter used to not cleanup SV, so the Close() below would hit an assertion
|
||||
// error.
|
||||
Close();
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#include "db/db_test_util.h"
|
||||
#include "db/version_set.h"
|
||||
#include "port/stack_trace.h"
|
||||
#include "rocksdb/utilities/write_batch_with_index.h"
|
||||
#include "test_util/testutil.h"
|
||||
@@ -1756,6 +1757,993 @@ TEST_F(DBRangeDelTest, IteratorRefresh) {
|
||||
}
|
||||
}
|
||||
|
||||
void VerifyIteratorReachesEnd(InternalIterator* iter) {
|
||||
ASSERT_TRUE(!iter->Valid() && iter->status().ok());
|
||||
}
|
||||
|
||||
void VerifyIteratorReachesEnd(Iterator* iter) {
|
||||
ASSERT_TRUE(!iter->Valid() && iter->status().ok());
|
||||
}
|
||||
|
||||
TEST_F(DBRangeDelTest, IteratorReseek) {
|
||||
// Range tombstone triggers reseek (seeking to a range tombstone end key) in
|
||||
// merging iterator. Test set up:
|
||||
// one memtable: range tombstone [0, 1)
|
||||
// one immutable memtable: range tombstone [1, 2)
|
||||
// one L0 file with range tombstone [2, 3)
|
||||
// one L1 file with range tombstone [3, 4)
|
||||
// Seek(0) should trigger cascading reseeks at all levels below memtable.
|
||||
// Seek(1) should trigger cascading reseeks at all levels below immutable
|
||||
// memtable. SeekToFirst and SeekToLast trigger no reseek.
|
||||
Options options = CurrentOptions();
|
||||
options.compression = kNoCompression;
|
||||
options.disable_auto_compactions = true;
|
||||
|
||||
DestroyAndReopen(options);
|
||||
// L1
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), Key(3),
|
||||
Key(4)));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
MoveFilesToLevel(1);
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(1));
|
||||
// L0
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), Key(2),
|
||||
Key(3)));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(0));
|
||||
// Immutable memtable
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), Key(1),
|
||||
Key(2)));
|
||||
ASSERT_OK(static_cast_with_check<DBImpl>(db_)->TEST_SwitchMemtable());
|
||||
std::string value;
|
||||
ASSERT_TRUE(dbfull()->GetProperty(db_->DefaultColumnFamily(),
|
||||
"rocksdb.num-immutable-mem-table", &value));
|
||||
ASSERT_EQ(1, std::stoi(value));
|
||||
// live memtable
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), Key(0),
|
||||
Key(1)));
|
||||
// this memtable is still active
|
||||
ASSERT_TRUE(dbfull()->GetProperty(db_->DefaultColumnFamily(),
|
||||
"rocksdb.num-immutable-mem-table", &value));
|
||||
ASSERT_EQ(1, std::stoi(value));
|
||||
|
||||
auto iter = db_->NewIterator(ReadOptions());
|
||||
get_perf_context()->Reset();
|
||||
iter->Seek(Key(0));
|
||||
// Reseeked immutable memtable, L0 and L1
|
||||
ASSERT_EQ(get_perf_context()->internal_range_del_reseek_count, 3);
|
||||
VerifyIteratorReachesEnd(iter);
|
||||
get_perf_context()->Reset();
|
||||
iter->SeekForPrev(Key(1));
|
||||
// Reseeked L0 and L1
|
||||
ASSERT_EQ(get_perf_context()->internal_range_del_reseek_count, 2);
|
||||
VerifyIteratorReachesEnd(iter);
|
||||
get_perf_context()->Reset();
|
||||
iter->SeekToFirst();
|
||||
ASSERT_EQ(get_perf_context()->internal_range_del_reseek_count, 0);
|
||||
VerifyIteratorReachesEnd(iter);
|
||||
iter->SeekToLast();
|
||||
ASSERT_EQ(get_perf_context()->internal_range_del_reseek_count, 0);
|
||||
VerifyIteratorReachesEnd(iter);
|
||||
delete iter;
|
||||
}
|
||||
|
||||
TEST_F(DBRangeDelTest, ReseekDuringNextAndPrev) {
|
||||
// Range tombstone triggers reseek during Next()/Prev() in merging iterator.
|
||||
// Test set up:
|
||||
// memtable has: [0, 1) [2, 3)
|
||||
// L0 has: 2
|
||||
// L1 has: 1, 2, 3
|
||||
// Seek(0) will reseek to 1 for L0 and L1. Seek(1) will not trigger any
|
||||
// reseek. Then Next() determines 2 is covered by [2, 3), it will try to
|
||||
// reseek to 3 for L0 and L1. Similar story for Prev() and SeekForPrev() is
|
||||
// tested.
|
||||
Options options = CurrentOptions();
|
||||
options.compression = kNoCompression;
|
||||
options.disable_auto_compactions = true;
|
||||
|
||||
DestroyAndReopen(options);
|
||||
// L1
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(1), "foo"));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(2), "foo"));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(3), "foo"));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
MoveFilesToLevel(1);
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(1));
|
||||
|
||||
// L0
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(2), "foo"));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(0));
|
||||
|
||||
// Memtable
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), Key(0),
|
||||
Key(1)));
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), Key(2),
|
||||
Key(3)));
|
||||
|
||||
auto iter = db_->NewIterator(ReadOptions());
|
||||
auto iter_test_forward = [&] {
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(iter->key().ToString(), Key(1));
|
||||
|
||||
get_perf_context()->Reset();
|
||||
iter->Next();
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(iter->key().ToString(), Key(3));
|
||||
// Reseeked L0 and L1
|
||||
ASSERT_EQ(get_perf_context()->internal_range_del_reseek_count, 2);
|
||||
|
||||
// Next to Prev
|
||||
get_perf_context()->Reset();
|
||||
iter->Prev();
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(iter->key().ToString(), Key(1));
|
||||
// Reseeked L0 and L1
|
||||
ASSERT_EQ(get_perf_context()->internal_range_del_reseek_count, 2);
|
||||
|
||||
// Prev to Next
|
||||
get_perf_context()->Reset();
|
||||
iter->Next();
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(iter->key().ToString(), Key(3));
|
||||
// Reseeked L0 and L1
|
||||
ASSERT_EQ(get_perf_context()->internal_range_del_reseek_count, 2);
|
||||
|
||||
iter->Next();
|
||||
VerifyIteratorReachesEnd(iter);
|
||||
};
|
||||
|
||||
get_perf_context()->Reset();
|
||||
iter->Seek(Key(0));
|
||||
// Reseeked L0 and L1
|
||||
ASSERT_EQ(get_perf_context()->internal_range_del_reseek_count, 2);
|
||||
iter_test_forward();
|
||||
get_perf_context()->Reset();
|
||||
iter->Seek(Key(1));
|
||||
ASSERT_EQ(get_perf_context()->internal_range_del_reseek_count, 0);
|
||||
iter_test_forward();
|
||||
|
||||
get_perf_context()->Reset();
|
||||
iter->SeekForPrev(Key(2));
|
||||
// Reseeked L0 and L1
|
||||
ASSERT_EQ(get_perf_context()->internal_range_del_reseek_count, 2);
|
||||
iter_test_forward();
|
||||
get_perf_context()->Reset();
|
||||
iter->SeekForPrev(Key(1));
|
||||
ASSERT_EQ(get_perf_context()->internal_range_del_reseek_count, 0);
|
||||
iter_test_forward();
|
||||
|
||||
get_perf_context()->Reset();
|
||||
iter->SeekToFirst();
|
||||
ASSERT_EQ(get_perf_context()->internal_range_del_reseek_count, 0);
|
||||
iter_test_forward();
|
||||
|
||||
iter->SeekToLast();
|
||||
iter->Prev();
|
||||
iter_test_forward();
|
||||
delete iter;
|
||||
}
|
||||
|
||||
TEST_F(DBRangeDelTest, TombstoneFromCurrentLevel) {
|
||||
// Range tombstone triggers reseek when covering key from the same level.
|
||||
// in merging iterator. Test set up:
|
||||
// memtable has: [0, 1)
|
||||
// L0 has: [2, 3), 2
|
||||
// L1 has: 1, 2, 3
|
||||
// Seek(0) will reseek to 1 for L0 and L1.
|
||||
// Then Next() will reseek to 3 for L1 since 2 in L0 is covered by [2, 3) in
|
||||
// L0.
|
||||
Options options = CurrentOptions();
|
||||
options.compression = kNoCompression;
|
||||
options.disable_auto_compactions = true;
|
||||
|
||||
DestroyAndReopen(options);
|
||||
// L1
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(1), "foo"));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(2), "foo"));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(3), "foo"));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
MoveFilesToLevel(1);
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(1));
|
||||
|
||||
// L0
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(2), "foo"));
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), Key(2),
|
||||
Key(3)));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(0));
|
||||
|
||||
// Memtable
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), Key(0),
|
||||
Key(1)));
|
||||
|
||||
auto iter = db_->NewIterator(ReadOptions());
|
||||
get_perf_context()->Reset();
|
||||
iter->Seek(Key(0));
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(iter->key().ToString(), Key(1));
|
||||
ASSERT_EQ(get_perf_context()->internal_range_del_reseek_count, 2);
|
||||
|
||||
get_perf_context()->Reset();
|
||||
iter->Next();
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(iter->key().ToString(), Key(3));
|
||||
ASSERT_EQ(get_perf_context()->internal_range_del_reseek_count, 1);
|
||||
|
||||
delete iter;
|
||||
}
|
||||
|
||||
TEST_F(DBRangeDelTest, TombstoneAcrossFileBoundary) {
|
||||
// Verify that a range tombstone across file boundary covers keys from older
|
||||
// levels. Test set up:
|
||||
// L1_0: 1, 3, [2, 6) L1_1: 5, 7, [2, 6) ([2, 6) is from compaction with
|
||||
// L1_0) L2 has: 5
|
||||
// Seek(1) and then Next() should move the L1 level iterator to
|
||||
// L1_1. Check if 5 is returned after Next().
|
||||
Options options = CurrentOptions();
|
||||
options.compression = kNoCompression;
|
||||
options.disable_auto_compactions = true;
|
||||
options.target_file_size_base = 2 * 1024;
|
||||
options.max_compaction_bytes = 2 * 1024;
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
Random rnd(301);
|
||||
// L2
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(5), rnd.RandomString(1 << 10)));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
MoveFilesToLevel(2);
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(2));
|
||||
|
||||
// L1_1
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(5), rnd.RandomString(1 << 10)));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(7), rnd.RandomString(1 << 10)));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(0));
|
||||
|
||||
// L1_0
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(1), rnd.RandomString(1 << 10)));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(3), rnd.RandomString(1 << 10)));
|
||||
// Prevent keys being compacted away
|
||||
const Snapshot* snapshot = db_->GetSnapshot();
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), Key(2),
|
||||
Key(6)));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
ASSERT_EQ(2, NumTableFilesAtLevel(0));
|
||||
MoveFilesToLevel(1);
|
||||
ASSERT_EQ(2, NumTableFilesAtLevel(1));
|
||||
|
||||
auto iter = db_->NewIterator(ReadOptions());
|
||||
get_perf_context()->Reset();
|
||||
iter->Seek(Key(1));
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(iter->key().ToString(), Key(1));
|
||||
iter->Next();
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(iter->key().ToString(), Key(7));
|
||||
// 1 reseek into L2 when key 5 in L2 is covered by [2, 6) from L1
|
||||
ASSERT_EQ(get_perf_context()->internal_range_del_reseek_count, 1);
|
||||
|
||||
delete iter;
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
}
|
||||
|
||||
TEST_F(DBRangeDelTest, NonOverlappingTombstonAtBoundary) {
|
||||
// Verify that a range tombstone across file boundary covers keys from older
|
||||
// levels.
|
||||
// Test set up:
|
||||
// L1_0: 1, 3, [4, 7) L1_1: 6, 8, [4, 7)
|
||||
// L2: 5
|
||||
// Note that [4, 7) is at end of L1_0 and not overlapping with any point key
|
||||
// in L1_0. [4, 7) from L1_0 should cover 5 is sentinel works
|
||||
Options options = CurrentOptions();
|
||||
options.compression = kNoCompression;
|
||||
options.disable_auto_compactions = true;
|
||||
options.target_file_size_base = 2 * 1024;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
Random rnd(301);
|
||||
// L2
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(5), rnd.RandomString(4 << 10)));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
MoveFilesToLevel(2);
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(2));
|
||||
|
||||
// L1_1
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(6), rnd.RandomString(4 << 10)));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(8), rnd.RandomString(4 << 10)));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(0));
|
||||
|
||||
// L1_0
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(1), rnd.RandomString(4 << 10)));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(3), rnd.RandomString(4 << 10)));
|
||||
// Prevent keys being compacted away
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), Key(4),
|
||||
Key(7)));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
ASSERT_EQ(2, NumTableFilesAtLevel(0));
|
||||
MoveFilesToLevel(1);
|
||||
ASSERT_EQ(2, NumTableFilesAtLevel(1));
|
||||
|
||||
auto iter = db_->NewIterator(ReadOptions());
|
||||
iter->Seek(Key(3));
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(iter->key(), Key(3));
|
||||
get_perf_context()->Reset();
|
||||
iter->Next();
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(iter->key().ToString(), Key(8));
|
||||
// 1 reseek into L1 since 5 from L2 is covered by [4, 7) from L1
|
||||
ASSERT_EQ(get_perf_context()->internal_range_del_reseek_count, 1);
|
||||
for (auto& k : {4, 5, 6}) {
|
||||
get_perf_context()->Reset();
|
||||
iter->Seek(Key(k));
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(iter->key().ToString(), Key(8));
|
||||
// 1 reseek into L1
|
||||
ASSERT_EQ(get_perf_context()->internal_range_del_reseek_count, 1);
|
||||
}
|
||||
delete iter;
|
||||
}
|
||||
|
||||
TEST_F(DBRangeDelTest, OlderLevelHasNewerData) {
|
||||
// L1_0: 1, 3, [2, 7) L1_1: 5, 6 at a newer sequence number than [2, 7)
|
||||
// Compact L1_1 to L2. Seek(3) should not skip 5 or 6.
|
||||
Options options = CurrentOptions();
|
||||
options.compression = kNoCompression;
|
||||
options.disable_auto_compactions = true;
|
||||
options.target_file_size_base = 3 * 1024;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
Random rnd(301);
|
||||
// L1_0
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(1), rnd.RandomString(4 << 10)));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(3), rnd.RandomString(4 << 10)));
|
||||
const Snapshot* snapshot = db_->GetSnapshot();
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), Key(2),
|
||||
Key(7)));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
MoveFilesToLevel(1);
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(1));
|
||||
|
||||
// L1_1
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(5), rnd.RandomString(4 << 10)));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(6), rnd.RandomString(4 << 10)));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(0));
|
||||
MoveFilesToLevel(1);
|
||||
ASSERT_EQ(2, NumTableFilesAtLevel(1));
|
||||
|
||||
auto key = Key(6);
|
||||
Slice begin(key);
|
||||
EXPECT_OK(dbfull()->TEST_CompactRange(1, &begin, nullptr));
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(1));
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(2));
|
||||
|
||||
auto iter = db_->NewIterator(ReadOptions());
|
||||
iter->Seek(Key(3));
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(iter->key().ToString(), Key(5));
|
||||
iter->Next();
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(iter->key().ToString(), Key(6));
|
||||
delete iter;
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
}
|
||||
|
||||
TEST_F(DBRangeDelTest, LevelBoundaryDefinedByTombstone) {
|
||||
// L1 has: 1, 2, [4, 5)
|
||||
// L2 has: 4
|
||||
// Seek(3), which is over all points keys in L1, check whether
|
||||
// sentinel key from L1 works in this case.
|
||||
Options options = CurrentOptions();
|
||||
options.compression = kNoCompression;
|
||||
options.disable_auto_compactions = true;
|
||||
options.target_file_size_base = 3 * 1024;
|
||||
DestroyAndReopen(options);
|
||||
Random rnd(301);
|
||||
// L2
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(4), "foo"));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
const Snapshot* snapshot = db_->GetSnapshot();
|
||||
MoveFilesToLevel(2);
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(2));
|
||||
|
||||
// L1_0
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(1), rnd.RandomString(4 << 10)));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(2), rnd.RandomString(4 << 10)));
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), Key(4),
|
||||
Key(5)));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
MoveFilesToLevel(1);
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(1));
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(2));
|
||||
|
||||
auto iter = db_->NewIterator(ReadOptions());
|
||||
iter->Seek(Key(3));
|
||||
ASSERT_TRUE(!iter->Valid());
|
||||
ASSERT_OK(iter->status());
|
||||
|
||||
get_perf_context()->Reset();
|
||||
iter->SeekForPrev(Key(5));
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(iter->key(), Key(2));
|
||||
db_->ReleaseSnapshot(snapshot);
|
||||
delete iter;
|
||||
}
|
||||
|
||||
TEST_F(DBRangeDelTest, TombstoneOnlyFile) {
|
||||
// L1_0: 1, 2, L1_1: [3, 5)
|
||||
// L2: 3
|
||||
// Seek(2) then Next() should advance L1 iterator into L1_1.
|
||||
// If sentinel works with tombstone only file, it should cover the key in L2.
|
||||
// Similar story for SeekForPrev(4).
|
||||
Options options = CurrentOptions();
|
||||
options.compression = kNoCompression;
|
||||
options.disable_auto_compactions = true;
|
||||
options.target_file_size_base = 3 * 1024;
|
||||
|
||||
DestroyAndReopen(options);
|
||||
Random rnd(301);
|
||||
// L2
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(3), "foo"));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
MoveFilesToLevel(2);
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(2));
|
||||
|
||||
// L1_0
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(1), rnd.RandomString(4 << 10)));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(2), rnd.RandomString(4 << 10)));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
MoveFilesToLevel(1);
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(1));
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(2));
|
||||
|
||||
// L1_1
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), Key(3),
|
||||
Key(5)));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
MoveFilesToLevel(1);
|
||||
ASSERT_EQ(2, NumTableFilesAtLevel(1));
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(2));
|
||||
|
||||
auto iter = db_->NewIterator(ReadOptions());
|
||||
iter->Seek(Key(2));
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(iter->key(), Key(2));
|
||||
iter->Next();
|
||||
VerifyIteratorReachesEnd(iter);
|
||||
iter->SeekForPrev(Key(4));
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(iter->key(), Key(2));
|
||||
iter->Next();
|
||||
VerifyIteratorReachesEnd(iter);
|
||||
delete iter;
|
||||
}
|
||||
|
||||
void VerifyIteratorKey(InternalIterator* iter,
|
||||
const std::vector<std::string>& expected_keys,
|
||||
bool forward = true) {
|
||||
for (auto& key : expected_keys) {
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(iter->user_key(), key);
|
||||
if (forward) {
|
||||
iter->Next();
|
||||
} else {
|
||||
iter->Prev();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DBRangeDelTest, TombstoneOnlyLevel) {
|
||||
// L1 [3, 5)
|
||||
// L2 has: 3, 4
|
||||
// Any kind of iterator seek should skip 3 and 4 in L2.
|
||||
// L1 level iterator should produce sentinel key.
|
||||
Options options = CurrentOptions();
|
||||
options.compression = kNoCompression;
|
||||
options.disable_auto_compactions = true;
|
||||
options.target_file_size_base = 3 * 1024;
|
||||
|
||||
DestroyAndReopen(options);
|
||||
// L2
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(3), "foo"));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(4), "bar"));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
MoveFilesToLevel(2);
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(2));
|
||||
|
||||
// L1
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), Key(3),
|
||||
Key(5)));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
MoveFilesToLevel(1);
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(1));
|
||||
|
||||
auto iter = db_->NewIterator(ReadOptions());
|
||||
get_perf_context()->Reset();
|
||||
uint64_t expected_reseek = 0;
|
||||
for (auto i = 0; i < 7; ++i) {
|
||||
iter->Seek(Key(i));
|
||||
VerifyIteratorReachesEnd(iter);
|
||||
if (i < 5) {
|
||||
++expected_reseek;
|
||||
}
|
||||
ASSERT_EQ(get_perf_context()->internal_range_del_reseek_count,
|
||||
expected_reseek);
|
||||
iter->SeekForPrev(Key(i));
|
||||
VerifyIteratorReachesEnd(iter);
|
||||
if (i > 2) {
|
||||
++expected_reseek;
|
||||
}
|
||||
ASSERT_EQ(get_perf_context()->internal_range_del_reseek_count,
|
||||
expected_reseek);
|
||||
iter->SeekToFirst();
|
||||
VerifyIteratorReachesEnd(iter);
|
||||
ASSERT_EQ(get_perf_context()->internal_range_del_reseek_count,
|
||||
++expected_reseek);
|
||||
iter->SeekToLast();
|
||||
VerifyIteratorReachesEnd(iter);
|
||||
ASSERT_EQ(get_perf_context()->internal_range_del_reseek_count,
|
||||
++expected_reseek);
|
||||
}
|
||||
delete iter;
|
||||
|
||||
// Check L1 LevelIterator behavior
|
||||
ColumnFamilyData* cfd =
|
||||
static_cast_with_check<ColumnFamilyHandleImpl>(db_->DefaultColumnFamily())
|
||||
->cfd();
|
||||
SuperVersion* sv = cfd->GetSuperVersion();
|
||||
Arena arena;
|
||||
ReadOptions read_options;
|
||||
MergeIteratorBuilder merge_iter_builder(&cfd->internal_comparator(), &arena,
|
||||
false /* prefix seek */);
|
||||
InternalIterator* level_iter = sv->current->TEST_GetLevelIterator(
|
||||
read_options, &merge_iter_builder, 1 /* level */, true);
|
||||
// This is needed to make LevelIterator range tombstone aware
|
||||
auto miter = merge_iter_builder.Finish();
|
||||
auto k = Key(3);
|
||||
IterKey target;
|
||||
target.SetInternalKey(k, kMaxSequenceNumber, kValueTypeForSeek);
|
||||
level_iter->Seek(target.GetInternalKey());
|
||||
// sentinel key (file boundary as a fake key)
|
||||
VerifyIteratorKey(level_iter, {Key(5)});
|
||||
VerifyIteratorReachesEnd(level_iter);
|
||||
|
||||
k = Key(5);
|
||||
target.SetInternalKey(k, 0, kValueTypeForSeekForPrev);
|
||||
level_iter->SeekForPrev(target.GetInternalKey());
|
||||
VerifyIteratorKey(level_iter, {Key(3)}, false);
|
||||
VerifyIteratorReachesEnd(level_iter);
|
||||
|
||||
level_iter->SeekToFirst();
|
||||
VerifyIteratorKey(level_iter, {Key(5)});
|
||||
VerifyIteratorReachesEnd(level_iter);
|
||||
|
||||
level_iter->SeekToLast();
|
||||
VerifyIteratorKey(level_iter, {Key(3)}, false);
|
||||
VerifyIteratorReachesEnd(level_iter);
|
||||
|
||||
miter->~InternalIterator();
|
||||
}
|
||||
|
||||
TEST_F(DBRangeDelTest, TombstoneOnlyWithOlderVisibleKey) {
|
||||
// L1: [3, 5)
|
||||
// L2: 2, 4, 5
|
||||
// 2 and 5 should be visible
|
||||
Options options = CurrentOptions();
|
||||
options.compression = kNoCompression;
|
||||
options.disable_auto_compactions = true;
|
||||
options.target_file_size_base = 3 * 1024;
|
||||
|
||||
DestroyAndReopen(options);
|
||||
// L2
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(2), "foo"));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(4), "bar"));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(5), "foobar"));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
MoveFilesToLevel(2);
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(2));
|
||||
|
||||
// l1
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), Key(3),
|
||||
Key(5)));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
MoveFilesToLevel(1);
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(1));
|
||||
|
||||
auto iter = db_->NewIterator(ReadOptions());
|
||||
auto iter_test_backward = [&] {
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(iter->key(), Key(5));
|
||||
iter->Prev();
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(iter->key(), Key(2));
|
||||
iter->Prev();
|
||||
VerifyIteratorReachesEnd(iter);
|
||||
};
|
||||
auto iter_test_forward = [&] {
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(iter->key(), Key(2));
|
||||
iter->Next();
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(iter->key(), Key(5));
|
||||
iter->Next();
|
||||
VerifyIteratorReachesEnd(iter);
|
||||
};
|
||||
iter->Seek(Key(4));
|
||||
iter_test_backward();
|
||||
iter->SeekForPrev(Key(4));
|
||||
iter->Next();
|
||||
iter_test_backward();
|
||||
|
||||
iter->Seek(Key(4));
|
||||
iter->Prev();
|
||||
iter_test_forward();
|
||||
iter->SeekForPrev(Key(4));
|
||||
iter_test_forward();
|
||||
|
||||
iter->SeekToFirst();
|
||||
iter_test_forward();
|
||||
iter->SeekToLast();
|
||||
iter_test_backward();
|
||||
|
||||
delete iter;
|
||||
}
|
||||
|
||||
TEST_F(DBRangeDelTest, TombstoneSentinelDirectionChange) {
|
||||
// L1: 7
|
||||
// L2: [4, 6)
|
||||
// L3: 4
|
||||
// Seek(5) will have sentinel key 6 at the top of minHeap in merging iterator.
|
||||
// then do a prev, how would sentinel work?
|
||||
// Redo the test after Put(5) into L1 so that there is a visible key in range
|
||||
// [4, 6).
|
||||
Options options = CurrentOptions();
|
||||
options.compression = kNoCompression;
|
||||
options.disable_auto_compactions = true;
|
||||
options.target_file_size_base = 3 * 1024;
|
||||
|
||||
DestroyAndReopen(options);
|
||||
// L3
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(4), "bar"));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
MoveFilesToLevel(3);
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(3));
|
||||
// L2
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), Key(4),
|
||||
Key(6)));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
MoveFilesToLevel(2);
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(2));
|
||||
|
||||
// L1
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(7), "foobar"));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
MoveFilesToLevel(1);
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(1));
|
||||
|
||||
auto iter = db_->NewIterator(ReadOptions());
|
||||
iter->Seek(Key(5));
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(iter->key(), Key(7));
|
||||
iter->Prev();
|
||||
ASSERT_TRUE(!iter->Valid() && iter->status().ok());
|
||||
delete iter;
|
||||
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(5), "foobar"));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
MoveFilesToLevel(1);
|
||||
ASSERT_EQ(2, NumTableFilesAtLevel(1));
|
||||
|
||||
iter = db_->NewIterator(ReadOptions());
|
||||
iter->Seek(Key(5));
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(iter->key(), Key(5));
|
||||
iter->Prev();
|
||||
ASSERT_TRUE(!iter->Valid() && iter->status().ok());
|
||||
delete iter;
|
||||
}
|
||||
|
||||
// Right sentinel tested in many test cases above
|
||||
TEST_F(DBRangeDelTest, LeftSentinelKeyTest) {
|
||||
// L1_0: 0, 1 L1_1: [2, 3), 5
|
||||
// L2: 2
|
||||
// SeekForPrev(4) should give 1 due to sentinel key keeping [2, 3) alive.
|
||||
Options options = CurrentOptions();
|
||||
options.compression = kNoCompression;
|
||||
options.disable_auto_compactions = true;
|
||||
options.target_file_size_base = 3 * 1024;
|
||||
options.max_compaction_bytes = 1024;
|
||||
|
||||
DestroyAndReopen(options);
|
||||
// L2
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(2), "foo"));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
MoveFilesToLevel(2);
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(2));
|
||||
|
||||
// L1_0
|
||||
Random rnd(301);
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(0), rnd.RandomString(4 << 10)));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(1), rnd.RandomString(4 << 10)));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
MoveFilesToLevel(1);
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(1));
|
||||
|
||||
// L1_1
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(5), "bar"));
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), Key(2),
|
||||
Key(3)));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
MoveFilesToLevel(1);
|
||||
ASSERT_EQ(2, NumTableFilesAtLevel(1));
|
||||
|
||||
auto iter = db_->NewIterator(ReadOptions());
|
||||
iter->SeekForPrev(Key(4));
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(iter->key(), Key(1));
|
||||
iter->Prev();
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(iter->key(), Key(0));
|
||||
iter->Prev();
|
||||
ASSERT_TRUE(!iter->Valid());
|
||||
ASSERT_OK(iter->status());
|
||||
delete iter;
|
||||
}
|
||||
|
||||
TEST_F(DBRangeDelTest, LeftSentinelKeyTestWithNewerKey) {
|
||||
// L1_0: 1, 2 newer than L1_1, L1_1: [2, 4), 5
|
||||
// L2: 3
|
||||
// SeekForPrev(4) then Prev() should give 2 and then 1.
|
||||
Options options = CurrentOptions();
|
||||
options.compression = kNoCompression;
|
||||
options.disable_auto_compactions = true;
|
||||
options.target_file_size_base = 3 * 1024;
|
||||
options.max_compaction_bytes = 1024;
|
||||
|
||||
DestroyAndReopen(options);
|
||||
// L2
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(3), "foo"));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
MoveFilesToLevel(2);
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(2));
|
||||
|
||||
// L1_1
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(5), "bar"));
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), Key(2),
|
||||
Key(4)));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
MoveFilesToLevel(1);
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(1));
|
||||
|
||||
// L1_0
|
||||
Random rnd(301);
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(1), rnd.RandomString(4 << 10)));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(2), rnd.RandomString(4 << 10)));
|
||||
// Used to verify sequence number of iterator key later.
|
||||
auto seq = dbfull()->TEST_GetLastVisibleSequence();
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
MoveFilesToLevel(1);
|
||||
ASSERT_EQ(2, NumTableFilesAtLevel(1));
|
||||
|
||||
Arena arena;
|
||||
InternalKeyComparator icmp(options.comparator);
|
||||
ReadOptions read_options;
|
||||
ScopedArenaIterator iter;
|
||||
iter.set(
|
||||
dbfull()->NewInternalIterator(read_options, &arena, kMaxSequenceNumber));
|
||||
|
||||
auto k = Key(4);
|
||||
IterKey target;
|
||||
target.SetInternalKey(k, 0 /* sequence_number */, kValueTypeForSeekForPrev);
|
||||
iter->SeekForPrev(target.GetInternalKey());
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(iter->user_key(), Key(2));
|
||||
SequenceNumber actual_seq;
|
||||
ValueType type;
|
||||
UnPackSequenceAndType(ExtractInternalKeyFooter(iter->key()), &actual_seq,
|
||||
&type);
|
||||
ASSERT_EQ(seq, actual_seq);
|
||||
// might as well check type
|
||||
ASSERT_EQ(type, kTypeValue);
|
||||
|
||||
iter->Prev();
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(iter->user_key(), Key(1));
|
||||
iter->Prev();
|
||||
ASSERT_TRUE(!iter->Valid());
|
||||
ASSERT_OK(iter->status());
|
||||
}
|
||||
|
||||
TEST_F(DBRangeDelTest, SentinelKeyCommonCaseTest) {
|
||||
// L1 has 3 files
|
||||
// L1_0: 1, 2 L1_1: [3, 4) 5, 6, [7, 8) L1_2: 9
|
||||
// Check iterator operations on LevelIterator.
|
||||
Options options = CurrentOptions();
|
||||
options.compression = kNoCompression;
|
||||
options.disable_auto_compactions = true;
|
||||
options.target_file_size_base = 3 * 1024;
|
||||
|
||||
DestroyAndReopen(options);
|
||||
Random rnd(301);
|
||||
// L1_0
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(1), rnd.RandomString(4 << 10)));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(2), rnd.RandomString(4 << 10)));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
MoveFilesToLevel(1);
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(1));
|
||||
|
||||
// L1_1
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), Key(3),
|
||||
Key(4)));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(5), rnd.RandomString(4 << 10)));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(6), rnd.RandomString(4 << 10)));
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), Key(7),
|
||||
Key(8)));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
MoveFilesToLevel(1);
|
||||
ASSERT_EQ(2, NumTableFilesAtLevel(1));
|
||||
|
||||
// L1_2
|
||||
ASSERT_OK(db_->Put(WriteOptions(), Key(9), rnd.RandomString(4 << 10)));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
MoveFilesToLevel(1);
|
||||
ASSERT_EQ(3, NumTableFilesAtLevel(1));
|
||||
|
||||
ColumnFamilyData* cfd =
|
||||
static_cast_with_check<ColumnFamilyHandleImpl>(db_->DefaultColumnFamily())
|
||||
->cfd();
|
||||
SuperVersion* sv = cfd->GetSuperVersion();
|
||||
Arena arena;
|
||||
ReadOptions read_options;
|
||||
MergeIteratorBuilder merge_iter_builder(&cfd->internal_comparator(), &arena,
|
||||
false /* prefix seek */);
|
||||
InternalIterator* level_iter = sv->current->TEST_GetLevelIterator(
|
||||
read_options, &merge_iter_builder, 1 /* level */, true);
|
||||
// This is needed to make LevelIterator range tombstone aware
|
||||
auto miter = merge_iter_builder.Finish();
|
||||
auto k = Key(7);
|
||||
IterKey target;
|
||||
target.SetInternalKey(k, kMaxSequenceNumber, kValueTypeForSeek);
|
||||
level_iter->Seek(target.GetInternalKey());
|
||||
// The last Key(9) is a sentinel key.
|
||||
VerifyIteratorKey(level_iter, {Key(8), Key(9), Key(9)});
|
||||
ASSERT_TRUE(!level_iter->Valid() && level_iter->status().ok());
|
||||
|
||||
k = Key(6);
|
||||
target.SetInternalKey(k, kMaxSequenceNumber, kValueTypeForSeek);
|
||||
level_iter->Seek(target.GetInternalKey());
|
||||
VerifyIteratorKey(level_iter, {Key(6), Key(8), Key(9), Key(9)});
|
||||
ASSERT_TRUE(!level_iter->Valid() && level_iter->status().ok());
|
||||
|
||||
k = Key(4);
|
||||
target.SetInternalKey(k, 0, kValueTypeForSeekForPrev);
|
||||
level_iter->SeekForPrev(target.GetInternalKey());
|
||||
VerifyIteratorKey(level_iter, {Key(3), Key(2), Key(1), Key(1)}, false);
|
||||
ASSERT_TRUE(!level_iter->Valid() && level_iter->status().ok());
|
||||
|
||||
k = Key(5);
|
||||
target.SetInternalKey(k, 0, kValueTypeForSeekForPrev);
|
||||
level_iter->SeekForPrev(target.GetInternalKey());
|
||||
VerifyIteratorKey(level_iter, {Key(5), Key(3), Key(2), Key(1), Key(1)},
|
||||
false);
|
||||
|
||||
level_iter->SeekToFirst();
|
||||
VerifyIteratorKey(level_iter, {Key(1), Key(2), Key(2), Key(5), Key(6), Key(8),
|
||||
Key(9), Key(9)});
|
||||
ASSERT_TRUE(!level_iter->Valid() && level_iter->status().ok());
|
||||
|
||||
level_iter->SeekToLast();
|
||||
VerifyIteratorKey(
|
||||
level_iter,
|
||||
{Key(9), Key(9), Key(6), Key(5), Key(3), Key(2), Key(1), Key(1)}, false);
|
||||
ASSERT_TRUE(!level_iter->Valid() && level_iter->status().ok());
|
||||
|
||||
miter->~InternalIterator();
|
||||
}
|
||||
|
||||
TEST_F(DBRangeDelTest, PrefixSentinelKey) {
|
||||
// L1: ['aaaa', 'aaad'), 'bbbb'
|
||||
// L2: 'aaac', 'aaae'
|
||||
// Prefix extracts first 3 chars
|
||||
// Seek('aaab') should give 'aaae' as first key.
|
||||
// This is to test a previous bug where prefix seek sees there is no prefix in
|
||||
// the SST file, and will just set file iter to null in LevelIterator and may
|
||||
// just skip to the next SST file. But in this case, we should keep the file's
|
||||
// tombstone alive.
|
||||
Options options = CurrentOptions();
|
||||
options.compression = kNoCompression;
|
||||
options.disable_auto_compactions = true;
|
||||
options.prefix_extractor.reset(NewFixedPrefixTransform(3));
|
||||
BlockBasedTableOptions table_options;
|
||||
table_options.filter_policy.reset(NewBloomFilterPolicy(10, false));
|
||||
table_options.whole_key_filtering = false;
|
||||
options.table_factory.reset(NewBlockBasedTableFactory(table_options));
|
||||
DestroyAndReopen(options);
|
||||
Random rnd(301);
|
||||
|
||||
// L2:
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "aaac", rnd.RandomString(10)));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "aaae", rnd.RandomString(10)));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
MoveFilesToLevel(2);
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(2));
|
||||
|
||||
// L1
|
||||
ASSERT_OK(db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), "aaaa",
|
||||
"aaad"));
|
||||
ASSERT_OK(db_->Put(WriteOptions(), "bbbb", rnd.RandomString(10)));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
MoveFilesToLevel(1);
|
||||
ASSERT_EQ(1, NumTableFilesAtLevel(1));
|
||||
|
||||
auto iter = db_->NewIterator(ReadOptions());
|
||||
iter->Seek("aaab");
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(iter->key(), "aaae");
|
||||
delete iter;
|
||||
}
|
||||
|
||||
TEST_F(DBRangeDelTest, RefreshMemtableIter) {
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
DestroyAndReopen(options);
|
||||
ASSERT_OK(
|
||||
db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), "a", "z"));
|
||||
ReadOptions ro;
|
||||
ro.read_tier = kMemtableTier;
|
||||
std::unique_ptr<Iterator> iter{db_->NewIterator(ro)};
|
||||
ASSERT_OK(Flush());
|
||||
// First refresh reinits iter, which had a bug where
|
||||
// iter.memtable_range_tombstone_iter_ was not set to nullptr, and caused
|
||||
// subsequent refresh to double free.
|
||||
ASSERT_OK(iter->Refresh());
|
||||
ASSERT_OK(iter->Refresh());
|
||||
}
|
||||
|
||||
TEST_F(DBRangeDelTest, RangeTombstoneRespectIterateUpperBound) {
|
||||
// Memtable: a, [b, bz)
|
||||
// Do a Seek on `a` with iterate_upper_bound being az
|
||||
// range tombstone [b, bz) should not be processed (added to and
|
||||
// popped from the min_heap in MergingIterator).
|
||||
Options options = CurrentOptions();
|
||||
options.disable_auto_compactions = true;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
ASSERT_OK(Put("a", "bar"));
|
||||
ASSERT_OK(
|
||||
db_->DeleteRange(WriteOptions(), db_->DefaultColumnFamily(), "b", "bz"));
|
||||
|
||||
// I could not find a cleaner way to test this without relying on
|
||||
// implementation detail. Tried to test the value of
|
||||
// `internal_range_del_reseek_count` but that did not work
|
||||
// since BlockBasedTable iterator becomes !Valid() when point key
|
||||
// is out of bound and that reseek only happens when a point key
|
||||
// is covered by some range tombstone.
|
||||
SyncPoint::GetInstance()->SetCallBack("MergeIterator::PopDeleteRangeStart",
|
||||
[](void*) {
|
||||
// there should not be any range
|
||||
// tombstone in the heap.
|
||||
FAIL();
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
ReadOptions read_opts;
|
||||
std::string upper_bound = "az";
|
||||
Slice upper_bound_slice = upper_bound;
|
||||
read_opts.iterate_upper_bound = &upper_bound_slice;
|
||||
std::unique_ptr<Iterator> iter{db_->NewIterator(read_opts)};
|
||||
iter->Seek("a");
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
ASSERT_EQ(iter->key(), "a");
|
||||
iter->Next();
|
||||
ASSERT_FALSE(iter->Valid());
|
||||
ASSERT_OK(iter->status());
|
||||
}
|
||||
|
||||
#endif // ROCKSDB_LITE
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
+2
-2
@@ -695,10 +695,10 @@ TEST_P(DBSSTTestRateLimit, RateLimitedDelete) {
|
||||
*abs_time_us = Env::Default()->NowMicros();
|
||||
});
|
||||
|
||||
// Disable PeriodicWorkScheduler as it also has TimedWait, which could update
|
||||
// Disable PeriodicTaskScheduler as it also has TimedWait, which could update
|
||||
// the simulated sleep time
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::StartPeriodicWorkScheduler:DisableScheduler", [&](void* arg) {
|
||||
"DBImpl::StartPeriodicTaskScheduler:DisableScheduler", [&](void* arg) {
|
||||
bool* disable_scheduler = static_cast<bool*>(arg);
|
||||
*disable_scheduler = true;
|
||||
});
|
||||
|
||||
@@ -186,6 +186,12 @@ TEST_F(DBTablePropertiesTest, InvalidIgnored) {
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
// Corrupting the table properties corrupts the unique id.
|
||||
// Ignore the unique id recorded in the manifest.
|
||||
auto options = CurrentOptions();
|
||||
options.verify_sst_unique_id_in_manifest = false;
|
||||
Reopen(options);
|
||||
|
||||
// Build file
|
||||
for (int i = 0; i < 10; ++i) {
|
||||
ASSERT_OK(db_->Put(WriteOptions(), std::to_string(i), "val"));
|
||||
|
||||
+41
-18
@@ -7339,18 +7339,18 @@ TEST_F(DBTest2, SstUniqueIdVerifyBackwardCompatible) {
|
||||
auto options = CurrentOptions();
|
||||
options.level0_file_num_compaction_trigger = kLevel0Trigger;
|
||||
options.statistics = CreateDBStatistics();
|
||||
// Skip for now
|
||||
options.verify_sst_unique_id_in_manifest = false;
|
||||
Reopen(options);
|
||||
|
||||
// Existing manifest doesn't have unique id
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"VersionEdit::EncodeTo:UniqueId", [&](void* arg) {
|
||||
auto unique_id = static_cast<UniqueId64x2*>(arg);
|
||||
// remove id before writing it to manifest
|
||||
(*unique_id)[0] = 0;
|
||||
(*unique_id)[1] = 0;
|
||||
});
|
||||
std::atomic_int skipped = 0;
|
||||
SyncPoint::GetInstance()->SetCallBack("Version::VerifySstUniqueIds::Skipped",
|
||||
[&](void* /*arg*/) { skipped++; });
|
||||
std::atomic_int passed = 0;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"BlockBasedTable::Open::SkippedVerifyUniqueId",
|
||||
[&](void* /*arg*/) { skipped++; });
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"BlockBasedTable::Open::PassedVerifyUniqueId",
|
||||
[&](void* /*arg*/) { passed++; });
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
// generate a few SSTs
|
||||
@@ -7361,13 +7361,28 @@ TEST_F(DBTest2, SstUniqueIdVerifyBackwardCompatible) {
|
||||
ASSERT_OK(Flush());
|
||||
}
|
||||
|
||||
// Reopen without verification
|
||||
Reopen(options);
|
||||
// Verification has been skipped on files so far
|
||||
EXPECT_EQ(skipped, kNumSst);
|
||||
EXPECT_EQ(passed, 0);
|
||||
|
||||
// Reopen with verification, but it's skipped because manifest doesn't have id
|
||||
// Reopen with verification
|
||||
options.verify_sst_unique_id_in_manifest = true;
|
||||
skipped = 0;
|
||||
passed = 0;
|
||||
Reopen(options);
|
||||
ASSERT_EQ(skipped, kNumSst);
|
||||
EXPECT_EQ(skipped, 0);
|
||||
EXPECT_EQ(passed, kNumSst);
|
||||
|
||||
// Now simulate no unique id in manifest for next file
|
||||
// NOTE: this only works for loading manifest from disk,
|
||||
// not in-memory manifest, so we need to re-open below.
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"VersionEdit::EncodeTo:UniqueId", [&](void* arg) {
|
||||
auto unique_id = static_cast<UniqueId64x2*>(arg);
|
||||
// remove id before writing it to manifest
|
||||
(*unique_id)[0] = 0;
|
||||
(*unique_id)[1] = 0;
|
||||
});
|
||||
|
||||
// test compaction generated Sst
|
||||
for (int i = kNumSst; i < kLevel0Trigger; i++) {
|
||||
@@ -7382,11 +7397,13 @@ TEST_F(DBTest2, SstUniqueIdVerifyBackwardCompatible) {
|
||||
ASSERT_EQ("0,1", FilesPerLevel(0));
|
||||
#endif // ROCKSDB_LITE
|
||||
|
||||
// Reopen with verification should fail
|
||||
options.verify_sst_unique_id_in_manifest = true;
|
||||
// Reopen (with verification)
|
||||
ASSERT_TRUE(options.verify_sst_unique_id_in_manifest);
|
||||
skipped = 0;
|
||||
passed = 0;
|
||||
Reopen(options);
|
||||
ASSERT_EQ(skipped, 1);
|
||||
EXPECT_EQ(skipped, 1);
|
||||
EXPECT_EQ(passed, 0);
|
||||
}
|
||||
|
||||
TEST_F(DBTest2, SstUniqueIdVerify) {
|
||||
@@ -7394,11 +7411,15 @@ TEST_F(DBTest2, SstUniqueIdVerify) {
|
||||
const int kLevel0Trigger = 4;
|
||||
auto options = CurrentOptions();
|
||||
options.level0_file_num_compaction_trigger = kLevel0Trigger;
|
||||
// Allow mismatch for now
|
||||
options.verify_sst_unique_id_in_manifest = false;
|
||||
Reopen(options);
|
||||
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"PropertyBlockBuilder::AddTableProperty:Start", [&](void* props_vs) {
|
||||
auto props = static_cast<TableProperties*>(props_vs);
|
||||
// update table property session_id to a different one
|
||||
// update table property session_id to a different one, which
|
||||
// changes unique ID
|
||||
props->db_session_id = DBImpl::GenerateDbSessionId(nullptr);
|
||||
});
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
@@ -7444,6 +7465,8 @@ TEST_F(DBTest2, SstUniqueIdVerifyMultiCFs) {
|
||||
const int kLevel0Trigger = 4;
|
||||
auto options = CurrentOptions();
|
||||
options.level0_file_num_compaction_trigger = kLevel0Trigger;
|
||||
// Allow mismatch for now
|
||||
options.verify_sst_unique_id_in_manifest = false;
|
||||
|
||||
CreateAndReopenWithCF({"one", "two"}, options);
|
||||
|
||||
|
||||
+6
-14
@@ -967,15 +967,13 @@ std::string DBTestBase::AllEntriesFor(const Slice& user_key, int cf) {
|
||||
Arena arena;
|
||||
auto options = CurrentOptions();
|
||||
InternalKeyComparator icmp(options.comparator);
|
||||
ReadRangeDelAggregator range_del_agg(&icmp,
|
||||
kMaxSequenceNumber /* upper_bound */);
|
||||
ReadOptions read_options;
|
||||
ScopedArenaIterator iter;
|
||||
if (cf == 0) {
|
||||
iter.set(dbfull()->NewInternalIterator(read_options, &arena, &range_del_agg,
|
||||
iter.set(dbfull()->NewInternalIterator(read_options, &arena,
|
||||
kMaxSequenceNumber));
|
||||
} else {
|
||||
iter.set(dbfull()->NewInternalIterator(read_options, &arena, &range_del_agg,
|
||||
iter.set(dbfull()->NewInternalIterator(read_options, &arena,
|
||||
kMaxSequenceNumber, handles_[cf]));
|
||||
}
|
||||
InternalKey target(user_key, kMaxSequenceNumber, kTypeValue);
|
||||
@@ -1431,17 +1429,13 @@ void DBTestBase::validateNumberOfEntries(int numValues, int cf) {
|
||||
Arena arena;
|
||||
auto options = CurrentOptions();
|
||||
InternalKeyComparator icmp(options.comparator);
|
||||
ReadRangeDelAggregator range_del_agg(&icmp,
|
||||
kMaxSequenceNumber /* upper_bound */);
|
||||
// This should be defined after range_del_agg so that it destructs the
|
||||
// assigned iterator before it range_del_agg is already destructed.
|
||||
ReadOptions read_options;
|
||||
ScopedArenaIterator iter;
|
||||
if (cf != 0) {
|
||||
iter.set(dbfull()->NewInternalIterator(read_options, &arena, &range_del_agg,
|
||||
iter.set(dbfull()->NewInternalIterator(read_options, &arena,
|
||||
kMaxSequenceNumber, handles_[cf]));
|
||||
} else {
|
||||
iter.set(dbfull()->NewInternalIterator(read_options, &arena, &range_del_agg,
|
||||
iter.set(dbfull()->NewInternalIterator(read_options, &arena,
|
||||
kMaxSequenceNumber));
|
||||
}
|
||||
iter->SeekToFirst();
|
||||
@@ -1646,11 +1640,9 @@ void DBTestBase::VerifyDBInternal(
|
||||
std::vector<std::pair<std::string, std::string>> true_data) {
|
||||
Arena arena;
|
||||
InternalKeyComparator icmp(last_options_.comparator);
|
||||
ReadRangeDelAggregator range_del_agg(&icmp,
|
||||
kMaxSequenceNumber /* upper_bound */);
|
||||
ReadOptions read_options;
|
||||
auto iter = dbfull()->NewInternalIterator(read_options, &arena,
|
||||
&range_del_agg, kMaxSequenceNumber);
|
||||
auto iter =
|
||||
dbfull()->NewInternalIterator(read_options, &arena, kMaxSequenceNumber);
|
||||
iter->SeekToFirst();
|
||||
for (auto p : true_data) {
|
||||
ASSERT_TRUE(iter->Valid());
|
||||
|
||||
@@ -170,6 +170,161 @@ TEST_F(TimestampCompatibleCompactionTest, MultipleSubCompactions) {
|
||||
}
|
||||
}
|
||||
|
||||
class TestFilePartitioner : public SstPartitioner {
|
||||
public:
|
||||
explicit TestFilePartitioner() {}
|
||||
~TestFilePartitioner() override {}
|
||||
|
||||
const char* Name() const override { return "TestFilePartitioner"; }
|
||||
PartitionerResult ShouldPartition(
|
||||
const PartitionerRequest& /*request*/) override {
|
||||
return PartitionerResult::kRequired;
|
||||
}
|
||||
bool CanDoTrivialMove(const Slice& /*smallest_user_key*/,
|
||||
const Slice& /*largest_user_key*/) override {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
class TestFilePartitionerFactory : public SstPartitionerFactory {
|
||||
public:
|
||||
explicit TestFilePartitionerFactory() {}
|
||||
std::unique_ptr<SstPartitioner> CreatePartitioner(
|
||||
const SstPartitioner::Context& /*context*/) const override {
|
||||
std::unique_ptr<SstPartitioner> ret =
|
||||
std::make_unique<TestFilePartitioner>();
|
||||
return ret;
|
||||
}
|
||||
const char* Name() const override { return "TestFilePartitionerFactory"; }
|
||||
};
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
TEST_F(TimestampCompatibleCompactionTest, CompactFilesRangeCheckL0) {
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
options.sst_partitioner_factory =
|
||||
std::make_shared<TestFilePartitionerFactory>();
|
||||
options.comparator = test::BytewiseComparatorWithU64TsWrapper();
|
||||
options.disable_auto_compactions = true;
|
||||
DestroyAndReopen(options);
|
||||
|
||||
constexpr int kNumFiles = 10;
|
||||
constexpr int kKeysPerFile = 2;
|
||||
const std::string user_key = "foo";
|
||||
constexpr uint64_t start_ts = 10000;
|
||||
|
||||
uint64_t cur_ts = start_ts;
|
||||
for (int k = 0; k < kNumFiles; ++k) {
|
||||
for (int i = 0; i < kKeysPerFile; ++i) {
|
||||
ASSERT_OK(db_->Put(WriteOptions(), user_key, Timestamp(cur_ts),
|
||||
"v" + std::to_string(i)));
|
||||
++cur_ts;
|
||||
}
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
}
|
||||
|
||||
std::vector<std::string> input_files{};
|
||||
{
|
||||
std::vector<std::string> files;
|
||||
ASSERT_OK(env_->GetChildren(dbname_, &files));
|
||||
for (const auto& f : files) {
|
||||
uint64_t file_num = 0;
|
||||
FileType file_type = FileType::kWalFile;
|
||||
if (!ParseFileName(f, &file_num, &file_type) ||
|
||||
file_type != FileType::kTableFile) {
|
||||
continue;
|
||||
}
|
||||
input_files.emplace_back(f);
|
||||
}
|
||||
// sorting here by name, which also happens to sort by generation date.
|
||||
std::sort(input_files.begin(), input_files.end());
|
||||
assert(kNumFiles == input_files.size());
|
||||
std::vector<std::string> tmp;
|
||||
tmp.emplace_back(input_files[input_files.size() / 2]);
|
||||
input_files.swap(tmp);
|
||||
}
|
||||
|
||||
{
|
||||
std::vector<std::string> output_file_names;
|
||||
CompactionJobInfo compaction_job_info;
|
||||
ASSERT_OK(db_->CompactFiles(CompactionOptions(), input_files,
|
||||
/*output_level=*/1, /*output_path_id=*/-1,
|
||||
&output_file_names, &compaction_job_info));
|
||||
// We expect the L0 files older than the original provided input were all
|
||||
// included in the compaction.
|
||||
ASSERT_EQ(static_cast<size_t>(kNumFiles / 2 + 1),
|
||||
compaction_job_info.input_files.size());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(TimestampCompatibleCompactionTest, CompactFilesRangeCheckL1) {
|
||||
Options options = CurrentOptions();
|
||||
options.env = env_;
|
||||
options.sst_partitioner_factory =
|
||||
std::make_shared<TestFilePartitionerFactory>();
|
||||
options.comparator = test::BytewiseComparatorWithU64TsWrapper();
|
||||
|
||||
constexpr int kNumFiles = 4;
|
||||
options.level0_file_num_compaction_trigger = kNumFiles;
|
||||
|
||||
DestroyAndReopen(options);
|
||||
|
||||
constexpr int kKeysPerFile = 2;
|
||||
const std::string user_key = "foo";
|
||||
constexpr uint64_t start_ts = 10000;
|
||||
|
||||
uint64_t cur_ts = start_ts;
|
||||
// Generate some initial files in both L0 and L1.
|
||||
for (int k = 0; k < kNumFiles; ++k) {
|
||||
for (int i = 0; i < kKeysPerFile; ++i) {
|
||||
ASSERT_OK(db_->Put(WriteOptions(), user_key, Timestamp(cur_ts),
|
||||
"v" + std::to_string(i)));
|
||||
++cur_ts;
|
||||
}
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
}
|
||||
ASSERT_OK(dbfull()->TEST_WaitForCompact());
|
||||
|
||||
ASSERT_EQ(0, NumTableFilesAtLevel(/*level=*/0, /*cf=*/0));
|
||||
ASSERT_EQ(kNumFiles * kKeysPerFile,
|
||||
NumTableFilesAtLevel(/*level=*/1, /*cf=*/0));
|
||||
|
||||
constexpr int additional_l0s = 2;
|
||||
for (int i = 0; i < additional_l0s; ++i, ++cur_ts) {
|
||||
ASSERT_OK(db_->Put(WriteOptions(), user_key, Timestamp(cur_ts), "v"));
|
||||
ASSERT_OK(db_->Flush(FlushOptions()));
|
||||
}
|
||||
ASSERT_EQ(additional_l0s, NumTableFilesAtLevel(/*level=*/0, /*cf=*/0));
|
||||
|
||||
std::vector<std::string> inputs;
|
||||
{
|
||||
std::vector<LiveFileMetaData> fmetas;
|
||||
db_->GetLiveFilesMetaData(&fmetas);
|
||||
bool included_one_l1 = false;
|
||||
for (const auto& meta : fmetas) {
|
||||
if (meta.level == 0) {
|
||||
inputs.emplace_back(meta.relative_filename);
|
||||
} else if (!included_one_l1) {
|
||||
inputs.emplace_back(meta.relative_filename);
|
||||
included_one_l1 = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
ASSERT_EQ(static_cast<size_t>(3), inputs.size());
|
||||
{
|
||||
std::vector<std::string> output_file_names;
|
||||
CompactionJobInfo compaction_job_info;
|
||||
|
||||
ASSERT_OK(db_->CompactFiles(CompactionOptions(), inputs, /*output_level=*/1,
|
||||
/*output_path_id=*/-1, &output_file_names,
|
||||
&compaction_job_info));
|
||||
ASSERT_EQ(kNumFiles * kKeysPerFile + 2, output_file_names.size());
|
||||
ASSERT_EQ(kNumFiles * kKeysPerFile + 2,
|
||||
static_cast<int>(compaction_job_info.input_files.size()));
|
||||
}
|
||||
}
|
||||
#endif // !ROCKSDB_LITE
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
|
||||
@@ -499,6 +499,51 @@ TEST_P(DBWriteTest, UnflushedPutRaceWithTrackedWalSync) {
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_P(DBWriteTest, InactiveWalFullySyncedBeforeUntracked) {
|
||||
// Repro bug where a WAL is appended and switched after
|
||||
// `FlushWAL(true /* sync */)`'s sync finishes and before it untracks fully
|
||||
// synced inactive logs. Previously such a WAL would be wrongly untracked
|
||||
// so the final append would never be synced.
|
||||
Options options = GetOptions();
|
||||
std::unique_ptr<FaultInjectionTestEnv> fault_env(
|
||||
new FaultInjectionTestEnv(env_));
|
||||
options.env = fault_env.get();
|
||||
Reopen(options);
|
||||
|
||||
ASSERT_OK(Put("key1", "val1"));
|
||||
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::SyncWAL:BeforeMarkLogsSynced:1", [this](void* /* arg */) {
|
||||
ASSERT_OK(Put("key2", "val2"));
|
||||
ASSERT_OK(dbfull()->TEST_SwitchMemtable());
|
||||
});
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
ASSERT_OK(db_->FlushWAL(true /* sync */));
|
||||
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->DisableProcessing();
|
||||
ROCKSDB_NAMESPACE::SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
|
||||
ASSERT_OK(Put("key3", "val3"));
|
||||
|
||||
ASSERT_OK(db_->FlushWAL(true /* sync */));
|
||||
|
||||
Close();
|
||||
|
||||
// Simulate full loss of unsynced data. This should drop nothing since we did
|
||||
// `FlushWAL(true /* sync */)` before `Close()`.
|
||||
fault_env->DropUnsyncedFileData();
|
||||
|
||||
Reopen(options);
|
||||
|
||||
ASSERT_EQ("val1", Get("key1"));
|
||||
ASSERT_EQ("val2", Get("key2"));
|
||||
ASSERT_EQ("val3", Get("key3"));
|
||||
|
||||
// Need to close before `fault_env` goes out of scope.
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_P(DBWriteTest, IOErrorOnWALWriteTriggersReadOnlyMode) {
|
||||
std::unique_ptr<FaultInjectionTestEnv> mock_env(
|
||||
new FaultInjectionTestEnv(env_));
|
||||
|
||||
+2
-1
@@ -139,7 +139,8 @@ inline size_t InternalKeyEncodingLength(const ParsedInternalKey& key) {
|
||||
// Pack a sequence number and a ValueType into a uint64_t
|
||||
inline uint64_t PackSequenceAndType(uint64_t seq, ValueType t) {
|
||||
assert(seq <= kMaxSequenceNumber);
|
||||
assert(IsExtendedValueType(t));
|
||||
// kTypeMaxValid is used in TruncatedRangeDelIterator, see its constructor.
|
||||
assert(IsExtendedValueType(t) || t == kTypeMaxValid);
|
||||
return (seq << 8) | t;
|
||||
}
|
||||
|
||||
|
||||
@@ -1308,6 +1308,10 @@ TEST_F(DBErrorHandlingFSTest, WALWriteError) {
|
||||
ASSERT_EQ(s, s.NoSpace());
|
||||
}
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
// `ClearAllCallBacks()` is needed in addition to `DisableProcessing()` to
|
||||
// drain all callbacks. Otherwise, a pending callback in the background
|
||||
// could re-disable `fault_fs_` after we enable it below.
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
fault_fs_->SetFilesystemActive(true);
|
||||
ASSERT_EQ(listener->WaitForRecovery(5000000), true);
|
||||
for (auto i = 0; i < 199; ++i) {
|
||||
@@ -1474,6 +1478,10 @@ TEST_F(DBErrorHandlingFSTest, MultiCFWALWriteError) {
|
||||
ASSERT_TRUE(s.IsNoSpace());
|
||||
}
|
||||
SyncPoint::GetInstance()->DisableProcessing();
|
||||
// `ClearAllCallBacks()` is needed in addition to `DisableProcessing()` to
|
||||
// drain all callbacks. Otherwise, a pending callback in the background
|
||||
// could re-disable `fault_fs_` after we enable it below.
|
||||
SyncPoint::GetInstance()->ClearAllCallBacks();
|
||||
fault_fs_->SetFilesystemActive(true);
|
||||
ASSERT_EQ(listener->WaitForRecovery(5000000), true);
|
||||
|
||||
|
||||
+5
-1
@@ -122,7 +122,11 @@ Status UpdateManifestForFilesState(
|
||||
}
|
||||
|
||||
if (s.ok() && edit.NumEntries() > 0) {
|
||||
s = w.LogAndApply(cfd, &edit);
|
||||
std::unique_ptr<FSDirectory> db_dir;
|
||||
s = fs->NewDirectory(db_name, IOOptions(), &db_dir, nullptr);
|
||||
if (s.ok()) {
|
||||
s = w.LogAndApply(cfd, &edit, db_dir.get());
|
||||
}
|
||||
if (s.ok()) {
|
||||
++cfs_updated;
|
||||
}
|
||||
|
||||
@@ -1874,9 +1874,6 @@ TEST_F(ExternalSSTFileBasicTest, VerifySstUniqueId) {
|
||||
|
||||
ASSERT_OK(db_->IngestExternalFile(db_->DefaultColumnFamily(), {external_file},
|
||||
IngestExternalFileOptions()));
|
||||
auto options = CurrentOptions();
|
||||
options.verify_sst_unique_id_in_manifest = true;
|
||||
Reopen(options);
|
||||
|
||||
// Test ingest file without session_id and db_id (for example generated by an
|
||||
// older version of sst_writer)
|
||||
@@ -1887,12 +1884,21 @@ TEST_F(ExternalSSTFileBasicTest, VerifySstUniqueId) {
|
||||
props->db_session_id = "";
|
||||
props->db_id = "";
|
||||
});
|
||||
std::atomic_int skipped = 0;
|
||||
SyncPoint::GetInstance()->SetCallBack("Version::VerifySstUniqueIds::Skipped",
|
||||
[&](void* /*arg*/) { skipped++; });
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
std::atomic_int skipped = 0, passed = 0;
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"BlockBasedTable::Open::SkippedVerifyUniqueId",
|
||||
[&](void* /*arg*/) { skipped++; });
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"BlockBasedTable::Open::PassedVerifyUniqueId",
|
||||
[&](void* /*arg*/) { passed++; });
|
||||
SyncPoint::GetInstance()->EnableProcessing();
|
||||
|
||||
auto options = CurrentOptions();
|
||||
ASSERT_TRUE(options.verify_sst_unique_id_in_manifest);
|
||||
Reopen(options);
|
||||
ASSERT_EQ(skipped, 0);
|
||||
ASSERT_EQ(passed, 2); // one flushed + one ingested
|
||||
|
||||
external_file = sst_files_dir_ + "/file_to_ingest2.sst";
|
||||
{
|
||||
SstFileWriter sst_file_writer{EnvOptions(), CurrentOptions()};
|
||||
@@ -1905,12 +1911,18 @@ TEST_F(ExternalSSTFileBasicTest, VerifySstUniqueId) {
|
||||
ASSERT_OK(db_->IngestExternalFile(db_->DefaultColumnFamily(), {external_file},
|
||||
IngestExternalFileOptions()));
|
||||
|
||||
options.statistics = CreateDBStatistics();
|
||||
options.verify_sst_unique_id_in_manifest = true;
|
||||
ASSERT_EQ(skipped, 0);
|
||||
// Two table file opens skipping verification:
|
||||
// * ExternalSstFileIngestionJob::GetIngestedFileInfo
|
||||
// * TableCache::GetTableReader
|
||||
ASSERT_EQ(skipped, 2);
|
||||
ASSERT_EQ(passed, 2);
|
||||
|
||||
// Check same after re-open (except no GetIngestedFileInfo)
|
||||
skipped = 0;
|
||||
passed = 0;
|
||||
Reopen(options);
|
||||
// only one sst file is not verified because of missing unique_id
|
||||
ASSERT_EQ(skipped, 1);
|
||||
ASSERT_EQ(passed, 2);
|
||||
}
|
||||
|
||||
TEST_F(ExternalSSTFileBasicTest, StableSnapshotWhileLoggingToManifest) {
|
||||
|
||||
@@ -671,6 +671,9 @@ void InternalStats::CacheEntryRoleStats::BeginCollection(
|
||||
<< port::GetProcessID();
|
||||
cache_id = str.str();
|
||||
cache_capacity = cache->GetCapacity();
|
||||
cache_usage = cache->GetUsage();
|
||||
table_size = cache->GetTableAddressCount();
|
||||
occupancy = cache->GetOccupancyCount();
|
||||
}
|
||||
|
||||
void InternalStats::CacheEntryRoleStats::EndCollection(
|
||||
@@ -695,6 +698,8 @@ std::string InternalStats::CacheEntryRoleStats::ToString(
|
||||
std::ostringstream str;
|
||||
str << "Block cache " << cache_id
|
||||
<< " capacity: " << BytesToHumanString(cache_capacity)
|
||||
<< " usage: " << BytesToHumanString(cache_usage)
|
||||
<< " table_size: " << table_size << " occupancy: " << occupancy
|
||||
<< " collections: " << collection_count
|
||||
<< " last_copies: " << copies_of_last_collection
|
||||
<< " last_secs: " << (GetLastDurationMicros() / 1000000.0)
|
||||
@@ -1592,7 +1597,8 @@ void InternalStats::DumpCFMapStats(
|
||||
int files = vstorage->NumLevelFiles(level);
|
||||
total_files += files;
|
||||
total_files_being_compacted += files_being_compacted[level];
|
||||
if (comp_stats_[level].micros > 0 || files > 0) {
|
||||
if (comp_stats_[level].micros > 0 || comp_stats_[level].cpu_micros > 0 ||
|
||||
files > 0) {
|
||||
compaction_stats_sum->Add(comp_stats_[level]);
|
||||
total_file_size += vstorage->NumLevelBytes(level);
|
||||
uint64_t input_bytes;
|
||||
|
||||
@@ -453,6 +453,9 @@ class InternalStats {
|
||||
// For use with CacheEntryStatsCollector
|
||||
struct CacheEntryRoleStats {
|
||||
uint64_t cache_capacity = 0;
|
||||
uint64_t cache_usage = 0;
|
||||
size_t table_size = 0;
|
||||
size_t occupancy = 0;
|
||||
std::string cache_id;
|
||||
std::array<uint64_t, kNumCacheEntryRoles> total_charges;
|
||||
std::array<size_t, kNumCacheEntryRoles> entry_counts;
|
||||
|
||||
+79
-18
@@ -127,6 +127,22 @@ MemTable::MemTable(const InternalKeyComparator& cmp,
|
||||
6 /* hard coded 6 probes */,
|
||||
moptions_.memtable_huge_page_size, ioptions.logger));
|
||||
}
|
||||
// Initialize cached_range_tombstone_ here since it could
|
||||
// be read before it is constructed in MemTable::Add(), which could also lead
|
||||
// to a data race on the global mutex table backing atomic shared_ptr.
|
||||
auto new_cache = std::make_shared<FragmentedRangeTombstoneListCache>();
|
||||
size_t size = cached_range_tombstone_.Size();
|
||||
for (size_t i = 0; i < size; ++i) {
|
||||
std::shared_ptr<FragmentedRangeTombstoneListCache>* local_cache_ref_ptr =
|
||||
cached_range_tombstone_.AccessAtCore(i);
|
||||
auto new_local_cache_ref = std::make_shared<
|
||||
const std::shared_ptr<FragmentedRangeTombstoneListCache>>(new_cache);
|
||||
std::atomic_store_explicit(
|
||||
local_cache_ref_ptr,
|
||||
std::shared_ptr<FragmentedRangeTombstoneListCache>(new_local_cache_ref,
|
||||
new_cache.get()),
|
||||
std::memory_order_relaxed);
|
||||
}
|
||||
}
|
||||
|
||||
MemTable::~MemTable() {
|
||||
@@ -560,16 +576,28 @@ FragmentedRangeTombstoneIterator* MemTable::NewRangeTombstoneIteratorInternal(
|
||||
read_seq);
|
||||
}
|
||||
|
||||
auto* unfragmented_iter = new MemTableIterator(
|
||||
*this, read_options, nullptr /* arena */, true /* use_range_del_table */);
|
||||
auto fragmented_tombstone_list =
|
||||
std::make_shared<FragmentedRangeTombstoneList>(
|
||||
std::unique_ptr<InternalIterator>(unfragmented_iter),
|
||||
comparator_.comparator);
|
||||
// takes current cache
|
||||
std::shared_ptr<FragmentedRangeTombstoneListCache> cache =
|
||||
std::atomic_load_explicit(cached_range_tombstone_.Access(),
|
||||
std::memory_order_relaxed);
|
||||
// construct fragmented tombstone list if necessary
|
||||
if (!cache->initialized.load(std::memory_order_acquire)) {
|
||||
cache->reader_mutex.lock();
|
||||
if (!cache->tombstones) {
|
||||
auto* unfragmented_iter =
|
||||
new MemTableIterator(*this, read_options, nullptr /* arena */,
|
||||
true /* use_range_del_table */);
|
||||
cache->tombstones = std::make_unique<FragmentedRangeTombstoneList>(
|
||||
FragmentedRangeTombstoneList(
|
||||
std::unique_ptr<InternalIterator>(unfragmented_iter),
|
||||
comparator_.comparator));
|
||||
cache->initialized.store(true, std::memory_order_release);
|
||||
}
|
||||
cache->reader_mutex.unlock();
|
||||
}
|
||||
|
||||
auto* fragmented_iter = new FragmentedRangeTombstoneIterator(
|
||||
fragmented_tombstone_list, comparator_.comparator, read_seq);
|
||||
return fragmented_iter;
|
||||
return new FragmentedRangeTombstoneIterator(cache, comparator_.comparator,
|
||||
read_seq);
|
||||
}
|
||||
|
||||
void MemTable::ConstructFragmentedRangeTombstones() {
|
||||
@@ -819,6 +847,30 @@ Status MemTable::Add(SequenceNumber s, ValueType type,
|
||||
}
|
||||
}
|
||||
if (type == kTypeRangeDeletion) {
|
||||
auto new_cache = std::make_shared<FragmentedRangeTombstoneListCache>();
|
||||
size_t size = cached_range_tombstone_.Size();
|
||||
if (allow_concurrent) {
|
||||
range_del_mutex_.lock();
|
||||
}
|
||||
for (size_t i = 0; i < size; ++i) {
|
||||
std::shared_ptr<FragmentedRangeTombstoneListCache>* local_cache_ref_ptr =
|
||||
cached_range_tombstone_.AccessAtCore(i);
|
||||
auto new_local_cache_ref = std::make_shared<
|
||||
const std::shared_ptr<FragmentedRangeTombstoneListCache>>(new_cache);
|
||||
// It is okay for some reader to load old cache during invalidation as
|
||||
// the new sequence number is not published yet.
|
||||
// Each core will have a shared_ptr to a shared_ptr to the cached
|
||||
// fragmented range tombstones, so that ref count is maintianed locally
|
||||
// per-core using the per-core shared_ptr.
|
||||
std::atomic_store_explicit(
|
||||
local_cache_ref_ptr,
|
||||
std::shared_ptr<FragmentedRangeTombstoneListCache>(
|
||||
new_local_cache_ref, new_cache.get()),
|
||||
std::memory_order_relaxed);
|
||||
}
|
||||
if (allow_concurrent) {
|
||||
range_del_mutex_.unlock();
|
||||
}
|
||||
is_range_del_table_empty_.store(false, std::memory_order_relaxed);
|
||||
}
|
||||
UpdateOldestKeyTime();
|
||||
@@ -906,7 +958,24 @@ static bool SaveValue(void* arg, const char* entry) {
|
||||
return true; // to continue to the next seq
|
||||
}
|
||||
|
||||
s->seq = seq;
|
||||
if (s->seq == kMaxSequenceNumber) {
|
||||
s->seq = seq;
|
||||
}
|
||||
|
||||
s->seq = std::max(s->seq, max_covering_tombstone_seq);
|
||||
|
||||
if (ts_sz > 0 && s->timestamp != nullptr) {
|
||||
if (!s->timestamp->empty()) {
|
||||
assert(ts_sz == s->timestamp->size());
|
||||
}
|
||||
// TODO optimize for smaller size ts
|
||||
const std::string kMaxTs(ts_sz, '\xff');
|
||||
if (s->timestamp->empty() ||
|
||||
user_comparator->CompareTimestamp(*(s->timestamp), kMaxTs) == 0) {
|
||||
Slice ts = ExtractTimestampFromUserKey(user_key_slice, ts_sz);
|
||||
s->timestamp->assign(ts.data(), ts_sz);
|
||||
}
|
||||
}
|
||||
|
||||
if ((type == kTypeValue || type == kTypeMerge || type == kTypeBlobIndex ||
|
||||
type == kTypeWideColumnEntity) &&
|
||||
@@ -987,10 +1056,6 @@ static bool SaveValue(void* arg, const char* entry) {
|
||||
*(s->is_blob_index) = (type == kTypeBlobIndex);
|
||||
}
|
||||
|
||||
if (ts_sz > 0 && s->timestamp != nullptr) {
|
||||
Slice ts = ExtractTimestampFromUserKey(user_key_slice, ts_sz);
|
||||
s->timestamp->assign(ts.data(), ts.size());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case kTypeDeletion:
|
||||
@@ -1006,10 +1071,6 @@ static bool SaveValue(void* arg, const char* entry) {
|
||||
}
|
||||
} else {
|
||||
*(s->status) = Status::NotFound();
|
||||
if (ts_sz > 0 && s->timestamp != nullptr) {
|
||||
Slice ts = ExtractTimestampFromUserKey(user_key_slice, ts_sz);
|
||||
s->timestamp->assign(ts.data(), ts.size());
|
||||
}
|
||||
}
|
||||
*(s->found_final_value) = true;
|
||||
return false;
|
||||
|
||||
@@ -649,6 +649,11 @@ class MemTable {
|
||||
std::unique_ptr<FragmentedRangeTombstoneList>
|
||||
fragmented_range_tombstone_list_;
|
||||
|
||||
// makes sure there is a single range tombstone writer to invalidate cache
|
||||
std::mutex range_del_mutex_;
|
||||
CoreLocalArray<std::shared_ptr<FragmentedRangeTombstoneListCache>>
|
||||
cached_range_tombstone_;
|
||||
|
||||
void UpdateEntryChecksum(const ProtectionInfoKVOS64* kv_prot_info,
|
||||
const Slice& key, const Slice& value, ValueType type,
|
||||
SequenceNumber s, char* checksum_ptr);
|
||||
|
||||
+33
-4
@@ -218,11 +218,33 @@ void MemTableListVersion::AddIterators(
|
||||
}
|
||||
}
|
||||
|
||||
void MemTableListVersion::AddIterators(
|
||||
const ReadOptions& options, MergeIteratorBuilder* merge_iter_builder) {
|
||||
void MemTableListVersion::AddIterators(const ReadOptions& options,
|
||||
MergeIteratorBuilder* merge_iter_builder,
|
||||
bool add_range_tombstone_iter) {
|
||||
for (auto& m : memlist_) {
|
||||
merge_iter_builder->AddIterator(
|
||||
m->NewIterator(options, merge_iter_builder->GetArena()));
|
||||
auto mem_iter = m->NewIterator(options, merge_iter_builder->GetArena());
|
||||
if (!add_range_tombstone_iter || options.ignore_range_deletions) {
|
||||
merge_iter_builder->AddIterator(mem_iter);
|
||||
} else {
|
||||
// Except for snapshot read, using kMaxSequenceNumber is OK because these
|
||||
// are immutable memtables.
|
||||
SequenceNumber read_seq = options.snapshot != nullptr
|
||||
? options.snapshot->GetSequenceNumber()
|
||||
: kMaxSequenceNumber;
|
||||
TruncatedRangeDelIterator* mem_tombstone_iter = nullptr;
|
||||
auto range_del_iter = m->NewRangeTombstoneIterator(
|
||||
options, read_seq, true /* immutale_memtable */);
|
||||
if (range_del_iter == nullptr || range_del_iter->empty()) {
|
||||
delete range_del_iter;
|
||||
} else {
|
||||
mem_tombstone_iter = new TruncatedRangeDelIterator(
|
||||
std::unique_ptr<FragmentedRangeTombstoneIterator>(range_del_iter),
|
||||
&m->GetInternalKeyComparator(), nullptr /* smallest */,
|
||||
nullptr /* largest */);
|
||||
}
|
||||
merge_iter_builder->AddPointAndTombstoneIterator(mem_iter,
|
||||
mem_tombstone_iter);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -397,6 +419,13 @@ void MemTableList::PickMemtablesToFlush(uint64_t max_memtable_id,
|
||||
std::max(m->GetNextLogNumber(), *max_next_log_number);
|
||||
}
|
||||
ret->push_back(m);
|
||||
} else if (!ret->empty()) {
|
||||
// This `break` is necessary to prevent picking non-consecutive memtables
|
||||
// in case `memlist` has one or more entries with
|
||||
// `flush_in_progress_ == true` sandwiched between entries with
|
||||
// `flush_in_progress_ == false`. This could happen after parallel flushes
|
||||
// are picked and the one flushing older memtables is rolled back.
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!atomic_flush || num_flush_not_started_ == 0) {
|
||||
|
||||
+2
-1
@@ -116,7 +116,8 @@ class MemTableListVersion {
|
||||
Arena* arena);
|
||||
|
||||
void AddIterators(const ReadOptions& options,
|
||||
MergeIteratorBuilder* merge_iter_builder);
|
||||
MergeIteratorBuilder* merge_iter_builder,
|
||||
bool add_range_tombstone_iter);
|
||||
|
||||
uint64_t GetTotalNumEntries() const;
|
||||
|
||||
|
||||
+35
-13
@@ -742,28 +742,40 @@ TEST_F(MemTableListTest, FlushPendingTest) {
|
||||
// Pick tables to flush
|
||||
list.PickMemtablesToFlush(
|
||||
std::numeric_limits<uint64_t>::max() /* memtable_id */, &to_flush);
|
||||
// Should pick 4 of 5 since 1 table has been picked in to_flush2
|
||||
ASSERT_EQ(4, to_flush.size());
|
||||
// Picks three oldest memtables. The fourth oldest is picked in `to_flush2` so
|
||||
// must be excluded. The newest (fifth oldest) is non-consecutive with the
|
||||
// three oldest due to omitting the fourth oldest so must not be picked.
|
||||
ASSERT_EQ(3, to_flush.size());
|
||||
ASSERT_EQ(5, list.NumNotFlushed());
|
||||
ASSERT_FALSE(list.IsFlushPending());
|
||||
ASSERT_FALSE(list.imm_flush_needed.load(std::memory_order_acquire));
|
||||
ASSERT_TRUE(list.imm_flush_needed.load(std::memory_order_acquire));
|
||||
|
||||
// Pick tables to flush again
|
||||
autovector<MemTable*> to_flush3;
|
||||
list.PickMemtablesToFlush(
|
||||
std::numeric_limits<uint64_t>::max() /* memtable_id */, &to_flush3);
|
||||
ASSERT_EQ(0, to_flush3.size()); // nothing not in progress of being flushed
|
||||
// Picks newest (fifth oldest)
|
||||
ASSERT_EQ(1, to_flush3.size());
|
||||
ASSERT_EQ(5, list.NumNotFlushed());
|
||||
ASSERT_FALSE(list.IsFlushPending());
|
||||
ASSERT_FALSE(list.imm_flush_needed.load(std::memory_order_acquire));
|
||||
|
||||
// Flush the 4 memtables that were picked in to_flush
|
||||
// Nothing left to flush
|
||||
autovector<MemTable*> to_flush4;
|
||||
list.PickMemtablesToFlush(
|
||||
std::numeric_limits<uint64_t>::max() /* memtable_id */, &to_flush4);
|
||||
ASSERT_EQ(0, to_flush4.size());
|
||||
ASSERT_EQ(5, list.NumNotFlushed());
|
||||
ASSERT_FALSE(list.IsFlushPending());
|
||||
ASSERT_FALSE(list.imm_flush_needed.load(std::memory_order_acquire));
|
||||
|
||||
// Flush the 3 memtables that were picked in to_flush
|
||||
s = Mock_InstallMemtableFlushResults(&list, mutable_cf_options, to_flush,
|
||||
&to_delete);
|
||||
ASSERT_OK(s);
|
||||
|
||||
// Note: now to_flush contains tables[0,1,2,4]. to_flush2 contains
|
||||
// tables[3].
|
||||
// Note: now to_flush contains tables[0,1,2]. to_flush2 contains
|
||||
// tables[3]. to_flush3 contains tables[4].
|
||||
// Current implementation will only commit memtables in the order they were
|
||||
// created. So TryInstallMemtableFlushResults will install the first 3 tables
|
||||
// in to_flush and stop when it encounters a table not yet flushed.
|
||||
@@ -779,7 +791,17 @@ TEST_F(MemTableListTest, FlushPendingTest) {
|
||||
ASSERT_FALSE(list.IsFlushPending());
|
||||
ASSERT_FALSE(list.imm_flush_needed.load(std::memory_order_acquire));
|
||||
|
||||
// Flush the 1 memtable that was picked in to_flush2
|
||||
// Flush the 1 memtable (tables[4]) that was picked in to_flush3
|
||||
s = MemTableListTest::Mock_InstallMemtableFlushResults(
|
||||
&list, mutable_cf_options, to_flush3, &to_delete);
|
||||
ASSERT_OK(s);
|
||||
|
||||
// This will install 0 tables since tables[4] flushed while tables[3] has not
|
||||
// yet flushed.
|
||||
ASSERT_EQ(2, list.NumNotFlushed());
|
||||
ASSERT_EQ(0, to_delete.size());
|
||||
|
||||
// Flush the 1 memtable (tables[3]) that was picked in to_flush2
|
||||
s = MemTableListTest::Mock_InstallMemtableFlushResults(
|
||||
&list, mutable_cf_options, to_flush2, &to_delete);
|
||||
ASSERT_OK(s);
|
||||
@@ -809,11 +831,11 @@ TEST_F(MemTableListTest, FlushPendingTest) {
|
||||
memtable_id = 4;
|
||||
// Pick tables to flush. The tables to pick must have ID smaller than or
|
||||
// equal to 4. Therefore, no table will be selected in this case.
|
||||
autovector<MemTable*> to_flush4;
|
||||
autovector<MemTable*> to_flush5;
|
||||
list.FlushRequested();
|
||||
ASSERT_TRUE(list.HasFlushRequested());
|
||||
list.PickMemtablesToFlush(memtable_id, &to_flush4);
|
||||
ASSERT_TRUE(to_flush4.empty());
|
||||
list.PickMemtablesToFlush(memtable_id, &to_flush5);
|
||||
ASSERT_TRUE(to_flush5.empty());
|
||||
ASSERT_EQ(1, list.NumNotFlushed());
|
||||
ASSERT_TRUE(list.imm_flush_needed.load(std::memory_order_acquire));
|
||||
ASSERT_FALSE(list.IsFlushPending());
|
||||
@@ -823,8 +845,8 @@ TEST_F(MemTableListTest, FlushPendingTest) {
|
||||
// equal to 5. Therefore, only tables[5] will be selected.
|
||||
memtable_id = 5;
|
||||
list.FlushRequested();
|
||||
list.PickMemtablesToFlush(memtable_id, &to_flush4);
|
||||
ASSERT_EQ(1, static_cast<int>(to_flush4.size()));
|
||||
list.PickMemtablesToFlush(memtable_id, &to_flush5);
|
||||
ASSERT_EQ(1, static_cast<int>(to_flush5.size()));
|
||||
ASSERT_EQ(1, list.NumNotFlushed());
|
||||
ASSERT_FALSE(list.imm_flush_needed.load(std::memory_order_acquire));
|
||||
ASSERT_FALSE(list.IsFlushPending());
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
//
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#include "db/periodic_task_scheduler.h"
|
||||
|
||||
#include "rocksdb/system_clock.h"
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
// `timer_mutex` is a global mutex serves 3 purposes currently:
|
||||
// (1) to ensure calls to `Start()` and `Shutdown()` are serialized, as
|
||||
// they are currently not implemented in a thread-safe way; and
|
||||
// (2) to ensure the `Timer::Add()`s and `Timer::Start()` run atomically, and
|
||||
// the `Timer::Cancel()`s and `Timer::Shutdown()` run atomically.
|
||||
// (3) protect tasks_map_ in PeriodicTaskScheduler
|
||||
// Note: It's not efficient to have a static global mutex, for
|
||||
// PeriodicTaskScheduler it should be okay, as the operations are called
|
||||
// infrequently.
|
||||
static port::Mutex timer_mutex;
|
||||
|
||||
static const std::map<PeriodicTaskType, uint64_t> kDefaultPeriodSeconds = {
|
||||
{PeriodicTaskType::kDumpStats, kInvalidPeriodSec},
|
||||
{PeriodicTaskType::kPersistStats, kInvalidPeriodSec},
|
||||
{PeriodicTaskType::kFlushInfoLog, 10},
|
||||
{PeriodicTaskType::kRecordSeqnoTime, kInvalidPeriodSec},
|
||||
};
|
||||
|
||||
static const std::map<PeriodicTaskType, std::string> kPeriodicTaskTypeNames = {
|
||||
{PeriodicTaskType::kDumpStats, "dump_st"},
|
||||
{PeriodicTaskType::kPersistStats, "pst_st"},
|
||||
{PeriodicTaskType::kFlushInfoLog, "flush_info_log"},
|
||||
{PeriodicTaskType::kRecordSeqnoTime, "record_seq_time"},
|
||||
};
|
||||
|
||||
Status PeriodicTaskScheduler::Register(PeriodicTaskType task_type,
|
||||
const PeriodicTaskFunc& fn) {
|
||||
return Register(task_type, fn, kDefaultPeriodSeconds.at(task_type));
|
||||
}
|
||||
|
||||
Status PeriodicTaskScheduler::Register(PeriodicTaskType task_type,
|
||||
const PeriodicTaskFunc& fn,
|
||||
uint64_t repeat_period_seconds) {
|
||||
MutexLock l(&timer_mutex);
|
||||
static std::atomic<uint64_t> initial_delay(0);
|
||||
|
||||
if (repeat_period_seconds == kInvalidPeriodSec) {
|
||||
return Status::InvalidArgument("Invalid task repeat period");
|
||||
}
|
||||
auto it = tasks_map_.find(task_type);
|
||||
if (it != tasks_map_.end()) {
|
||||
// the task already exists and it's the same, no update needed
|
||||
if (it->second.repeat_every_sec == repeat_period_seconds) {
|
||||
return Status::OK();
|
||||
}
|
||||
// cancel the existing one before register new one
|
||||
timer_->Cancel(it->second.name);
|
||||
tasks_map_.erase(it);
|
||||
}
|
||||
|
||||
timer_->Start();
|
||||
// put task type name as prefix, for easy debug
|
||||
std::string unique_id =
|
||||
kPeriodicTaskTypeNames.at(task_type) + std::to_string(id_++);
|
||||
|
||||
bool succeeded = timer_->Add(
|
||||
fn, unique_id,
|
||||
(initial_delay.fetch_add(1) % repeat_period_seconds) * kMicrosInSecond,
|
||||
repeat_period_seconds * kMicrosInSecond);
|
||||
if (!succeeded) {
|
||||
return Status::Aborted("Failed to register periodic task");
|
||||
}
|
||||
auto result = tasks_map_.try_emplace(
|
||||
task_type, TaskInfo{unique_id, repeat_period_seconds});
|
||||
if (!result.second) {
|
||||
return Status::Aborted("Failed to add periodic task");
|
||||
};
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status PeriodicTaskScheduler::Unregister(PeriodicTaskType task_type) {
|
||||
MutexLock l(&timer_mutex);
|
||||
auto it = tasks_map_.find(task_type);
|
||||
if (it != tasks_map_.end()) {
|
||||
timer_->Cancel(it->second.name);
|
||||
tasks_map_.erase(it);
|
||||
}
|
||||
if (!timer_->HasPendingTask()) {
|
||||
timer_->Shutdown();
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Timer* PeriodicTaskScheduler::Default() {
|
||||
static Timer timer(SystemClock::Default().get());
|
||||
return &timer;
|
||||
}
|
||||
|
||||
#ifndef NDEBUG
|
||||
void PeriodicTaskScheduler::TEST_OverrideTimer(SystemClock* clock) {
|
||||
static Timer test_timer(clock);
|
||||
test_timer.TEST_OverrideTimer(clock);
|
||||
MutexLock l(&timer_mutex);
|
||||
timer_ = &test_timer;
|
||||
}
|
||||
#endif // NDEBUG
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
#endif // ROCKSDB_LITE
|
||||
@@ -0,0 +1,110 @@
|
||||
// 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
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
|
||||
#include "util/timer.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
class SystemClock;
|
||||
|
||||
using PeriodicTaskFunc = std::function<void()>;
|
||||
|
||||
constexpr uint64_t kInvalidPeriodSec = 0;
|
||||
|
||||
// List of task types
|
||||
enum class PeriodicTaskType : uint8_t {
|
||||
kDumpStats = 0,
|
||||
kPersistStats,
|
||||
kFlushInfoLog,
|
||||
kRecordSeqnoTime,
|
||||
kMax,
|
||||
};
|
||||
|
||||
// PeriodicTaskScheduler contains the periodic task scheduled from the DB
|
||||
// instance. It's used to schedule/unschedule DumpStats(), PersistStats(),
|
||||
// FlushInfoLog(), etc. Each type of the task can only have one instance,
|
||||
// re-register the same task type would only update the repeat period.
|
||||
//
|
||||
// Internally, it uses a global single threaded timer object to run the periodic
|
||||
// task functions. Timer thread will always be started since the info log
|
||||
// flushing cannot be disabled.
|
||||
class PeriodicTaskScheduler {
|
||||
public:
|
||||
explicit PeriodicTaskScheduler() = default;
|
||||
|
||||
PeriodicTaskScheduler(const PeriodicTaskScheduler&) = delete;
|
||||
PeriodicTaskScheduler(PeriodicTaskScheduler&&) = delete;
|
||||
PeriodicTaskScheduler& operator=(const PeriodicTaskScheduler&) = delete;
|
||||
PeriodicTaskScheduler& operator=(PeriodicTaskScheduler&&) = delete;
|
||||
|
||||
// Register a task with its default repeat period
|
||||
Status Register(PeriodicTaskType task_type, const PeriodicTaskFunc& fn);
|
||||
|
||||
// Register a task with specified repeat period. 0 is an invalid argument
|
||||
// (kInvalidPeriodSec). To stop the task, please use Unregister() specifically
|
||||
Status Register(PeriodicTaskType task_type, const PeriodicTaskFunc& fn,
|
||||
uint64_t repeat_period_seconds);
|
||||
|
||||
// Unregister the task
|
||||
Status Unregister(PeriodicTaskType task_type);
|
||||
|
||||
#ifndef NDEBUG
|
||||
// Override the timer for the unittest
|
||||
void TEST_OverrideTimer(SystemClock* clock);
|
||||
|
||||
// Call Timer TEST_WaitForRun() which wait until Timer starting waiting.
|
||||
void TEST_WaitForRun(const std::function<void()>& callback) const {
|
||||
if (timer_ != nullptr) {
|
||||
timer_->TEST_WaitForRun(callback);
|
||||
}
|
||||
}
|
||||
|
||||
// Get global valid task number in the Timer
|
||||
size_t TEST_GetValidTaskNum() const {
|
||||
if (timer_ != nullptr) {
|
||||
return timer_->TEST_GetPendingTaskNum();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// If it has the specified task type registered
|
||||
bool TEST_HasTask(PeriodicTaskType task_type) const {
|
||||
auto it = tasks_map_.find(task_type);
|
||||
return it != tasks_map_.end();
|
||||
}
|
||||
#endif // NDEBUG
|
||||
|
||||
private:
|
||||
// default global Timer instance
|
||||
static Timer* Default();
|
||||
|
||||
// Internal structure to store task information
|
||||
struct TaskInfo {
|
||||
TaskInfo(std::string _name, uint64_t _repeat_every_sec)
|
||||
: name(std::move(_name)), repeat_every_sec(_repeat_every_sec) {}
|
||||
std::string name;
|
||||
uint64_t repeat_every_sec;
|
||||
};
|
||||
|
||||
// Internal tasks map
|
||||
std::map<PeriodicTaskType, TaskInfo> tasks_map_;
|
||||
|
||||
// Global timer pointer, which doesn't support synchronous add/cancel tasks
|
||||
// so having a global `timer_mutex` for add/cancel task.
|
||||
Timer* timer_ = Default();
|
||||
|
||||
// Global task id, protected by the global `timer_mutex`
|
||||
inline static uint64_t id_;
|
||||
|
||||
static constexpr uint64_t kMicrosInSecond = 1000U * 1000U;
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
#endif // ROCKSDB_LITE
|
||||
@@ -1,9 +1,10 @@
|
||||
// Copyright (c) 2011-present, Facebook, Inc. All rights reserved.
|
||||
// Copyright (c) Meta Platforms, Inc. and affiliates.
|
||||
//
|
||||
// This source code is licensed under both the GPLv2 (found in the
|
||||
// COPYING file in the root directory) and Apache 2.0 License
|
||||
// (found in the LICENSE.Apache file in the root directory).
|
||||
|
||||
#include "db/periodic_work_scheduler.h"
|
||||
#include "db/periodic_task_scheduler.h"
|
||||
|
||||
#include "db/db_test_util.h"
|
||||
#include "env/composite_env_wrapper.h"
|
||||
@@ -12,10 +13,10 @@
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
class PeriodicWorkSchedulerTest : public DBTestBase {
|
||||
class PeriodicTaskSchedulerTest : public DBTestBase {
|
||||
public:
|
||||
PeriodicWorkSchedulerTest()
|
||||
: DBTestBase("periodic_work_scheduler_test", /*env_do_fsync=*/true) {
|
||||
PeriodicTaskSchedulerTest()
|
||||
: DBTestBase("periodic_task_scheduler_test", /*env_do_fsync=*/true) {
|
||||
mock_clock_ = std::make_shared<MockSystemClock>(env_->GetSystemClock());
|
||||
mock_env_.reset(new CompositeEnvWrapper(env_, mock_clock_));
|
||||
}
|
||||
@@ -27,18 +28,16 @@ class PeriodicWorkSchedulerTest : public DBTestBase {
|
||||
void SetUp() override {
|
||||
mock_clock_->InstallTimedWaitFixCallback();
|
||||
SyncPoint::GetInstance()->SetCallBack(
|
||||
"DBImpl::StartPeriodicWorkScheduler:Init", [&](void* arg) {
|
||||
auto* periodic_work_scheduler_ptr =
|
||||
reinterpret_cast<PeriodicWorkScheduler**>(arg);
|
||||
*periodic_work_scheduler_ptr =
|
||||
PeriodicWorkTestScheduler::Default(mock_clock_);
|
||||
"DBImpl::StartPeriodicTaskScheduler:Init", [&](void* arg) {
|
||||
auto periodic_task_scheduler_ptr =
|
||||
reinterpret_cast<PeriodicTaskScheduler*>(arg);
|
||||
periodic_task_scheduler_ptr->TEST_OverrideTimer(mock_clock_.get());
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(PeriodicWorkSchedulerTest, Basic) {
|
||||
constexpr unsigned int kPeriodSec =
|
||||
PeriodicWorkScheduler::kDefaultFlushInfoLogPeriodSec;
|
||||
TEST_F(PeriodicTaskSchedulerTest, Basic) {
|
||||
constexpr unsigned int kPeriodSec = 10;
|
||||
Close();
|
||||
Options options;
|
||||
options.stats_dump_period_sec = kPeriodSec;
|
||||
@@ -66,26 +65,26 @@ TEST_F(PeriodicWorkSchedulerTest, Basic) {
|
||||
ASSERT_EQ(kPeriodSec, dbfull()->GetDBOptions().stats_persist_period_sec);
|
||||
|
||||
ASSERT_GT(kPeriodSec, 1u);
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun([&] {
|
||||
dbfull()->TEST_WaitForPeridicTaskRun([&] {
|
||||
mock_clock_->MockSleepForSeconds(static_cast<int>(kPeriodSec) - 1);
|
||||
});
|
||||
|
||||
auto scheduler = dbfull()->TEST_GetPeriodicWorkScheduler();
|
||||
ASSERT_NE(nullptr, scheduler);
|
||||
ASSERT_EQ(3, scheduler->TEST_GetValidTaskNum());
|
||||
const PeriodicTaskScheduler& scheduler =
|
||||
dbfull()->TEST_GetPeriodicTaskScheduler();
|
||||
ASSERT_EQ(3, scheduler.TEST_GetValidTaskNum());
|
||||
|
||||
ASSERT_EQ(1, dump_st_counter);
|
||||
ASSERT_EQ(1, pst_st_counter);
|
||||
ASSERT_EQ(1, flush_info_log_counter);
|
||||
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
dbfull()->TEST_WaitForPeridicTaskRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(static_cast<int>(kPeriodSec)); });
|
||||
|
||||
ASSERT_EQ(2, dump_st_counter);
|
||||
ASSERT_EQ(2, pst_st_counter);
|
||||
ASSERT_EQ(2, flush_info_log_counter);
|
||||
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
dbfull()->TEST_WaitForPeridicTaskRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(static_cast<int>(kPeriodSec)); });
|
||||
|
||||
ASSERT_EQ(3, dump_st_counter);
|
||||
@@ -99,25 +98,22 @@ TEST_F(PeriodicWorkSchedulerTest, Basic) {
|
||||
ASSERT_EQ(0u, dbfull()->GetDBOptions().stats_persist_period_sec);
|
||||
|
||||
// Info log flush should still run.
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
dbfull()->TEST_WaitForPeridicTaskRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(static_cast<int>(kPeriodSec)); });
|
||||
ASSERT_EQ(3, dump_st_counter);
|
||||
ASSERT_EQ(3, pst_st_counter);
|
||||
ASSERT_EQ(4, flush_info_log_counter);
|
||||
|
||||
scheduler = dbfull()->TEST_GetPeriodicWorkScheduler();
|
||||
ASSERT_EQ(1u, scheduler->TEST_GetValidTaskNum());
|
||||
ASSERT_EQ(1u, scheduler.TEST_GetValidTaskNum());
|
||||
|
||||
// Re-enable one task
|
||||
ASSERT_OK(dbfull()->SetDBOptions({{"stats_dump_period_sec", "5"}}));
|
||||
ASSERT_EQ(5u, dbfull()->GetDBOptions().stats_dump_period_sec);
|
||||
ASSERT_EQ(0u, dbfull()->GetDBOptions().stats_persist_period_sec);
|
||||
|
||||
scheduler = dbfull()->TEST_GetPeriodicWorkScheduler();
|
||||
ASSERT_NE(nullptr, scheduler);
|
||||
ASSERT_EQ(2, scheduler->TEST_GetValidTaskNum());
|
||||
ASSERT_EQ(2, scheduler.TEST_GetValidTaskNum());
|
||||
|
||||
dbfull()->TEST_WaitForPeridicWorkerRun(
|
||||
dbfull()->TEST_WaitForPeridicTaskRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(static_cast<int>(kPeriodSec)); });
|
||||
ASSERT_EQ(4, dump_st_counter);
|
||||
ASSERT_EQ(3, pst_st_counter);
|
||||
@@ -126,7 +122,7 @@ TEST_F(PeriodicWorkSchedulerTest, Basic) {
|
||||
Close();
|
||||
}
|
||||
|
||||
TEST_F(PeriodicWorkSchedulerTest, MultiInstances) {
|
||||
TEST_F(PeriodicTaskSchedulerTest, MultiInstances) {
|
||||
constexpr int kPeriodSec = 5;
|
||||
const int kInstanceNum = 10;
|
||||
|
||||
@@ -153,23 +149,24 @@ TEST_F(PeriodicWorkSchedulerTest, MultiInstances) {
|
||||
}
|
||||
|
||||
auto dbi = static_cast_with_check<DBImpl>(dbs[kInstanceNum - 1]);
|
||||
auto scheduler = dbi->TEST_GetPeriodicWorkScheduler();
|
||||
ASSERT_EQ(kInstanceNum * 3, scheduler->TEST_GetValidTaskNum());
|
||||
|
||||
const PeriodicTaskScheduler& scheduler = dbi->TEST_GetPeriodicTaskScheduler();
|
||||
ASSERT_EQ(kInstanceNum * 3, scheduler.TEST_GetValidTaskNum());
|
||||
|
||||
int expected_run = kInstanceNum;
|
||||
dbi->TEST_WaitForPeridicWorkerRun(
|
||||
dbi->TEST_WaitForPeridicTaskRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(kPeriodSec - 1); });
|
||||
ASSERT_EQ(expected_run, dump_st_counter);
|
||||
ASSERT_EQ(expected_run, pst_st_counter);
|
||||
|
||||
expected_run += kInstanceNum;
|
||||
dbi->TEST_WaitForPeridicWorkerRun(
|
||||
dbi->TEST_WaitForPeridicTaskRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(kPeriodSec); });
|
||||
ASSERT_EQ(expected_run, dump_st_counter);
|
||||
ASSERT_EQ(expected_run, pst_st_counter);
|
||||
|
||||
expected_run += kInstanceNum;
|
||||
dbi->TEST_WaitForPeridicWorkerRun(
|
||||
dbi->TEST_WaitForPeridicTaskRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(kPeriodSec); });
|
||||
ASSERT_EQ(expected_run, dump_st_counter);
|
||||
ASSERT_EQ(expected_run, pst_st_counter);
|
||||
@@ -181,9 +178,9 @@ TEST_F(PeriodicWorkSchedulerTest, MultiInstances) {
|
||||
|
||||
expected_run += (kInstanceNum - half) * 2;
|
||||
|
||||
dbi->TEST_WaitForPeridicWorkerRun(
|
||||
dbi->TEST_WaitForPeridicTaskRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(kPeriodSec); });
|
||||
dbi->TEST_WaitForPeridicWorkerRun(
|
||||
dbi->TEST_WaitForPeridicTaskRun(
|
||||
[&] { mock_clock_->MockSleepForSeconds(kPeriodSec); });
|
||||
ASSERT_EQ(expected_run, dump_st_counter);
|
||||
ASSERT_EQ(expected_run, pst_st_counter);
|
||||
@@ -194,7 +191,7 @@ TEST_F(PeriodicWorkSchedulerTest, MultiInstances) {
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(PeriodicWorkSchedulerTest, MultiEnv) {
|
||||
TEST_F(PeriodicTaskSchedulerTest, MultiEnv) {
|
||||
constexpr int kDumpPeriodSec = 5;
|
||||
constexpr int kPersistPeriodSec = 10;
|
||||
Close();
|
||||
@@ -217,15 +214,12 @@ TEST_F(PeriodicWorkSchedulerTest, MultiEnv) {
|
||||
std::string dbname = test::PerThreadDBPath("multi_env_test");
|
||||
DB* db;
|
||||
ASSERT_OK(DB::Open(options2, dbname, &db));
|
||||
DBImpl* dbi = static_cast_with_check<DBImpl>(db);
|
||||
|
||||
ASSERT_EQ(dbi->TEST_GetPeriodicWorkScheduler(),
|
||||
dbfull()->TEST_GetPeriodicWorkScheduler());
|
||||
|
||||
ASSERT_OK(db->Close());
|
||||
delete db;
|
||||
Close();
|
||||
}
|
||||
|
||||
#endif // !ROCKSDB_LITE
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
// 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).
|
||||
|
||||
#include "db/periodic_work_scheduler.h"
|
||||
|
||||
#include "db/db_impl/db_impl.h"
|
||||
#include "rocksdb/system_clock.h"
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
|
||||
const std::string PeriodicWorkTaskNames::kDumpStats = "dump_st";
|
||||
const std::string PeriodicWorkTaskNames::kPersistStats = "pst_st";
|
||||
const std::string PeriodicWorkTaskNames::kFlushInfoLog = "flush_info_log";
|
||||
const std::string PeriodicWorkTaskNames::kRecordSeqnoTime = "record_seq_time";
|
||||
|
||||
PeriodicWorkScheduler::PeriodicWorkScheduler(
|
||||
const std::shared_ptr<SystemClock>& clock) {
|
||||
timer = std::unique_ptr<Timer>(new Timer(clock.get()));
|
||||
}
|
||||
|
||||
Status PeriodicWorkScheduler::Register(DBImpl* dbi,
|
||||
unsigned int stats_dump_period_sec,
|
||||
unsigned int stats_persist_period_sec) {
|
||||
MutexLock l(&timer_mu_);
|
||||
static std::atomic<uint64_t> initial_delay(0);
|
||||
timer->Start();
|
||||
if (stats_dump_period_sec > 0) {
|
||||
bool succeeded = timer->Add(
|
||||
[dbi]() { dbi->DumpStats(); },
|
||||
GetTaskName(dbi, PeriodicWorkTaskNames::kDumpStats),
|
||||
initial_delay.fetch_add(1) %
|
||||
static_cast<uint64_t>(stats_dump_period_sec) * kMicrosInSecond,
|
||||
static_cast<uint64_t>(stats_dump_period_sec) * kMicrosInSecond);
|
||||
if (!succeeded) {
|
||||
return Status::Aborted("Unable to add periodic task DumpStats");
|
||||
}
|
||||
}
|
||||
if (stats_persist_period_sec > 0) {
|
||||
bool succeeded = timer->Add(
|
||||
[dbi]() { dbi->PersistStats(); },
|
||||
GetTaskName(dbi, PeriodicWorkTaskNames::kPersistStats),
|
||||
initial_delay.fetch_add(1) %
|
||||
static_cast<uint64_t>(stats_persist_period_sec) * kMicrosInSecond,
|
||||
static_cast<uint64_t>(stats_persist_period_sec) * kMicrosInSecond);
|
||||
if (!succeeded) {
|
||||
return Status::Aborted("Unable to add periodic task PersistStats");
|
||||
}
|
||||
}
|
||||
bool succeeded =
|
||||
timer->Add([dbi]() { dbi->FlushInfoLog(); },
|
||||
GetTaskName(dbi, PeriodicWorkTaskNames::kFlushInfoLog),
|
||||
initial_delay.fetch_add(1) % kDefaultFlushInfoLogPeriodSec *
|
||||
kMicrosInSecond,
|
||||
kDefaultFlushInfoLogPeriodSec * kMicrosInSecond);
|
||||
if (!succeeded) {
|
||||
return Status::Aborted("Unable to add periodic task FlushInfoLog");
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
Status PeriodicWorkScheduler::RegisterRecordSeqnoTimeWorker(
|
||||
DBImpl* dbi, uint64_t record_cadence_sec) {
|
||||
MutexLock l(&timer_mu_);
|
||||
timer->Start();
|
||||
static std::atomic_uint64_t initial_delay(0);
|
||||
bool succeeded = timer->Add(
|
||||
[dbi]() { dbi->RecordSeqnoToTimeMapping(); },
|
||||
GetTaskName(dbi, PeriodicWorkTaskNames::kRecordSeqnoTime),
|
||||
initial_delay.fetch_add(1) % record_cadence_sec * kMicrosInSecond,
|
||||
record_cadence_sec * kMicrosInSecond);
|
||||
if (!succeeded) {
|
||||
return Status::NotSupported(
|
||||
"Updating seqno to time worker cadence is not supported yet");
|
||||
}
|
||||
return Status::OK();
|
||||
}
|
||||
|
||||
void PeriodicWorkScheduler::UnregisterRecordSeqnoTimeWorker(DBImpl* dbi) {
|
||||
MutexLock l(&timer_mu_);
|
||||
timer->Cancel(GetTaskName(dbi, PeriodicWorkTaskNames::kRecordSeqnoTime));
|
||||
if (!timer->HasPendingTask()) {
|
||||
timer->Shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
void PeriodicWorkScheduler::Unregister(DBImpl* dbi) {
|
||||
MutexLock l(&timer_mu_);
|
||||
timer->Cancel(GetTaskName(dbi, PeriodicWorkTaskNames::kDumpStats));
|
||||
timer->Cancel(GetTaskName(dbi, PeriodicWorkTaskNames::kPersistStats));
|
||||
timer->Cancel(GetTaskName(dbi, PeriodicWorkTaskNames::kFlushInfoLog));
|
||||
if (!timer->HasPendingTask()) {
|
||||
timer->Shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
PeriodicWorkScheduler* PeriodicWorkScheduler::Default() {
|
||||
// Always use the default SystemClock for the scheduler, as we only use the
|
||||
// NowMicros which is the same for all clocks. The Env could only be
|
||||
// overridden in test.
|
||||
static PeriodicWorkScheduler scheduler(SystemClock::Default());
|
||||
return &scheduler;
|
||||
}
|
||||
|
||||
std::string PeriodicWorkScheduler::GetTaskName(
|
||||
const DBImpl* dbi, const std::string& func_name) const {
|
||||
std::string db_session_id;
|
||||
// TODO: Should this error be ignored?
|
||||
dbi->GetDbSessionId(db_session_id).PermitUncheckedError();
|
||||
return db_session_id + ":" + func_name;
|
||||
}
|
||||
|
||||
#ifndef NDEBUG
|
||||
|
||||
// Get the static scheduler. For a new SystemClock, it needs to re-create the
|
||||
// internal timer, so only re-create it when there's no running task. Otherwise,
|
||||
// return the existing scheduler. Which means if the unittest needs to update
|
||||
// MockClock, Close all db instances and then re-open them.
|
||||
PeriodicWorkTestScheduler* PeriodicWorkTestScheduler::Default(
|
||||
const std::shared_ptr<SystemClock>& clock) {
|
||||
static PeriodicWorkTestScheduler scheduler(clock);
|
||||
static port::Mutex mutex;
|
||||
{
|
||||
MutexLock l(&mutex);
|
||||
if (scheduler.timer.get() != nullptr &&
|
||||
scheduler.timer->TEST_GetPendingTaskNum() == 0) {
|
||||
{
|
||||
MutexLock timer_mu_guard(&scheduler.timer_mu_);
|
||||
scheduler.timer->Shutdown();
|
||||
}
|
||||
scheduler.timer.reset(new Timer(clock.get()));
|
||||
}
|
||||
}
|
||||
return &scheduler;
|
||||
}
|
||||
|
||||
void PeriodicWorkTestScheduler::TEST_WaitForRun(
|
||||
std::function<void()> callback) const {
|
||||
if (timer != nullptr) {
|
||||
timer->TEST_WaitForRun(callback);
|
||||
}
|
||||
}
|
||||
|
||||
size_t PeriodicWorkTestScheduler::TEST_GetValidTaskNum() const {
|
||||
if (timer != nullptr) {
|
||||
return timer->TEST_GetPendingTaskNum();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool PeriodicWorkTestScheduler::TEST_HasValidTask(
|
||||
const DBImpl* dbi, const std::string& func_name) const {
|
||||
if (timer == nullptr) {
|
||||
return false;
|
||||
}
|
||||
return timer->TEST_HasVaildTask(GetTaskName(dbi, func_name));
|
||||
}
|
||||
|
||||
PeriodicWorkTestScheduler::PeriodicWorkTestScheduler(
|
||||
const std::shared_ptr<SystemClock>& clock)
|
||||
: PeriodicWorkScheduler(clock) {}
|
||||
|
||||
#endif // !NDEBUG
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
#endif // ROCKSDB_LITE
|
||||
@@ -1,90 +0,0 @@
|
||||
// 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).
|
||||
|
||||
#pragma once
|
||||
|
||||
#ifndef ROCKSDB_LITE
|
||||
|
||||
#include "db/db_impl/db_impl.h"
|
||||
#include "util/timer.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
class SystemClock;
|
||||
|
||||
// PeriodicWorkScheduler is a singleton object, which is scheduling/running
|
||||
// DumpStats(), PersistStats(), and FlushInfoLog() for all DB instances. All DB
|
||||
// instances use the same object from `Default()`.
|
||||
//
|
||||
// Internally, it uses a single threaded timer object to run the periodic work
|
||||
// functions. Timer thread will always be started since the info log flushing
|
||||
// cannot be disabled.
|
||||
class PeriodicWorkScheduler {
|
||||
public:
|
||||
static PeriodicWorkScheduler* Default();
|
||||
|
||||
PeriodicWorkScheduler() = delete;
|
||||
PeriodicWorkScheduler(const PeriodicWorkScheduler&) = delete;
|
||||
PeriodicWorkScheduler(PeriodicWorkScheduler&&) = delete;
|
||||
PeriodicWorkScheduler& operator=(const PeriodicWorkScheduler&) = delete;
|
||||
PeriodicWorkScheduler& operator=(PeriodicWorkScheduler&&) = delete;
|
||||
|
||||
Status Register(DBImpl* dbi, unsigned int stats_dump_period_sec,
|
||||
unsigned int stats_persist_period_sec);
|
||||
Status RegisterRecordSeqnoTimeWorker(DBImpl* dbi, uint64_t record_cadence);
|
||||
|
||||
void Unregister(DBImpl* dbi);
|
||||
void UnregisterRecordSeqnoTimeWorker(DBImpl* dbi);
|
||||
|
||||
// Periodically flush info log out of application buffer at a low frequency.
|
||||
// This improves debuggability in case of RocksDB hanging since it ensures the
|
||||
// log messages leading up to the hang will eventually become visible in the
|
||||
// log.
|
||||
static const uint64_t kDefaultFlushInfoLogPeriodSec = 10;
|
||||
|
||||
protected:
|
||||
std::unique_ptr<Timer> timer;
|
||||
// `timer_mu_` serves two purposes currently:
|
||||
// (1) to ensure calls to `Start()` and `Shutdown()` are serialized, as
|
||||
// they are currently not implemented in a thread-safe way; and
|
||||
// (2) to ensure the `Timer::Add()`s and `Timer::Start()` run atomically, and
|
||||
// the `Timer::Cancel()`s and `Timer::Shutdown()` run atomically.
|
||||
port::Mutex timer_mu_;
|
||||
|
||||
explicit PeriodicWorkScheduler(const std::shared_ptr<SystemClock>& clock);
|
||||
|
||||
// Get the unique task name (prefix with db session id)
|
||||
std::string GetTaskName(const DBImpl* dbi,
|
||||
const std::string& func_name) const;
|
||||
};
|
||||
|
||||
#ifndef NDEBUG
|
||||
// PeriodicWorkTestScheduler is for unittest, which can specify the SystemClock
|
||||
// It also contains functions for unittest.
|
||||
class PeriodicWorkTestScheduler : public PeriodicWorkScheduler {
|
||||
public:
|
||||
static PeriodicWorkTestScheduler* Default(
|
||||
const std::shared_ptr<SystemClock>& clock);
|
||||
|
||||
void TEST_WaitForRun(std::function<void()> callback) const;
|
||||
|
||||
size_t TEST_GetValidTaskNum() const;
|
||||
|
||||
bool TEST_HasValidTask(const DBImpl* dbi, const std::string& func_name) const;
|
||||
|
||||
private:
|
||||
explicit PeriodicWorkTestScheduler(const std::shared_ptr<SystemClock>& clock);
|
||||
};
|
||||
#endif // !NDEBUG
|
||||
|
||||
struct PeriodicWorkTaskNames {
|
||||
static const std::string kDumpStats;
|
||||
static const std::string kPersistStats;
|
||||
static const std::string kFlushInfoLog;
|
||||
static const std::string kRecordSeqnoTime;
|
||||
};
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
|
||||
#endif // ROCKSDB_LITE
|
||||
+12
-20
@@ -37,7 +37,6 @@ TruncatedRangeDelIterator::TruncatedRangeDelIterator(
|
||||
false /* log_err_key */); // TODO
|
||||
pik_status.PermitUncheckedError();
|
||||
assert(pik_status.ok());
|
||||
|
||||
smallest_ = &parsed_smallest;
|
||||
}
|
||||
if (largest != nullptr) {
|
||||
@@ -69,12 +68,16 @@ TruncatedRangeDelIterator::TruncatedRangeDelIterator(
|
||||
// the truncated end key can cover the largest key in this sstable, reduce
|
||||
// its sequence number by 1.
|
||||
parsed_largest.sequence -= 1;
|
||||
// This line is not needed for correctness, but it ensures that the
|
||||
// truncated end key is not covering keys from the next SST file.
|
||||
parsed_largest.type = kValueTypeForSeek;
|
||||
}
|
||||
largest_ = &parsed_largest;
|
||||
}
|
||||
}
|
||||
|
||||
bool TruncatedRangeDelIterator::Valid() const {
|
||||
assert(iter_ != nullptr);
|
||||
return iter_->Valid() &&
|
||||
(smallest_ == nullptr ||
|
||||
icmp_->Compare(*smallest_, iter_->parsed_end_key()) < 0) &&
|
||||
@@ -82,12 +85,6 @@ bool TruncatedRangeDelIterator::Valid() const {
|
||||
icmp_->Compare(iter_->parsed_start_key(), *largest_) < 0);
|
||||
}
|
||||
|
||||
void TruncatedRangeDelIterator::Next() { iter_->TopNext(); }
|
||||
|
||||
void TruncatedRangeDelIterator::Prev() { iter_->TopPrev(); }
|
||||
|
||||
void TruncatedRangeDelIterator::InternalNext() { iter_->Next(); }
|
||||
|
||||
// NOTE: target is a user key
|
||||
void TruncatedRangeDelIterator::Seek(const Slice& target) {
|
||||
if (largest_ != nullptr &&
|
||||
@@ -149,9 +146,8 @@ TruncatedRangeDelIterator::SplitBySnapshot(
|
||||
std::for_each(
|
||||
split_untruncated_iters.begin(), split_untruncated_iters.end(),
|
||||
[&](FragmentedIterPair& iter_pair) {
|
||||
std::unique_ptr<TruncatedRangeDelIterator> truncated_iter(
|
||||
new TruncatedRangeDelIterator(std::move(iter_pair.second), icmp_,
|
||||
smallest_ikey_, largest_ikey_));
|
||||
auto truncated_iter = std::make_unique<TruncatedRangeDelIterator>(
|
||||
std::move(iter_pair.second), icmp_, smallest_ikey_, largest_ikey_);
|
||||
split_truncated_iters.emplace(iter_pair.first,
|
||||
std::move(truncated_iter));
|
||||
});
|
||||
@@ -322,9 +318,8 @@ void ReadRangeDelAggregator::AddTombstones(
|
||||
if (input_iter == nullptr || input_iter->empty()) {
|
||||
return;
|
||||
}
|
||||
rep_.AddTombstones(
|
||||
std::unique_ptr<TruncatedRangeDelIterator>(new TruncatedRangeDelIterator(
|
||||
std::move(input_iter), icmp_, smallest, largest)));
|
||||
rep_.AddTombstones(std::make_unique<TruncatedRangeDelIterator>(
|
||||
std::move(input_iter), icmp_, smallest, largest));
|
||||
}
|
||||
|
||||
bool ReadRangeDelAggregator::ShouldDeleteImpl(const ParsedInternalKey& parsed,
|
||||
@@ -471,19 +466,16 @@ CompactionRangeDelAggregator::NewIterator(const Slice* lower_bound,
|
||||
const Slice* upper_bound,
|
||||
bool upper_bound_inclusive) {
|
||||
InvalidateRangeDelMapPositions();
|
||||
std::unique_ptr<TruncatedRangeDelMergingIter> merging_iter(
|
||||
new TruncatedRangeDelMergingIter(icmp_, lower_bound, upper_bound,
|
||||
upper_bound_inclusive, parent_iters_));
|
||||
auto merging_iter = std::make_unique<TruncatedRangeDelMergingIter>(
|
||||
icmp_, lower_bound, upper_bound, upper_bound_inclusive, parent_iters_);
|
||||
|
||||
auto fragmented_tombstone_list =
|
||||
std::make_shared<FragmentedRangeTombstoneList>(
|
||||
std::move(merging_iter), *icmp_, true /* for_compaction */,
|
||||
*snapshots_);
|
||||
|
||||
return std::unique_ptr<FragmentedRangeTombstoneIterator>(
|
||||
new FragmentedRangeTombstoneIterator(
|
||||
fragmented_tombstone_list, *icmp_,
|
||||
kMaxSequenceNumber /* upper_bound */));
|
||||
return std::make_unique<FragmentedRangeTombstoneIterator>(
|
||||
fragmented_tombstone_list, *icmp_, kMaxSequenceNumber /* upper_bound */);
|
||||
}
|
||||
|
||||
} // namespace ROCKSDB_NAMESPACE
|
||||
@@ -38,14 +38,15 @@ class TruncatedRangeDelIterator {
|
||||
|
||||
bool Valid() const;
|
||||
|
||||
void Next();
|
||||
void Prev();
|
||||
void Next() { iter_->TopNext(); }
|
||||
void Prev() { iter_->TopPrev(); }
|
||||
|
||||
void InternalNext();
|
||||
void InternalNext() { iter_->Next(); }
|
||||
|
||||
// Seeks to the tombstone with the highest visible sequence number that covers
|
||||
// target (a user key). If no such tombstone exists, the position will be at
|
||||
// the earliest tombstone that ends after target.
|
||||
// REQUIRES: target is a user key.
|
||||
void Seek(const Slice& target);
|
||||
|
||||
// Seeks to the tombstone with the highest visible sequence number that covers
|
||||
@@ -281,11 +282,11 @@ class RangeDelAggregator {
|
||||
const InternalKey* smallest = nullptr,
|
||||
const InternalKey* largest = nullptr) = 0;
|
||||
|
||||
bool ShouldDelete(const Slice& key, RangeDelPositioningMode mode) {
|
||||
bool ShouldDelete(const Slice& ikey, RangeDelPositioningMode mode) {
|
||||
ParsedInternalKey parsed;
|
||||
|
||||
Status pik_status =
|
||||
ParseInternalKey(key, &parsed, false /* log_err_key */); // TODO
|
||||
ParseInternalKey(ikey, &parsed, false /* log_err_key */); // TODO
|
||||
assert(pik_status.ok());
|
||||
if (!pik_status.ok()) {
|
||||
return false;
|
||||
|
||||
@@ -76,8 +76,9 @@ ParsedInternalKey UncutEndpoint(const Slice& s) {
|
||||
return ParsedInternalKey(s, kMaxSequenceNumber, kTypeRangeDeletion);
|
||||
}
|
||||
|
||||
ParsedInternalKey InternalValue(const Slice& key, SequenceNumber seq) {
|
||||
return ParsedInternalKey(key, seq, kTypeValue);
|
||||
ParsedInternalKey InternalValue(const Slice& key, SequenceNumber seq,
|
||||
ValueType type = kTypeValue) {
|
||||
return ParsedInternalKey(key, seq, type);
|
||||
}
|
||||
|
||||
void VerifyIterator(
|
||||
@@ -292,16 +293,18 @@ TEST_F(RangeDelAggregatorTest, TruncatedIterPartiallyCutTombstones) {
|
||||
TruncatedRangeDelIterator iter(std::move(input_iter), &bytewise_icmp,
|
||||
&smallest, &largest);
|
||||
|
||||
VerifyIterator(&iter, bytewise_icmp,
|
||||
{{InternalValue("d", 7), UncutEndpoint("e"), 10},
|
||||
{UncutEndpoint("e"), UncutEndpoint("g"), 8},
|
||||
{UncutEndpoint("j"), InternalValue("m", 8), 4}});
|
||||
VerifyIterator(
|
||||
&iter, bytewise_icmp,
|
||||
{{InternalValue("d", 7), UncutEndpoint("e"), 10},
|
||||
{UncutEndpoint("e"), UncutEndpoint("g"), 8},
|
||||
{UncutEndpoint("j"), InternalValue("m", 8, kValueTypeForSeek), 4}});
|
||||
|
||||
VerifySeek(
|
||||
&iter, bytewise_icmp,
|
||||
{{"d", InternalValue("d", 7), UncutEndpoint("e"), 10},
|
||||
{"e", UncutEndpoint("e"), UncutEndpoint("g"), 8},
|
||||
{"ia", UncutEndpoint("j"), InternalValue("m", 8), 4},
|
||||
{"ia", UncutEndpoint("j"), InternalValue("m", 8, kValueTypeForSeek), 4,
|
||||
false /* invalid */},
|
||||
{"n", UncutEndpoint(""), UncutEndpoint(""), 0, true /* invalid */},
|
||||
{"", InternalValue("d", 7), UncutEndpoint("e"), 10}});
|
||||
|
||||
@@ -310,7 +313,8 @@ TEST_F(RangeDelAggregatorTest, TruncatedIterPartiallyCutTombstones) {
|
||||
{{"d", InternalValue("d", 7), UncutEndpoint("e"), 10},
|
||||
{"e", UncutEndpoint("e"), UncutEndpoint("g"), 8},
|
||||
{"ia", UncutEndpoint("e"), UncutEndpoint("g"), 8},
|
||||
{"n", UncutEndpoint("j"), InternalValue("m", 8), 4},
|
||||
{"n", UncutEndpoint("j"), InternalValue("m", 8, kValueTypeForSeek), 4,
|
||||
false /* invalid */},
|
||||
{"", UncutEndpoint(""), UncutEndpoint(""), 0, true /* invalid */}});
|
||||
}
|
||||
|
||||
|
||||
@@ -67,8 +67,8 @@ FragmentedRangeTombstoneList::FragmentedRangeTombstoneList(
|
||||
unfragmented_tombstones->value().size());
|
||||
}
|
||||
// VectorIterator implicitly sorts by key during construction.
|
||||
auto iter = std::unique_ptr<VectorIterator>(
|
||||
new VectorIterator(std::move(keys), std::move(values), &icmp));
|
||||
auto iter = std::make_unique<VectorIterator>(std::move(keys),
|
||||
std::move(values), &icmp);
|
||||
FragmentTombstones(std::move(iter), icmp, for_compaction, snapshots);
|
||||
}
|
||||
|
||||
@@ -251,6 +251,22 @@ FragmentedRangeTombstoneIterator::FragmentedRangeTombstoneIterator(
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
FragmentedRangeTombstoneIterator::FragmentedRangeTombstoneIterator(
|
||||
const std::shared_ptr<FragmentedRangeTombstoneListCache>& tombstones_cache,
|
||||
const InternalKeyComparator& icmp, SequenceNumber _upper_bound,
|
||||
SequenceNumber _lower_bound)
|
||||
: tombstone_start_cmp_(icmp.user_comparator()),
|
||||
tombstone_end_cmp_(icmp.user_comparator()),
|
||||
icmp_(&icmp),
|
||||
ucmp_(icmp.user_comparator()),
|
||||
tombstones_cache_ref_(tombstones_cache),
|
||||
tombstones_(tombstones_cache_ref_->tombstones.get()),
|
||||
upper_bound_(_upper_bound),
|
||||
lower_bound_(_lower_bound) {
|
||||
assert(tombstones_ != nullptr);
|
||||
Invalidate();
|
||||
}
|
||||
|
||||
void FragmentedRangeTombstoneIterator::SeekToFirst() {
|
||||
pos_ = tombstones_->begin();
|
||||
seq_pos_ = tombstones_->seq_begin();
|
||||
@@ -433,9 +449,8 @@ FragmentedRangeTombstoneIterator::SplitBySnapshot(
|
||||
upper = snapshots[i];
|
||||
}
|
||||
if (tombstones_->ContainsRange(lower, upper)) {
|
||||
splits.emplace(upper, std::unique_ptr<FragmentedRangeTombstoneIterator>(
|
||||
new FragmentedRangeTombstoneIterator(
|
||||
tombstones_, *icmp_, upper, lower)));
|
||||
splits.emplace(upper, std::make_unique<FragmentedRangeTombstoneIterator>(
|
||||
tombstones_, *icmp_, upper, lower));
|
||||
}
|
||||
lower = upper + 1;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,15 @@
|
||||
#include "table/internal_iterator.h"
|
||||
|
||||
namespace ROCKSDB_NAMESPACE {
|
||||
struct FragmentedRangeTombstoneList;
|
||||
|
||||
struct FragmentedRangeTombstoneListCache {
|
||||
// ensure only the first reader needs to initialize l
|
||||
std::mutex reader_mutex;
|
||||
std::unique_ptr<FragmentedRangeTombstoneList> tombstones = nullptr;
|
||||
// readers will first check this bool to avoid
|
||||
std::atomic<bool> initialized = false;
|
||||
};
|
||||
|
||||
struct FragmentedRangeTombstoneList {
|
||||
public:
|
||||
@@ -113,6 +122,10 @@ class FragmentedRangeTombstoneIterator : public InternalIterator {
|
||||
const std::shared_ptr<const FragmentedRangeTombstoneList>& tombstones,
|
||||
const InternalKeyComparator& icmp, SequenceNumber upper_bound,
|
||||
SequenceNumber lower_bound = 0);
|
||||
FragmentedRangeTombstoneIterator(
|
||||
const std::shared_ptr<FragmentedRangeTombstoneListCache>& tombstones,
|
||||
const InternalKeyComparator& icmp, SequenceNumber upper_bound,
|
||||
SequenceNumber lower_bound = 0);
|
||||
|
||||
void SeekToFirst() override;
|
||||
void SeekToLast() override;
|
||||
@@ -260,6 +273,7 @@ class FragmentedRangeTombstoneIterator : public InternalIterator {
|
||||
const InternalKeyComparator* icmp_;
|
||||
const Comparator* ucmp_;
|
||||
std::shared_ptr<const FragmentedRangeTombstoneList> tombstones_ref_;
|
||||
std::shared_ptr<FragmentedRangeTombstoneListCache> tombstones_cache_ref_;
|
||||
const FragmentedRangeTombstoneList* tombstones_;
|
||||
SequenceNumber upper_bound_;
|
||||
SequenceNumber lower_bound_;
|
||||
|
||||
Loaded 100 of 209 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user